2 using System.Text.RegularExpressions;
3 using System.Threading;
9 public class SlUriParser
11 private enum ResolveType
14 /// Display (first.last)
35 // Regular expression created by following the majority of http://wiki.secondlife.com/wiki/Viewer_URI_Name_Space (excluding support for secondlife:///app/login).
36 // This is a nasty one and should really only be used on single links to minimize processing time.
37 private readonly Regex patternUri = new Regex(
38 @"(?<startingbrace>\[)?(" +
39 @"(?<regionuri>secondlife://(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?)|" +
40 @"(?<appuri>secondlife:///app/(" +
41 @"(?<appcommand>agent)/(?<agent_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/(?<action>[a-z]+)|" +
42 @"(?<appcommand>apperance)/show|" +
43 @"(?<appcommand>balance)/request|" +
44 @"(?<appcommand>chat)/(?<channel>\d+)/(?<text>[^\] ]+)|" +
45 @"(?<appcommand>classified)/(?<classified_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/about|" +
46 @"(?<appcommand>event)/(?<event_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/about|" +
47 @"(?<appcommand>group)/(" +
48 @"(?<group_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/(?<action>[a-z]+)|" +
49 @"(?<action>create)|" +
50 @"(?<action>list/show))|" +
51 @"(?<appcommand>help)/?<help_query>([^\] ]+)|" +
52 @"(?<appcommand>inventory)/(" +
53 @"(?<inventory_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/(?<action>select)/?" +
55 @"name=(?<name>[^& ]+)" +
57 @"(?<action>show))|" +
58 @"(?<appcommand>maptrackavatar)/(?<friend_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
59 @"(?<appcommand>objectim)/(?<object_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/?" +
61 @"name=(?<name>[^& ]+)|" +
62 @"owner=(?<owner>[^& ]+)|" +
63 @"groupowned=(?<groupowned>true)|" +
64 @"slurl=(?<region_name>[^\]/ ]+)(/(?<x>[0-9]+\.?[0-9]*))?(/(?<y>[0-9]+\.?[0-9]*))?(/(?<z>[0-9]+\.?[0-9]*))?" +
66 @"(?<appcommand>parcel)/(?<parcel_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/about|" +
67 @"(?<appcommand>search)/(?<category>[a-z]+)/(?<search_term>[^\]/ ]+)|" +
68 @"(?<appcommand>sharewithavatar)/(?<agent_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
69 @"(?<appcommand>teleport)/(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?|" +
70 @"(?<appcommand>voicecallavatar)/(?<agent_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
71 @"(?<appcommand>wear_folder)/?folder_id=(?<inventory_folder_uuid>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
72 @"(?<appcommand>worldmap)/(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?)))" +
73 @"( (?<endingbrace>[^\]]*)\])?", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
76 /// Gets the display text for the specified URI
78 /// <param name="uri">URI to get the display text of</param>
79 /// <returns>Display text for URI</returns>
80 public string GetLinkName(string uri)
82 Match match = patternUri.Match(uri);
88 // Custom named links in the form of [secondlife://<truncated> Custom%20Link%20Name] will
89 // result in a link named 'Custom Link Name' regardless of the previous secondlife URI.
90 if (match.Groups["startingbrace"].Success && match.Groups["endingbrace"].Length > 0)
92 return HttpUtility.UrlDecode(match.Groups["endingbrace"].Value);
95 if (match.Groups["regionuri"].Success)
97 return GetLinkNameRegionUri(match);
100 if (match.Groups["appuri"].Success)
102 string appcommand = match.Groups["appcommand"].Value;
107 return GetLinkNameAgent(match);
109 return match.ToString();
111 return match.ToString();
113 return GetLinkNameChat(match);
115 return GetLinkNameClassified(match);
117 return GetLinkNameEvent(match);
119 return GetLinkNameGroup(match);
121 return GetLinkNameHelp(match);
123 return GetLinkNameInventory(match);
124 case "maptrackavatar":
125 return GetLinkNameTrackAvatar(match);
127 return GetLinkNameObjectIm(match);
129 return GetLinkNameParcel(match);
131 return GetLinkNameSearch(match);
132 case "sharewithavatar":
133 return GetLinkNameShareWithAvatar(match);
135 return GetLinkNameTeleport(match);
136 case "voicecallavatar":
137 return GetLinkNameVoiceCallAvatar(match);
139 return GetLinkNameWearFolder(match);
141 return GetLinkNameWorldMap(match);
143 return match.ToString();
147 return match.ToString();
151 /// Parses and executes the specified SecondLife URI if valid
153 /// <param name="uri">URI to parse and execute</param>
154 public void ExecuteLink(string uri)
156 Match match = patternUri.Match(uri);
162 if (match.Groups["regionuri"].Success)
164 ExecuteLinkRegionUri(match);
166 else if (match.Groups["appuri"].Success)
168 string appcommand = match.Groups["appcommand"].Value;
173 ExecuteLinkAgent(match);
176 ExecuteLinkShowApperance();
179 ExecuteLinkShowBalance();
182 ExecuteLinkChat(match);
185 ExecuteLinkClassified(match);
188 ExecuteLinkEvent(match);
191 ExecuteLinkGroup(match);
194 ExecuteLinkHelp(match);
197 ExecuteLinkInventory(match);
199 case "maptrackavatar":
200 ExecuteLinkTrackAvatar(match);
203 ExecuteLinkObjectIm(match);
206 ExecuteLinkParcel(match);
209 ExecuteLinkSearch(match);
211 case "sharewithavatar":
212 ExecuteLinkShareWithAvatar(match);
215 ExecuteLinkTeleport(match);
217 case "voicecallavatar":
218 ExecuteLinkVoiceCallAvatar(match);
221 ExecuteLinkWearFolder(match);
224 ExecuteLinkWorldMap(match);
230 #region Name Resolution
233 /// Amount of time in milliseconds to wait before giving up on name resolution
235 private const int NameResolveTimeoutInMs = 150;
238 /// Gets the name of an agent by UUID. Will block for a short period of time to allow for name resolution.
240 /// <param name="agentID">Agent UUID</param>
241 /// <param name="nameType">Type of name resolution. See ResolveType</param>
242 /// <returns>Name of agent on success, INCOMPLETE_NAME on failure or timeout</returns>
243 private string GetAgentName(UUID agentID, ResolveType nameType)
245 RadegastInstance instance = RadegastInstance.GlobalInstance;
246 string name = RadegastInstance.INCOMPLETE_NAME;
248 using (ManualResetEvent gotName = new ManualResetEvent(false))
250 EventHandler<UUIDNameReplyEventArgs> handler = (object sender, UUIDNameReplyEventArgs e) =>
252 if (e.Names.ContainsKey(agentID))
254 name = e.Names[agentID];
259 catch (ObjectDisposedException) { }
263 instance.Names.NameUpdated += handler;
265 if (nameType == ResolveType.AgentCompleteName)
267 name = instance.Names.GetLegacyName(agentID);
269 else if (nameType == ResolveType.AgentUsername)
271 name = instance.Names.GetUserName(agentID);
273 else if (nameType == ResolveType.AgentDisplayName)
275 name = instance.Names.GetDisplayName(agentID);
279 return agentID.ToString();
282 if (name == RadegastInstance.INCOMPLETE_NAME)
284 gotName.WaitOne(NameResolveTimeoutInMs, false);
287 instance.Names.NameUpdated -= handler;
294 /// Gets the name of a group by UUID. Will block for a short period of time to allow for name resolution.
296 /// <param name="groupID">Group UUID</param>
297 /// <returns>Name of the group on success, INCOMPLETE_NAME on failure or timeout</returns>
298 private string GetGroupName(UUID groupID)
300 RadegastInstance instance = RadegastInstance.GlobalInstance;
301 string name = RadegastInstance.INCOMPLETE_NAME;
303 using (ManualResetEvent gotName = new ManualResetEvent(false))
305 EventHandler<GroupNamesEventArgs> handler = (object sender, GroupNamesEventArgs e) =>
307 if (e.GroupNames.ContainsKey(groupID))
309 name = e.GroupNames[groupID];
314 catch (ObjectDisposedException) { }
318 instance.Client.Groups.GroupNamesReply += handler;
319 instance.Client.Groups.RequestGroupName(groupID);
320 if (name == RadegastInstance.INCOMPLETE_NAME)
322 gotName.WaitOne(NameResolveTimeoutInMs, false);
325 instance.Client.Groups.GroupNamesReply -= handler;
332 /// Gets the name of a parcel by UUID. Will block for a short period of time to allow for name resolution.
334 /// <param name="parcelID">Parcel UUID</param>
335 /// <returns>Name of the parcel on success, INCOMPLETE_NAME on failure or timeout</returns>
336 private string GetParcelName(UUID parcelID)
338 RadegastInstance instance = RadegastInstance.GlobalInstance;
339 string name = RadegastInstance.INCOMPLETE_NAME;
341 using (ManualResetEvent gotName = new ManualResetEvent(false))
343 EventHandler<ParcelInfoReplyEventArgs> handler = (object sender, ParcelInfoReplyEventArgs e) =>
345 if (e.Parcel.ID == parcelID)
347 name = e.Parcel.Name;
352 catch (ObjectDisposedException) { }
356 instance.Client.Parcels.ParcelInfoReply += handler;
357 instance.Client.Parcels.RequestParcelInfo(parcelID);
358 if (name == RadegastInstance.INCOMPLETE_NAME)
360 gotName.WaitOne(NameResolveTimeoutInMs, false);
363 instance.Client.Parcels.ParcelInfoReply -= handler;
371 /// Attempts to resolve the name of a given key by type (Agent, Group, Parce, etc)
373 /// <param name="id">UUID of object to resolve</param>
374 /// <param name="type">Type of object</param>
375 /// <returns>Revoled name</returns>
376 private string Resolve(UUID id, ResolveType type)
380 case ResolveType.AgentCompleteName:
381 case ResolveType.AgentDisplayName:
382 case ResolveType.AgentUsername:
383 return GetAgentName(id, type);
384 case ResolveType.Group:
385 return GetGroupName(id);
386 case ResolveType.Parcel:
387 return GetParcelName(id);
389 return id.ToString();
393 #region Link name resolution
395 private string GetLinkNameRegionUri(Match match)
397 string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
399 string coordinateString = "";
400 if (match.Groups["local_x"].Success)
402 coordinateString += " (" + match.Groups["local_x"].Value;
404 if (match.Groups["local_y"].Success)
406 coordinateString += "," + match.Groups["local_y"].Value;
408 if (match.Groups["local_z"].Success)
410 coordinateString += "," + match.Groups["local_z"].Value;
412 if (coordinateString != "")
414 coordinateString += ")";
417 return string.Format("{0}{1}", name, coordinateString);
420 private string GetLinkNameAgent(Match match)
422 UUID agentID = new UUID(match.Groups["agent_id"].Value);
423 string action = match.Groups["action"].Value;
430 return Resolve(agentID, ResolveType.AgentCompleteName);
432 return Resolve(agentID, ResolveType.AgentDisplayName);
434 return Resolve(agentID, ResolveType.AgentUsername);
436 return "IM " + Resolve(agentID, ResolveType.AgentCompleteName);
437 case "offerteleport":
438 return "Offer Teleport to " + Resolve(agentID, ResolveType.AgentCompleteName);
440 return "Pay " + Resolve(agentID, ResolveType.AgentCompleteName);
441 case "requestfriend":
442 return "Friend Request " + Resolve(agentID, ResolveType.AgentCompleteName);
444 return "Mute " + Resolve(agentID, ResolveType.AgentCompleteName);
446 return "Unmute " + Resolve(agentID, ResolveType.AgentCompleteName);
448 return match.ToString();
452 private string GetLinkNameChat(Match match)
454 //string channel = match.Groups["channel"].Value;
455 //string text = System.Web.HttpUtility.UrlDecode(match.Groups["text"].Value);
457 return match.ToString();
460 private string GetLinkNameClassified(Match match)
462 //UUID classifiedID = new UUID(match.Groups["classified_id"].Value);
464 return match.ToString();
467 private string GetLinkNameEvent(Match match)
469 //UUID eventID = new UUID(match.Groups["event_id"].Value);
471 return match.ToString();
474 private string GetLinkNameGroup(Match match)
476 string action = match.Groups["action"].Value;
483 UUID groupID = new UUID(match.Groups["group_id"].Value);
484 return Resolve(groupID, ResolveType.Group);
488 return match.ToString();
491 return match.ToString();
494 private string GetLinkNameHelp(Match match)
496 //string helpQuery = HttpUtility.UrlDecode(match.Groups["help_query"].Value);
498 return match.ToString();
501 private string GetLinkNameInventory(Match match)
503 //UUID inventoryID = new UUID(match.Groups["agent_id"].Value);
504 string action = match.Groups["action"].Value;
506 if (action == "select" && match.Groups["name"].Success)
508 return HttpUtility.UrlDecode(match.Groups["name"].Value);
511 return match.ToString();
514 private string GetLinkNameTrackAvatar(Match match)
516 //UUID agentID = new UUID(match.Groups["friend_id"].Value);
518 return match.ToString();
521 private string GetLinkNameObjectIm(Match match)
523 //UUID objectID = new UUID(match.Groups["object_id"].Value);
524 string name = HttpUtility.UrlDecode(match.Groups["name"].Value);
525 //UUID ownerID = new UUID(match.Groups["owner"].Value);
526 //string groupowned = match.Groups["groupowned"].Value;
527 //string slurl = match.Groups["slurl"].Value;
529 if (name != string.Empty)
534 return match.ToString();
537 private string GetLinkNameParcel(Match match)
539 UUID parcelID = new UUID(match.Groups["parcel_id"].Value);
540 return Resolve(parcelID, ResolveType.Parcel);
543 private string GetLinkNameSearch(Match match)
545 //string category = match.Groups["category"].Value;
546 //string searchTerm = HttpUtility.UrlDecode(match.Groups["search_term"].Value);
548 return match.ToString();
551 private string GetLinkNameShareWithAvatar(Match match)
553 //UUID agentID = new UUID(match.Groups["agent_id"].Value);
555 return match.ToString();
558 private string GetLinkNameTeleport(Match match)
560 string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
562 string coordinateString = "";
563 if (match.Groups["local_x"].Success)
565 coordinateString += " (" + match.Groups["local_x"].Value;
567 if (match.Groups["local_y"].Success)
569 coordinateString += "," + match.Groups["local_y"].Value;
571 if (match.Groups["local_z"].Success)
573 coordinateString += "," + match.Groups["local_z"].Value;
575 if (coordinateString != "")
577 coordinateString += ")";
580 return string.Format("Teleport to {0}{1}", name, coordinateString);
583 private string GetLinkNameVoiceCallAvatar(Match match)
585 //UUID agentID = new UUID(match.Groups["agent_id"].Value);
587 return match.ToString();
590 private string GetLinkNameWearFolder(Match match)
592 //UUID folderID = new UUID(match.Groups["inventory_folder_uuid"].Value);
594 return match.ToString();
597 private string GetLinkNameWorldMap(Match match)
599 string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
600 string x = match.Groups["local_x"].Success ? match.Groups["local_x"].Value : "128";
601 string y = match.Groups["local_y"].Success ? match.Groups["local_y"].Value : "128";
602 string z = match.Groups["local_z"].Success ? match.Groups["local_z"].Value : "0";
604 return string.Format("Show Map for {0} ({1},{2},{3})", name, x, y, z);
608 #region Link Execution
609 private void ExecuteLinkRegionUri(Match match)
611 RadegastInstance instance = RadegastInstance.GlobalInstance;
613 string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
614 int x = match.Groups["local_x"].Success ? int.Parse(match.Groups["local_x"].Value) : 128;
615 int y = match.Groups["local_y"].Success ? int.Parse(match.Groups["local_y"].Value) : 128;
616 int z = match.Groups["local_z"].Success ? int.Parse(match.Groups["local_z"].Value) : 0;
618 instance.MainForm.MapTab.Select();
619 instance.MainForm.WorldMap.DisplayLocation(name, x, y, z);
622 private void ExecuteLinkAgent(Match match)
624 RadegastInstance instance = RadegastInstance.GlobalInstance;
625 UUID agentID = new UUID(match.Groups["agent_id"].Value);
626 //string action = match.Groups["action"].Value;
628 RadegastInstance.GlobalInstance.MainForm.ShowAgentProfile(instance.Names.Get(agentID), agentID);
631 private void ExecuteLinkShowApperance()
636 private void ExecuteLinkShowBalance()
641 private void ExecuteLinkChat(Match match)
643 //string channel = match.Groups["channel"].Value;
644 //string text = System.Web.HttpUtility.UrlDecode(match.Groups["text"].Value);
647 private void ExecuteLinkClassified(Match match)
649 //UUID classifiedID = new UUID(match.Groups["classified_id"].Value);
652 private void ExecuteLinkEvent(Match match)
654 //UUID eventID = new UUID(match.Groups["event_id"].Value);
657 private void ExecuteLinkGroup(Match match)
659 RadegastInstance instance = RadegastInstance.GlobalInstance;
660 string action = match.Groups["action"].Value;
667 UUID groupID = new UUID(match.Groups["group_id"].Value);
668 instance.MainForm.ShowGroupProfile(groupID);
678 private void ExecuteLinkHelp(Match match)
680 //string helpQuery = HttpUtility.UrlDecode(match.Groups["help_query"].Value);
683 private void ExecuteLinkInventory(Match match)
685 //UUID inventoryID = new UUID(match.Groups["agent_id"].Value);
686 //string action = match.Groups["action"].Value;
689 private void ExecuteLinkTrackAvatar(Match match)
691 //UUID agentID = new UUID(match.Groups["friend_id"].Value);
694 private void ExecuteLinkObjectIm(Match match)
696 //UUID objectID = new UUID(match.Groups["object_id"].Value);
697 //string name = HttpUtility.UrlDecode(match.Groups["name"].Value);
698 //UUID ownerID = new UUID(match.Groups["owner"].Value);
699 //string groupowned = match.Groups["groupowned"].Value;
700 //string slurl = match.Groups["slurl"].Value;
703 private void ExecuteLinkParcel(Match match)
705 //UUID parcelID = new UUID(match.Groups["parcel_id"].Value);
708 private void ExecuteLinkSearch(Match match)
710 //string category = match.Groups["category"].Value;
711 //string searchTerm = HttpUtility.UrlDecode(match.Groups["search_term"].Value);
714 private void ExecuteLinkShareWithAvatar(Match match)
716 //UUID agentID = new UUID(match.Groups["agent_id"].Value);
719 private void ExecuteLinkTeleport(Match match)
721 //string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
722 //string x = match.Groups["local_x"].Success ? match.Groups["local_x"].Value : "128";
723 //string y = match.Groups["local_y"].Success ? match.Groups["local_y"].Value : "128";
724 //string z = match.Groups["local_z"].Success ? match.Groups["local_z"].Value : "0";
727 private void ExecuteLinkVoiceCallAvatar(Match match)
729 //UUID agentID = new UUID(match.Groups["agent_id"].Value);
732 private void ExecuteLinkWearFolder(Match match)
734 //UUID folderID = new UUID(match.Groups["inventory_folder_uuid"].Value);
737 private void ExecuteLinkWorldMap(Match match)
739 RadegastInstance instance = RadegastInstance.GlobalInstance;
741 string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
742 int x = match.Groups["local_x"].Success ? int.Parse(match.Groups["local_x"].Value) : 128;
743 int y = match.Groups["local_y"].Success ? int.Parse(match.Groups["local_y"].Value) : 128;
744 int z = match.Groups["local_z"].Success ? int.Parse(match.Groups["local_z"].Value) : 0;
746 instance.MainForm.MapTab.Select();
747 instance.MainForm.WorldMap.DisplayLocation(name, x, y, z);