SecondBot/SecondBotEvents/Services/CommandsService.cs
Madpeter d46f0457ef correct issue with some commands
god dam json converter has issues converting UUIDs
2025-10-15 20:17:33 +01:00

650 lines
24 KiB
C#

using Newtonsoft.Json.Linq;
using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Config;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SecondBotEvents.Services
{
public class BotCommandNotice(string command, string args, string source, bool accepted, string results)
{
public string command { get; set; } = command;
public string args { get; set; } = args;
public string source { get; set; } = source;
public bool accepted { get; set; } = accepted;
public string results { get; set; } = results;
}
public class CommandsService : BotServices
{
private EventHandler<BotCommandNotice> BotclientCommandEventNotices;
public event EventHandler<BotCommandNotice> BotclientCommandEventNotice
{
add { lock (BotCommandNoticeLockable) { BotclientCommandEventNotices += value; } }
remove { lock (BotCommandNoticeLockable) { BotclientCommandEventNotices -= value; } }
}
private readonly object BotCommandNoticeLockable = new();
public new CommandsConfig myConfig = null;
public bool acceptNewCommands = false;
public CommandsService(EventsSecondBot setMaster) : base(setMaster)
{
myConfig = new CommandsConfig(master.fromEnv, master.fromFolder);
if (myConfig.GetEnabled() == false)
{
return;
}
LoadCommands();
}
public bool GetAllowServiceCommands()
{
return myConfig.GetAllowServiceControl();
}
protected void LoadCommands()
{
Dictionary<string, Type> commandmodules = http_commands_helper.getCommandModules();
foreach (Type entry in commandmodules.Values)
{
LoadCommandEndpoint(entry);
}
}
protected virtual void LoadCommandEndpoint(Type endpointtype)
{
CommandsAPI controler = (CommandsAPI)Activator.CreateInstance(endpointtype, args: [master]);
commandEndpoints.Add(endpointtype.Name, controler);
foreach (MethodInfo M in endpointtype.GetMethods())
{
bool isCallable = false;
foreach (CustomAttributeData At in M.CustomAttributes)
{
if (At.AttributeType.Name == "About")
{
isCallable = true;
break;
}
}
if (isCallable == true)
{
if (endpointcommandmap.ContainsKey(M.Name) == true)
{
LogFormater.Warn("Namespace: " + endpointtype.Name + " / Command: " + M.Name + " already found in " + endpointcommandmap[M.Name]);
continue;
}
if (commandnameLowerToReal.ContainsKey(M.Name.ToLowerInvariant()) == true)
{
LogFormater.Warn("Namespace: " + endpointtype.Name + " / Command: " + M.Name + " already found in " + endpointcommandmap[M.Name]);
continue;
}
endpointcommandmap.Add(M.Name, endpointtype.Name);
commandnameLowerToReal.Add(M.Name.ToLowerInvariant(), M.Name);
}
}
}
public override void Start(bool updateEnabled = false, bool setEnabledTo = false)
{
running = true;
master.BotClientNoticeEvent += BotClientRestart;
}
public override void Stop()
{
running = false;
acceptNewCommands = false;
}
public override string Status()
{
if(myConfig == null)
{
return "No Config";
}
else if(myConfig.GetHideStatusOutput() == true)
{
return "hidden";
}
if (acceptNewCommands == false)
{
return "Disabled";
}
return "Enabled";
}
protected void BotImMessage(object o, InstantMessageEventArgs e)
{
if(acceptNewCommands == false)
{
return;
}
if (e.IM.FromAgentName == GetClient().Self.Name)
{
return;
}
if (myConfig.GetAllowIMcontrol() == false)
{
return;
}
bool acceptMessage = false;
bool requireSigning = true;
switch (e.IM.Dialog)
{
case InstantMessageDialog.MessageFromObject:
{
acceptMessage = true;
if(myConfig.GetObjectMasterOptout() == true)
{
string ownername = master.DataStoreService.GetAvatarName(e.IM.FromAgentID);
requireSigning = !myConfig.GetMastersCSV().Contains(ownername);
if ((requireSigning == false) && (myConfig.GetCheckDotNames() == true))
{
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
if (bits.Length == 2)
{
requireSigning = myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
}
}
}
break;
}
case InstantMessageDialog.MessageFromAgent: // shared with SessionSend
case InstantMessageDialog.SessionSend:
{
if(e.IM.GroupIM == true) // groups are unable to issue commands
{
break;
}
if (myConfig.GetEnableMasterControls() == false)
{
break;
}
master.DataStoreService.AddAvatar(e.IM.FromAgentID, e.IM.FromAgentName);
requireSigning = false;
acceptMessage = myConfig.GetMastersCSV().Contains(e.IM.FromAgentName);
if ((acceptMessage == false) && (myConfig.GetCheckDotNames() == true))
{
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
if (bits.Length == 2)
{
acceptMessage = myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper()+" " + bits[1].FirstCharToUpper());
}
}
break;
}
default:
{
break;
}
}
if(acceptMessage == false)
{
return;
}
CommandInterfaceCaller(e.IM.Message, requireSigning);
}
public bool AvatarUUIDIsMaster(UUID avatar)
{
foreach(string name in myConfig.GetMastersCSV())
{
string find = master.DataStoreService.GetAvatarUUID(name);
if (find != "lookup")
{
if(avatar.ToString() == find)
{
return true;
}
}
}
return false;
}
public KeyValuePair<bool, string> CommandInterfaceCaller(string message, bool requireSigning=false, bool viaCustomCommand=false, string forceSetSource= "command interface")
{
string source = forceSetSource;
if(viaCustomCommand == true)
{
source = "customcommand";
}
SignedCommand C = new(this,source,message,requireSigning,myConfig.GetEnforceTimeWindow(),myConfig.GetTimeWindowSecs(),myConfig.GetSharedSecret());
if (C.accepted == false)
{
return new KeyValuePair<bool, string>(false, "Not accepted via signing");
}
KeyValuePair<bool, string> reply = RunCommand(source, C, viaCustomCommand);
return reply;
}
public void SmartCommandReply(string target, string output, string command)
{
string mode = "CHAT";
UUID target_avatar = UUID.Zero;
int target_channel = 0;
if (target.StartsWith("http://"))
{
mode = "HTTP";
}
else if (UUID.TryParse(target, out target_avatar) == true)
{
mode = "IM";
}
else if (target.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length == 2)
{
string UUIDfetch = master.DataStoreService.GetAvatarUUID(target);
if (UUIDfetch == "lookup")
{
return;
}
mode = "IM";
if(UUID.TryParse(UUIDfetch, out target_avatar) == false)
{
return;
}
}
else if (new EmailAddressAttribute().IsValid(target) == true)
{
mode = "EMAIL";
}
else if (int.TryParse(target, out target_channel) == false)
{
return;
}
if (mode == "CHAT")
{
if (target_channel < 0)
{
LogFormater.Crit("[SmartReply] output Channel must be zero or higher - using 99");
target_channel = 999;
}
GetClient().Self.Chat(output, target_channel, ChatType.Normal);
}
else if (mode == "IM")
{
GetClient().Self.InstantMessage(target_avatar, output);
}
else if (mode == "HTTP")
{
Dictionary<string, string> values = new()
{
{ "reply", output },
{ "command", command },
};
var content = new FormUrlEncodedContent(values);
try
{
HTTPclient.PostAsync(target, content);
}
catch (Exception e)
{
LogFormater.Crit("[SmartReply] HTTP failed: " + e.Message + "");
}
}
else if(mode == "EMAIL")
{
KeyValuePair<bool,string> reply = master.SmtpService.sendReplyTarget(target,command,output);
if(reply.Key == false)
{
LogFormater.Crit("[SmartReply] EMAIL failed: " + reply.Value);
}
}
}
protected HttpClient HTTPclient = new();
protected KeyValuePair<bool, string> RunBaseCommand(SignedCommand C, string source)
{
try
{
CommandsAPI Endpoint = commandEndpoints[endpointcommandmap[C.command]];
MethodInfo theMethod = Endpoint.GetType().GetMethod(C.command);
if (theMethod != null)
{
List<string> argsList = [.. C.args];
string reply = "incorrect number of args expected: " + theMethod.GetParameters().Length.ToString() + " but got: " + argsList.Count.ToString();
bool status = false;
if (argsList.Count == theMethod.GetParameters().Length)
{
status = true;
try
{
if (Endpoint == null)
{
return new KeyValuePair<bool, string>(false, "Endpoint is null");
}
object[] argsWorker = [.. argsList];
object processed = theMethod.Invoke(Endpoint, argsWorker);
reply = "Error";
if (processed != null)
{
reply = JsonSerializer.Serialize(processed, JsonOptions.UnsafeRelaxed);
}
CommandNotice(C.command, source, String.Join("@@@", C.args), true, reply);
return new KeyValuePair<bool, string>(status, reply);
}
catch (Exception e)
{
CommandNotice(C.command, source+" ~~ Failure: "+e.Message, String.Join("@@@", C.args), false, e.Message);
return new KeyValuePair<bool, string>(false, e.Message);
}
}
CommandNotice(C.command, source + " ~~ Failure: incorrect number of args expected: "+ theMethod.GetParameters().Length.ToString()+" but got "+ argsList.Count.ToString()+"", String.Join("@@@", C.args), false, "no results");
return new KeyValuePair<bool, string>(status, reply);
}
return new KeyValuePair<bool, string>(false, "theMethod is null");
}
catch (Exception e)
{
return new KeyValuePair<bool, string>(false, e.Message);
}
}
public KeyValuePair<bool, string> RunCommand(string source, SignedCommand C, bool inCustomCommand=false)
{
if(acceptNewCommands == false)
{
return new KeyValuePair<bool, string>(false, "Not accepting commands");
}
try
{
string lowerName = C.command.ToLower();
if(commandnameLowerToReal.ContainsKey(lowerName) == true)
{
C.command = commandnameLowerToReal[lowerName];
}
else if(master.CustomCommandsService.HasCommand(C.command) == true)
{
if(inCustomCommand == true)
{
return new KeyValuePair<bool, string>(false, "Custom command chaining is not allowed");
}
return master.CustomCommandsService.RunCommand(C, source);
}
else
{
return new KeyValuePair<bool, string>(false, "Unknown command");
}
KeyValuePair<bool,string> reply = RunBaseCommand(C, source);
if (C.replyTarget != null)
{
SmartCommandReply(C.replyTarget, reply.Value, C.command);
}
return reply;
}
catch (Exception e)
{
return new KeyValuePair<bool, string>(false, e.Message);
}
}
public string[] GetFullListOfCommands()
{
List<string> output = [];
foreach (string A in endpointcommandmap.Keys)
{
output.Add(A.ToLowerInvariant());
}
return [.. output];
}
protected Dictionary<string, CommandsAPI> commandEndpoints = [];
protected Dictionary<string, string> endpointcommandmap = []; // command = endpoint
protected Dictionary<string, string> commandnameLowerToReal = [];
protected void BotClientRestart(object o, BotClientNotice e)
{
if (e.isStart == false)
{
return;
}
acceptNewCommands = false;
LogFormater.Info("Commands service [Attached to new client]");
if (e.isDC == false)
{
GetClient().Network.LoggedOut += BotLoggedOut;
GetClient().Network.SimConnected += BotLoggedIn;
}
}
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
{
GetClient().Network.SimConnected += BotLoggedIn;
LogFormater.Info("Commands service [Waiting for connect]");
}
protected void BotLoggedIn(object o, SimConnectedEventArgs e)
{
GetClient().Network.SimConnected -= BotLoggedIn;
GetClient().Self.IM += BotImMessage;
if (myConfig.GetEnableMasterControls() == true)
{
foreach(string A in myConfig.GetMastersCSV())
{
master.DataStoreService.KnownAvatar(A);
}
}
acceptNewCommands = true;
LogFormater.Info("Commands service [accepting IM commands]");
}
public void CommandNotice(string command,string source, string args,bool accepted, string results)
{
BotCommandNotice e = new(command, args, source, accepted, results);
EventHandler<BotCommandNotice> handler = BotclientCommandEventNotices;
handler?.Invoke(this, e);
if (master.BotClient.basicCfg.GetLogCommands() == true)
{
if (myConfig.GetCommandHistoryLogResults() == false)
{
e.results = "~hidden~";
}
LogFormater.Info("Command log:" + JsonSerializer.Serialize(e, JsonOptions.UnsafeRelaxed));
}
}
}
public class SignedCommand
{
public string command;
public string signingCode;
public string[] args;
public string replyTarget = null;
public bool accepted = false;
public long unixtimeOfCommand = 0;
protected CommandsService master;
public SignedCommand(CommandsService setMaster, string source, string input, bool requireSigning, bool requireTimewindow, int windowSize, string secret)
{
master = setMaster;
UnpackInput(input);
if (requireSigning == false)
{
accepted = true; // just accept the command
return;
}
Vaildate(requireTimewindow, windowSize, secret);
}
public SignedCommand(CommandsService setMaster, string source, string setCommand, string setSigningCode, string[] setArgs,
int setUnixtime, string setReplyTarget, bool requireTimewindow, int windowSize, string secret, bool requireSigning = true)
{
master = setMaster;
command = setCommand;
signingCode = setSigningCode;
args = setArgs;
unixtimeOfCommand = setUnixtime;
replyTarget = setReplyTarget;
if (requireSigning == false)
{
accepted = true; // just accept the command
return;
}
Vaildate(requireTimewindow, windowSize, secret);
}
protected void Vaildate(bool requireTimewindow, int windowSize, string secret)
{
accepted = false;
long dif = SecondbotHelpers.UnixTimeNow() - unixtimeOfCommand;
if(requireTimewindow == true)
{
if ((unixtimeOfCommand == 0) || (dif > windowSize))
{
return;
}
}
if (signingCode == null) // no signing code
{
return;
}
string raw = command;
raw += string.Join("~#~", args);
if (requireTimewindow == true)
{
raw += unixtimeOfCommand.ToString();
}
raw += secret;
string cooked = SecondbotHelpers.GetSHA1(raw);
if (cooked != signingCode)
{
return; // invaild signing code
}
accepted = true;
}
protected void UnpackInput(string input)
{
if (IsValidJson(input) == false)
{
UnpackStringToClass(input);
return;
}
UnpackJsonToClass();
}
protected JToken obj = null;
protected void UnpackStringToClass(string input)
{
// command|||args~#~args~#~args#|#reply_target@@@sha1 [inc time if required];|;unixtime of command
string[] bits = input.Split(";|;");
if(bits.Length == 2)
{
unixtimeOfCommand = Convert.ToInt32(bits[1]);
}
// command|||args~#~args~#~args#|#reply_target@@@sha1 [inc time if required]
// unixtime of command
bits = bits[0].Split("@@@");
if(bits.Length == 2)
{
signingCode = bits[1];
}
// command|||args~#~args~#~args#|#reply_target
// sha1 [inc time if required]
bits = bits[0].Split("#|#");
if(bits.Length == 2)
{
replyTarget = bits[1];
}
// command|||args~#~args~#~args
// reply_target
bits = bits[0].Split("|||");
command = bits[0];
// command
// args~#~args~#~args
args = [];
if(bits.Length == 2)
{
args = bits[1].Split("~#~");
}
}
protected void UnpackJsonToClass()
{
command = JsonHelper.GetValue<string>(obj, "cmd", null);
args = JsonHelper.GetValue<string[]>(obj, "args", null);
signingCode = JsonHelper.GetValue<string>(obj, "signing", null);
replyTarget = JsonHelper.GetValue<string>(obj, "reply", null);
unixtimeOfCommand = JsonHelper.GetValue<long>(obj, "unixtime", 0);
}
private bool IsValidJson(string strInput)
{
if (string.IsNullOrWhiteSpace(strInput)) { return false; }
strInput = strInput.Trim();
if ((strInput.StartsWith("{") && strInput.EndsWith("}")) || //For object
(strInput.StartsWith("[") && strInput.EndsWith("]"))) //For array
{
try
{
obj = JToken.Parse(strInput);
return true;
}
catch
{
return false;
}
}
return false;
}
}
public static class JsonHelper
{
public static T GetValue<T>(this JToken jToken, string key, T defaultValue = default)
{
dynamic ret = jToken[key];
if (ret == null) return defaultValue;
if (ret is JObject) return JsonSerializer.Deserialize<T>(ret.ToString());
return (T)ret;
}
}
public class UUIDJsonConverter : JsonConverter<UUID>
{
public override UUID Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Read UUID from string
return new UUID(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, UUID value, JsonSerializerOptions options)
{
// Write UUID as string
writer.WriteStringValue(value.ToString());
}
// For dictionary key support
public override UUID ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return new UUID(reader.GetString());
}
public override void WriteAsPropertyName(Utf8JsonWriter writer, UUID value, JsonSerializerOptions options)
{
writer.WritePropertyName(value.ToString());
}
}
public static class JsonOptions
{
public static readonly JsonSerializerOptions UnsafeRelaxed = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
IncludeFields = true,
Converters = { new UUIDJsonConverter() }
};
}
}