Finished Ellie.Bot.Modules.Expressions
This commit is contained in:
parent
ad85599ebc
commit
2aef2ac973
9 changed files with 1361 additions and 2 deletions
|
@ -36,10 +36,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Econ", "src\Ellie.Eco
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Modules.Expresssions", "src\Ellie.Bot.Modules.Expresssions\Ellie.Bot.Modules.Expresssions.csproj", "{F4D3A613-320A-40DD-975A-247186A20173}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Modules.Expresssions", "src\Ellie.Bot.Modules.Expresssions\Ellie.Bot.Modules.Expresssions.csproj", "{F4D3A613-320A-40DD-975A-247186A20173}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Common", "src\Ellie.Bot.Common\Ellie.Bot.Common.csproj", "{3EC0F005-560F-4E90-88CF-199520133BBA}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Db", "src\Ellie.Bot.Db\Ellie.Bot.Db.csproj", "{D3411F6C-320C-456D-BA86-24481EB000EA}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Db", "src\Ellie.Bot.Db\Ellie.Bot.Db.csproj", "{D3411F6C-320C-456D-BA86-24481EB000EA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Common", "src\Ellie.Bot.Common\Ellie.Bot.Common.csproj", "{3EC0F005-560F-4E90-88CF-199520133BBA}"
|
||||||
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Cloneable", "src\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj", "{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Cloneable", "src\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj", "{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Ellie.Modules.Expresssions</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ellie.Bot.Common\Ellie.Bot.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Ellise.Common\Ellise.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Ellie.Bot.Db\Ellie.Bot.Db.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net" Version="3.104.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
395
src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs
Normal file
395
src/Ellie.Bot.Modules.Expresssions/EllieExpressions.cs
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Ellie.Common.Attributes;
|
||||||
|
|
||||||
|
namespace Ellie.Modules.EllieExpressions;
|
||||||
|
|
||||||
|
[Name("Expressions")]
|
||||||
|
public partial class EllieExpressions : EllieModule<EllieExpressionsService>
|
||||||
|
{
|
||||||
|
public enum All
|
||||||
|
{
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IBotCredentials _creds;
|
||||||
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
|
||||||
|
public EllieExpressions(IBotCredentials creds, IHttpClientFactory clientFactory)
|
||||||
|
{
|
||||||
|
_creds = creds;
|
||||||
|
_clientFactory = clientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AdminInGuildOrOwnerInDm()
|
||||||
|
=> (ctx.Guild is null && _creds.IsOwner(ctx.User))
|
||||||
|
|| (ctx.Guild is not null && ((IGuildUser)ctx.User).GuildPermissions.Administrator);
|
||||||
|
|
||||||
|
private async Task ExprAddInternalAsync(string key, string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ex = await _service.AddAsync(ctx.Guild?.Id, key, message);
|
||||||
|
|
||||||
|
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.expr_new))
|
||||||
|
.WithDescription($"#{new kwum(ex.Id)}")
|
||||||
|
.AddField(GetText(strs.trigger), key)
|
||||||
|
.AddField(GetText(strs.response),
|
||||||
|
message.Length > 1024 ? GetText(strs.redacted_too_long) : message));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task ExprToggleGlobal()
|
||||||
|
{
|
||||||
|
var result = await _service.ToggleGlobalExpressionsAsync(ctx.Guild.Id);
|
||||||
|
if (result)
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.expr_global_disabled);
|
||||||
|
else
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.expr_global_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task ExprAddServer(string key, [Leftover] string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExprAddInternalAsync(key, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprAdd(string key, [Leftover] string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExprAddInternalAsync(key, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprEdit(kwum id, [Leftover] string message)
|
||||||
|
{
|
||||||
|
var channel = ctx.Channel as ITextChannel;
|
||||||
|
if (string.IsNullOrWhiteSpace(message) || id < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((channel is null && !_creds.IsOwner(ctx.User))
|
||||||
|
|| (channel is not null && !((IGuildUser)ctx.User).GuildPermissions.Administrator))
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ex = await _service.EditAsync(ctx.Guild?.Id, id, message);
|
||||||
|
if (ex is not null)
|
||||||
|
{
|
||||||
|
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.expr_edited))
|
||||||
|
.WithDescription($"#{id}")
|
||||||
|
.AddField(GetText(strs.trigger), ex.Trigger)
|
||||||
|
.AddField(GetText(strs.response),
|
||||||
|
message.Length > 1024 ? GetText(strs.redacted_too_long) : message));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[Priority(1)]
|
||||||
|
public async Task ExprList(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0 || page > 999)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expressions = _service.GetExpressionsFor(ctx.Guild?.Id);
|
||||||
|
|
||||||
|
if (expressions is null || !expressions.Any())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SendPaginatedConfirmAsync(page,
|
||||||
|
curPage =>
|
||||||
|
{
|
||||||
|
var desc = expressions.OrderBy(ex => ex.Trigger)
|
||||||
|
.Skip(curPage * 20)
|
||||||
|
.Take(20)
|
||||||
|
.Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}"
|
||||||
|
+ $"{(ex.DmResponse ? "✉" : "◾")}"
|
||||||
|
+ $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}"
|
||||||
|
+ $"`{(kwum)ex.Id}` {ex.Trigger}"
|
||||||
|
+ (string.IsNullOrWhiteSpace(ex.Reactions)
|
||||||
|
? string.Empty
|
||||||
|
: " // " + string.Join(" ", ex.GetReactions())))
|
||||||
|
.Join('\n');
|
||||||
|
|
||||||
|
return _eb.Create().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc);
|
||||||
|
},
|
||||||
|
expressions.Length,
|
||||||
|
20);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprShow(kwum id)
|
||||||
|
{
|
||||||
|
var found = _service.GetExpression(ctx.Guild?.Id, id);
|
||||||
|
|
||||||
|
if (found is null)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription($"#{id}")
|
||||||
|
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
|
||||||
|
.AddField(GetText(strs.response),
|
||||||
|
found.Response.TrimTo(1000).Replace("](", "]\\(")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExprDeleteInternalAsync(kwum id)
|
||||||
|
{
|
||||||
|
var ex = await _service.DeleteAsync(ctx.Guild?.Id, id);
|
||||||
|
|
||||||
|
if (ex is not null)
|
||||||
|
{
|
||||||
|
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.expr_deleted))
|
||||||
|
.WithDescription($"#{id}")
|
||||||
|
.AddField(GetText(strs.trigger), ex.Trigger.TrimTo(1024))
|
||||||
|
.AddField(GetText(strs.response), ex.Response.TrimTo(1024)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task ExprDeleteServer(kwum id)
|
||||||
|
=> await ExprDeleteInternalAsync(id);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprDelete(kwum id)
|
||||||
|
{
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExprDeleteInternalAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprReact(kwum id, params string[] emojiStrs)
|
||||||
|
{
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ex = _service.GetExpression(ctx.Guild?.Id, id);
|
||||||
|
if (ex is null)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojiStrs.Length == 0)
|
||||||
|
{
|
||||||
|
await _service.ResetExprReactions(ctx.Guild?.Id, id);
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.expr_reset(Format.Bold(id.ToString())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var succ = new List<string>();
|
||||||
|
foreach (var emojiStr in emojiStrs)
|
||||||
|
{
|
||||||
|
var emote = emojiStr.ToIEmote();
|
||||||
|
|
||||||
|
// i should try adding these emojis right away to the message, to make sure the bot can react with these emojis. If it fails, skip that emoji
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ctx.Message.AddReactionAsync(emote);
|
||||||
|
await Task.Delay(100);
|
||||||
|
succ.Add(emojiStr);
|
||||||
|
|
||||||
|
if (succ.Count >= 3)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (succ.Count == 0)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.invalid_emojis);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _service.SetExprReactions(ctx.Guild?.Id, id, succ);
|
||||||
|
|
||||||
|
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.expr_set(Format.Bold(id.ToString()),
|
||||||
|
succ.Select(static x => x.ToString()).Join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public Task ExprCa(kwum id)
|
||||||
|
=> InternalExprEdit(id, ExprField.ContainsAnywhere);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public Task ExprDm(kwum id)
|
||||||
|
=> InternalExprEdit(id, ExprField.DmResponse);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public Task ExprAd(kwum id)
|
||||||
|
=> InternalExprEdit(id, ExprField.AutoDelete);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public Task ExprAt(kwum id)
|
||||||
|
=> InternalExprEdit(id, ExprField.AllowTarget);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task ExprsReload()
|
||||||
|
{
|
||||||
|
await _service.TriggerReloadExpressions();
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InternalExprEdit(kwum id, ExprField option)
|
||||||
|
{
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, newVal) = await _service.ToggleExprOptionAsync(ctx.Guild?.Id, id, option);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_no_found_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVal)
|
||||||
|
{
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.option_enabled(Format.Code(option.ToString()),
|
||||||
|
Format.Code(id.ToString())));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.option_disabled(Format.Code(option.ToString()),
|
||||||
|
Format.Code(id.ToString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task ExprClear()
|
||||||
|
{
|
||||||
|
if (await PromptUserConfirmAsync(_eb.Create()
|
||||||
|
.WithTitle("Expression clear")
|
||||||
|
.WithDescription("This will delete all expressions on this server.")))
|
||||||
|
{
|
||||||
|
var count = _service.DeleteAllExpressions(ctx.Guild.Id);
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.exprs_cleared(count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task ExprsExport()
|
||||||
|
{
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.Channel.TriggerTypingAsync();
|
||||||
|
|
||||||
|
var serialized = _service.ExportExpressions(ctx.Guild?.Id);
|
||||||
|
await using var stream = await serialized.ToStream();
|
||||||
|
await ctx.Channel.SendFileAsync(stream, "exprs-export.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
#if GLOBAL_ELLIE
|
||||||
|
[OwnerOnly]
|
||||||
|
#endif
|
||||||
|
public async Task ExprsImport([Leftover] string input = null)
|
||||||
|
{
|
||||||
|
if (!AdminInGuildOrOwnerInDm())
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input = input?.Trim();
|
||||||
|
|
||||||
|
_ = ctx.Channel.TriggerTypingAsync();
|
||||||
|
|
||||||
|
if (input is null)
|
||||||
|
{
|
||||||
|
var attachment = ctx.Message.Attachments.FirstOrDefault();
|
||||||
|
if (attachment is null)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = _clientFactory.CreateClient();
|
||||||
|
input = await client.GetStringAsync(attachment.Url);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var succ = await _service.ImportExpressionsAsync(ctx.Guild?.Id, input);
|
||||||
|
if (!succ)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
#nullable disable
|
||||||
|
using Ellie.Services.Database.Models;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ellie.Modules.EllieExpressions;
|
||||||
|
|
||||||
|
public static class EllieExpressionExtensions
|
||||||
|
{
|
||||||
|
private static string ResolveTriggerString(this string str, DiscordSocketClient client)
|
||||||
|
=> str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public static async Task<IUserMessage> Send(
|
||||||
|
this EllieExpression cr,
|
||||||
|
IUserMessage ctx,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
bool sanitize)
|
||||||
|
{
|
||||||
|
var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel;
|
||||||
|
|
||||||
|
var trigger = cr.Trigger.ResolveTriggerString(client);
|
||||||
|
var substringIndex = trigger.Length;
|
||||||
|
if (cr.ContainsAnywhere)
|
||||||
|
{
|
||||||
|
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
|
||||||
|
if (pos == WordPosition.Start)
|
||||||
|
substringIndex += 1;
|
||||||
|
else if (pos == WordPosition.End)
|
||||||
|
substringIndex = ctx.Content.Length;
|
||||||
|
else if (pos == WordPosition.Middle)
|
||||||
|
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
|
||||||
|
|
||||||
|
var rep = new ReplacementBuilder()
|
||||||
|
.WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client)
|
||||||
|
.WithOverride("%target%",
|
||||||
|
() => canMentionEveryone
|
||||||
|
? ctx.Content[substringIndex..].Trim()
|
||||||
|
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true))
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var text = SmartText.CreateFrom(cr.Response);
|
||||||
|
text = rep.Replace(text);
|
||||||
|
|
||||||
|
return await channel.SendAsync(text, sanitize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)
|
||||||
|
{
|
||||||
|
var wordIndex = str.IndexOf(word, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (wordIndex == -1)
|
||||||
|
return WordPosition.None;
|
||||||
|
|
||||||
|
if (wordIndex == 0)
|
||||||
|
{
|
||||||
|
if (word.Length < str.Length && str.IsValidWordDivider(word.Length))
|
||||||
|
return WordPosition.Start;
|
||||||
|
}
|
||||||
|
else if (wordIndex + word.Length == str.Length)
|
||||||
|
{
|
||||||
|
if (str.IsValidWordDivider(wordIndex - 1))
|
||||||
|
return WordPosition.End;
|
||||||
|
}
|
||||||
|
else if (str.IsValidWordDivider(wordIndex - 1) && str.IsValidWordDivider(wordIndex + word.Length))
|
||||||
|
return WordPosition.Middle;
|
||||||
|
|
||||||
|
return WordPosition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidWordDivider(this in ReadOnlySpan<char> str, int index)
|
||||||
|
{
|
||||||
|
var ch = str[index];
|
||||||
|
if (ch is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '1' and <= '9')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WordPosition
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Start,
|
||||||
|
Middle,
|
||||||
|
End
|
||||||
|
}
|
765
src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs
Normal file
765
src/Ellie.Bot.Modules.Expresssions/EllieExpressionsService.cs
Normal file
|
@ -0,0 +1,765 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Ellie.Common.ModuleBehaviors;
|
||||||
|
using Ellie.Common.Yml;
|
||||||
|
using Ellie.Db;
|
||||||
|
using Ellie.Services.Database.Models;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Ellie.Bot.Common;
|
||||||
|
using Ellie.Services;
|
||||||
|
using Serilog;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace Ellie.Modules.EllieExpressions;
|
||||||
|
|
||||||
|
public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
|
||||||
|
{
|
||||||
|
private const string MENTION_PH = "%bot.mention%";
|
||||||
|
|
||||||
|
private const string PREPEND_EXPORT =
|
||||||
|
"""
|
||||||
|
# Keys are triggers, Each key has a LIST of expressions in the following format:
|
||||||
|
# - res: Response string
|
||||||
|
# id: Alphanumeric id used for commands related to the expression. (Note, when using .exprsimport, a new id will be generated.)
|
||||||
|
# react:
|
||||||
|
# - <List
|
||||||
|
# - of
|
||||||
|
# - reactions>
|
||||||
|
# at: Whether expression allows targets (see .h .exprat)
|
||||||
|
# ca: Whether expression expects trigger anywhere (see .h .exprca)
|
||||||
|
# dm: Whether expression DMs the response (see .h .exprdm)
|
||||||
|
# ad: Whether expression automatically deletes triggering message (see .h .exprad)
|
||||||
|
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
|
||||||
|
.WithEventEmitter(args
|
||||||
|
=> new MultilineScalarFlowStyleEmitter(args))
|
||||||
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.WithIndentedSequences()
|
||||||
|
.ConfigureDefaultValuesHandling(DefaultValuesHandling
|
||||||
|
.OmitDefaults)
|
||||||
|
.DisableAliases()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
public int Priority
|
||||||
|
=> 0;
|
||||||
|
|
||||||
|
private readonly object _gexprWriteLock = new();
|
||||||
|
|
||||||
|
private readonly TypedKey<EllieExpression> _gexprAddedKey = new("gexpr.added");
|
||||||
|
private readonly TypedKey<int> _gexprDeletedkey = new("gexpr.deleted");
|
||||||
|
private readonly TypedKey<EllieExpression> _gexprEditedKey = new("gexpr.edited");
|
||||||
|
private readonly TypedKey<bool> _exprsReloadedKey = new("exprs.reloaded");
|
||||||
|
|
||||||
|
// it is perfectly fine to have global expressions as an array
|
||||||
|
// 1. expressions are almost never added (compared to how many times they are being looped through)
|
||||||
|
// 2. only need write locks for this as we'll rebuild+replace the array on every edit
|
||||||
|
// 3. there's never many of them (at most a thousand, usually < 100)
|
||||||
|
private EllieExpression[] globalExpressions = Array.Empty<EllieExpression>();
|
||||||
|
private ConcurrentDictionary<ulong, EllieExpression[]> newguildExpressions = new();
|
||||||
|
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
// private readonly PermissionService _perms;
|
||||||
|
// private readonly GlobalPermissionService _gperm;
|
||||||
|
// private readonly CmdCdService _cmdCds;
|
||||||
|
private readonly IPermissionChecker _permChecker;
|
||||||
|
private readonly ICommandHandler _cmd;
|
||||||
|
private readonly IBotStrings _strings;
|
||||||
|
private readonly IBot _bot;
|
||||||
|
private readonly IPubSub _pubSub;
|
||||||
|
private readonly IEmbedBuilderService _eb;
|
||||||
|
private readonly Random _rng;
|
||||||
|
|
||||||
|
private bool ready;
|
||||||
|
private ConcurrentHashSet<ulong> _disabledGlobalExpressionGuilds;
|
||||||
|
|
||||||
|
public EllieExpressionsService(
|
||||||
|
DbService db,
|
||||||
|
IBotStrings strings,
|
||||||
|
IBot bot,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
ICommandHandler cmd,
|
||||||
|
IPubSub pubSub,
|
||||||
|
IEmbedBuilderService eb,
|
||||||
|
IPermissionChecker permChecker)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_client = client;
|
||||||
|
_cmd = cmd;
|
||||||
|
_strings = strings;
|
||||||
|
_bot = bot;
|
||||||
|
_pubSub = pubSub;
|
||||||
|
_eb = eb;
|
||||||
|
_permChecker = permChecker;
|
||||||
|
_rng = new EllieRandom();
|
||||||
|
|
||||||
|
_pubSub.Sub(_exprsReloadedKey, OnExprsShouldReload);
|
||||||
|
pubSub.Sub(_gexprAddedKey, OnGexprAdded);
|
||||||
|
pubSub.Sub(_gexprDeletedkey, OnGexprDeleted);
|
||||||
|
pubSub.Sub(_gexprEditedKey, OnGexprEdited);
|
||||||
|
|
||||||
|
bot.JoinedGuild += OnJoinedGuild;
|
||||||
|
_client.LeftGuild += OnLeftGuild;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadInternal(IReadOnlyList<ulong> allGuildIds)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var guildItems = await uow.Expressions.AsNoTracking()
|
||||||
|
.Where(x => allGuildIds.Contains(x.GuildId.Value))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value)
|
||||||
|
.ToDictionary(g => g.Key,
|
||||||
|
g => g.Select(x =>
|
||||||
|
{
|
||||||
|
x.Trigger = x.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention);
|
||||||
|
return x;
|
||||||
|
})
|
||||||
|
.ToArray())
|
||||||
|
.ToConcurrent();
|
||||||
|
|
||||||
|
_disabledGlobalExpressionGuilds = new(await uow.GuildConfigs
|
||||||
|
.Where(x => x.DisableGlobalExpressions)
|
||||||
|
.Select(x => x.GuildId)
|
||||||
|
.ToListAsyncLinqToDB());
|
||||||
|
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
var globalItems = uow.Expressions.AsNoTracking()
|
||||||
|
.Where(x => x.GuildId == null || x.GuildId == 0)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
x.Trigger = x.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention);
|
||||||
|
return x;
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
globalExpressions = globalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EllieExpression TryGetExpression(IUserMessage umsg)
|
||||||
|
{
|
||||||
|
if (!ready)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (umsg.Channel is not SocketTextChannel channel)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var content = umsg.Content.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
if (newguildExpressions.TryGetValue(channel.Guild.Id, out var expressions) && expressions.Length > 0)
|
||||||
|
{
|
||||||
|
var expr = MatchExpressions(content, expressions);
|
||||||
|
if (expr is not null)
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_disabledGlobalExpressionGuilds.Contains(channel.Guild.Id))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var localGrs = globalExpressions;
|
||||||
|
|
||||||
|
return MatchExpressions(content, localGrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private EllieExpression MatchExpressions(in ReadOnlySpan<char> content, EllieExpression[] exprs)
|
||||||
|
{
|
||||||
|
var result = new List<EllieExpression>(1);
|
||||||
|
for (var i = 0; i < exprs.Length; i++)
|
||||||
|
{
|
||||||
|
var expr = exprs[i];
|
||||||
|
var trigger = expr.Trigger;
|
||||||
|
if (content.Length > trigger.Length)
|
||||||
|
{
|
||||||
|
// if input is greater than the trigger, it can only work if:
|
||||||
|
// it has CA enabled
|
||||||
|
if (expr.ContainsAnywhere)
|
||||||
|
{
|
||||||
|
// if ca is enabled, we have to check if it is a word within the content
|
||||||
|
var wp = content.GetWordPosition(trigger);
|
||||||
|
|
||||||
|
// if it is, then that's valid
|
||||||
|
if (wp != WordPosition.None)
|
||||||
|
result.Add(expr);
|
||||||
|
|
||||||
|
// if it's not, then it cant' work under any circumstance,
|
||||||
|
// because content is greater than the trigger length
|
||||||
|
// so it can't be equal, and it's not contained as a word
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if CA is disabled, and expr has AllowTarget, then the
|
||||||
|
// content has to start with the trigger followed by a space
|
||||||
|
if (expr.AllowTarget
|
||||||
|
&& content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& content[trigger.Length] == ' ')
|
||||||
|
result.Add(expr);
|
||||||
|
}
|
||||||
|
else if (content.Length < expr.Trigger.Length)
|
||||||
|
{
|
||||||
|
// if input length is less than trigger length, it means
|
||||||
|
// that the reaction can never be triggered
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if input length is the same as trigger length
|
||||||
|
// reaction can only trigger if the strings are equal
|
||||||
|
if (content.SequenceEqual(expr.Trigger))
|
||||||
|
result.Add(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var cancelled = result.FirstOrDefault(x => x.Response == "-");
|
||||||
|
if (cancelled is not null)
|
||||||
|
return cancelled;
|
||||||
|
|
||||||
|
return result[_rng.Next(0, result.Count)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
|
||||||
|
{
|
||||||
|
// maybe this message is an expression
|
||||||
|
var expr = TryGetExpression(msg);
|
||||||
|
|
||||||
|
if (expr is null || expr.Response == "-")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _permChecker.CheckAsync(
|
||||||
|
guild,
|
||||||
|
msg.Channel,
|
||||||
|
msg.Author,
|
||||||
|
"ACTUALEXPRESSIONS",
|
||||||
|
expr.Trigger
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.IsT0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// todo print error etc
|
||||||
|
|
||||||
|
// if (await _cmdCds.TryBlock(guild, msg.Author, expr.Trigger))
|
||||||
|
// return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// if (_gperm.BlockedModules.Contains("ACTUALEXPRESSIONS"))
|
||||||
|
// {
|
||||||
|
// Log.Information(
|
||||||
|
// "User {UserName} [{UserId}] tried to use an expression but 'ActualExpressions' are globally disabled",
|
||||||
|
// msg.Author.ToString(),
|
||||||
|
// msg.Author.Id);
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (guild is SocketGuild sg)
|
||||||
|
// {
|
||||||
|
// var pc = _perms.GetCacheFor(guild.Id);
|
||||||
|
// if (!pc.Permissions.CheckPermissions(msg, expr.Trigger, "ACTUALEXPRESSIONS", out var index))
|
||||||
|
// {
|
||||||
|
// if (pc.Verbose)
|
||||||
|
// {
|
||||||
|
// var permissionMessage = _strings.GetText(strs.perm_prevent(index + 1,
|
||||||
|
// Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg))),
|
||||||
|
// sg.Id);
|
||||||
|
//
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// await msg.Channel.SendErrorAsync(_eb, permissionMessage);
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Log.Information("{PermissionMessage}", permissionMessage);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
var sentMsg = await expr.Send(msg, _client, false);
|
||||||
|
|
||||||
|
var reactions = expr.GetReactions();
|
||||||
|
foreach (var reaction in reactions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sentMsg.AddReactionAsync(reaction.ToIEmote());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Log.Warning("Unable to add reactions to message {Message} in server {GuildId}",
|
||||||
|
sentMsg.Id,
|
||||||
|
expr.GuildId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr.AutoDeleteTrigger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await msg.DeleteAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("s: {GuildId} c: {ChannelId} u: {UserId} | {UserName} executed expression {Expr}",
|
||||||
|
guild.Id,
|
||||||
|
msg.Channel.Id,
|
||||||
|
msg.Author.Id,
|
||||||
|
msg.Author.ToString(),
|
||||||
|
expr.Trigger);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error in Expression RunBehavior: {ErrorMessage}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetExprReactions(ulong? maybeGuildId, int id)
|
||||||
|
{
|
||||||
|
EllieExpression expr;
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
expr = uow.Expressions.GetById(id);
|
||||||
|
if (expr is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
expr.Reactions = string.Empty;
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UpdateInternalAsync(ulong? maybeGuildId, EllieExpression expr)
|
||||||
|
{
|
||||||
|
if (maybeGuildId is { } guildId)
|
||||||
|
UpdateInternal(guildId, expr);
|
||||||
|
else
|
||||||
|
return _pubSub.Pub(_gexprEditedKey, expr);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateInternal(ulong? maybeGuildId, EllieExpression expr)
|
||||||
|
{
|
||||||
|
if (maybeGuildId is { } guildId)
|
||||||
|
{
|
||||||
|
newguildExpressions.AddOrUpdate(guildId,
|
||||||
|
new[] { expr },
|
||||||
|
(_, old) =>
|
||||||
|
{
|
||||||
|
var newArray = old.ToArray();
|
||||||
|
for (var i = 0; i < newArray.Length; i++)
|
||||||
|
{
|
||||||
|
if (newArray[i].Id == expr.Id)
|
||||||
|
newArray[i] = expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArray;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
var exprs = globalExpressions;
|
||||||
|
for (var i = 0; i < exprs.Length; i++)
|
||||||
|
{
|
||||||
|
if (exprs[i].Id == expr.Id)
|
||||||
|
exprs[i] = expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task AddInternalAsync(ulong? maybeGuildId, EllieExpression expr)
|
||||||
|
{
|
||||||
|
// only do this for perf purposes
|
||||||
|
expr.Trigger = expr.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention);
|
||||||
|
|
||||||
|
if (maybeGuildId is { } guildId)
|
||||||
|
newguildExpressions.AddOrUpdate(guildId, new[] { expr }, (_, old) => old.With(expr));
|
||||||
|
else
|
||||||
|
return _pubSub.Pub(_gexprAddedKey, expr);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DeleteInternalAsync(ulong? maybeGuildId, int id)
|
||||||
|
{
|
||||||
|
if (maybeGuildId is { } guildId)
|
||||||
|
{
|
||||||
|
newguildExpressions.AddOrUpdate(guildId,
|
||||||
|
Array.Empty<EllieExpression>(),
|
||||||
|
(key, old) => DeleteInternal(old, id, out _));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
var expr = Array.Find(globalExpressions, item => item.Id == id);
|
||||||
|
if (expr is not null)
|
||||||
|
return _pubSub.Pub(_gexprDeletedkey, expr.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EllieExpression[] DeleteInternal(
|
||||||
|
IReadOnlyList<EllieExpression> exprs,
|
||||||
|
int id,
|
||||||
|
out EllieExpression deleted)
|
||||||
|
{
|
||||||
|
deleted = null;
|
||||||
|
if (exprs is null || exprs.Count == 0)
|
||||||
|
return exprs as EllieExpression[] ?? exprs?.ToArray();
|
||||||
|
|
||||||
|
var newExprs = new EllieExpression[exprs.Count - 1];
|
||||||
|
for (int i = 0, k = 0; i < exprs.Count; i++, k++)
|
||||||
|
{
|
||||||
|
if (exprs[i].Id == id)
|
||||||
|
{
|
||||||
|
deleted = exprs[i];
|
||||||
|
k--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newExprs[k] = exprs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newExprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetExprReactions(ulong? guildId, int id, IEnumerable<string> emojis)
|
||||||
|
{
|
||||||
|
EllieExpression expr;
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
expr = uow.Expressions.GetById(id);
|
||||||
|
if (expr is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
expr.Reactions = string.Join("@@@", emojis);
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateInternalAsync(guildId, expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Sucess, bool NewValue)> ToggleExprOptionAsync(ulong? guildId, int id, ExprField field)
|
||||||
|
{
|
||||||
|
var newVal = false;
|
||||||
|
EllieExpression expr;
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
expr = uow.Expressions.GetById(id);
|
||||||
|
|
||||||
|
if (expr is null || expr.GuildId != guildId)
|
||||||
|
return (false, false);
|
||||||
|
if (field == ExprField.AutoDelete)
|
||||||
|
newVal = expr.AutoDeleteTrigger = !expr.AutoDeleteTrigger;
|
||||||
|
else if (field == ExprField.ContainsAnywhere)
|
||||||
|
newVal = expr.ContainsAnywhere = !expr.ContainsAnywhere;
|
||||||
|
else if (field == ExprField.DmResponse)
|
||||||
|
newVal = expr.DmResponse = !expr.DmResponse;
|
||||||
|
else if (field == ExprField.AllowTarget)
|
||||||
|
newVal = expr.AllowTarget = !expr.AllowTarget;
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateInternalAsync(guildId, expr);
|
||||||
|
|
||||||
|
return (true, newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EllieExpression GetExpression(ulong? guildId, int id)
|
||||||
|
{
|
||||||
|
using var uow = _db.GetDbContext();
|
||||||
|
var expr = uow.Expressions.GetById(id);
|
||||||
|
if (expr is null || expr.GuildId != guildId)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int DeleteAllExpressions(ulong guildId)
|
||||||
|
{
|
||||||
|
using var uow = _db.GetDbContext();
|
||||||
|
var count = uow.Expressions.ClearFromGuild(guildId);
|
||||||
|
uow.SaveChanges();
|
||||||
|
|
||||||
|
newguildExpressions.TryRemove(guildId, out _);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ExpressionExists(ulong? guildId, string input)
|
||||||
|
{
|
||||||
|
input = input.ToLowerInvariant();
|
||||||
|
|
||||||
|
var gexprs = globalExpressions;
|
||||||
|
foreach (var t in gexprs)
|
||||||
|
{
|
||||||
|
if (t.Trigger == input)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guildId is ulong gid && newguildExpressions.TryGetValue(gid, out var guildExprs))
|
||||||
|
{
|
||||||
|
foreach (var t in guildExprs)
|
||||||
|
{
|
||||||
|
if (t.Trigger == input)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExportExpressions(ulong? guildId)
|
||||||
|
{
|
||||||
|
var exprs = GetExpressionsFor(guildId);
|
||||||
|
|
||||||
|
var exprsDict = exprs.GroupBy(x => x.Trigger).ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel));
|
||||||
|
|
||||||
|
return PREPEND_EXPORT + _exportSerializer.Serialize(exprsDict).UnescapeUnicodeCodePoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ImportExpressionsAsync(ulong? guildId, string input)
|
||||||
|
{
|
||||||
|
Dictionary<string, List<ExportedExpr>> data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = Yaml.Deserializer.Deserialize<Dictionary<string, List<ExportedExpr>>>(input);
|
||||||
|
if (data.Sum(x => x.Value.Count) == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
foreach (var entry in data)
|
||||||
|
{
|
||||||
|
var trigger = entry.Key;
|
||||||
|
await uow.Expressions.AddRangeAsync(entry.Value.Where(expr => !string.IsNullOrWhiteSpace(expr.Res))
|
||||||
|
.Select(expr => new EllieExpression
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
Response = expr.Res,
|
||||||
|
Reactions = expr.React?.Join("@@@"),
|
||||||
|
Trigger = trigger,
|
||||||
|
AllowTarget = expr.At,
|
||||||
|
ContainsAnywhere = expr.Ca,
|
||||||
|
DmResponse = expr.Dm,
|
||||||
|
AutoDeleteTrigger = expr.Ad
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
await TriggerReloadExpressions();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
=> await OnExprsShouldReload(true);
|
||||||
|
|
||||||
|
private ValueTask OnExprsShouldReload(bool _)
|
||||||
|
=> new(ReloadInternal(_bot.GetCurrentGuildIds()));
|
||||||
|
|
||||||
|
private ValueTask OnGexprAdded(EllieExpression c)
|
||||||
|
{
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
var newGlobalReactions = new EllieExpression[globalExpressions.Length + 1];
|
||||||
|
Array.Copy(globalExpressions, newGlobalReactions, globalExpressions.Length);
|
||||||
|
newGlobalReactions[globalExpressions.Length] = c;
|
||||||
|
globalExpressions = newGlobalReactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask OnGexprEdited(EllieExpression c)
|
||||||
|
{
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < globalExpressions.Length; i++)
|
||||||
|
{
|
||||||
|
if (globalExpressions[i].Id == c.Id)
|
||||||
|
{
|
||||||
|
globalExpressions[i] = c;
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if edited expr is not found?!
|
||||||
|
// add it
|
||||||
|
OnGexprAdded(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask OnGexprDeleted(int id)
|
||||||
|
{
|
||||||
|
lock (_gexprWriteLock)
|
||||||
|
{
|
||||||
|
var newGlobalReactions = DeleteInternal(globalExpressions, id, out _);
|
||||||
|
globalExpressions = newGlobalReactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task TriggerReloadExpressions()
|
||||||
|
=> _pubSub.Pub(_exprsReloadedKey, true);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Client Event Handlers
|
||||||
|
|
||||||
|
private Task OnLeftGuild(SocketGuild arg)
|
||||||
|
{
|
||||||
|
newguildExpressions.TryRemove(arg.Id, out _);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnJoinedGuild(GuildConfig gc)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var exprs = await uow.Expressions.AsNoTracking().Where(x => x.GuildId == gc.GuildId).ToArrayAsync();
|
||||||
|
|
||||||
|
newguildExpressions[gc.GuildId] = exprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Basic Operations
|
||||||
|
|
||||||
|
public async Task<EllieExpression> AddAsync(ulong? guildId, string key, string message)
|
||||||
|
{
|
||||||
|
key = key.ToLowerInvariant();
|
||||||
|
var expr = new EllieExpression
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
Trigger = key,
|
||||||
|
Response = message
|
||||||
|
};
|
||||||
|
|
||||||
|
if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||||
|
expr.AllowTarget = true;
|
||||||
|
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
uow.Expressions.Add(expr);
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await AddInternalAsync(guildId, expr);
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EllieExpression> EditAsync(ulong? guildId, int id, string message)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var expr = uow.Expressions.GetById(id);
|
||||||
|
|
||||||
|
if (expr is null || expr.GuildId != guildId)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// disable allowtarget if message had target, but it was removed from it
|
||||||
|
if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||||
|
expr.AllowTarget = false;
|
||||||
|
|
||||||
|
expr.Response = message;
|
||||||
|
|
||||||
|
// enable allow target if message is edited to contain target
|
||||||
|
if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||||
|
expr.AllowTarget = true;
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
await UpdateInternalAsync(guildId, expr);
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<EllieExpression> DeleteAsync(ulong? guildId, int id)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var toDelete = uow.Expressions.GetById(id);
|
||||||
|
|
||||||
|
if (toDelete is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ((toDelete.IsGlobal() && guildId is null) || guildId == toDelete.GuildId)
|
||||||
|
{
|
||||||
|
uow.Expressions.Remove(toDelete);
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
await DeleteInternalAsync(guildId, id);
|
||||||
|
return toDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public EllieExpression[] GetExpressionsFor(ulong? maybeGuildId)
|
||||||
|
{
|
||||||
|
if (maybeGuildId is { } guildId)
|
||||||
|
return newguildExpressions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<EllieExpression>();
|
||||||
|
|
||||||
|
return globalExpressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task<bool> ToggleGlobalExpressionsAsync(ulong guildId)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
var gc = ctx.GuildConfigsForId(guildId, set => set);
|
||||||
|
var toReturn = gc.DisableGlobalExpressions = !gc.DisableGlobalExpressions;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (toReturn)
|
||||||
|
_disabledGlobalExpressionGuilds.Add(guildId);
|
||||||
|
else
|
||||||
|
_disabledGlobalExpressionGuilds.TryRemove(guildId);
|
||||||
|
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs
Normal file
27
src/Ellie.Bot.Modules.Expresssions/ExportedExpr.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#nullable disable
|
||||||
|
using Ellie.Services.Database.Models;
|
||||||
|
|
||||||
|
namespace Ellie.Modules.EllieExpressions;
|
||||||
|
|
||||||
|
public class ExportedExpr
|
||||||
|
{
|
||||||
|
public string Res { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public bool Ad { get; set; }
|
||||||
|
public bool Dm { get; set; }
|
||||||
|
public bool At { get; set; }
|
||||||
|
public bool Ca { get; set; }
|
||||||
|
public string[] React;
|
||||||
|
|
||||||
|
public static ExportedExpr FromModel(EllieExpression cr)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Res = cr.Response,
|
||||||
|
Id = ((kwum)cr.Id).ToString(),
|
||||||
|
Ad = cr.AutoDeleteTrigger,
|
||||||
|
At = cr.AllowTarget,
|
||||||
|
Ca = cr.ContainsAnywhere,
|
||||||
|
Dm = cr.DmResponse,
|
||||||
|
React = string.IsNullOrWhiteSpace(cr.Reactions) ? null : cr.GetReactions()
|
||||||
|
};
|
||||||
|
}
|
10
src/Ellie.Bot.Modules.Expresssions/ExprField.cs
Normal file
10
src/Ellie.Bot.Modules.Expresssions/ExprField.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellie.Modules.EllieExpressions;
|
||||||
|
|
||||||
|
public enum ExprField
|
||||||
|
{
|
||||||
|
AutoDelete,
|
||||||
|
DmResponse,
|
||||||
|
AllowTarget,
|
||||||
|
ContainsAnywhere,
|
||||||
|
Message
|
||||||
|
}
|
19
src/Ellie.Bot.Modules.Expresssions/GlobalUsings.cs
Normal file
19
src/Ellie.Bot.Modules.Expresssions/GlobalUsings.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// // global using System.Collections.Concurrent;
|
||||||
|
global using NonBlocking;
|
||||||
|
// // ellie
|
||||||
|
global using Ellise.Common; // new project
|
||||||
|
global using Ellie.Common; // old + ellie specific things
|
||||||
|
global using Ellie.Extensions;
|
||||||
|
|
||||||
|
// discord
|
||||||
|
global using Discord;
|
||||||
|
global using Discord.Commands;
|
||||||
|
global using Discord.Net;
|
||||||
|
global using Discord.WebSocket;
|
||||||
|
|
||||||
|
// aliases
|
||||||
|
global using GuildPerm = Discord.GuildPermission;
|
||||||
|
global using ChannelPerm = Discord.ChannelPermission;
|
||||||
|
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||||
|
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||||
|
global using TypeReaderResult = Ellie.Common.TypeReaders.TypeReaderResult;
|
|
@ -0,0 +1,34 @@
|
||||||
|
#nullable disable
|
||||||
|
using Ellie.Modules.EllieExpressions;
|
||||||
|
using Ellie.Services;
|
||||||
|
|
||||||
|
namespace Ellie.Common.TypeReaders;
|
||||||
|
|
||||||
|
public sealed class CommandOrExprTypeReader : EllieTypeReader<CommandOrExprInfo>
|
||||||
|
{
|
||||||
|
private readonly CommandService _cmds;
|
||||||
|
private readonly ICommandHandler _commandHandler;
|
||||||
|
private readonly EllieExpressionsService _exprs;
|
||||||
|
|
||||||
|
public CommandOrExprTypeReader(CommandService cmds, EllieExpressionsService exprs, ICommandHandler commandHandler)
|
||||||
|
{
|
||||||
|
_cmds = cmds;
|
||||||
|
_commandHandler = commandHandler;
|
||||||
|
_exprs = exprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<TypeReaderResult<CommandOrExprInfo>> ReadAsync(ICommandContext ctx, string input)
|
||||||
|
{
|
||||||
|
if (_exprs.ExpressionExists(ctx.Guild?.Id, input))
|
||||||
|
return TypeReaderResult.FromSuccess(new CommandOrExprInfo(input, CommandOrExprInfo.Type.Custom));
|
||||||
|
|
||||||
|
var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input);
|
||||||
|
if (cmd.IsSuccess)
|
||||||
|
{
|
||||||
|
return TypeReaderResult.FromSuccess(new CommandOrExprInfo(((CommandInfo)cmd.Values.First().Value).Name,
|
||||||
|
CommandOrExprInfo.Type.Normal));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromError<CommandOrExprInfo>(CommandError.ParseFailed, "No such command or expression found.");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue