2024-06-26 23:52:29 +12:00
|
|
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
|
|
|
using EllieBot.Modules.Administration;
|
|
|
|
|
using EllieBot.Modules.Games.Services;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http.Json;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
|
|
|
|
|
|
|
|
namespace EllieBot.Modules.Utility;
|
|
|
|
|
|
|
|
|
|
public enum GetCommandErrorResult
|
|
|
|
|
{
|
|
|
|
|
RateLimitHit,
|
|
|
|
|
NotAuthorized,
|
|
|
|
|
Disregard,
|
|
|
|
|
Unknown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class AiAssistantService
|
|
|
|
|
: IAiAssistantService, IReadyExecutor,
|
|
|
|
|
IExecOnMessage,
|
|
|
|
|
IEService
|
|
|
|
|
{
|
|
|
|
|
private IReadOnlyCollection<AiCommandModel> _commands = [];
|
|
|
|
|
|
|
|
|
|
private readonly IBotStrings _strings;
|
|
|
|
|
private readonly IHttpClientFactory _httpFactory;
|
|
|
|
|
private readonly CommandService _cmds;
|
|
|
|
|
private readonly IBotCredsProvider _credsProvider;
|
|
|
|
|
private readonly DiscordSocketClient _client;
|
|
|
|
|
private readonly ICommandHandler _cmdHandler;
|
|
|
|
|
private readonly BotConfigService _bcs;
|
|
|
|
|
private readonly IMessageSenderService _sender;
|
|
|
|
|
|
|
|
|
|
private readonly JsonSerializerOptions _serializerOptions = new();
|
|
|
|
|
private readonly IPermissionChecker _permChecker;
|
|
|
|
|
private readonly IBotCache _botCache;
|
|
|
|
|
private readonly ChatterBotService _cbs;
|
|
|
|
|
|
|
|
|
|
public AiAssistantService(
|
|
|
|
|
DiscordSocketClient client,
|
|
|
|
|
IBotStrings strings,
|
|
|
|
|
IHttpClientFactory httpFactory,
|
|
|
|
|
CommandService cmds,
|
|
|
|
|
IBotCredsProvider credsProvider,
|
|
|
|
|
ICommandHandler cmdHandler,
|
|
|
|
|
BotConfigService bcs,
|
|
|
|
|
IPermissionChecker permChecker,
|
|
|
|
|
IBotCache botCache,
|
|
|
|
|
ChatterBotService cbs,
|
|
|
|
|
IMessageSenderService sender)
|
|
|
|
|
{
|
|
|
|
|
_client = client;
|
|
|
|
|
_strings = strings;
|
|
|
|
|
_httpFactory = httpFactory;
|
|
|
|
|
_cmds = cmds;
|
|
|
|
|
_credsProvider = credsProvider;
|
|
|
|
|
_cmdHandler = cmdHandler;
|
|
|
|
|
_bcs = bcs;
|
|
|
|
|
_sender = sender;
|
|
|
|
|
_permChecker = permChecker;
|
|
|
|
|
_botCache = botCache;
|
|
|
|
|
_cbs = cbs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<OneOf.OneOf<EllieCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
|
|
|
|
|
ulong userId,
|
|
|
|
|
string prompt,
|
|
|
|
|
IReadOnlyCollection<AiCommandModel> commands,
|
|
|
|
|
string prefix)
|
|
|
|
|
{
|
|
|
|
|
using var content = new StringContent(
|
|
|
|
|
JsonSerializer.Serialize(new
|
|
|
|
|
{
|
|
|
|
|
query = prompt,
|
|
|
|
|
commands = commands.ToDictionary(x => x.Name,
|
|
|
|
|
x => new AiCommandModel()
|
|
|
|
|
{
|
|
|
|
|
Desc = string.Format(x.Desc ?? "", prefix),
|
|
|
|
|
Params = x.Params,
|
|
|
|
|
Name = x.Name
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
Encoding.UTF8,
|
|
|
|
|
"application/json"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
using var request = new HttpRequestMessage();
|
|
|
|
|
request.Method = HttpMethod.Post;
|
|
|
|
|
// request.RequestUri = new("https://nai.nadeko.bot/get-command");
|
|
|
|
|
request.RequestUri = new("https://nai.nadeko.bot/get-command");
|
|
|
|
|
request.Content = content;
|
|
|
|
|
|
|
|
|
|
var creds = _credsProvider.GetCreds();
|
|
|
|
|
|
|
|
|
|
request.Headers.TryAddWithoutValidation("x-auth-token", creds.EllieAiToken);
|
|
|
|
|
request.Headers.TryAddWithoutValidation("x-auth-userid", userId.ToString());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using var client = _httpFactory.CreateClient();
|
|
|
|
|
|
|
|
|
|
// todo customize according to the bot's config
|
|
|
|
|
// - CurrencyName
|
|
|
|
|
// -
|
|
|
|
|
|
|
|
|
|
using var response = await client.SendAsync(request);
|
|
|
|
|
|
|
|
|
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
|
|
|
|
{
|
|
|
|
|
return GetCommandErrorResult.RateLimitHit;
|
|
|
|
|
}
|
|
|
|
|
else if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
|
|
|
{
|
|
|
|
|
return GetCommandErrorResult.NotAuthorized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var funcModel = await response.Content.ReadFromJsonAsync<CommandPromptResultModel>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (funcModel?.Name == "disregard")
|
|
|
|
|
{
|
|
|
|
|
Log.Warning("Disregarding the prompt: {Prompt}", prompt);
|
|
|
|
|
return GetCommandErrorResult.Disregard;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (funcModel is null)
|
|
|
|
|
return GetCommandErrorResult.Unknown;
|
|
|
|
|
|
|
|
|
|
var comModel = new EllieCommandCallModel()
|
|
|
|
|
{
|
|
|
|
|
Name = funcModel.Name,
|
|
|
|
|
Arguments = funcModel.Arguments
|
|
|
|
|
.OrderBy(param => _commands.FirstOrDefault(x => x.Name == funcModel.Name)
|
|
|
|
|
?.Params
|
|
|
|
|
.Select((x, i) => (x, i))
|
|
|
|
|
.Where(x => x.x.Name == param.Key)
|
|
|
|
|
.Select(x => x.i)
|
|
|
|
|
.FirstOrDefault())
|
|
|
|
|
.Select(x => x.Value)
|
|
|
|
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
|
|
|
.ToArray(),
|
|
|
|
|
Remaining = funcModel.Remaining
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return comModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IReadOnlyCollection<AiCommandModel> GetCommands()
|
|
|
|
|
=> _commands;
|
|
|
|
|
|
|
|
|
|
public Task OnReadyAsync()
|
|
|
|
|
{
|
|
|
|
|
var cmds = _cmds.Commands
|
|
|
|
|
.Select(x => (MethodName: x.Summary, CommandName: x.Aliases[0]))
|
|
|
|
|
.Where(x => !x.MethodName.Contains("///"))
|
|
|
|
|
.Distinct()
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var funcs = new List<AiCommandModel>();
|
|
|
|
|
foreach (var (method, cmd) in cmds)
|
|
|
|
|
{
|
|
|
|
|
var commandStrings = _strings.GetCommandStrings(method);
|
|
|
|
|
|
|
|
|
|
if (commandStrings is null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
funcs.Add(new()
|
|
|
|
|
{
|
|
|
|
|
Name = cmd,
|
|
|
|
|
Desc = commandStrings?.Desc?.Replace("currency", "flowers") ?? string.Empty,
|
|
|
|
|
Params = commandStrings?.Params.FirstOrDefault()
|
|
|
|
|
?.Select(x => new AiCommandParamModel()
|
|
|
|
|
{
|
|
|
|
|
Desc = x.Value.Desc,
|
|
|
|
|
Name = x.Key,
|
|
|
|
|
})
|
|
|
|
|
.ToArray()
|
|
|
|
|
?? []
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_commands = funcs;
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int Priority
|
|
|
|
|
=> 2;
|
|
|
|
|
|
|
|
|
|
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_credsProvider.GetCreds().EllieAiToken))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (guild is not SocketGuild sg)
|
|
|
|
|
return false;
|
|
|
|
|
|
2024-07-08 01:34:34 +12:00
|
|
|
|
var ellieId = _client.CurrentUser.Id;
|
2024-06-26 23:52:29 +12:00
|
|
|
|
|
|
|
|
|
var channel = msg.Channel as ITextChannel;
|
|
|
|
|
if (channel is null)
|
|
|
|
|
return false;
|
|
|
|
|
|
2024-07-08 01:34:34 +12:00
|
|
|
|
var normalMention = $"<@{ellieId}> ";
|
|
|
|
|
var nickMention = $"<@!{ellieId}> ";
|
2024-06-26 23:52:29 +12:00
|
|
|
|
string query;
|
|
|
|
|
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
|
|
|
|
|
query = msg.Content[normalMention.Length..].Trim();
|
|
|
|
|
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
|
|
|
|
query = msg.Content[nickMention.Length..].Trim();
|
|
|
|
|
else
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var success = await TryExecuteAiCommand(guild, msg, channel, query);
|
|
|
|
|
|
|
|
|
|
return success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> TryExecuteAiCommand(
|
|
|
|
|
IGuild guild,
|
|
|
|
|
IUserMessage msg,
|
|
|
|
|
ITextChannel channel,
|
|
|
|
|
string query)
|
|
|
|
|
{
|
|
|
|
|
// check permissions
|
|
|
|
|
var pcResult = await _permChecker.CheckPermsAsync(
|
|
|
|
|
guild,
|
|
|
|
|
msg.Channel,
|
|
|
|
|
msg.Author,
|
|
|
|
|
"Utility",
|
|
|
|
|
"prompt"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!pcResult.IsAllowed)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
using var _ = channel.EnterTypingState();
|
|
|
|
|
|
|
|
|
|
var result = await TryGetCommandAsync(msg.Author.Id, query, _commands, _cmdHandler.GetPrefix(guild.Id));
|
|
|
|
|
|
|
|
|
|
if (result.TryPickT0(out var model, out var error))
|
|
|
|
|
{
|
|
|
|
|
if (model.Name == ".ai_chat")
|
|
|
|
|
{
|
|
|
|
|
if (guild is not SocketGuild sg)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var sess = _cbs.GetOrCreateSession(guild.Id);
|
|
|
|
|
if (sess is null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
await _cbs.RunChatterBot(sg, msg, channel, sess, query);
|
2024-07-08 00:36:45 +12:00
|
|
|
|
return true;
|
2024-06-26 23:52:29 +12:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commandString = GetCommandString(model);
|
|
|
|
|
|
|
|
|
|
var msgTask = _sender.Response(channel)
|
|
|
|
|
.Embed(_sender.CreateEmbed()
|
|
|
|
|
.WithOkColor()
|
|
|
|
|
.WithAuthor(msg.Author.GlobalName,
|
|
|
|
|
msg.Author.RealAvatarUrl().ToString())
|
|
|
|
|
.WithDescription(commandString))
|
|
|
|
|
.SendAsync();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await _cmdHandler.TryRunCommand(
|
|
|
|
|
(SocketGuild)guild,
|
|
|
|
|
(ISocketMessageChannel)channel,
|
|
|
|
|
new DoAsUserMessage((SocketUserMessage)msg, msg.Author, commandString));
|
|
|
|
|
|
|
|
|
|
var cmdMsg = await msgTask;
|
|
|
|
|
|
|
|
|
|
cmdMsg.DeleteAfter(5);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error == GetCommandErrorResult.Disregard)
|
|
|
|
|
{
|
|
|
|
|
// await msg.ErrorAsync();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var key = new TypedKey<bool>($"sub_error:{msg.Author.Id}:{error}");
|
|
|
|
|
|
|
|
|
|
if (!await _botCache.AddAsync(key, true, TimeSpan.FromDays(1), overwrite: false))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var errorMsg = error switch
|
|
|
|
|
{
|
|
|
|
|
GetCommandErrorResult.RateLimitHit
|
|
|
|
|
=> "You've spent your daily requests quota.",
|
|
|
|
|
GetCommandErrorResult.NotAuthorized
|
2024-07-08 01:34:34 +12:00
|
|
|
|
=> "In order to use this command you have to have a 5$ or higher subscription at <https://patreon.com/elliebot>",
|
2024-06-26 23:52:29 +12:00
|
|
|
|
GetCommandErrorResult.Unknown
|
|
|
|
|
=> "The service is temporarily unavailable.",
|
|
|
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await _sender.Response(channel)
|
|
|
|
|
.Error(errorMsg)
|
|
|
|
|
.SendAsync();
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetCommandString(EllieCommandCallModel res)
|
|
|
|
|
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
|
|
|
|
|
|
|
|
|
|
private static string GetParamString(string val, bool isLast)
|
|
|
|
|
=> isLast ? val : "\"" + val + "\"";
|
|
|
|
|
}
|