Updated Utility module

This commit is contained in:
Toastie 2024-06-26 23:52:29 +12:00
parent 0466701e28
commit 4732254805
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
29 changed files with 470 additions and 64 deletions

View file

@ -0,0 +1,314 @@
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;
var nadekoId = _client.CurrentUser.Id;
var channel = msg.Channel as ITextChannel;
if (channel is null)
return false;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
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);
return false;
}
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
=> "In order to use this command you have to have a 5$ or higher subscription at <https://patreon.com/nadekobot>",
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 + "\"";
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class AiCommandModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("desc")]
public required string Desc { get; set; }
[JsonPropertyName("params")]
public required IReadOnlyList<AiCommandParamModel> Params { get; set; }
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class AiCommandParamModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("desc")]
public required string Desc { get; set; }
}

View file

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class CommandPromptResultModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("arguments")]
public required Dictionary<string, string> Arguments { get; set; }
[JsonPropertyName("remaining")]
[JsonConverter(typeof(NumberToStringConverter))]
public required string Remaining { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Utility;
public sealed class EllieCommandCallModel
{
public required string Name { get; set; }
public required IReadOnlyList<string> Arguments { get; set; }
public required string Remaining { get; set; }
}

View file

@ -0,0 +1,20 @@
using OneOf;
namespace EllieBot.Modules.Utility;
public interface IAiAssistantService
{
Task<OneOf<EllieCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
ulong userId,
string prompt,
IReadOnlyCollection<AiCommandModel> commands,
string prefix);
IReadOnlyCollection<AiCommandModel> GetCommands();
Task<bool> TryExecuteAiCommand(
IGuild guild,
IUserMessage msg,
ITextChannel channel,
string query);
}

View file

@ -0,0 +1,23 @@
using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Utility;
public partial class UtilityCommands
{
public class PromptCommands : EllieModule<IAiAssistantService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Prompt([Leftover] string query)
{
await ctx.Channel.TriggerTypingAsync();
var res = await _service.TryExecuteAiCommand(ctx.Guild, ctx.Message, (ITextChannel)ctx.Channel, query);
}
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 + "\"";
}
}

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Utility;
public partial class Utility
@ -138,12 +138,12 @@ public partial class Utility
private string GetPropsAndValuesString(IConfigService config, IReadOnlyCollection<string> names)
{
var propValues = names.Select(pr =>
{
var val = config.GetSetting(pr);
if (pr != "currency.sign")
val = val?.TrimTo(28);
return val?.Replace("\n", "") ?? "-";
})
{
var val = config.GetSetting(pr);
if (pr != "currency.sign")
val = val?.TrimTo(28);
return val?.Replace("\n", "") ?? "-";
})
.ToList();
var strings = names.Zip(propValues, (name, value) => $"{name,-25} = {value}\n");

View file

@ -48,15 +48,15 @@ public partial class Utility
[UserPerm(GuildPerm.ManageMessages)]
public async Task GiveawayEnd(kwum id)
{
var success = await _service.EndGiveawayAsync(ctx.Guild.Id, id);
var success = await _service.EndGiveawayAsync(ctx.Guild.Id, id);
if (!success)
{
await Response().Error(strs.giveaway_not_found).SendAsync();
return;
}
if(!success)
{
await Response().Error(strs.giveaway_not_found).SendAsync();
return;
}
await ctx.OkAsync();
await ctx.OkAsync();
_ = ctx.Message.DeleteAfter(5);
}

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System.Text;
using EllieBot.Modules.Patronage;
@ -144,9 +144,9 @@ public partial class Utility
true)
.WithOkColor();
var patron = await _ps.GetPatronAsync(user.Id);
var mPatron = await _ps.GetPatronAsync(user.Id);
if (patron.Tier != PatronTier.None)
if (mPatron is {} patron && patron.Tier != PatronTier.None)
{
embed.WithFooter(patron.Tier switch
{

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using CommandLine;
namespace EllieBot.Modules.Utility.Services;

View file

@ -1,4 +1,4 @@
#nullable disable warnings
#nullable disable warnings
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.Yml;
@ -158,7 +158,7 @@ public partial class Utility
{
var msg = sm.Data.Components.FirstOrDefault()?.Value;
if (!string.IsNullOrWhiteSpace(msg))
if(!string.IsNullOrWhiteSpace(msg))
await QuoteEdit(id, msg);
}
);

View file

@ -1,4 +1,4 @@
#nullable disable warnings
#nullable disable warnings
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Db.Models;

View file

@ -113,10 +113,9 @@ public partial class Utility
foreach (var rem in rems)
{
var when = rem.When;
var diff = when - DateTime.UtcNow;
embed.AddField(
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC "
+ $"(in {diff.ToPrettyStringHm()})",
+ $"{TimestampTag.FromDateTime(when)}",
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
`TargetId:` {rem.ChannelId}
`Message:` {rem.Message?.TrimTo(50)}");
@ -203,16 +202,15 @@ public partial class Utility
await uow.SaveChangesAsync();
}
var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
// var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
try
{
await Response()
.Confirm($"\u23f0 {GetText(strs.remind(
.Confirm($"\u23f0 {GetText(strs.remind2(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
Format.Bold(message),
ts.ToPrettyStringHm(),
gTime,
gTime))}")
TimestampTag.FromDateTime(DateTime.UtcNow.Add(ts), TimestampTagStyles.Relative),
TimestampTag.FormatFromDateTime(time, TimestampTagStyles.ShortDateTime)))}")
.SendAsync();
}
catch

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.Modules.Utility.Services;

View file

@ -63,16 +63,16 @@ public class ConverterService : IEService, IReadyExecutor
UnitType = unitTypeString
};
var units = currencyRates.ConversionRates.Select(u => new ConvertUnit
{
Triggers = [u.Key],
Modifier = u.Value,
UnitType = unitTypeString
})
{
Triggers = [u.Key],
Modifier = u.Value,
UnitType = unitTypeString
})
.ToList();
var stream = File.OpenRead("data/units.json");
var stream = File.OpenRead("data/units.json");
var defaultUnits = await JsonSerializer.DeserializeAsync<ConvertUnit[]>(stream);
if (defaultUnits is not null)
if(defaultUnits is not null)
units.AddRange(defaultUnits);
units.Add(baseType);

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Modules.Utility.Services;
using Newtonsoft.Json;
using System.Diagnostics;

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System.Diagnostics;
namespace EllieBot.Modules.Utility.Common;

View file

@ -1,4 +1,4 @@
// ReSharper disable InconsistentNaming
// ReSharper disable InconsistentNaming
#nullable disable
namespace EllieBot.Modules.Utility;

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Utility.Common.Exceptions;
public class StreamRoleNotFoundException : Exception

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Utility.Common.Exceptions;
public class StreamRolePermissionException : Exception

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Utility.Common;
public enum StreamRoleListType