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