diff options
Diffstat (limited to 'OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs')
-rw-r--r-- | OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs | 1328 |
1 files changed, 1328 insertions, 0 deletions
diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs new file mode 100644 index 0000000..dd44564 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs | |||
@@ -0,0 +1,1328 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSim Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.IO; | ||
30 | using System.Net; | ||
31 | using System.Text; | ||
32 | using System.Xml; | ||
33 | using System.Collections; | ||
34 | using System.Collections.Generic; | ||
35 | using System.Reflection; | ||
36 | using System.Threading; | ||
37 | using OpenMetaverse; | ||
38 | using log4net; | ||
39 | using Mono.Addins; | ||
40 | using Nini.Config; | ||
41 | using Nwc.XmlRpc; | ||
42 | using OpenSim.Framework; | ||
43 | |||
44 | using OpenSim.Framework.Capabilities; | ||
45 | using OpenSim.Framework.Servers; | ||
46 | using OpenSim.Framework.Servers.HttpServer; | ||
47 | using OpenSim.Region.Framework.Interfaces; | ||
48 | using OpenSim.Region.Framework.Scenes; | ||
49 | using Caps = OpenSim.Framework.Capabilities.Caps; | ||
50 | |||
51 | namespace OpenSim.Region.OptionalModules.Avatar.Voice.VivoxVoice | ||
52 | { | ||
53 | [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "VivoxVoiceModule")] | ||
54 | public class VivoxVoiceModule : ISharedRegionModule | ||
55 | { | ||
56 | |||
57 | // channel distance model values | ||
58 | public const int CHAN_DIST_NONE = 0; // no attenuation | ||
59 | public const int CHAN_DIST_INVERSE = 1; // inverse distance attenuation | ||
60 | public const int CHAN_DIST_LINEAR = 2; // linear attenuation | ||
61 | public const int CHAN_DIST_EXPONENT = 3; // exponential attenuation | ||
62 | public const int CHAN_DIST_DEFAULT = CHAN_DIST_LINEAR; | ||
63 | |||
64 | // channel type values | ||
65 | public static readonly string CHAN_TYPE_POSITIONAL = "positional"; | ||
66 | public static readonly string CHAN_TYPE_CHANNEL = "channel"; | ||
67 | public static readonly string CHAN_TYPE_DEFAULT = CHAN_TYPE_POSITIONAL; | ||
68 | |||
69 | // channel mode values | ||
70 | public static readonly string CHAN_MODE_OPEN = "open"; | ||
71 | public static readonly string CHAN_MODE_LECTURE = "lecture"; | ||
72 | public static readonly string CHAN_MODE_PRESENTATION = "presentation"; | ||
73 | public static readonly string CHAN_MODE_AUDITORIUM = "auditorium"; | ||
74 | public static readonly string CHAN_MODE_DEFAULT = CHAN_MODE_OPEN; | ||
75 | |||
76 | // unconstrained default values | ||
77 | public const double CHAN_ROLL_OFF_DEFAULT = 2.0; // rate of attenuation | ||
78 | public const double CHAN_ROLL_OFF_MIN = 1.0; | ||
79 | public const double CHAN_ROLL_OFF_MAX = 4.0; | ||
80 | public const int CHAN_MAX_RANGE_DEFAULT = 80; // distance at which channel is silent | ||
81 | public const int CHAN_MAX_RANGE_MIN = 0; | ||
82 | public const int CHAN_MAX_RANGE_MAX = 160; | ||
83 | public const int CHAN_CLAMPING_DISTANCE_DEFAULT = 10; // distance before attenuation applies | ||
84 | public const int CHAN_CLAMPING_DISTANCE_MIN = 0; | ||
85 | public const int CHAN_CLAMPING_DISTANCE_MAX = 160; | ||
86 | |||
87 | // Infrastructure | ||
88 | private static readonly ILog m_log = | ||
89 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
90 | private static readonly Object vlock = new Object(); | ||
91 | |||
92 | // Capability strings | ||
93 | private static readonly string m_parcelVoiceInfoRequestPath = "0107/"; | ||
94 | private static readonly string m_provisionVoiceAccountRequestPath = "0108/"; | ||
95 | //private static readonly string m_chatSessionRequestPath = "0109/"; | ||
96 | |||
97 | // Control info, e.g. vivox server, admin user, admin password | ||
98 | private static bool m_pluginEnabled = false; | ||
99 | private static bool m_adminConnected = false; | ||
100 | |||
101 | private static string m_vivoxServer; | ||
102 | private static string m_vivoxSipUri; | ||
103 | private static string m_vivoxVoiceAccountApi; | ||
104 | private static string m_vivoxAdminUser; | ||
105 | private static string m_vivoxAdminPassword; | ||
106 | private static string m_authToken = String.Empty; | ||
107 | |||
108 | private static int m_vivoxChannelDistanceModel; | ||
109 | private static double m_vivoxChannelRollOff; | ||
110 | private static int m_vivoxChannelMaximumRange; | ||
111 | private static string m_vivoxChannelMode; | ||
112 | private static string m_vivoxChannelType; | ||
113 | private static int m_vivoxChannelClampingDistance; | ||
114 | |||
115 | private static Dictionary<string,string> m_parents = new Dictionary<string,string>(); | ||
116 | private static bool m_dumpXml; | ||
117 | |||
118 | private IConfig m_config; | ||
119 | |||
120 | private object m_Lock; | ||
121 | |||
122 | public void Initialise(IConfigSource config) | ||
123 | { | ||
124 | |||
125 | m_config = config.Configs["VivoxVoice"]; | ||
126 | |||
127 | if (null == m_config) | ||
128 | return; | ||
129 | |||
130 | if (!m_config.GetBoolean("enabled", false)) | ||
131 | return; | ||
132 | |||
133 | m_Lock = new object(); | ||
134 | |||
135 | try | ||
136 | { | ||
137 | // retrieve configuration variables | ||
138 | m_vivoxServer = m_config.GetString("vivox_server", String.Empty); | ||
139 | m_vivoxSipUri = m_config.GetString("vivox_sip_uri", String.Empty); | ||
140 | m_vivoxAdminUser = m_config.GetString("vivox_admin_user", String.Empty); | ||
141 | m_vivoxAdminPassword = m_config.GetString("vivox_admin_password", String.Empty); | ||
142 | |||
143 | m_vivoxChannelDistanceModel = m_config.GetInt("vivox_channel_distance_model", CHAN_DIST_DEFAULT); | ||
144 | m_vivoxChannelRollOff = m_config.GetDouble("vivox_channel_roll_off", CHAN_ROLL_OFF_DEFAULT); | ||
145 | m_vivoxChannelMaximumRange = m_config.GetInt("vivox_channel_max_range", CHAN_MAX_RANGE_DEFAULT); | ||
146 | m_vivoxChannelMode = m_config.GetString("vivox_channel_mode", CHAN_MODE_DEFAULT).ToLower(); | ||
147 | m_vivoxChannelType = m_config.GetString("vivox_channel_type", CHAN_TYPE_DEFAULT).ToLower(); | ||
148 | m_vivoxChannelClampingDistance = m_config.GetInt("vivox_channel_clamping_distance", | ||
149 | CHAN_CLAMPING_DISTANCE_DEFAULT); | ||
150 | m_dumpXml = m_config.GetBoolean("dump_xml", false); | ||
151 | |||
152 | // Validate against constraints and default if necessary | ||
153 | if (m_vivoxChannelRollOff < CHAN_ROLL_OFF_MIN || m_vivoxChannelRollOff > CHAN_ROLL_OFF_MAX) | ||
154 | { | ||
155 | m_log.WarnFormat("[VivoxVoice] Invalid value for roll off ({0}), reset to {1}.", | ||
156 | m_vivoxChannelRollOff, CHAN_ROLL_OFF_DEFAULT); | ||
157 | m_vivoxChannelRollOff = CHAN_ROLL_OFF_DEFAULT; | ||
158 | } | ||
159 | |||
160 | if (m_vivoxChannelMaximumRange < CHAN_MAX_RANGE_MIN || m_vivoxChannelMaximumRange > CHAN_MAX_RANGE_MAX) | ||
161 | { | ||
162 | m_log.WarnFormat("[VivoxVoice] Invalid value for maximum range ({0}), reset to {1}.", | ||
163 | m_vivoxChannelMaximumRange, CHAN_MAX_RANGE_DEFAULT); | ||
164 | m_vivoxChannelMaximumRange = CHAN_MAX_RANGE_DEFAULT; | ||
165 | } | ||
166 | |||
167 | if (m_vivoxChannelClampingDistance < CHAN_CLAMPING_DISTANCE_MIN || | ||
168 | m_vivoxChannelClampingDistance > CHAN_CLAMPING_DISTANCE_MAX) | ||
169 | { | ||
170 | m_log.WarnFormat("[VivoxVoice] Invalid value for clamping distance ({0}), reset to {1}.", | ||
171 | m_vivoxChannelClampingDistance, CHAN_CLAMPING_DISTANCE_DEFAULT); | ||
172 | m_vivoxChannelClampingDistance = CHAN_CLAMPING_DISTANCE_DEFAULT; | ||
173 | } | ||
174 | |||
175 | switch (m_vivoxChannelMode) | ||
176 | { | ||
177 | case "open" : break; | ||
178 | case "lecture" : break; | ||
179 | case "presentation" : break; | ||
180 | case "auditorium" : break; | ||
181 | default : | ||
182 | m_log.WarnFormat("[VivoxVoice] Invalid value for channel mode ({0}), reset to {1}.", | ||
183 | m_vivoxChannelMode, CHAN_MODE_DEFAULT); | ||
184 | m_vivoxChannelMode = CHAN_MODE_DEFAULT; | ||
185 | break; | ||
186 | } | ||
187 | |||
188 | switch (m_vivoxChannelType) | ||
189 | { | ||
190 | case "positional" : break; | ||
191 | case "channel" : break; | ||
192 | default : | ||
193 | m_log.WarnFormat("[VivoxVoice] Invalid value for channel type ({0}), reset to {1}.", | ||
194 | m_vivoxChannelType, CHAN_TYPE_DEFAULT); | ||
195 | m_vivoxChannelType = CHAN_TYPE_DEFAULT; | ||
196 | break; | ||
197 | } | ||
198 | |||
199 | m_vivoxVoiceAccountApi = String.Format("http://{0}/api2", m_vivoxServer); | ||
200 | |||
201 | // Admin interface required values | ||
202 | if (String.IsNullOrEmpty(m_vivoxServer) || | ||
203 | String.IsNullOrEmpty(m_vivoxSipUri) || | ||
204 | String.IsNullOrEmpty(m_vivoxAdminUser) || | ||
205 | String.IsNullOrEmpty(m_vivoxAdminPassword)) | ||
206 | { | ||
207 | m_log.Error("[VivoxVoice] plugin mis-configured"); | ||
208 | m_log.Info("[VivoxVoice] plugin disabled: incomplete configuration"); | ||
209 | return; | ||
210 | } | ||
211 | |||
212 | m_log.InfoFormat("[VivoxVoice] using vivox server {0}", m_vivoxServer); | ||
213 | |||
214 | // Get admin rights and cleanup any residual channel definition | ||
215 | |||
216 | DoAdminLogin(); | ||
217 | |||
218 | m_pluginEnabled = true; | ||
219 | |||
220 | m_log.Info("[VivoxVoice] plugin enabled"); | ||
221 | } | ||
222 | catch (Exception e) | ||
223 | { | ||
224 | m_log.ErrorFormat("[VivoxVoice] plugin initialization failed: {0}", e.Message); | ||
225 | m_log.DebugFormat("[VivoxVoice] plugin initialization failed: {0}", e.ToString()); | ||
226 | return; | ||
227 | } | ||
228 | } | ||
229 | |||
230 | public void AddRegion(Scene scene) | ||
231 | { | ||
232 | if (m_pluginEnabled) | ||
233 | { | ||
234 | lock (vlock) | ||
235 | { | ||
236 | string channelId; | ||
237 | |||
238 | string sceneUUID = scene.RegionInfo.RegionID.ToString(); | ||
239 | string sceneName = scene.RegionInfo.RegionName; | ||
240 | |||
241 | // Make sure that all local channels are deleted. | ||
242 | // So we have to search for the children, and then do an | ||
243 | // iteration over the set of chidren identified. | ||
244 | // This assumes that there is just one directory per | ||
245 | // region. | ||
246 | |||
247 | if (VivoxTryGetDirectory(sceneUUID + "D", out channelId)) | ||
248 | { | ||
249 | m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}", | ||
250 | sceneName, sceneUUID, channelId); | ||
251 | |||
252 | XmlElement children = VivoxListChildren(channelId); | ||
253 | string count; | ||
254 | |||
255 | if (XmlFind(children, "response.level0.channel-search.count", out count)) | ||
256 | { | ||
257 | int cnum = Convert.ToInt32(count); | ||
258 | for (int i = 0; i < cnum; i++) | ||
259 | { | ||
260 | string id; | ||
261 | if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) | ||
262 | { | ||
263 | if (!IsOK(VivoxDeleteChannel(channelId, id))) | ||
264 | m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id); | ||
265 | } | ||
266 | } | ||
267 | } | ||
268 | } | ||
269 | else | ||
270 | { | ||
271 | if (!VivoxTryCreateDirectory(sceneUUID + "D", sceneName, out channelId)) | ||
272 | { | ||
273 | m_log.WarnFormat("[VivoxVoice] Create failed <{0}:{1}:{2}>", | ||
274 | "*", sceneUUID, sceneName); | ||
275 | channelId = String.Empty; | ||
276 | } | ||
277 | } | ||
278 | |||
279 | // Create a dictionary entry unconditionally. This eliminates the | ||
280 | // need to check for a parent in the core code. The end result is | ||
281 | // the same, if the parent table entry is an empty string, then | ||
282 | // region channels will be created as first-level channels. | ||
283 | lock (m_parents) | ||
284 | { | ||
285 | if (m_parents.ContainsKey(sceneUUID)) | ||
286 | { | ||
287 | RemoveRegion(scene); | ||
288 | m_parents.Add(sceneUUID, channelId); | ||
289 | } | ||
290 | else | ||
291 | { | ||
292 | m_parents.Add(sceneUUID, channelId); | ||
293 | } | ||
294 | } | ||
295 | } | ||
296 | |||
297 | // we need to capture scene in an anonymous method | ||
298 | // here as we need it later in the callbacks | ||
299 | scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps) | ||
300 | { | ||
301 | OnRegisterCaps(scene, agentID, caps); | ||
302 | }; | ||
303 | } | ||
304 | } | ||
305 | |||
306 | public void RegionLoaded(Scene scene) | ||
307 | { | ||
308 | // Do nothing. | ||
309 | } | ||
310 | |||
311 | public void RemoveRegion(Scene scene) | ||
312 | { | ||
313 | if (m_pluginEnabled) | ||
314 | { | ||
315 | lock (vlock) | ||
316 | { | ||
317 | string channelId; | ||
318 | |||
319 | string sceneUUID = scene.RegionInfo.RegionID.ToString(); | ||
320 | string sceneName = scene.RegionInfo.RegionName; | ||
321 | |||
322 | // Make sure that all local channels are deleted. | ||
323 | // So we have to search for the children, and then do an | ||
324 | // iteration over the set of chidren identified. | ||
325 | // This assumes that there is just one directory per | ||
326 | // region. | ||
327 | if (VivoxTryGetDirectory(sceneUUID + "D", out channelId)) | ||
328 | { | ||
329 | m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}", | ||
330 | sceneName, sceneUUID, channelId); | ||
331 | |||
332 | XmlElement children = VivoxListChildren(channelId); | ||
333 | string count; | ||
334 | |||
335 | if (XmlFind(children, "response.level0.channel-search.count", out count)) | ||
336 | { | ||
337 | int cnum = Convert.ToInt32(count); | ||
338 | for (int i = 0; i < cnum; i++) | ||
339 | { | ||
340 | string id; | ||
341 | if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) | ||
342 | { | ||
343 | if (!IsOK(VivoxDeleteChannel(channelId, id))) | ||
344 | m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id); | ||
345 | } | ||
346 | } | ||
347 | } | ||
348 | } | ||
349 | |||
350 | if (!IsOK(VivoxDeleteChannel(null, channelId))) | ||
351 | m_log.WarnFormat("[VivoxVoice] Parent channel delete failed {0}:{1}:{2}", sceneName, sceneUUID, channelId); | ||
352 | |||
353 | // Remove the channel umbrella entry | ||
354 | |||
355 | lock (m_parents) | ||
356 | { | ||
357 | if (m_parents.ContainsKey(sceneUUID)) | ||
358 | { | ||
359 | m_parents.Remove(sceneUUID); | ||
360 | } | ||
361 | } | ||
362 | } | ||
363 | } | ||
364 | } | ||
365 | |||
366 | public void PostInitialise() | ||
367 | { | ||
368 | // Do nothing. | ||
369 | } | ||
370 | |||
371 | public void Close() | ||
372 | { | ||
373 | if (m_pluginEnabled) | ||
374 | VivoxLogout(); | ||
375 | } | ||
376 | |||
377 | public Type ReplaceableInterface | ||
378 | { | ||
379 | get { return null; } | ||
380 | } | ||
381 | |||
382 | public string Name | ||
383 | { | ||
384 | get { return "VivoxVoiceModule"; } | ||
385 | } | ||
386 | |||
387 | public bool IsSharedModule | ||
388 | { | ||
389 | get { return true; } | ||
390 | } | ||
391 | |||
392 | // <summary> | ||
393 | // OnRegisterCaps is invoked via the scene.EventManager | ||
394 | // everytime OpenSim hands out capabilities to a client | ||
395 | // (login, region crossing). We contribute two capabilities to | ||
396 | // the set of capabilities handed back to the client: | ||
397 | // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest. | ||
398 | // | ||
399 | // ProvisionVoiceAccountRequest allows the client to obtain | ||
400 | // the voice account credentials for the avatar it is | ||
401 | // controlling (e.g., user name, password, etc). | ||
402 | // | ||
403 | // ParcelVoiceInfoRequest is invoked whenever the client | ||
404 | // changes from one region or parcel to another. | ||
405 | // | ||
406 | // Note that OnRegisterCaps is called here via a closure | ||
407 | // delegate containing the scene of the respective region (see | ||
408 | // Initialise()). | ||
409 | // </summary> | ||
410 | public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) | ||
411 | { | ||
412 | m_log.DebugFormat("[VivoxVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps); | ||
413 | |||
414 | string capsBase = "/CAPS/" + caps.CapsObjectPath; | ||
415 | |||
416 | caps.RegisterHandler( | ||
417 | "ProvisionVoiceAccountRequest", | ||
418 | new RestStreamHandler( | ||
419 | "POST", | ||
420 | capsBase + m_provisionVoiceAccountRequestPath, | ||
421 | (request, path, param, httpRequest, httpResponse) | ||
422 | => ProvisionVoiceAccountRequest(scene, request, path, param, agentID, caps), | ||
423 | "ProvisionVoiceAccountRequest", | ||
424 | agentID.ToString())); | ||
425 | |||
426 | caps.RegisterHandler( | ||
427 | "ParcelVoiceInfoRequest", | ||
428 | new RestStreamHandler( | ||
429 | "POST", | ||
430 | capsBase + m_parcelVoiceInfoRequestPath, | ||
431 | (request, path, param, httpRequest, httpResponse) | ||
432 | => ParcelVoiceInfoRequest(scene, request, path, param, agentID, caps), | ||
433 | "ParcelVoiceInfoRequest", | ||
434 | agentID.ToString())); | ||
435 | |||
436 | //caps.RegisterHandler( | ||
437 | // "ChatSessionRequest", | ||
438 | // new RestStreamHandler( | ||
439 | // "POST", | ||
440 | // capsBase + m_chatSessionRequestPath, | ||
441 | // (request, path, param, httpRequest, httpResponse) | ||
442 | // => ChatSessionRequest(scene, request, path, param, agentID, caps), | ||
443 | // "ChatSessionRequest", | ||
444 | // agentID.ToString())); | ||
445 | } | ||
446 | |||
447 | /// <summary> | ||
448 | /// Callback for a client request for Voice Account Details | ||
449 | /// </summary> | ||
450 | /// <param name="scene">current scene object of the client</param> | ||
451 | /// <param name="request"></param> | ||
452 | /// <param name="path"></param> | ||
453 | /// <param name="param"></param> | ||
454 | /// <param name="agentID"></param> | ||
455 | /// <param name="caps"></param> | ||
456 | /// <returns></returns> | ||
457 | public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param, | ||
458 | UUID agentID, Caps caps) | ||
459 | { | ||
460 | try | ||
461 | { | ||
462 | ScenePresence avatar = null; | ||
463 | string avatarName = null; | ||
464 | |||
465 | if (scene == null) | ||
466 | throw new Exception("[VivoxVoice][PROVISIONVOICE]: Invalid scene"); | ||
467 | |||
468 | avatar = scene.GetScenePresence(agentID); | ||
469 | while (avatar == null) | ||
470 | { | ||
471 | Thread.Sleep(100); | ||
472 | avatar = scene.GetScenePresence(agentID); | ||
473 | } | ||
474 | |||
475 | avatarName = avatar.Name; | ||
476 | |||
477 | m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: scene = {0}, agentID = {1}", scene, agentID); | ||
478 | m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}", | ||
479 | request, path, param); | ||
480 | |||
481 | XmlElement resp; | ||
482 | bool retry = false; | ||
483 | string agentname = "x" + Convert.ToBase64String(agentID.GetBytes()); | ||
484 | string password = new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16); | ||
485 | string code = String.Empty; | ||
486 | |||
487 | agentname = agentname.Replace('+', '-').Replace('/', '_'); | ||
488 | |||
489 | do | ||
490 | { | ||
491 | resp = VivoxGetAccountInfo(agentname); | ||
492 | |||
493 | if (XmlFind(resp, "response.level0.status", out code)) | ||
494 | { | ||
495 | if (code != "OK") | ||
496 | { | ||
497 | if (XmlFind(resp, "response.level0.body.code", out code)) | ||
498 | { | ||
499 | // If the request was recognized, then this should be set to something | ||
500 | switch (code) | ||
501 | { | ||
502 | case "201" : // Account expired | ||
503 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : expired credentials", | ||
504 | avatarName); | ||
505 | m_adminConnected = false; | ||
506 | retry = DoAdminLogin(); | ||
507 | break; | ||
508 | |||
509 | case "202" : // Missing credentials | ||
510 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : missing credentials", | ||
511 | avatarName); | ||
512 | break; | ||
513 | |||
514 | case "212" : // Not authorized | ||
515 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : not authorized", | ||
516 | avatarName); | ||
517 | break; | ||
518 | |||
519 | case "300" : // Required parameter missing | ||
520 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : parameter missing", | ||
521 | avatarName); | ||
522 | break; | ||
523 | |||
524 | case "403" : // Account does not exist | ||
525 | resp = VivoxCreateAccount(agentname,password); | ||
526 | // Note: This REALLY MUST BE status. Create Account does not return code. | ||
527 | if (XmlFind(resp, "response.level0.status", out code)) | ||
528 | { | ||
529 | switch (code) | ||
530 | { | ||
531 | case "201" : // Account expired | ||
532 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : expired credentials", | ||
533 | avatarName); | ||
534 | m_adminConnected = false; | ||
535 | retry = DoAdminLogin(); | ||
536 | break; | ||
537 | |||
538 | case "202" : // Missing credentials | ||
539 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : missing credentials", | ||
540 | avatarName); | ||
541 | break; | ||
542 | |||
543 | case "212" : // Not authorized | ||
544 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : not authorized", | ||
545 | avatarName); | ||
546 | break; | ||
547 | |||
548 | case "300" : // Required parameter missing | ||
549 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : parameter missing", | ||
550 | avatarName); | ||
551 | break; | ||
552 | |||
553 | case "400" : // Create failed | ||
554 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : create failed", | ||
555 | avatarName); | ||
556 | break; | ||
557 | } | ||
558 | } | ||
559 | break; | ||
560 | |||
561 | case "404" : // Failed to retrieve account | ||
562 | m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : retrieve failed"); | ||
563 | // [AMW] Sleep and retry for a fixed period? Or just abandon? | ||
564 | break; | ||
565 | } | ||
566 | } | ||
567 | } | ||
568 | } | ||
569 | } | ||
570 | while (retry); | ||
571 | |||
572 | if (code != "OK") | ||
573 | { | ||
574 | m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: Get Account Request failed for \"{0}\"", avatarName); | ||
575 | throw new Exception("Unable to execute request"); | ||
576 | } | ||
577 | |||
578 | // Unconditionally change the password on each request | ||
579 | VivoxPassword(agentname, password); | ||
580 | |||
581 | LLSDVoiceAccountResponse voiceAccountResponse = | ||
582 | new LLSDVoiceAccountResponse(agentname, password, m_vivoxSipUri, m_vivoxVoiceAccountApi); | ||
583 | |||
584 | string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse); | ||
585 | |||
586 | m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r); | ||
587 | |||
588 | return r; | ||
589 | } | ||
590 | catch (Exception e) | ||
591 | { | ||
592 | m_log.ErrorFormat("[VivoxVoice][PROVISIONVOICE]: : {0}, retry later", e.Message); | ||
593 | m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: : {0} failed", e.ToString()); | ||
594 | return "<llsd><undef /></llsd>"; | ||
595 | } | ||
596 | } | ||
597 | |||
598 | /// <summary> | ||
599 | /// Callback for a client request for ParcelVoiceInfo | ||
600 | /// </summary> | ||
601 | /// <param name="scene">current scene object of the client</param> | ||
602 | /// <param name="request"></param> | ||
603 | /// <param name="path"></param> | ||
604 | /// <param name="param"></param> | ||
605 | /// <param name="agentID"></param> | ||
606 | /// <param name="caps"></param> | ||
607 | /// <returns></returns> | ||
608 | public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param, | ||
609 | UUID agentID, Caps caps) | ||
610 | { | ||
611 | ScenePresence avatar = scene.GetScenePresence(agentID); | ||
612 | string avatarName = avatar.Name; | ||
613 | |||
614 | // - check whether we have a region channel in our cache | ||
615 | // - if not: | ||
616 | // create it and cache it | ||
617 | // - send it to the client | ||
618 | // - send channel_uri: as "sip:regionID@m_sipDomain" | ||
619 | try | ||
620 | { | ||
621 | LLSDParcelVoiceInfoResponse parcelVoiceInfo; | ||
622 | string channel_uri; | ||
623 | |||
624 | if (null == scene.LandChannel) | ||
625 | throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available", | ||
626 | scene.RegionInfo.RegionName, avatarName)); | ||
627 | |||
628 | // get channel_uri: check first whether estate | ||
629 | // settings allow voice, then whether parcel allows | ||
630 | // voice, if all do retrieve or obtain the parcel | ||
631 | // voice channel | ||
632 | LandData land = scene.GetLandData(avatar.AbsolutePosition); | ||
633 | |||
634 | m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}", | ||
635 | scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param); | ||
636 | // m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: avatar \"{0}\": location: {1} {2} {3}", | ||
637 | // avatarName, avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y, avatar.AbsolutePosition.Z); | ||
638 | |||
639 | // TODO: EstateSettings don't seem to get propagated... | ||
640 | if (!scene.RegionInfo.EstateSettings.AllowVoice) | ||
641 | { | ||
642 | m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings", | ||
643 | scene.RegionInfo.RegionName); | ||
644 | channel_uri = String.Empty; | ||
645 | } | ||
646 | |||
647 | if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0) | ||
648 | { | ||
649 | m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel", | ||
650 | scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName); | ||
651 | channel_uri = String.Empty; | ||
652 | } | ||
653 | else | ||
654 | { | ||
655 | channel_uri = RegionGetOrCreateChannel(scene, land); | ||
656 | } | ||
657 | |||
658 | // fill in our response to the client | ||
659 | Hashtable creds = new Hashtable(); | ||
660 | creds["channel_uri"] = channel_uri; | ||
661 | |||
662 | parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds); | ||
663 | string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo); | ||
664 | |||
665 | m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}", | ||
666 | scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r); | ||
667 | return r; | ||
668 | } | ||
669 | catch (Exception e) | ||
670 | { | ||
671 | m_log.ErrorFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later", | ||
672 | scene.RegionInfo.RegionName, avatarName, e.Message); | ||
673 | m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed", | ||
674 | scene.RegionInfo.RegionName, avatarName, e.ToString()); | ||
675 | |||
676 | return "<llsd><undef /></llsd>"; | ||
677 | } | ||
678 | } | ||
679 | |||
680 | /// <summary> | ||
681 | /// Callback for a client request for a private chat channel | ||
682 | /// </summary> | ||
683 | /// <param name="scene">current scene object of the client</param> | ||
684 | /// <param name="request"></param> | ||
685 | /// <param name="path"></param> | ||
686 | /// <param name="param"></param> | ||
687 | /// <param name="agentID"></param> | ||
688 | /// <param name="caps"></param> | ||
689 | /// <returns></returns> | ||
690 | public string ChatSessionRequest(Scene scene, string request, string path, string param, | ||
691 | UUID agentID, Caps caps) | ||
692 | { | ||
693 | ScenePresence avatar = scene.GetScenePresence(agentID); | ||
694 | string avatarName = avatar.Name; | ||
695 | |||
696 | m_log.DebugFormat("[VivoxVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}", | ||
697 | avatarName, request, path, param); | ||
698 | return "<llsd>true</llsd>"; | ||
699 | } | ||
700 | |||
701 | private string RegionGetOrCreateChannel(Scene scene, LandData land) | ||
702 | { | ||
703 | string channelUri = null; | ||
704 | string channelId = null; | ||
705 | |||
706 | string landUUID; | ||
707 | string landName; | ||
708 | string parentId; | ||
709 | |||
710 | lock (m_parents) | ||
711 | parentId = m_parents[scene.RegionInfo.RegionID.ToString()]; | ||
712 | |||
713 | // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same | ||
714 | // as the directory ID. Otherwise, it reflects the parcel's ID. | ||
715 | if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0) | ||
716 | { | ||
717 | landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name); | ||
718 | landUUID = land.GlobalID.ToString(); | ||
719 | m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", | ||
720 | landName, land.LocalID, landUUID); | ||
721 | } | ||
722 | else | ||
723 | { | ||
724 | landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName); | ||
725 | landUUID = scene.RegionInfo.RegionID.ToString(); | ||
726 | m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", | ||
727 | landName, land.LocalID, landUUID); | ||
728 | } | ||
729 | |||
730 | lock (vlock) | ||
731 | { | ||
732 | // Added by Adam to help debug channel not availible errors. | ||
733 | if (VivoxTryGetChannel(parentId, landUUID, out channelId, out channelUri)) | ||
734 | m_log.DebugFormat("[VivoxVoice] Found existing channel at " + channelUri); | ||
735 | else if (VivoxTryCreateChannel(parentId, landUUID, landName, out channelUri)) | ||
736 | m_log.DebugFormat("[VivoxVoice] Created new channel at " + channelUri); | ||
737 | else | ||
738 | throw new Exception("vivox channel uri not available"); | ||
739 | |||
740 | m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parent channel id {1}: retrieved parcel channel_uri {2} ", | ||
741 | landName, parentId, channelUri); | ||
742 | } | ||
743 | |||
744 | return channelUri; | ||
745 | } | ||
746 | |||
747 | private static readonly string m_vivoxLoginPath = "http://{0}/api2/viv_signin.php?userid={1}&pwd={2}"; | ||
748 | |||
749 | /// <summary> | ||
750 | /// Perform administrative login for Vivox. | ||
751 | /// Returns a hash table containing values returned from the request. | ||
752 | /// </summary> | ||
753 | private XmlElement VivoxLogin(string name, string password) | ||
754 | { | ||
755 | string requrl = String.Format(m_vivoxLoginPath, m_vivoxServer, name, password); | ||
756 | return VivoxCall(requrl, false); | ||
757 | } | ||
758 | |||
759 | private static readonly string m_vivoxLogoutPath = "http://{0}/api2/viv_signout.php?auth_token={1}"; | ||
760 | |||
761 | /// <summary> | ||
762 | /// Perform administrative logout for Vivox. | ||
763 | /// </summary> | ||
764 | private XmlElement VivoxLogout() | ||
765 | { | ||
766 | string requrl = String.Format(m_vivoxLogoutPath, m_vivoxServer, m_authToken); | ||
767 | return VivoxCall(requrl, false); | ||
768 | } | ||
769 | |||
770 | private static readonly string m_vivoxGetAccountPath = "http://{0}/api2/viv_get_acct.php?auth_token={1}&user_name={2}"; | ||
771 | |||
772 | /// <summary> | ||
773 | /// Retrieve account information for the specified user. | ||
774 | /// Returns a hash table containing values returned from the request. | ||
775 | /// </summary> | ||
776 | private XmlElement VivoxGetAccountInfo(string user) | ||
777 | { | ||
778 | string requrl = String.Format(m_vivoxGetAccountPath, m_vivoxServer, m_authToken, user); | ||
779 | return VivoxCall(requrl, true); | ||
780 | } | ||
781 | |||
782 | private static readonly string m_vivoxNewAccountPath = "http://{0}/api2/viv_adm_acct_new.php?username={1}&pwd={2}&auth_token={3}"; | ||
783 | |||
784 | /// <summary> | ||
785 | /// Creates a new account. | ||
786 | /// For now we supply the minimum set of values, which | ||
787 | /// is user name and password. We *can* supply a lot more | ||
788 | /// demographic data. | ||
789 | /// </summary> | ||
790 | private XmlElement VivoxCreateAccount(string user, string password) | ||
791 | { | ||
792 | string requrl = String.Format(m_vivoxNewAccountPath, m_vivoxServer, user, password, m_authToken); | ||
793 | return VivoxCall(requrl, true); | ||
794 | } | ||
795 | |||
796 | private static readonly string m_vivoxPasswordPath = "http://{0}/api2/viv_adm_password.php?user_name={1}&new_pwd={2}&auth_token={3}"; | ||
797 | |||
798 | /// <summary> | ||
799 | /// Change the user's password. | ||
800 | /// </summary> | ||
801 | private XmlElement VivoxPassword(string user, string password) | ||
802 | { | ||
803 | string requrl = String.Format(m_vivoxPasswordPath, m_vivoxServer, user, password, m_authToken); | ||
804 | return VivoxCall(requrl, true); | ||
805 | } | ||
806 | |||
807 | private static readonly string m_vivoxChannelPath = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_name={2}&auth_token={3}"; | ||
808 | |||
809 | /// <summary> | ||
810 | /// Create a channel. | ||
811 | /// Once again, there a multitude of options possible. In the simplest case | ||
812 | /// we specify only the name and get a non-persistent cannel in return. Non | ||
813 | /// persistent means that the channel gets deleted if no-one uses it for | ||
814 | /// 5 hours. To accomodate future requirements, it may be a good idea to | ||
815 | /// initially create channels under the umbrella of a parent ID based upon | ||
816 | /// the region name. That way we have a context for side channels, if those | ||
817 | /// are required in a later phase. | ||
818 | /// | ||
819 | /// In this case the call handles parent and description as optional values. | ||
820 | /// </summary> | ||
821 | private bool VivoxTryCreateChannel(string parent, string channelId, string description, out string channelUri) | ||
822 | { | ||
823 | string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", channelId, m_authToken); | ||
824 | |||
825 | if (!string.IsNullOrEmpty(parent)) | ||
826 | { | ||
827 | requrl = String.Format("{0}&chan_parent={1}", requrl, parent); | ||
828 | } | ||
829 | if (!string.IsNullOrEmpty(description)) | ||
830 | { | ||
831 | requrl = String.Format("{0}&chan_desc={1}", requrl, description); | ||
832 | } | ||
833 | |||
834 | requrl = String.Format("{0}&chan_type={1}", requrl, m_vivoxChannelType); | ||
835 | requrl = String.Format("{0}&chan_mode={1}", requrl, m_vivoxChannelMode); | ||
836 | requrl = String.Format("{0}&chan_roll_off={1}", requrl, m_vivoxChannelRollOff); | ||
837 | requrl = String.Format("{0}&chan_dist_model={1}", requrl, m_vivoxChannelDistanceModel); | ||
838 | requrl = String.Format("{0}&chan_max_range={1}", requrl, m_vivoxChannelMaximumRange); | ||
839 | requrl = String.Format("{0}&chan_clamping_distance={1}", requrl, m_vivoxChannelClampingDistance); | ||
840 | |||
841 | XmlElement resp = VivoxCall(requrl, true); | ||
842 | if (XmlFind(resp, "response.level0.body.chan_uri", out channelUri)) | ||
843 | return true; | ||
844 | |||
845 | channelUri = String.Empty; | ||
846 | return false; | ||
847 | } | ||
848 | |||
849 | /// <summary> | ||
850 | /// Create a directory. | ||
851 | /// Create a channel with an unconditional type of "dir" (indicating directory). | ||
852 | /// This is used to create an arbitrary name tree for partitioning of the | ||
853 | /// channel name space. | ||
854 | /// The parent and description are optional values. | ||
855 | /// </summary> | ||
856 | private bool VivoxTryCreateDirectory(string dirId, string description, out string channelId) | ||
857 | { | ||
858 | string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", dirId, m_authToken); | ||
859 | |||
860 | // if (parent != null && parent != String.Empty) | ||
861 | // { | ||
862 | // requrl = String.Format("{0}&chan_parent={1}", requrl, parent); | ||
863 | // } | ||
864 | |||
865 | if (!string.IsNullOrEmpty(description)) | ||
866 | { | ||
867 | requrl = String.Format("{0}&chan_desc={1}", requrl, description); | ||
868 | } | ||
869 | requrl = String.Format("{0}&chan_type={1}", requrl, "dir"); | ||
870 | |||
871 | XmlElement resp = VivoxCall(requrl, true); | ||
872 | if (IsOK(resp) && XmlFind(resp, "response.level0.body.chan_id", out channelId)) | ||
873 | return true; | ||
874 | |||
875 | channelId = String.Empty; | ||
876 | return false; | ||
877 | } | ||
878 | |||
879 | private static readonly string m_vivoxChannelSearchPath = "http://{0}/api2/viv_chan_search.php?cond_channame={1}&auth_token={2}"; | ||
880 | |||
881 | /// <summary> | ||
882 | /// Retrieve a channel. | ||
883 | /// Once again, there a multitude of options possible. In the simplest case | ||
884 | /// we specify only the name and get a non-persistent cannel in return. Non | ||
885 | /// persistent means that the channel gets deleted if no-one uses it for | ||
886 | /// 5 hours. To accomodate future requirements, it may be a good idea to | ||
887 | /// initially create channels under the umbrella of a parent ID based upon | ||
888 | /// the region name. That way we have a context for side channels, if those | ||
889 | /// are required in a later phase. | ||
890 | /// In this case the call handles parent and description as optional values. | ||
891 | /// </summary> | ||
892 | private bool VivoxTryGetChannel(string channelParent, string channelName, | ||
893 | out string channelId, out string channelUri) | ||
894 | { | ||
895 | string count; | ||
896 | |||
897 | string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, channelName, m_authToken); | ||
898 | XmlElement resp = VivoxCall(requrl, true); | ||
899 | |||
900 | if (XmlFind(resp, "response.level0.channel-search.count", out count)) | ||
901 | { | ||
902 | int channels = Convert.ToInt32(count); | ||
903 | |||
904 | // Bug in Vivox Server r2978 where count returns 0 | ||
905 | // Found by Adam | ||
906 | if (channels == 0) | ||
907 | { | ||
908 | for (int j=0;j<100;j++) | ||
909 | { | ||
910 | string tmpId; | ||
911 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", j, out tmpId)) | ||
912 | break; | ||
913 | |||
914 | channels = j + 1; | ||
915 | } | ||
916 | } | ||
917 | |||
918 | for (int i = 0; i < channels; i++) | ||
919 | { | ||
920 | string name; | ||
921 | string id; | ||
922 | string type; | ||
923 | string uri; | ||
924 | string parent; | ||
925 | |||
926 | // skip if not a channel | ||
927 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) || | ||
928 | (type != "channel" && type != "positional_M")) | ||
929 | { | ||
930 | m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it's not a channel."); | ||
931 | continue; | ||
932 | } | ||
933 | |||
934 | // skip if not the name we are looking for | ||
935 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) || | ||
936 | name != channelName) | ||
937 | { | ||
938 | m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it has no name."); | ||
939 | continue; | ||
940 | } | ||
941 | |||
942 | // skip if parent does not match | ||
943 | if (channelParent != null && !XmlFind(resp, "response.level0.channel-search.channels.channels.level4.parent", i, out parent)) | ||
944 | { | ||
945 | m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it's parent doesnt match"); | ||
946 | continue; | ||
947 | } | ||
948 | |||
949 | // skip if no channel id available | ||
950 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id)) | ||
951 | { | ||
952 | m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel ID"); | ||
953 | continue; | ||
954 | } | ||
955 | |||
956 | // skip if no channel uri available | ||
957 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.uri", i, out uri)) | ||
958 | { | ||
959 | m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel URI"); | ||
960 | continue; | ||
961 | } | ||
962 | |||
963 | channelId = id; | ||
964 | channelUri = uri; | ||
965 | |||
966 | return true; | ||
967 | } | ||
968 | } | ||
969 | else | ||
970 | { | ||
971 | m_log.Debug("[VivoxVoice] No count element?"); | ||
972 | } | ||
973 | |||
974 | channelId = String.Empty; | ||
975 | channelUri = String.Empty; | ||
976 | |||
977 | // Useful incase something goes wrong. | ||
978 | //m_log.Debug("[VivoxVoice] Could not find channel in XMLRESP: " + resp.InnerXml); | ||
979 | |||
980 | return false; | ||
981 | } | ||
982 | |||
983 | private bool VivoxTryGetDirectory(string directoryName, out string directoryId) | ||
984 | { | ||
985 | string count; | ||
986 | |||
987 | string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, directoryName, m_authToken); | ||
988 | XmlElement resp = VivoxCall(requrl, true); | ||
989 | |||
990 | if (XmlFind(resp, "response.level0.channel-search.count", out count)) | ||
991 | { | ||
992 | int channels = Convert.ToInt32(count); | ||
993 | for (int i = 0; i < channels; i++) | ||
994 | { | ||
995 | string name; | ||
996 | string id; | ||
997 | string type; | ||
998 | |||
999 | // skip if not a directory | ||
1000 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) || | ||
1001 | type != "dir") | ||
1002 | continue; | ||
1003 | |||
1004 | // skip if not the name we are looking for | ||
1005 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) || | ||
1006 | name != directoryName) | ||
1007 | continue; | ||
1008 | |||
1009 | // skip if no channel id available | ||
1010 | if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id)) | ||
1011 | continue; | ||
1012 | |||
1013 | directoryId = id; | ||
1014 | return true; | ||
1015 | } | ||
1016 | } | ||
1017 | |||
1018 | directoryId = String.Empty; | ||
1019 | return false; | ||
1020 | } | ||
1021 | |||
1022 | // private static readonly string m_vivoxChannelById = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}"; | ||
1023 | |||
1024 | // private XmlElement VivoxGetChannelById(string parent, string channelid) | ||
1025 | // { | ||
1026 | // string requrl = String.Format(m_vivoxChannelById, m_vivoxServer, "get", channelid, m_authToken); | ||
1027 | |||
1028 | // if (parent != null && parent != String.Empty) | ||
1029 | // return VivoxGetChild(parent, channelid); | ||
1030 | // else | ||
1031 | // return VivoxCall(requrl, true); | ||
1032 | // } | ||
1033 | |||
1034 | private static readonly string m_vivoxChannelDel = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}"; | ||
1035 | |||
1036 | /// <summary> | ||
1037 | /// Delete a channel. | ||
1038 | /// Once again, there a multitude of options possible. In the simplest case | ||
1039 | /// we specify only the name and get a non-persistent cannel in return. Non | ||
1040 | /// persistent means that the channel gets deleted if no-one uses it for | ||
1041 | /// 5 hours. To accomodate future requirements, it may be a good idea to | ||
1042 | /// initially create channels under the umbrella of a parent ID based upon | ||
1043 | /// the region name. That way we have a context for side channels, if those | ||
1044 | /// are required in a later phase. | ||
1045 | /// In this case the call handles parent and description as optional values. | ||
1046 | /// </summary> | ||
1047 | private XmlElement VivoxDeleteChannel(string parent, string channelid) | ||
1048 | { | ||
1049 | string requrl = String.Format(m_vivoxChannelDel, m_vivoxServer, "delete", channelid, m_authToken); | ||
1050 | if (!string.IsNullOrEmpty(parent)) | ||
1051 | { | ||
1052 | requrl = String.Format("{0}&chan_parent={1}", requrl, parent); | ||
1053 | } | ||
1054 | return VivoxCall(requrl, true); | ||
1055 | } | ||
1056 | |||
1057 | private static readonly string m_vivoxChannelSearch = "http://{0}/api2/viv_chan_search.php?&cond_chanparent={1}&auth_token={2}"; | ||
1058 | |||
1059 | /// <summary> | ||
1060 | /// Return information on channels in the given directory | ||
1061 | /// </summary> | ||
1062 | private XmlElement VivoxListChildren(string channelid) | ||
1063 | { | ||
1064 | string requrl = String.Format(m_vivoxChannelSearch, m_vivoxServer, channelid, m_authToken); | ||
1065 | return VivoxCall(requrl, true); | ||
1066 | } | ||
1067 | |||
1068 | // private XmlElement VivoxGetChild(string parent, string child) | ||
1069 | // { | ||
1070 | |||
1071 | // XmlElement children = VivoxListChildren(parent); | ||
1072 | // string count; | ||
1073 | |||
1074 | // if (XmlFind(children, "response.level0.channel-search.count", out count)) | ||
1075 | // { | ||
1076 | // int cnum = Convert.ToInt32(count); | ||
1077 | // for (int i = 0; i < cnum; i++) | ||
1078 | // { | ||
1079 | // string name; | ||
1080 | // string id; | ||
1081 | // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.name", i, out name)) | ||
1082 | // { | ||
1083 | // if (name == child) | ||
1084 | // { | ||
1085 | // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) | ||
1086 | // { | ||
1087 | // return VivoxGetChannelById(null, id); | ||
1088 | // } | ||
1089 | // } | ||
1090 | // } | ||
1091 | // } | ||
1092 | // } | ||
1093 | |||
1094 | // // One we *know* does not exist. | ||
1095 | // return VivoxGetChannel(null, Guid.NewGuid().ToString()); | ||
1096 | |||
1097 | // } | ||
1098 | |||
1099 | /// <summary> | ||
1100 | /// This method handles the WEB side of making a request over the | ||
1101 | /// Vivox interface. The returned values are tansferred to a has | ||
1102 | /// table which is returned as the result. | ||
1103 | /// The outcome of the call can be determined by examining the | ||
1104 | /// status value in the hash table. | ||
1105 | /// </summary> | ||
1106 | private XmlElement VivoxCall(string requrl, bool admin) | ||
1107 | { | ||
1108 | |||
1109 | XmlDocument doc = null; | ||
1110 | |||
1111 | // If this is an admin call, and admin is not connected, | ||
1112 | // and the admin id cannot be connected, then fail. | ||
1113 | if (admin && !m_adminConnected && !DoAdminLogin()) | ||
1114 | return null; | ||
1115 | |||
1116 | doc = new XmlDocument(); | ||
1117 | |||
1118 | // Let's serialize all calls to Vivox. Most of these are driven by | ||
1119 | // the clients (CAPs), when the user arrives at the region. We don't | ||
1120 | // want to issue many simultaneous http requests to Vivox, because mono | ||
1121 | // doesn't like that | ||
1122 | lock (m_Lock) | ||
1123 | { | ||
1124 | try | ||
1125 | { | ||
1126 | // Otherwise prepare the request | ||
1127 | m_log.DebugFormat("[VivoxVoice] Sending request <{0}>", requrl); | ||
1128 | |||
1129 | HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requrl); | ||
1130 | |||
1131 | // We are sending just parameters, no content | ||
1132 | req.ContentLength = 0; | ||
1133 | |||
1134 | // Send request and retrieve the response | ||
1135 | using (HttpWebResponse rsp = (HttpWebResponse)req.GetResponse()) | ||
1136 | using (Stream s = rsp.GetResponseStream()) | ||
1137 | using (XmlTextReader rdr = new XmlTextReader(s)) | ||
1138 | doc.Load(rdr); | ||
1139 | } | ||
1140 | catch (Exception e) | ||
1141 | { | ||
1142 | m_log.ErrorFormat("[VivoxVoice] Error in admin call : {0}", e.Message); | ||
1143 | } | ||
1144 | } | ||
1145 | |||
1146 | // If we're debugging server responses, dump the whole | ||
1147 | // load now | ||
1148 | if (m_dumpXml) XmlScanl(doc.DocumentElement,0); | ||
1149 | |||
1150 | return doc.DocumentElement; | ||
1151 | } | ||
1152 | |||
1153 | /// <summary> | ||
1154 | /// Just say if it worked. | ||
1155 | /// </summary> | ||
1156 | private bool IsOK(XmlElement resp) | ||
1157 | { | ||
1158 | string status; | ||
1159 | XmlFind(resp, "response.level0.status", out status); | ||
1160 | return (status == "OK"); | ||
1161 | } | ||
1162 | |||
1163 | /// <summary> | ||
1164 | /// Login has been factored in this way because it gets called | ||
1165 | /// from several places in the module, and we want it to work | ||
1166 | /// the same way each time. | ||
1167 | /// </summary> | ||
1168 | private bool DoAdminLogin() | ||
1169 | { | ||
1170 | m_log.Debug("[VivoxVoice] Establishing admin connection"); | ||
1171 | |||
1172 | lock (vlock) | ||
1173 | { | ||
1174 | if (!m_adminConnected) | ||
1175 | { | ||
1176 | string status = "Unknown"; | ||
1177 | XmlElement resp = null; | ||
1178 | |||
1179 | resp = VivoxLogin(m_vivoxAdminUser, m_vivoxAdminPassword); | ||
1180 | |||
1181 | if (XmlFind(resp, "response.level0.body.status", out status)) | ||
1182 | { | ||
1183 | if (status == "Ok") | ||
1184 | { | ||
1185 | m_log.Info("[VivoxVoice] Admin connection established"); | ||
1186 | if (XmlFind(resp, "response.level0.body.auth_token", out m_authToken)) | ||
1187 | { | ||
1188 | if (m_dumpXml) m_log.DebugFormat("[VivoxVoice] Auth Token <{0}>", | ||
1189 | m_authToken); | ||
1190 | m_adminConnected = true; | ||
1191 | } | ||
1192 | } | ||
1193 | else | ||
1194 | { | ||
1195 | m_log.WarnFormat("[VivoxVoice] Admin connection failed, status = {0}", | ||
1196 | status); | ||
1197 | } | ||
1198 | } | ||
1199 | } | ||
1200 | } | ||
1201 | |||
1202 | return m_adminConnected; | ||
1203 | } | ||
1204 | |||
1205 | /// <summary> | ||
1206 | /// The XmlScan routine is provided to aid in the | ||
1207 | /// reverse engineering of incompletely | ||
1208 | /// documented packets returned by the Vivox | ||
1209 | /// voice server. It is only called if the | ||
1210 | /// m_dumpXml switch is set. | ||
1211 | /// </summary> | ||
1212 | private void XmlScanl(XmlElement e, int index) | ||
1213 | { | ||
1214 | if (e.HasChildNodes) | ||
1215 | { | ||
1216 | m_log.DebugFormat("<{0}>".PadLeft(index+5), e.Name); | ||
1217 | XmlNodeList children = e.ChildNodes; | ||
1218 | foreach (XmlNode node in children) | ||
1219 | switch (node.NodeType) | ||
1220 | { | ||
1221 | case XmlNodeType.Element : | ||
1222 | XmlScanl((XmlElement)node, index+1); | ||
1223 | break; | ||
1224 | case XmlNodeType.Text : | ||
1225 | m_log.DebugFormat("\"{0}\"".PadLeft(index+5), node.Value); | ||
1226 | break; | ||
1227 | default : | ||
1228 | break; | ||
1229 | } | ||
1230 | m_log.DebugFormat("</{0}>".PadLeft(index+6), e.Name); | ||
1231 | } | ||
1232 | else | ||
1233 | { | ||
1234 | m_log.DebugFormat("<{0}/>".PadLeft(index+6), e.Name); | ||
1235 | } | ||
1236 | } | ||
1237 | |||
1238 | private static readonly char[] C_POINT = {'.'}; | ||
1239 | |||
1240 | /// <summary> | ||
1241 | /// The Find method is passed an element whose | ||
1242 | /// inner text is scanned in an attempt to match | ||
1243 | /// the name hierarchy passed in the 'tag' parameter. | ||
1244 | /// If the whole hierarchy is resolved, the InnerText | ||
1245 | /// value at that point is returned. Note that this | ||
1246 | /// may itself be a subhierarchy of the entire | ||
1247 | /// document. The function returns a boolean indicator | ||
1248 | /// of the search's success. The search is performed | ||
1249 | /// by the recursive Search method. | ||
1250 | /// </summary> | ||
1251 | private bool XmlFind(XmlElement root, string tag, int nth, out string result) | ||
1252 | { | ||
1253 | if (root == null || tag == null || tag == String.Empty) | ||
1254 | { | ||
1255 | result = String.Empty; | ||
1256 | return false; | ||
1257 | } | ||
1258 | return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result); | ||
1259 | } | ||
1260 | |||
1261 | private bool XmlFind(XmlElement root, string tag, out string result) | ||
1262 | { | ||
1263 | int nth = 0; | ||
1264 | if (root == null || tag == null || tag == String.Empty) | ||
1265 | { | ||
1266 | result = String.Empty; | ||
1267 | return false; | ||
1268 | } | ||
1269 | return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result); | ||
1270 | } | ||
1271 | |||
1272 | /// <summary> | ||
1273 | /// XmlSearch is initially called by XmlFind, and then | ||
1274 | /// recursively called by itself until the document | ||
1275 | /// supplied to XmlFind is either exhausted or the name hierarchy | ||
1276 | /// is matched. | ||
1277 | /// | ||
1278 | /// If the hierarchy is matched, the value is returned in | ||
1279 | /// result, and true returned as the function's | ||
1280 | /// value. Otherwise the result is set to the empty string and | ||
1281 | /// false is returned. | ||
1282 | /// </summary> | ||
1283 | private bool XmlSearch(XmlElement e, string[] tags, int index, ref int nth, out string result) | ||
1284 | { | ||
1285 | if (index == tags.Length || e.Name != tags[index]) | ||
1286 | { | ||
1287 | result = String.Empty; | ||
1288 | return false; | ||
1289 | } | ||
1290 | |||
1291 | if (tags.Length-index == 1) | ||
1292 | { | ||
1293 | if (nth == 0) | ||
1294 | { | ||
1295 | result = e.InnerText; | ||
1296 | return true; | ||
1297 | } | ||
1298 | else | ||
1299 | { | ||
1300 | nth--; | ||
1301 | result = String.Empty; | ||
1302 | return false; | ||
1303 | } | ||
1304 | } | ||
1305 | |||
1306 | if (e.HasChildNodes) | ||
1307 | { | ||
1308 | XmlNodeList children = e.ChildNodes; | ||
1309 | foreach (XmlNode node in children) | ||
1310 | { | ||
1311 | switch (node.NodeType) | ||
1312 | { | ||
1313 | case XmlNodeType.Element : | ||
1314 | if (XmlSearch((XmlElement)node, tags, index+1, ref nth, out result)) | ||
1315 | return true; | ||
1316 | break; | ||
1317 | |||
1318 | default : | ||
1319 | break; | ||
1320 | } | ||
1321 | } | ||
1322 | } | ||
1323 | |||
1324 | result = String.Empty; | ||
1325 | return false; | ||
1326 | } | ||
1327 | } | ||
1328 | } | ||