Files
RaindropViewer/Assets/Raindrop/Core/SlUriParser.cs
T

779 lines
30 KiB
C#

/**
* Radegast Metaverse Client
* Copyright(c) 2009-2014, Radegast Development Team
* Copyright(c) 2016-2020, Sjofn, LLC
* All rights reserved.
*
* Radegast is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.If not, see<https://www.gnu.org/licenses/>.
*/
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using OpenMetaverse;
using Plugins.CommonDependencies;
namespace Raindrop
{
public class SlUriParser
{
private enum ResolveType
{
/// <summary>
/// Client specified name format
/// </summary>
AgentDefaultName,
/// <summary>
/// Display
/// </summary>
AgentDisplayName,
/// <summary>
/// first.last
/// </summary>
AgentUsername,
/// <summary>
/// Group name
/// </summary>
Group,
/// <summary>
/// Parcel name
/// </summary>
Parcel
};
// Regular expression created by following the majority of http://wiki.secondlife.com/wiki/Viewer_URI_Name_Space (excluding support for secondlife:///app/login).
// This is a nasty one and should really only be used on single links to minimize processing time.
private readonly Regex patternUri = new Regex(
@"(?<startingbrace>\[)?(" +
@"(?<regionuri>secondlife://(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?)|" +
@"(?<appuri>secondlife:///app/(" +
@"(?<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]+)|" +
@"(?<appcommand>apperance)/show|" +
@"(?<appcommand>balance)/request|" +
@"(?<appcommand>chat)/(?<channel>\d+)/(?<text>[^\] ]+)|" +
@"(?<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|" +
@"(?<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|" +
@"(?<appcommand>group)/(" +
@"(?<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]+)|" +
@"(?<action>create)|" +
@"(?<action>list/show))|" +
@"(?<appcommand>help)/?<help_query>([^\] ]+)|" +
@"(?<appcommand>inventory)/(" +
@"(?<inventory_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/(?<action>select)/?" +
@"([?&](" +
@"name=(?<name>[^& ]+)" +
@"))*|" +
@"(?<action>show))|" +
@"(?<appcommand>maptrackavatar)/(?<friend_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
@"(?<appcommand>objectim)/(?<object_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/?" +
@"([?&](" +
@"name=(?<name>[^& ]+)|" +
@"owner=(?<owner>[^& ]+)|" +
@"groupowned=(?<groupowned>true)|" +
@"slurl=(?<region_name>[^\]/ ]+)(/(?<x>[0-9]+\.?[0-9]*))?(/(?<y>[0-9]+\.?[0-9]*))?(/(?<z>[0-9]+\.?[0-9]*))?" +
@"))*|" +
@"(?<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|" +
@"(?<appcommand>search)/(?<category>[a-z]+)/(?<search_term>[^\]/ ]+)|" +
@"(?<appcommand>sharewithavatar)/(?<agent_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
@"(?<appcommand>teleport)/(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?|" +
@"(?<appcommand>voicecallavatar)/(?<agent_id>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|" +
@"(?<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})|" +
@"(?<appcommand>worldmap)/(?<region_name>[^\]/ ]+)(/(?<local_x>[0-9]+))?(/(?<local_y>[0-9]+))?(/(?<local_z>[0-9]+))?)))" +
@"( (?<endingbrace>[^\]]*)\])?", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
/// <summary>
/// Gets the display text for the specified URI
/// </summary>
/// <param name="uri">URI to get the display text of</param>
/// <returns>Display text for URI</returns>
public string GetLinkName(string uri)
{
if (!RaindropInstance.GlobalInstance.GlobalSettings["resolve_uris"])
{
return uri;
}
Match match = patternUri.Match(uri);
if (!match.Success)
{
return uri;
}
// Custom named links in the form of [secondlife://<truncated> Custom%20Link%20Name] will
// result in a link named 'Custom Link Name' regardless of the previous secondlife URI.
if (match.Groups["startingbrace"].Success && match.Groups["endingbrace"].Length > 0)
{
return HttpUtility.UrlDecode(match.Groups["endingbrace"].Value);
}
if (match.Groups["regionuri"].Success)
{
return GetLinkNameRegionUri(match);
}
if (match.Groups["appuri"].Success)
{
string appcommand = match.Groups["appcommand"].Value;
switch (appcommand)
{
case "agent":
return GetLinkNameAgent(match);
case "appearance":
return match.ToString();
case "balance":
return match.ToString();
case "chat":
return GetLinkNameChat(match);
case "classified":
return GetLinkNameClassified(match);
case "event":
return GetLinkNameEvent(match);
case "group":
return GetLinkNameGroup(match);
case "help":
return GetLinkNameHelp(match);
case "inventory":
return GetLinkNameInventory(match);
case "maptrackavatar":
return GetLinkNameTrackAvatar(match);
case "objectim":
return GetLinkNameObjectIm(match);
case "parcel":
return GetLinkNameParcel(match);
case "search":
return GetLinkNameSearch(match);
case "sharewithavatar":
return GetLinkNameShareWithAvatar(match);
case "teleport":
return GetLinkNameTeleport(match);
case "voicecallavatar":
return GetLinkNameVoiceCallAvatar(match);
case "wear_folder":
return GetLinkNameWearFolder(match);
case "worldmap":
return GetLinkNameWorldMap(match);
default:
return match.ToString();
}
}
return match.ToString();
}
/// <summary>
/// Parses and executes the specified SecondLife URI if valid
/// </summary>
/// <param name="uri">URI to parse and execute</param>
public void ExecuteLink(string uri)
{
Match match = patternUri.Match(uri);
if (!match.Success)
{
return;
}
if (match.Groups["regionuri"].Success)
{
ExecuteLinkRegionUri(match);
}
else if (match.Groups["appuri"].Success)
{
string appcommand = match.Groups["appcommand"].Value;
switch (appcommand)
{
case "agent":
ExecuteLinkAgent(match);
return;
case "appearance":
ExecuteLinkShowApperance();
return;
case "balance":
ExecuteLinkShowBalance();
return;
case "chat":
ExecuteLinkChat(match);
return;
case "classified":
ExecuteLinkClassified(match);
return;
case "event":
ExecuteLinkEvent(match);
return;
case "group":
ExecuteLinkGroup(match);
return;
case "help":
ExecuteLinkHelp(match);
return;
case "inventory":
ExecuteLinkInventory(match);
return;
case "maptrackavatar":
ExecuteLinkTrackAvatar(match);
return;
case "objectim":
ExecuteLinkObjectIm(match);
return;
case "parcel":
ExecuteLinkParcel(match);
return;
case "search":
ExecuteLinkSearch(match);
return;
case "sharewithavatar":
ExecuteLinkShareWithAvatar(match);
return;
case "teleport":
ExecuteLinkTeleport(match);
return;
case "voicecallavatar":
ExecuteLinkVoiceCallAvatar(match);
return;
case "wear_folder":
ExecuteLinkWearFolder(match);
return;
case "worldmap":
ExecuteLinkWorldMap(match);
return;
}
}
}
#region Name Resolution
/// <summary>
/// Gets the name of an agent by UUID. Will block for a short period of time to allow for name resolution.
/// </summary>
/// <param name="agentID">Agent UUID</param>
/// <param name="nameType">Type of name resolution. See ResolveType</param>
/// <returns>Name of agent on success, INCOMPLETE_NAME on failure or timeout</returns>
private string GetAgentName(UUID agentID, ResolveType nameType)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string name = RaindropInstance.IncompleteName;
using (ManualResetEvent gotName = new ManualResetEvent(false))
{
EventHandler<UUIDNameReplyEventArgs> handler = (sender, e) =>
{
if (e.Names.ContainsKey(agentID))
{
name = e.Names[agentID];
try
{
gotName.Set();
}
catch (ObjectDisposedException) { }
}
};
instance.Names.NameUpdated += handler;
if (nameType == ResolveType.AgentDefaultName)
{
name = instance.Names.Get(agentID);
}
else if (nameType == ResolveType.AgentUsername)
{
name = instance.Names.GetUserName(agentID);
}
else if (nameType == ResolveType.AgentDisplayName)
{
name = instance.Names.GetDisplayName(agentID);
}
else
{
instance.Names.NameUpdated -= handler;
return agentID.ToString();
}
if (name == RaindropInstance.IncompleteName)
{
gotName.WaitOne(instance.GlobalSettings["resolve_uri_time"], false);
}
instance.Names.NameUpdated -= handler;
}
return name;
}
/// <summary>
/// Gets the name of a group by UUID. Will block for a short period of time to allow for name resolution.
/// </summary>
/// <param name="groupID">Group UUID</param>
/// <returns>Name of the group on success, INCOMPLETE_NAME on failure or timeout</returns>
private string GetGroupName(UUID groupID)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string name = RaindropInstance.IncompleteName;
using (ManualResetEvent gotName = new ManualResetEvent(false))
{
EventHandler<GroupNamesEventArgs> handler = (sender, e) =>
{
if (e.GroupNames.ContainsKey(groupID))
{
name = e.GroupNames[groupID];
try
{
gotName.Set();
}
catch (ObjectDisposedException) { }
}
};
instance.Client.Groups.GroupNamesReply += handler;
instance.Client.Groups.RequestGroupName(groupID);
if (name == RaindropInstance.IncompleteName)
{
gotName.WaitOne(instance.GlobalSettings["resolve_uri_time"], false);
}
instance.Client.Groups.GroupNamesReply -= handler;
}
return name;
}
/// <summary>
/// Gets the name of a parcel by UUID. Will block for a short period of time to allow for name resolution.
/// </summary>
/// <param name="parcelID">Parcel UUID</param>
/// <returns>Name of the parcel on success, INCOMPLETE_NAME on failure or timeout</returns>
private string GetParcelName(UUID parcelID)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string name = RaindropInstance.IncompleteName;
using (ManualResetEvent gotName = new ManualResetEvent(false))
{
EventHandler<ParcelInfoReplyEventArgs> handler = (sender, e) =>
{
if (e.Parcel.ID == parcelID)
{
name = e.Parcel.Name;
try
{
gotName.Set();
}
catch (ObjectDisposedException) { }
}
};
instance.Client.Parcels.ParcelInfoReply += handler;
instance.Client.Parcels.RequestParcelInfo(parcelID);
if (name == RaindropInstance.IncompleteName)
{
gotName.WaitOne(instance.GlobalSettings["resolve_uri_time"], false);
}
instance.Client.Parcels.ParcelInfoReply -= handler;
}
return name;
}
#endregion
/// <summary>
/// Attempts to resolve the name of a given key by type (Agent, Group, Parce, etc)
/// </summary>
/// <param name="id">UUID of object to resolve</param>
/// <param name="type">Type of object</param>
/// <returns>Revoled name</returns>
private string Resolve(UUID id, ResolveType type)
{
switch (type)
{
case ResolveType.AgentDefaultName:
case ResolveType.AgentDisplayName:
case ResolveType.AgentUsername:
return GetAgentName(id, type);
case ResolveType.Group:
return GetGroupName(id);
case ResolveType.Parcel:
return GetParcelName(id);
default:
return id.ToString();
}
}
#region Link name resolution
private string GetLinkNameRegionUri(Match match)
{
string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
string coordinateString = "";
if (match.Groups["local_x"].Success)
{
coordinateString += " (" + match.Groups["local_x"].Value;
}
if (match.Groups["local_y"].Success)
{
coordinateString += "," + match.Groups["local_y"].Value;
}
if (match.Groups["local_z"].Success)
{
coordinateString += "," + match.Groups["local_z"].Value;
}
if (coordinateString != "")
{
coordinateString += ")";
}
return string.Format("{0}{1}", name, coordinateString);
}
private string GetLinkNameAgent(Match match)
{
UUID agentID = new UUID(match.Groups["agent_id"].Value);
string action = match.Groups["action"].Value;
switch (action)
{
case "about":
case "inspect":
case "completename":
return Resolve(agentID, ResolveType.AgentDefaultName);
case "displayname":
return Resolve(agentID, ResolveType.AgentDisplayName);
case "username":
return Resolve(agentID, ResolveType.AgentUsername);
case "im":
return "IM " + Resolve(agentID, ResolveType.AgentDefaultName);
case "offerteleport":
return "Offer Teleport to " + Resolve(agentID, ResolveType.AgentDefaultName);
case "pay":
return "Pay " + Resolve(agentID, ResolveType.AgentDefaultName);
case "requestfriend":
return "Friend Request " + Resolve(agentID, ResolveType.AgentDefaultName);
case "mute":
return "Mute " + Resolve(agentID, ResolveType.AgentDefaultName);
case "unmute":
return "Unmute " + Resolve(agentID, ResolveType.AgentDefaultName);
default:
return match.ToString();
}
}
private string GetLinkNameChat(Match match)
{
//string channel = match.Groups["channel"].Value;
//string text = System.Web.HttpUtility.UrlDecode(match.Groups["text"].Value);
return match.ToString();
}
private string GetLinkNameClassified(Match match)
{
//UUID classifiedID = new UUID(match.Groups["classified_id"].Value);
return match.ToString();
}
private string GetLinkNameEvent(Match match)
{
//UUID eventID = new UUID(match.Groups["event_id"].Value);
return match.ToString();
}
private string GetLinkNameGroup(Match match)
{
string action = match.Groups["action"].Value;
switch (action)
{
case "about":
case "inspect":
{
UUID groupID = new UUID(match.Groups["group_id"].Value);
return Resolve(groupID, ResolveType.Group);
}
case "create":
case "list/show":
return match.ToString();
}
return match.ToString();
}
private string GetLinkNameHelp(Match match)
{
//string helpQuery = HttpUtility.UrlDecode(match.Groups["help_query"].Value);
return match.ToString();
}
private string GetLinkNameInventory(Match match)
{
//UUID inventoryID = new UUID(match.Groups["agent_id"].Value);
string action = match.Groups["action"].Value;
if (action == "select" && match.Groups["name"].Success)
{
return HttpUtility.UrlDecode(match.Groups["name"].Value);
}
return match.ToString();
}
private string GetLinkNameTrackAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["friend_id"].Value);
return match.ToString();
}
private string GetLinkNameObjectIm(Match match)
{
//UUID objectID = new UUID(match.Groups["object_id"].Value);
string name = HttpUtility.UrlDecode(match.Groups["name"].Value);
//UUID ownerID = new UUID(match.Groups["owner"].Value);
//string groupowned = match.Groups["groupowned"].Value;
//string slurl = match.Groups["slurl"].Value;
if (name != string.Empty)
{
return name;
}
return match.ToString();
}
private string GetLinkNameParcel(Match match)
{
UUID parcelID = new UUID(match.Groups["parcel_id"].Value);
return Resolve(parcelID, ResolveType.Parcel);
}
private string GetLinkNameSearch(Match match)
{
//string category = match.Groups["category"].Value;
//string searchTerm = HttpUtility.UrlDecode(match.Groups["search_term"].Value);
return match.ToString();
}
private string GetLinkNameShareWithAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["agent_id"].Value);
return match.ToString();
}
private string GetLinkNameTeleport(Match match)
{
string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
string coordinateString = "";
if (match.Groups["local_x"].Success)
{
coordinateString += " (" + match.Groups["local_x"].Value;
}
if (match.Groups["local_y"].Success)
{
coordinateString += "," + match.Groups["local_y"].Value;
}
if (match.Groups["local_z"].Success)
{
coordinateString += "," + match.Groups["local_z"].Value;
}
if (coordinateString != "")
{
coordinateString += ")";
}
return string.Format("Teleport to {0}{1}", name, coordinateString);
}
private string GetLinkNameVoiceCallAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["agent_id"].Value);
return match.ToString();
}
private string GetLinkNameWearFolder(Match match)
{
//UUID folderID = new UUID(match.Groups["inventory_folder_uuid"].Value);
return match.ToString();
}
private string GetLinkNameWorldMap(Match match)
{
string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
string x = match.Groups["local_x"].Success ? match.Groups["local_x"].Value : "128";
string y = match.Groups["local_y"].Success ? match.Groups["local_y"].Value : "128";
string z = match.Groups["local_z"].Success ? match.Groups["local_z"].Value : "0";
return string.Format("Show Map for {0} ({1},{2},{3})", name, x, y, z);
}
#endregion
#region Link Execution
private void ExecuteLinkRegionUri(Match match)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
int x = match.Groups["local_x"].Success ? int.Parse(match.Groups["local_x"].Value) : 128;
int y = match.Groups["local_y"].Success ? int.Parse(match.Groups["local_y"].Value) : 128;
int z = match.Groups["local_z"].Success ? int.Parse(match.Groups["local_z"].Value) : 0;
//todo: push map to user view and show the place!
//instance.MainCanvas.cm.pushCanvas();
//instance.MainCanvas.WorldMap.DisplayLocation(name, x, y, z);
}
private void ExecuteLinkAgent(Match match)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
UUID agentID = new UUID(match.Groups["agent_id"].Value);
//string action = match.Groups["action"].Value;
//todo: open the avatar screen and show the user the avatar in question!
//RaindropInstance.GlobalInstance.MainForm.ShowAgentProfile(instance.Names.Get(agentID), agentID);
}
private void ExecuteLinkShowApperance()
{
}
private void ExecuteLinkShowBalance()
{
}
private void ExecuteLinkChat(Match match)
{
//string channel = match.Groups["channel"].Value;
//string text = System.Web.HttpUtility.UrlDecode(match.Groups["text"].Value);
}
private void ExecuteLinkClassified(Match match)
{
//UUID classifiedID = new UUID(match.Groups["classified_id"].Value);
}
private void ExecuteLinkEvent(Match match)
{
//UUID eventID = new UUID(match.Groups["event_id"].Value);
}
private void ExecuteLinkGroup(Match match)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string action = match.Groups["action"].Value;
switch (action)
{
case "about":
case "inspect":
{
UUID groupID = new UUID(match.Groups["group_id"].Value);
//todo: push group interface to user view and show!
//instance.MainForm.ShowGroupProfile(groupID);
return;
}
case "create":
return;
case "list/show":
return;
}
}
private void ExecuteLinkHelp(Match match)
{
//string helpQuery = HttpUtility.UrlDecode(match.Groups["help_query"].Value);
}
private void ExecuteLinkInventory(Match match)
{
//UUID inventoryID = new UUID(match.Groups["agent_id"].Value);
//string action = match.Groups["action"].Value;
}
private void ExecuteLinkTrackAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["friend_id"].Value);
}
private void ExecuteLinkObjectIm(Match match)
{
//UUID objectID = new UUID(match.Groups["object_id"].Value);
//string name = HttpUtility.UrlDecode(match.Groups["name"].Value);
//UUID ownerID = new UUID(match.Groups["owner"].Value);
//string groupowned = match.Groups["groupowned"].Value;
//string slurl = match.Groups["slurl"].Value;
}
private void ExecuteLinkParcel(Match match)
{
//UUID parcelID = new UUID(match.Groups["parcel_id"].Value);
}
private void ExecuteLinkSearch(Match match)
{
//string category = match.Groups["category"].Value;
//string searchTerm = HttpUtility.UrlDecode(match.Groups["search_term"].Value);
}
private void ExecuteLinkShareWithAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["agent_id"].Value);
}
private void ExecuteLinkTeleport(Match match)
{
//string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
//string x = match.Groups["local_x"].Success ? match.Groups["local_x"].Value : "128";
//string y = match.Groups["local_y"].Success ? match.Groups["local_y"].Value : "128";
//string z = match.Groups["local_z"].Success ? match.Groups["local_z"].Value : "0";
}
private void ExecuteLinkVoiceCallAvatar(Match match)
{
//UUID agentID = new UUID(match.Groups["agent_id"].Value);
}
private void ExecuteLinkWearFolder(Match match)
{
//UUID folderID = new UUID(match.Groups["inventory_folder_uuid"].Value);
}
private void ExecuteLinkWorldMap(Match match)
{
RaindropInstance instance = RaindropInstance.GlobalInstance;
string name = HttpUtility.UrlDecode(match.Groups["region_name"].Value);
int x = match.Groups["local_x"].Success ? int.Parse(match.Groups["local_x"].Value) : 128;
int y = match.Groups["local_y"].Success ? int.Parse(match.Groups["local_y"].Value) : 128;
int z = match.Groups["local_z"].Success ? int.Parse(match.Groups["local_z"].Value) : 0;
//todo: push map to user view and show the place!
//instance.MainForm.MapTab.Select();
//instance.MainForm.WorldMap.DisplayLocation(name, x, y, z);
}
#endregion
}
}