aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/OptionalModules/Avatar/Voice
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/OptionalModules/Avatar/Voice')
-rw-r--r--OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs1301
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
28using System;
29using System.IO;
30using System.Net;
31using System.Text;
32using System.Xml;
33using System.Collections;
34using System.Collections.Generic;
35using System.Reflection;
36using System.Threading;
37using OpenMetaverse;
38using log4net;
39using Nini.Config;
40using Nwc.XmlRpc;
41using OpenSim.Framework;
42using OpenSim.Framework.Communications.Cache;
43using OpenSim.Framework.Communications.Capabilities;
44using OpenSim.Framework.Servers;
45using OpenSim.Framework.Servers.HttpServer;
46using OpenSim.Region.Framework.Interfaces;
47using OpenSim.Region.Framework.Scenes;
48using Caps = OpenSim.Framework.Communications.Capabilities.Caps;
49
50namespace 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}