} // end ParseArguments /// <summary> /// Register all RestPlugins to the RestBot static plugin dictionary /// </summary> /// <param name="assembly">Given assembly to search for</param> static void RegisterAllCommands(Assembly assembly) { foreach (Type t in assembly.GetTypes()) { try { if (t.IsSubclassOf(typeof(RestPlugin))) { ConstructorInfo?info = t.GetConstructor(Type.EmptyTypes); // ask for parameter-less constructor for this class, if it exists (gwyneth 20220425) if (info == null) { // Not a serious warning, some plugins might be incorrectly configured but still work well DebugUtilities.WriteWarning($"Couldn't get constructor without parameters for plugin {t.GetType().Name}!"); } else { RestPlugin plugin = (RestPlugin)info.Invoke(new object[0]); RestBot.AddPlugin(plugin); } } } catch (Exception e) { DebugUtilities.WriteError(e.Message); } } }
static void Reaper_Elapsed(object?sender, System.Timers.ElapsedEventArgs e) { if (Program.Sessions == null) { // No sessions to worry about, we can return safely. return; } // If the configuration says we shouldn't run the Reaper, return. (gwyneth 20220412) if (Program.config != null && Program.config.plugin.reaper != true) { return; } List <UUID> kc = new List <UUID>(); foreach (UUID key in Program.Sessions.Keys) //why not just lock() ? :P { kc.Add(key); } //Do reaping foreach (UUID key in kc) { if (Program.Sessions.ContainsKey(key)) { DateTime t = DateTime.Now; TimeSpan ts = t.Subtract(Program.Sessions[key].LastAccessed); if (ts > SESSION_TIMEOUT) { DebugUtilities.WriteWarning("Session expiring, " + key.ToString() + ", timespan=" + ts.ToString()); Program.DisposeSession(key); } } } }
} // end Avatars_OnDirPeopleReply() /// <summary> /// name2key (avatar name to UUID) /// </summary> /// <param name="b">RESTbot object</param> /// <param name="name">Name of avatar to check</param> /// <returns>UUID of corresponding avatar, if it exists</returns> public static UUID getKey(RestBot b, String name) { DebugUtilities.WriteInfo("getKey(): Looking up key for " + name); b.Client.Directory.DirPeopleReply += Avatars_OnDirPeopleReply; name = name.ToLower(); DebugUtilities.WriteDebug("getKey(): Looking up: " + name); DebugUtilities .WriteDebug("getKey(): Key not in cache, requesting directory lookup"); // how do you know? (gwyneth 20220128) lock (KeyLookupEvents) { KeyLookupEvents.Add(name, new AutoResetEvent(false)); } DebugUtilities .WriteDebug("getKey(): Lookup Event added, KeyLookupEvents now has a total of " + KeyLookupEvents.Count.ToString() + " entries"); DirFindQueryPacket find = new DirFindQueryPacket(); find.AgentData.AgentID = b.Client.Self.AgentID; // was Network and not Self find.AgentData.SessionID = b.Client.Self.SessionID; find.QueryData.QueryFlags = 1; //find.QueryData.QueryText = Helpers.StringToField(name); find.QueryData.QueryText = Utils.StringToBytes(name); find.QueryData.QueryID = new UUID("00000000000000000000000000000001"); find.QueryData.QueryStart = 0; b.Client.Network.SendPacket((Packet)find); DebugUtilities .WriteDebug("getKey(): Packet sent - KLE has " + KeyLookupEvents.Count.ToString() + " entries.. now waiting"); if (!KeyLookupEvents[name].WaitOne(15000, true)) { DebugUtilities .WriteWarning("getKey(): timed out on avatar name lookup for " + name); } DebugUtilities.WriteDebug("getKey(): Waiting done!"); lock (KeyLookupEvents) { KeyLookupEvents.Remove(name); } DebugUtilities .WriteDebug($"getKey(): Done with KLE, now has {KeyLookupEvents.Count.ToString()} entries"); UUID response = new UUID(); // hopefully this sets the response to UUID.Zero first... (gwyneth 20220128) if (avatarKeys.ContainsKey(name)) { response = avatarKeys[name]; lock (avatarKeys) { avatarKeys.Remove(name); } } b.Client.Directory.DirPeopleReply -= Avatars_OnDirPeopleReply; return(response); } // end getKey()
/// <summary> /// Callback for when this bot gets disconnected, attempting to connect again in 5 minutes. /// </summary> /// <param name="sender">Sender object</param> /// <param name="e">Arguments for the disconnected event</param> /// <remarks>rewrote to show message</remarks> void Network_OnDisconnected(object?sender, DisconnectedEventArgs e) { if (e.Reason != NetworkManager.DisconnectType.ClientInitiated) { myStatus = Status.Reconnecting; DebugUtilities.WriteWarning($"{sessionid.ToString()} was disconnected ({e.Message.ToString()}), but I'm logging back in again in 5 minutes."); ReloginTimer.Stop(); ReloginTimer.Interval = 5 * 60 * 1000; ReloginTimer.Start(); } }
} // end getName() /// <summary> /// Loop through all (pending) replies for UUID/Avatar names /// and process them if they contain any key we're looking for. /// </summary> /// <param name="sender">parameter ignored</param> /// <param name="e">List of UUID/Avatar names</param> /// <returns>void</returns> /// <remarks>using new Directory functionality</remarks> public static void Avatars_OnDirPeopleReply( object?sender, DirPeopleReplyEventArgs e ) { if (e.MatchedPeople.Count < 1) { DebugUtilities .WriteWarning("Avatars_OnDirPeopleReply() - Error: empty people directory reply"); } else { int replyCount = e.MatchedPeople.Count; DebugUtilities .WriteInfo("Avatars_OnDirPeopleReply() - Processing " + replyCount.ToString() + " DirPeople replies"); for (int i = 0; i < replyCount; i++) { string avatarName = e.MatchedPeople[i].FirstName + " " + e.MatchedPeople[i].LastName; UUID avatarKey = e.MatchedPeople[i].AgentID; DebugUtilities .WriteDebug("Avatars_OnDirPeopleReply() - Reply " + (i + 1).ToString() + " of " + replyCount.ToString() + " Key : " + avatarKey.ToString() + " Name : " + avatarName); if (!avatarKeys.ContainsKey(avatarName)) { /* || avatarKeys[avatarName] == null ) // apparently dictionary entries cannot be null */ lock (avatarKeys) { avatarKeys[avatarName.ToLower()] = avatarKey; } } lock (KeyLookupEvents) { if (KeyLookupEvents.ContainsKey(avatarName.ToLower())) { KeyLookupEvents[avatarName.ToLower()].Set(); DebugUtilities.WriteDebug(avatarName.ToLower() + " KLE set!"); } } } } } // end Avatars_OnDirPeopleReply()
/* * // obsoleted packet call * public void Avatars_OnDirPeopleReply(Packet packet, Simulator simulator) * { * DirPeopleReplyPacket reply = (DirPeopleReplyPacket)packet; * DebugUtilities.WriteDebug("Got DirPeopleReply!"); * if (reply.QueryReplies.Length < 1) { * DebugUtilities.WriteWarning(session + " {MethodName + " Error - empty people directory reply"); * } else { * int replyCount = reply.QueryReplies.Length; * DebugUtilities.WriteInfo(session + " {MethodName + " Proccesing {replyCount.ToString() + " DirPeople replies"); * for ( int i = 0 ; i < replyCount ; i++ ) { * string avatarName = Utils.BytesToString(reply.QueryReplies[i].FirstName) + " {Utils.BytesToString(reply.QueryReplies[i].LastName); * UUID avatarKey = reply.QueryReplies[i].AgentID; * DebugUtilities.WriteDebug(session + " {MethodName + " Reply {(i + 1).ToString() + " of {replyCount.ToString() + " Key : {avatarKey.ToString() + " Name : {avatarName); * * if ( !avatarKeys.ContainsKey(avatarName) ) // || avatarKeys[avatarName] == null ) { // apparently dictionary entries cannot be null * lock ( avatarKeys ) { * avatarKeys[avatarName.ToLower()] = avatarKey; * } * } * * lock(KeyLookupEvents) * { * if ( KeyLookupEvents.ContainsKey(avatarName.ToLower())) { * KeyLookupEvents[avatarName.ToLower()].Set(); * DebugUtilities.WriteDebug(avatarName.ToLower() + " KLE set!"); * } * } * } * } * } */ /// <summary> /// Loop through all (pending) replies for UUID/Avatar names /// and process them if they contain any key we're looking for. /// </summary> /// <param name="sender">parameter ignored</param> /// <param name="e">List of UUID/Avatar names</param> /// <returns>void</returns> /// <remarks>using new Directory functionality</remarks> public void Avatars_OnDirPeopleReply( object?sender, DirPeopleReplyEventArgs e ) { if (e.MatchedPeople.Count < 1) { DebugUtilities .WriteWarning($"{session} {MethodName} Error - empty people directory reply"); } else { int replyCount = e.MatchedPeople.Count; DebugUtilities .WriteInfo($"{session} {MethodName} Processing {replyCount.ToString()} DirPeople replies"); for (int i = 0; i < replyCount; i++) { string avatarName = e.MatchedPeople[i].FirstName + e.MatchedPeople[i].LastName; UUID avatarKey = e.MatchedPeople[i].AgentID; DebugUtilities .WriteDebug($"{session} {MethodName} Reply {(i + 1).ToString()} of {replyCount.ToString()} Key: {avatarKey.ToString()} Name: {avatarName}"); if (!avatarKeys.ContainsKey(avatarName)) { /* || avatarKeys[avatarName] == null ) // apparently dictionary entries cannot be null */ lock (avatarKeys) { avatarKeys[avatarName.ToLower()] = avatarKey; } } lock (KeyLookupEvents) { if (KeyLookupEvents.ContainsKey(avatarName.ToLower())) { KeyLookupEvents[avatarName.ToLower()].Set(); DebugUtilities.WriteDebug($"{avatarName.ToLower()} KLE set!"); } } } } }
} // end Avatars_OnAvatarNames() /// <summary> /// key2name (given an avatar UUID, returns the avatar name, if it exists) /// </summary> /// <param name="b">RESTbot object</param> /// <param name="key">UUID of avatar to check</param> /// <returns>Name of the avatar if it exists; String.Empty if not</returns> public static string getName(RestBot b, UUID id) { DebugUtilities .WriteInfo("getName(): Looking up name for " + id.ToString()); b.Client.Avatars.UUIDNameReply += Avatars_OnAvatarNames; lock (NameLookupEvents) { NameLookupEvents.Add(id, new AutoResetEvent(false)); } b.Client.Avatars.RequestAvatarName(id); if (!NameLookupEvents[id].WaitOne(15000, true)) { DebugUtilities .WriteWarning("getName(): timed out on avatar name lookup"); } lock (NameLookupEvents) { NameLookupEvents.Remove(id); } // C# 8+ is stricter with null assignments. // string? response = null; // technically this cannot ever be null, so it doesn't make sense... string response = String.Empty; if (avatarNames.ContainsKey(id)) { response = avatarNames[id]; // .Name removed lock (avatarNames) { avatarNames.Remove(id); } } /* else * { * response = String.Empty; * } */ b.Client.Avatars.UUIDNameReply -= Avatars_OnAvatarNames; return(response); } // end getName()
/// <summary> /// key2name (given an avatar UUID, returns the avatar name, if it exists) /// </summary> /// <param name="b">RESTbot object</param> /// <param name="key">UUID of avatar to check</param> /// <returns>Name of the avatar if it exists; String.Empty if not</returns> private string getName(RestBot b, UUID id) { DebugUtilities .WriteInfo($"{session.ToString()} {MethodName} Looking up name for {id.ToString()}"); lock (NameLookupEvents) { NameLookupEvents.Add(id, new AutoResetEvent(false)); } b.Client.Avatars.RequestAvatarName(id); if (!NameLookupEvents[id].WaitOne(15000, true)) { DebugUtilities .WriteWarning($"{session.ToString()} {MethodName} timed out on avatar name lookup"); } lock (NameLookupEvents) { NameLookupEvents.Remove(id); } // C# 8+ is stricter with null assignments. // string? response = null; // technically this cannot ever be null, so it doesn't make sense... string response = String.Empty; if (avatarNames.ContainsKey(id)) { response = avatarNames[id]; // .Name removed lock (avatarNames) { avatarNames.Remove(id); } } /* else * { * response = String.Empty; * } */ return(response); }
/// <summary> /// Activates a group for a 'bot, given the group key. /// </summary> /// <param name="b">A currently active RestBot</param> /// <param name="groupUUID">UUID of the group to activate</param> /// <returns>Activated group name, or empty if activation failed</returns> private string activateGroup(RestBot b, UUID groupUUID) { DebugUtilities.WriteInfo(session.ToString() + " " + MethodName + " Activating group " + groupUUID.ToString()); EventHandler <PacketReceivedEventArgs> pcallback = AgentDataUpdateHandler; b.Client.Network.RegisterCallback(PacketType.AgentDataUpdate, pcallback); b.Client.Groups.ActivateGroup(groupUUID); if (!GroupsEvent.WaitOne(15000, true)) { DebugUtilities.WriteWarning(session + " " + MethodName + " timed out on setting active group"); } // ignore everything and just reset the event b.Client.Network.UnregisterCallback(PacketType.AgentDataUpdate, pcallback); GroupsEvent.Reset(); if (String.IsNullOrEmpty(activeGroup)) { DebugUtilities.WriteWarning(session + " " + MethodName + " Failed to activate the group " + groupUUID); return(""); // maybe we ought to return something else? (gwyneth 20220127) } return(activeGroup); // guaranteed *not* to be null, nor empty! (gwyneth 20220127) }
/// <summary> /// Internal function that wil set the bot's group to a certain group UUID. /// </summary> /// <param name="b">A currently active RestBot</param> /// <param name="Parameters">A dictionary containing the UUID for a group.</param> /// <returns>String with the group name, or null if group not found</returns> private string?activateGroup(RestBot b, UUID groupUUID) { DebugUtilities.WriteInfo($"{session.ToString()} {MethodName} Activating group {groupUUID.ToString()}"); EventHandler <PacketReceivedEventArgs> pcallback = AgentDataUpdateHandler; b.Client.Network.RegisterCallback(PacketType.AgentDataUpdate, pcallback); b.Client.Groups.ActivateGroup(groupUUID); if (!GroupsEvent.WaitOne(15000, true)) { DebugUtilities.WriteWarning($"{session.ToString()} {MethodName} timed out on setting active group"); } // ignore everything and just reset the event b.Client.Network.UnregisterCallback(PacketType.AgentDataUpdate, pcallback); GroupsEvent.Reset(); if (String.IsNullOrEmpty(activeGroup)) { DebugUtilities.WriteWarning($"{session.ToString()} {MethodName} Failed to activate the group {groupUUID.ToString()}"); } return(activeGroup); }
/// <summary> /// Process a request (assuming it exists) /// </summary> /// <param name="headers">Request headers (including path, etc.)</param> /// <param name="body">Request body (will usually have all parameters from POST)</param> public static string DoProcessing(RequestHeaders headers, string body) { // Abort if we don't even have a valid configuration; too many things depend on it... (gwyneth 20220213) if (Program.config == null) { return("<error>No valid configuration loaded, aborting</error>"); } //Setup variables DebugUtilities.WriteInfo($"New request - {headers.RequestLine.Path}"); //Split the URL string[] parts = headers.RequestLine.Path.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 1) { return("<error>invalidmethod</error>"); } string Method = parts[0]; /// <summary>Process the request params from POST, URL</summary> Dictionary <string, string> Parameters = RestBot.HandleDataFromRequest(headers, body); string debugparams = String.Empty; string debugparts = String.Empty; foreach (KeyValuePair <string, string> kvp in Parameters) { debugparams = debugparams + "[" + kvp.Key + "=" + kvp.Value + "] "; } DebugUtilities.WriteDebug($"Parameters (total: {Parameters.Count()}) - '{debugparams}'"); foreach (string s in parts) { debugparts = debugparts + "[ " + s + " ]"; } DebugUtilities.WriteDebug($"Parts (total: {parts.Count()}) - '{debugparts}'"); if (Method == "establish_session") { DebugUtilities.WriteDebug("We have an `establish_session` method."); // Alright, we're going to try to establish a session // Start location is optional (gwyneth 20220421) if (parts.Length >= 2 && parts[1] == Program.config.security.serverPass && Parameters.ContainsKey("first") && Parameters.ContainsKey("last") && Parameters.ContainsKey("pass")) { DebugUtilities.WriteDebug("Found required parameters for establish_session"); if (Sessions != null) { foreach (KeyValuePair <UUID, Session> ss in Sessions) { if (ss.Value != null && ss.Value.Bot != null) { DebugUtilities.WriteSpecial($"Avatar check: [{ss.Value.Bot.First.ToLower()}/{ss.Value.Bot.Last.ToLower()}] = [{Parameters["first"].ToLower()}/{Parameters["last"].ToLower()}]"); if (Parameters["first"].ToLower() == ss.Value.Bot.First.ToLower() && Parameters["last"].ToLower() == ss.Value.Bot.Last.ToLower() ) { DebugUtilities.WriteWarning($"Already running avatar {Parameters["first"]} {Parameters["last"]}"); /// <value>Temporary string to construct a full response, if possible; if not, we catch the /// exception and return a much shorter version</value> /// <remarks>This is a hack. The issue is that we're probably acessing nullable /// elements without checking. (gwyneth 20220428)</remarks> string returnString = ""; try { // Attempt to get a returnString = $@"<existing_session>true</existing_session> <session_id>{ss.Key.ToString()}</session_id> <key>{ss.Value.Bot.Client.Self.AgentID.ToString()}</key> <name>{ss.Value.Bot.Client.Self.FirstName} {ss.Value.Bot.Client.Self.LastName}</name> <FirstName>{ss.Value.Bot.Client.Self.FirstName}</FirstName> <LastName>{ss.Value.Bot.Client.Self.LastName}</LastName> <status>{ss.Value.Bot.myStatus.ToString()}</status> <uptime>{ss.Value.Bot.getUptimeISO8601()}</uptime> <start>{ss.Value.Bot.Start}</start> <CurrentSim>{ss.Value.Bot.Client.Network.CurrentSim.ToString()}</CurrentSim> <Position>{ss.Value.Bot.Client.Self.SimPosition.X},{ss.Value.Bot.Client.Self.SimPosition.Y},{ss.Value.Bot.Client.Self.SimPosition.Z}</Position> <Rotation>{ss.Value.Bot.Client.Self.SimRotation.X},{ss.Value.Bot.Client.Self.SimRotation.Y},{ss.Value.Bot.Client.Self.SimRotation.Z},{ss.Value.Bot.Client.Self.SimRotation.W}</Rotation> "; } catch (Exception e) { DebugUtilities.WriteError($"Could not generate full response, error was: '{e.Message}'; falling back to the simple, minimalistic answer"); returnString = $"<existing_session>true</existing_session><session_id>{ss.Key.ToString()}</session_id>"; } return(returnString); } } } } else { DebugUtilities.WriteDebug("No available sessions..."); } UUID id = UUID.Random(); Session s = new Session(); s.ID = id; s.Hostname = headers.Hostname; s.LastAccessed = DateTime.Now; // Needs the $1$ for the md5 on the login for LibreMetaverse if (!Parameters["pass"].StartsWith("$1$")) { Parameters["pass"] = "******" + Parameters["pass"]; } // check if user has provided us with a starting location (default is to use the last location) // (gwyneth 20220420) string gridLocation = Parameters.ContainsKey("start") ? Parameters["start"] : "last"; s.Bot = new RestBot(s.ID, Parameters["first"], Parameters["last"], Parameters["pass"], gridLocation); if (Sessions != null) { lock (Sessions) { Sessions.Add(id, s); } } else { // no "else", we have no dictionary DebugUtilities.WriteWarning("Possible issue: we have null Sessions when adding, which shouldn't happen"); } RestBot.LoginReply reply = s.Bot.Login(); if (reply.wasFatal) { if (Sessions != null) { lock (Sessions) { if (Sessions.ContainsKey(id)) { Sessions.Remove(id); } } } else { // no "else", we have no dictionary DebugUtilities.WriteWarning("Possible issue: we have null Sessions when removing, which shouldn't happen"); } } return(reply.xmlReply); } else { String result = String.Empty; if (parts.Length < 2) { result = "Missing a part."; } if (!Parameters.ContainsKey("first")) { result = result + " Missing 'first' arg."; } if (!Parameters.ContainsKey("last")) { result = result + " Missing 'last' arg."; } if (!Parameters.ContainsKey("pass")) { result = result + " Missing 'pass' arg."; } return($"<error>arguments: {result}</error>"); } } // Note: formerly undocumented functionality!! (gwyneth 20220414) else if (Method == "server_quit") { if (parts.Length < 2) { return($"<error>{Method}: missing 'pass' arg.</error>"); } if (parts[1] == Program.config.security.serverPass) { if (Sessions != null) { foreach (KeyValuePair <UUID, Session> s in Sessions) { lock (Sessions) DisposeSession(s.Key); } StillRunning = false; // note: a caveat of this undocumented method is that it requires a _new_ // incoming request to actually kill the server... could be a ping, though. (gwyneth 20220414) return("<status>success - all bot sessions were logged out and a request was made for queued shutdown</status>"); } else { // it's fine if there are no sessions (gwyneth 20220414) return("<status>success - no sessions were active</status>"); } } else { // wrong password sent! (gwyneth 20220414) return($"<error>{Method}: server authentication failure</error>"); } } else if (Method == "ping") { if (parts.Length < 2) { return($"<error>{Method}: missing 'pass' arg.</error>"); } if (parts[1] == Program.config.security.serverPass) { return($"<{Method}>I'm alive!</{Method}>"); } else { // wrong password sent! (gwyneth 20220414) return($"<error>{Method}: server authentication failure</error>"); } } else if (Method == "session_list") { if (parts.Length < 2) { return("<error>missing 'pass' arg.</error>"); } if (parts[1] == Program.config.security.serverPass) { bool check = false; if (Program.Sessions.Count != 0) // no sessions? that's fine, no need to abort { check = true; } string response = $"<{Method}>"; if (check) // optimisation: if empty, no need to run the foreach (gwyneth 20220424) { foreach (KeyValuePair <OpenMetaverse.UUID, RESTBot.Session> kvp in Program.Sessions) { if (kvp.Value.Bot != null && kvp.Value.Bot.Client != null && kvp.Value.Bot.Client.Self != null && kvp.Value.Bot.Client.Network != null) { response += $@" <session> <session_id>{kvp.Key.ToString()}</session_id> <key>{kvp.Value.Bot.Client.Self.AgentID.ToString()}</key> <name>{kvp.Value.Bot.Client.Self.FirstName} {kvp.Value.Bot.Client.Self.LastName}</name> <FirstName>{kvp.Value.Bot.Client.Self.FirstName}</FirstName> <LastName>{kvp.Value.Bot.Client.Self.LastName}</LastName> <status>{kvp.Value.Bot.myStatus.ToString()}</status> <uptime>{kvp.Value.Bot.getUptimeISO8601()}</uptime> <start>{kvp.Value.Bot.Start}</start> <CurrentSim>{kvp.Value.Bot.Client.Network.CurrentSim.ToString()}</CurrentSim> <Position>{kvp.Value.Bot.Client.Self.SimPosition.X},{kvp.Value.Bot.Client.Self.SimPosition.Y},{kvp.Value.Bot.Client.Self.SimPosition.Z}</Position> <Rotation>{kvp.Value.Bot.Client.Self.SimRotation.X},{kvp.Value.Bot.Client.Self.SimRotation.Y},{kvp.Value.Bot.Client.Self.SimRotation.Z},{kvp.Value.Bot.Client.Self.SimRotation.W}</Rotation> </session>" ; } else { // Somehow, we have a session ID that has no bot assigned; // this should never be the case, but... (gwyneth 20220426) response += $"<session><session_id>{kvp.Key.ToString()}</session_id><key>{UUID.Zero.ToString()}</key></session>"; } } } else { response += "no sessions"; } response += $"</{Method}>"; return(response); } else { // wrong password sent! (gwyneth 20220414) return($"<error>{Method}: server authentication failure</error>"); } } else if (Method == "stats") { if (parts.Length < 2) { return($"<error>{Method}: missing 'pass' arg.</error>"); } if (parts[1] == Program.config.security.serverPass) { string response = "<stats><bots>" + ((Sessions != null) ? Sessions.Count.ToString() : "0") + "</bots>" + "<uptime>" + (DateTime.Now - uptime) + "</uptime></stats>"; return(response); } else { return($"<error>{Method}: server authentication failure</error>"); } } //Only a method? pssh. if (parts.Length == 1) { return("<error>no session key found</error>"); } UUID sess = new UUID(); try { sess = new UUID(parts[1]); } catch (FormatException) { return("<error>cannot parse the session key</error>"); } catch (Exception e) { DebugUtilities.WriteError(e.Message); } //Session checking if (!ValidSession(sess, headers.Hostname)) { return("<error>invalidsession</error>"); } //YEY PROCESSING RestBot?r = null; if (Sessions != null) { r = Sessions[sess].Bot; } if (r == null) { return($"<error>no RestBot found for session {sess.ToString()}</error>"); } //Last accessed for plugins if (Sessions != null) { Sessions[sess].LastAccessed = DateTime.Now; } //Pre-error checking if (r.myStatus != RestBot.Status.Connected) //Still logging in? { return($"<error>{r.myStatus.ToString()}</error>"); } else if (!r.Client.Network.Connected) //Disconnected? { return("<error>clientdisconnected</error>"); } else if (Method == "exit") { DisposeSession(sess); return("<disposed>true</disposed>"); } else if (Method == "stats") { string response = "<bots>" + ((Sessions != null) ? Sessions.Count.ToString() : "NaN") + "</bots>"; response += "<uptime>" + (DateTime.Now - uptime) + "</uptime>"; return(response); } return(r.DoProcessing(Parameters, parts)); } // end DoProcessing
/// <summary> /// Login block /// </summary> public LoginReply Login() { LoginReply response = new LoginReply(); DebugUtilities.WriteSpecial("Login block was called in Login()"); if (Client.Network.Connected) { DebugUtilities.WriteError("Uhm, Login() was called when we where already connected. Hurr"); return(new LoginReply()); } //Client.Network.LoginProgress += // delegate(object? sender, LoginProgressEventArgs e) // { // DebugUtilities.WriteDebug($"Login {e.Status}: {e.Message}"); // if (e.Status == LoginStatus.Success) // { // DebugUtilities.WriteSpecial("Logged in successfully"); // myStatus = Status.Connected; // response.wasFatal = false; // response.xmlReply = "<success><session_id>" + sessionid.ToString() + "</session_id></success>"; // } // else if (e.Status == LoginStatus.Failed) // { // DebugUtilities.WriteError("$There was an error while connecting: {Client.Network.LoginErrorKey}"); // response.wasFatal = true; // response.xmlReply = "<error></error>"; // } // }; // Optimize the throttle Client.Throttle.Wind = 0; Client.Throttle.Cloud = 0; Client.Throttle.Land = 1000000; Client.Throttle.Task = 1000000; // we add this check here because LOGIN_SERVER should never be assigned null (gwyneth 20220213) if (Program.config != null && Program.config.networking.loginuri != null) { Client.Settings.LOGIN_SERVER = Program.config.networking.loginuri; // could be String.Empty, so we check below... } else if (RESTBot.XMLConfig.Configuration.defaultLoginURI != null) { Client.Settings.LOGIN_SERVER = RESTBot.XMLConfig.Configuration.defaultLoginURI; // could ALSO be String.Empty, so we check below... } else { Client.Settings.LOGIN_SERVER = String.Empty; } // Any of the above _might_ have set LOGIN_SERVER to an empty string, so we check first if we have // something inside the string. (gwyneth 20220213) // To-do: validate the URL first? It's not clear if .NET 6 already does that at some point... if (Client.Settings.LOGIN_SERVER == String.Empty) { // we don't know where to login to! response.wasFatal = true; response.xmlReply = "<error fatal=\"true\">No login URI provided</error>"; DebugUtilities.WriteError("No login URI provided; aborting..."); return(response); } DebugUtilities.WriteDebug($"Login URI: {Client.Settings.LOGIN_SERVER}"); LoginParams loginParams = Client.Network.DefaultLoginParams(First, Last, MD5Password, "RestBot", Program.Version); loginParams.Start = Start; if (Client.Network.Login(loginParams)) { DebugUtilities.WriteSpecial($"{First} {Last} logged in successfully"); myStatus = Status.Connected; response.wasFatal = false; response.xmlReply = $@"<success> <session_id>{sessionid.ToString()}</session_id> <key>{Client.Self.AgentID.ToString()}</key> <name>{Client.Self.FirstName} {Client.Self.LastName}</name> <FirstName>{Client.Self.FirstName}</FirstName> <LastName>{Client.Self.LastName}</LastName> <CurrentSim>{Client.Network.CurrentSim.ToString()}</CurrentSim> <Position>{Client.Self.SimPosition.X},{Client.Self.SimPosition.Y},{Client.Self.SimPosition.Z}</Position> <Rotation>{Client.Self.SimRotation.X},{Client.Self.SimRotation.Y},{Client.Self.SimRotation.Z},{Client.Self.SimRotation.W}</Rotation> </success>"; } else { DebugUtilities .WriteError($"There was an error while connecting: {Client.Network.LoginErrorKey}"); switch (Client.Network.LoginErrorKey) { case "connect": case "key": case "disabled": response.wasFatal = true; response.xmlReply = $"<error fatal=\"true\">{Client.Network.LoginMessage}</error>"; break; case "presence": case "timed out": case "god": DebugUtilities .WriteWarning("Nonfatal error while logging in.. this may be normal"); response.wasFatal = false; response.xmlReply = $"<error fatal=\"false\">{Client.Network.LoginMessage}</error><retry>10</retry><session_id>{sessionid}</session_id>"; DebugUtilities .WriteSpecial("Relogin attempt will be made in 10 minutes"); ReloginTimer.Interval = 10 * 60 * 1000; //10 minutes ReloginTimer.Start(); break; default: DebugUtilities .WriteError($"{sessionid.ToString()} UNKNOWN ERROR {Client.Network.LoginErrorKey} WHILE ATTEMPTING TO LOGIN"); response.wasFatal = true; response.xmlReply = $"<error fatal=\"true\">Unknown error '{Client.Network.LoginErrorKey}' has occurred.</error>"; break; } if (response.wasFatal == false) { myStatus = Status.Reconnecting; } } //Client.Network.BeginLogin(loginParams); return(response); } // end Login()
/// <summary> /// Handler event for this plugin. /// </summary> /// <param name="b">A currently active RestBot</param> /// <param name="Parameters">A dictionary containing the message </param> /// <returns></returns> public override string Process(RestBot b, Dictionary <string, string> Parameters) { string message; string subject; UUID groupUUID = UUID.Zero; UUID attachmentUUID = UUID.Zero; GroupNotice notice; try { if (Parameters.ContainsKey("subject")) { subject = Parameters["subject"].ToString().Replace("%20", " ").Replace("+", " "); } else { return("<error>No notice subject</error>"); } if (Parameters.ContainsKey("message")) { message = Parameters["message"].ToString().Replace("%20", " ").Replace("+", " "); } else { return("<error>No notice message</error>"); } if (Parameters.ContainsKey("group")) { if (!UUID.TryParse(Parameters["group"].ToString().Replace("_", " "), out groupUUID)) { return("<error>parsekey group</error>"); } } else { return("<error>arguments: no group key</error>"); } if (Parameters.ContainsKey("attachment")) { if (!UUID.TryParse(Parameters["attachment"].ToString().Replace("_", " "), out attachmentUUID)) { return("<error>parsekey attachment</error>"); } } else { // just a warning, attachment can be empty DebugUtilities.WriteWarning(session + " " + MethodName + " Notice has no attachment (no problem)"); } DebugUtilities.WriteDebug(session + " " + MethodName + " Attempting to create a notice"); /* This doesn't work as it should! * if (!b.Client.Inventory.Store.Contains(attachmentUUID)) * { * DebugUtilities.WriteWarning(session + " " + MethodName + " Item UUID " + attachmentUUID.ToString() + " not found on inventory (are you using an Asset UUID by mistake?)"); * attachmentUUID = UUID.Zero; * } */ notice = new GroupNotice(); notice.Subject = subject; notice.Message = message; notice.AttachmentID = attachmentUUID; // this is the inventory UUID, not the asset UUID notice.OwnerID = b.Client.Self.AgentID; b.Client.Groups.SendGroupNotice(groupUUID, notice); DebugUtilities.WriteDebug($"{session} {MethodName} Sent Notice from avatar {notice.OwnerID.ToString()} to group: {groupUUID.ToString()} subject: '{notice.Subject.ToString()}' message: '{notice.Message.ToString()}' Optional attachment: {notice.AttachmentID.ToString()} Serialisation: {Utils.BytesToString(notice.SerializeAttachment())}"); return("<notice>sent</notice>"); } catch (Exception e) { DebugUtilities.WriteError(e.Message); return("<error>loads of errors</error>"); } }
Process(RestBot b, Dictionary <string, string> Parameters) { UUID avatarKey = UUID.Zero; bool check = true; string message = String.Empty; string avatarFirstName = String.Empty; string avatarLastName = String.Empty; if (Parameters.ContainsKey("key")) { check = UUID .TryParse(Parameters["key"].ToString().Replace("_", " "), out avatarKey); } else { if (Parameters.ContainsKey("last")) { avatarLastName = Parameters["last"]; } if (Parameters.ContainsKey("first")) { avatarFirstName = Parameters["first"]; check = true; } else { check = false; } // Massage the data we got; we can get away with an empty string for the last name (gwyneth 20220122). if (avatarLastName == String.Empty) { avatarLastName = "Resident"; check = true; } // InstantMessage *always* needs an avatar UUID! // We need to look it up; fortunately, there are plenty of options available to get those. String avatarFullName = avatarFirstName.ToString() + " " + avatarLastName.ToString(); avatarKey = Utilities.getKeySimple(b, avatarFullName); // handles conversion to lowercase. if (avatarKey == UUID.Zero) { DebugUtilities.WriteWarning($"Key not found for unknown avatar '{avatarFullName}'"); check = false; } else { DebugUtilities.WriteDebug($"Avatar '{avatarFullName}' has key '{avatarKey}'"); } } if (Parameters.ContainsKey("message")) { message = Parameters["message"]; if (message == String.Empty) { check = false; } } else { check = false; } if (!check) { return($"<error>{MethodName}: wrong parameters passed; IM not sent</error>"); } // make sure message is not too big (gwyneth 20220212) message = message.TrimEnd(); if (message.Length > 1023) /// <see href="https://wiki.secondlife.com/wiki/LlInstantMessage" /> { message = message.Remove(1023); } b.Client.Self.InstantMessage(avatarKey, message); return($"<{MethodName}><key>{avatarKey.ToString()}</key><message>{message}</message></{MethodName}>"); } // end Process
Process(RestBot b, Dictionary<string, string> Parameters) { bool check = false; /// <summary>temporary, so we don't need to lock the name while accessing it</summary> string tempName = String.Empty; /// <summary>temporary, so we don't need to lock the key while the UUID is being parsed</summary> UUID tempKey = UUID.Zero; // Name can be either an avatar name, or an object name, we can listen to both (gwyneth 20220213) // Note that either one is optional, but if *both* are empty/invalid, then this will listen to anything! if (Parameters.ContainsKey("name")) { tempName = Parameters["name"].ToString().Replace("+", " "); if (tempName == String.Empty) { check = false; } else { lock (listenAgentName) // make it thread-safe! { listenAgentName = tempName; } check = true; } // very likely, this will fail if the name isn't an avatar, but an object, but we'll see, // it's possible that we can still do something useful upstream (gwyneth 20220213) tempKey = Utilities.getKeySimple(b, tempName); if (tempKey == UUID.Zero) { check = false; } else { lock (listenAgentKey) { listenAgentKey = tempKey.ToString(); } } check = true; } else if (Parameters.ContainsKey("key")) { check = UUID.TryParse(Parameters["key"].ToString().Replace("_", " "), out tempKey); if (tempKey == UUID.Zero) { check = false; } else { lock (listenAgentKey) { listenAgentKey = tempKey.ToString(); } check = true; } } else check = false; // neither contains name, nor key; we listen to everything instead! if (check == false) { DebugUtilities.WriteWarning("Listen called without valid name/UUID, we're listening to anything"); } else { DebugUtilities.WriteDebug($"Listen called, after parsing we're going to check for '{tempName}' and/or '{tempKey}'"); } // add the callback, and start listening b.Client.Self.ChatFromSimulator += Self_ChatFromSimulator; // start listening! Note that this should not be blocking on a timer, but for the sake // of expediency, we're pretending that all of this is synchronised and single-threaded... (gwyneth 20220213) ListenEvent.WaitOne(30000, false); /// <summary>Eventually, someone will type something, and that means our <c>chatMetaData</c> will be non-null.</summary> /// <remarks>(gwyneth 20220214)</remarks> if (chatMetaData != null) { if (chatMetaData != (ChatEventArgs) EventArgs.Empty) { string ret = $@"<{MethodName}> <simulator>{chatMetaData.Simulator.ToString()}</simulator> <message>{chatMetaData.Message}</message> <audiblelevel>{chatMetaData.AudibleLevel.ToString()}</audiblelevel> <chattype>{chatMetaData.Type.ToString()}</chattype> <sourcetype>{chatMetaData.SourceType.ToString()}</sourcetype> <fromname>{chatMetaData.FromName}</fromname> <sourceid>{chatMetaData.SourceID.ToString()}</sourceid> <ownerid>{chatMetaData.OwnerID.ToString()}</ownerid> <position>{chatMetaData.Position.ToString()}</position> </{MethodName}>"; DebugUtilities.WriteDebug($"Captured message XML: {ret}"); return ret; } else { DebugUtilities.WriteError($"Error retrieving chatMetaData, got: '{chatMetaData.ToString()}'"); return "<error>Listen returned error or empty message</error>"; } } else { // shouldn't happen... DebugUtilities.WriteError("Error retrieving chatMetaData, was null"); return "<error>null</error>"; } }