mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Restructured folders and project names, ci should be fixed
This commit is contained in:
27
src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs
Normal file
27
src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.CustomReactions
|
||||
{
|
||||
public class ExportedExpr
|
||||
{
|
||||
public string Res { 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(CustomReaction cr)
|
||||
=> new ExportedExpr()
|
||||
{
|
||||
Res = cr.Response,
|
||||
Ad = cr.AutoDeleteTrigger,
|
||||
At = cr.AllowTarget,
|
||||
Ca = cr.ContainsAnywhere,
|
||||
Dm = cr.DmResponse,
|
||||
React = string.IsNullOrWhiteSpace(cr.Reactions)
|
||||
? null
|
||||
: cr.GetReactions(),
|
||||
};
|
||||
}
|
||||
}
|
362
src/NadekoBot/Modules/CustomReactions/CustomReactions.cs
Normal file
362
src/NadekoBot/Modules/CustomReactions/CustomReactions.cs
Normal file
@@ -0,0 +1,362 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.CustomReactions.Services;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Common;
|
||||
|
||||
namespace NadekoBot.Modules.CustomReactions
|
||||
{
|
||||
public class CustomReactions : NadekoModule<CustomReactionsService>
|
||||
{
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
|
||||
public CustomReactions(IBotCredentials creds, IHttpClientFactory clientFactory)
|
||||
{
|
||||
_creds = creds;
|
||||
_clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
private bool AdminInGuildOrOwnerInDm() => (ctx.Guild == null && _creds.IsOwner(ctx.User))
|
||||
|| (ctx.Guild != null && ((IGuildUser)ctx.User).GuildPermissions.Administrator);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task AddCustReact(string key, [Leftover] string message)
|
||||
{
|
||||
var channel = ctx.Channel as ITextChannel;
|
||||
if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key))
|
||||
return;
|
||||
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var cr = await _service.AddAsync(ctx.Guild?.Id, key, message);
|
||||
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("new_cust_react"))
|
||||
.WithDescription($"#{(kwum)cr.Id}")
|
||||
.AddField(efb => efb.WithName(GetText("trigger")).WithValue(key))
|
||||
.AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task EditCustReact(kwum id, [Leftover] string message)
|
||||
{
|
||||
var channel = ctx.Channel as ITextChannel;
|
||||
if (string.IsNullOrWhiteSpace(message) || id < 0)
|
||||
return;
|
||||
|
||||
if ((channel == null && !_creds.IsOwner(ctx.User)) || (channel != null && !((IGuildUser)ctx.User).GuildPermissions.Administrator))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var cr = await _service.EditAsync(ctx.Guild?.Id, (int)id, message).ConfigureAwait(false);
|
||||
if (cr != null)
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("edited_cust_react"))
|
||||
.WithDescription($"#{id}")
|
||||
.AddField(efb => efb.WithName(GetText("trigger")).WithValue(cr.Trigger))
|
||||
.AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("edit_fail").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[Priority(1)]
|
||||
public async Task ListCustReact(int page = 1)
|
||||
{
|
||||
if (--page < 0 || page > 999)
|
||||
return;
|
||||
|
||||
var customReactions = _service.GetCustomReactionsFor(ctx.Guild?.Id);
|
||||
|
||||
if (customReactions == null || !customReactions.Any())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("no_found").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page, pageFunc: curPage =>
|
||||
{
|
||||
var desc = customReactions.OrderBy(cr => cr.Trigger)
|
||||
.Skip(curPage * 20)
|
||||
.Take(20)
|
||||
.Select(cr => $"{(cr.ContainsAnywhere ? "🗯" : "◾")}" +
|
||||
$"{(cr.DmResponse ? "✉" : "◾")}" +
|
||||
$"{(cr.AutoDeleteTrigger ? "❌" : "◾")}" +
|
||||
$"`{(kwum) cr.Id}` {cr.Trigger}"
|
||||
+ (string.IsNullOrWhiteSpace(cr.Reactions)
|
||||
? string.Empty
|
||||
: " // " + string.Join(" ", cr.GetReactions())))
|
||||
.JoinWith('\n');
|
||||
|
||||
return new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("custom_reactions"))
|
||||
.WithDescription(desc);
|
||||
|
||||
}, customReactions.Length, 20);
|
||||
}
|
||||
|
||||
public enum All
|
||||
{
|
||||
All
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[Priority(0)]
|
||||
public async Task ListCustReact(All _)
|
||||
{
|
||||
await ReplyPendingLocalizedAsync("obsolete_use", Format.Code($"{Prefix}crsexport"));
|
||||
await CrsExport();
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task ListCustReactG(int page = 1)
|
||||
{
|
||||
await ReplyPendingLocalizedAsync("obsolete_use", Format.Code($"{Prefix}crsexport"));
|
||||
await CrsExport();
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task ShowCustReact(kwum id)
|
||||
{
|
||||
var found = _service.GetCustomReaction(ctx.Guild?.Id, (int)id);
|
||||
|
||||
if (found == null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithDescription($"#{id}")
|
||||
.AddField(efb => efb.WithName(GetText("trigger")).WithValue(found.Trigger.TrimTo(1024)))
|
||||
.AddField(efb => efb.WithName(GetText("response")).WithValue((found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```"))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task DelCustReact(kwum id)
|
||||
{
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var cr = await _service.DeleteAsync(ctx.Guild?.Id, (int)id);
|
||||
|
||||
if (cr != null)
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("deleted"))
|
||||
.WithDescription($"#{id}")
|
||||
.AddField(efb => efb.WithName(GetText("trigger")).WithValue(cr.Trigger.TrimTo(1024)))
|
||||
.AddField(efb => efb.WithName(GetText("response")).WithValue(cr.Response.TrimTo(1024)))).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task CrReact(kwum id, params string[] emojiStrs)
|
||||
{
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var cr = _service.GetCustomReaction(Context.Guild?.Id, id);
|
||||
if (cr is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("no_found").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (emojiStrs.Length == 0)
|
||||
{
|
||||
await _service.ResetCrReactions(ctx.Guild?.Id, id);
|
||||
await ReplyConfirmLocalizedAsync("crr_reset", Format.Bold(id.ToString())).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> 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 Context.Message.AddReactionAsync(emote).ConfigureAwait(false);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
succ.Add(emojiStr);
|
||||
|
||||
if (succ.Count >= 3)
|
||||
break;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if(succ.Count == 0)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("invalid_emojis").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await _service.SetCrReactions(ctx.Guild?.Id, id, succ);
|
||||
|
||||
|
||||
await ReplyConfirmLocalizedAsync("crr_set", Format.Bold(id.ToString()), string.Join(", ", succ.Select(x => x.ToString()))).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public Task CrCa(kwum id)
|
||||
=> InternalCrEdit(id, CustomReactionsService.CrField.ContainsAnywhere);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public Task CrDm(kwum id)
|
||||
=> InternalCrEdit(id, CustomReactionsService.CrField.DmResponse);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public Task CrAd(kwum id)
|
||||
=> InternalCrEdit(id, CustomReactionsService.CrField.AutoDelete);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public Task CrAt(kwum id)
|
||||
=> InternalCrEdit(id, CustomReactionsService.CrField.AllowTarget);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[OwnerOnly]
|
||||
public async Task CrsReload()
|
||||
{
|
||||
await _service.TriggerReloadCustomReactions();
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
private async Task InternalCrEdit(kwum id, CustomReactionsService.CrField option)
|
||||
{
|
||||
var cr = _service.GetCustomReaction(ctx.Guild?.Id, id);
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var (success, newVal) = await _service.ToggleCrOptionAsync(id, option).ConfigureAwait(false);
|
||||
if (!success)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newVal)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("option_enabled", Format.Code(option.ToString()), Format.Code(id.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("option_disabled", Format.Code(option.ToString()), Format.Code(id.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task CrClear()
|
||||
{
|
||||
if (await PromptUserConfirmAsync(new EmbedBuilder()
|
||||
.WithTitle("Custom reaction clear")
|
||||
.WithDescription("This will delete all custom reactions on this server.")).ConfigureAwait(false))
|
||||
{
|
||||
var count = _service.DeleteAllCustomReactions(ctx.Guild.Id);
|
||||
await ReplyConfirmLocalizedAsync("cleared", count).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task CrsExport()
|
||||
{
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var serialized = _service.ExportCrs(ctx.Guild?.Id);
|
||||
using var stream = await serialized.ToStream();
|
||||
await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
#if GLOBAL_NADEKO
|
||||
[OwnerOnly]
|
||||
#endif
|
||||
public async Task CrsImport([Leftover]string input = null)
|
||||
{
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
input = input?.Trim();
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
if (input is null)
|
||||
{
|
||||
var attachment = ctx.Message.Attachments.FirstOrDefault();
|
||||
if (attachment is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("expr_import_no_input");
|
||||
return;
|
||||
}
|
||||
|
||||
using var client = _clientFactory.CreateClient();
|
||||
input = await client.GetStringAsync(attachment.Url);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("expr_import_no_input");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var succ = await _service.ImportCrsAsync(ctx.Guild?.Id, input);
|
||||
if (!succ)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("expr_import_invalid_data");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
}
|
||||
}
|
167
src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs
Normal file
167
src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Html.Dom;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Replacements;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.CustomReactions.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
private static readonly Regex imgRegex = new Regex("%(img|image):(?<tag>.*?)%", RegexOptions.Compiled);
|
||||
|
||||
private static Dictionary<Regex, Func<Match, Task<string>>> regexPlaceholders { get; } = new Dictionary<Regex, Func<Match, Task<string>>>()
|
||||
{
|
||||
{ imgRegex, async (match) => {
|
||||
var tag = match.Groups["tag"].ToString();
|
||||
if(string.IsNullOrWhiteSpace(tag))
|
||||
return "";
|
||||
|
||||
var fullQueryLink = $"http://imgur.com/search?q={ tag }";
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
using(var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false))
|
||||
{
|
||||
var elems = document.QuerySelectorAll("a.image-list-link").ToArray();
|
||||
|
||||
if (!elems.Any())
|
||||
return "";
|
||||
|
||||
var img = (elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Length))?.Children?.FirstOrDefault() as IHtmlImageElement);
|
||||
|
||||
if (img?.Source == null)
|
||||
return "";
|
||||
|
||||
return " " + img.Source.Replace("b.", ".", StringComparison.InvariantCulture) + " ";
|
||||
}
|
||||
} }
|
||||
};
|
||||
|
||||
private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client)
|
||||
=> str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
|
||||
|
||||
private static async Task<string> ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger, bool containsAnywhere)
|
||||
{
|
||||
var substringIndex = resolvedTrigger.Length;
|
||||
if (containsAnywhere)
|
||||
{
|
||||
var pos = ctx.Content.AsSpan().GetWordPosition(resolvedTrigger);
|
||||
if (pos == WordPosition.Start)
|
||||
substringIndex += 1;
|
||||
else if (pos == WordPosition.End)
|
||||
substringIndex = ctx.Content.Length;
|
||||
else if (pos == WordPosition.Middle)
|
||||
substringIndex += ctx.Content.IndexOf(resolvedTrigger, 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.Substring(substringIndex).Trim()
|
||||
: ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true))
|
||||
.Build();
|
||||
|
||||
str = rep.Replace(str);
|
||||
#if !GLOBAL_NADEKO
|
||||
foreach (var ph in regexPlaceholders)
|
||||
{
|
||||
str = await ph.Key.ReplaceAsync(str, ph.Value).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
return str;
|
||||
}
|
||||
|
||||
public static Task<string> ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool containsAnywhere)
|
||||
=> cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client), containsAnywhere);
|
||||
|
||||
public static async Task<IUserMessage> Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool sanitize)
|
||||
{
|
||||
var channel = cr.DmResponse ? await ctx.Author.GetOrCreateDMChannelAsync().ConfigureAwait(false) : ctx.Channel;
|
||||
|
||||
if (CREmbed.TryParse(cr.Response, out CREmbed crembed))
|
||||
{
|
||||
var trigger = cr.Trigger.ResolveTriggerString(ctx, 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.Substring(substringIndex).Trim()
|
||||
: ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true))
|
||||
.Build();
|
||||
|
||||
rep.Replace(crembed);
|
||||
|
||||
return await channel.EmbedAsync(crembed, sanitize).ConfigureAwait(false);
|
||||
}
|
||||
return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client, cr.ContainsAnywhere).ConfigureAwait(false)).SanitizeMentions(sanitize)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[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 >= 'a' && ch <= 'z')
|
||||
return false;
|
||||
if (ch >= 'A' && ch <= 'Z')
|
||||
return false;
|
||||
if (ch >= '1' && ch <= '9')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public enum WordPosition
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
}
|
@@ -0,0 +1,727 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.CustomReactions.Extensions;
|
||||
using NadekoBot.Modules.Permissions.Common;
|
||||
using NadekoBot.Modules.Permissions.Services;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common.Yml;
|
||||
using NadekoBot.Core.Common;
|
||||
using Serilog;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.CustomReactions.Services
|
||||
{
|
||||
public sealed class CustomReactionsService : IEarlyBehavior, INService, IReadyExecutor
|
||||
{
|
||||
public enum CrField
|
||||
{
|
||||
AutoDelete,
|
||||
DmResponse,
|
||||
AllowTarget,
|
||||
ContainsAnywhere,
|
||||
Message,
|
||||
}
|
||||
|
||||
private readonly object _gcrWriteLock = new object();
|
||||
|
||||
private readonly TypedKey<CustomReaction> _gcrAddedKey = new TypedKey<CustomReaction>("gcr.added");
|
||||
private readonly TypedKey<int> _gcrDeletedkey = new TypedKey<int>("gcr.deleted");
|
||||
private readonly TypedKey<CustomReaction> _gcrEditedKey = new TypedKey<CustomReaction>("gcr.edited");
|
||||
private readonly TypedKey<bool> _crsReloadedKey = new TypedKey<bool>("crs.reloaded");
|
||||
private const string MentionPh = "%bot.mention%";
|
||||
|
||||
// it is perfectly fine to have global customreactions as an array
|
||||
// 1. custom reactions 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 CustomReaction[] _globalReactions;
|
||||
private ConcurrentDictionary<ulong, CustomReaction[]> _newGuildReactions;
|
||||
|
||||
public int Priority => -1;
|
||||
public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly PermissionService _perms;
|
||||
private readonly CommandHandler _cmd;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly NadekoBot _bot;
|
||||
private readonly GlobalPermissionService _gperm;
|
||||
private readonly CmdCdService _cmdCds;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly Random _rng;
|
||||
|
||||
public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, NadekoBot bot,
|
||||
DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds,
|
||||
IPubSub pubSub)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_perms = perms;
|
||||
_cmd = cmd;
|
||||
_strings = strings;
|
||||
_bot = bot;
|
||||
_gperm = gperm;
|
||||
_cmdCds = cmdCds;
|
||||
_pubSub = pubSub;
|
||||
_rng = new NadekoRandom();
|
||||
|
||||
_pubSub.Sub(_crsReloadedKey, OnCrsShouldReload);
|
||||
pubSub.Sub(_gcrAddedKey, OnGcrAdded);
|
||||
pubSub.Sub(_gcrDeletedkey, OnGcrDeleted);
|
||||
pubSub.Sub(_gcrEditedKey, OnGcrEdited);
|
||||
|
||||
bot.JoinedGuild += OnJoinedGuild;
|
||||
_client.LeftGuild += OnLeftGuild;
|
||||
}
|
||||
|
||||
private async Task ReloadInternal(IReadOnlyList<ulong> allGuildIds)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var guildItems = await uow._context.CustomReactions
|
||||
.AsNoTracking()
|
||||
.Where(x => allGuildIds.Contains(x.GuildId.Value))
|
||||
.ToListAsync();
|
||||
|
||||
_newGuildReactions = guildItems
|
||||
.GroupBy(k => k.GuildId!.Value)
|
||||
.ToDictionary(g => g.Key,
|
||||
g => g.Select(x =>
|
||||
{
|
||||
x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention);
|
||||
return x;
|
||||
}).ToArray())
|
||||
.ToConcurrent();
|
||||
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
var globalItems = uow._context
|
||||
.CustomReactions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == null || x.GuildId == 0)
|
||||
.AsEnumerable()
|
||||
.Select(x =>
|
||||
{
|
||||
x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention);
|
||||
return x;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
_globalReactions = globalItems;
|
||||
}
|
||||
|
||||
ready = true;
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
public Task OnReadyAsync()
|
||||
=> ReloadInternal(_bot.GetCurrentGuildIds());
|
||||
|
||||
private ValueTask OnCrsShouldReload(bool _)
|
||||
=> new ValueTask(ReloadInternal(_bot.GetCurrentGuildIds()));
|
||||
|
||||
private ValueTask OnGcrAdded(CustomReaction c)
|
||||
{
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
var newGlobalReactions = new CustomReaction[_globalReactions.Length + 1];
|
||||
Array.Copy(_globalReactions, newGlobalReactions, _globalReactions.Length);
|
||||
newGlobalReactions[_globalReactions.Length] = c;
|
||||
_globalReactions = newGlobalReactions;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private ValueTask OnGcrEdited(CustomReaction c)
|
||||
{
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
for (var i = 0; i < _globalReactions.Length; i++)
|
||||
{
|
||||
if (_globalReactions[i].Id == c.Id)
|
||||
{
|
||||
_globalReactions[i] = c;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// if edited cr is not found?!
|
||||
// add it
|
||||
OnGcrAdded(c);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private ValueTask OnGcrDeleted(int id)
|
||||
{
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
var newGlobalReactions = DeleteInternal(_globalReactions, id, out _);
|
||||
_globalReactions = newGlobalReactions;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public Task TriggerReloadCustomReactions()
|
||||
=> _pubSub.Pub(_crsReloadedKey, true);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client Event Handlers
|
||||
|
||||
private Task OnLeftGuild(SocketGuild arg)
|
||||
{
|
||||
_newGuildReactions.TryRemove(arg.Id, out _);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnJoinedGuild(GuildConfig gc)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var crs = await uow._context
|
||||
.CustomReactions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == gc.GuildId)
|
||||
.ToArrayAsync();
|
||||
|
||||
_newGuildReactions[gc.GuildId] = crs;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Operations
|
||||
|
||||
public async Task<CustomReaction> AddAsync(ulong? guildId, string key, string message)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
var cr = new CustomReaction()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Trigger = key,
|
||||
Response = message,
|
||||
};
|
||||
|
||||
if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||
cr.AllowTarget = true;
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.CustomReactions.Add(cr);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await AddInternalAsync(guildId, cr);
|
||||
|
||||
return cr;
|
||||
}
|
||||
|
||||
public async Task<CustomReaction> EditAsync(ulong? guildId, int id, string message)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var cr = uow.CustomReactions.GetById(id);
|
||||
|
||||
if (cr == null || cr.GuildId != guildId)
|
||||
return null;
|
||||
|
||||
// disable allowtarget if message had target, but it was removed from it
|
||||
if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase)
|
||||
&& cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cr.AllowTarget = false;
|
||||
}
|
||||
|
||||
cr.Response = message;
|
||||
|
||||
// enable allow target if message is edited to contain target
|
||||
if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
|
||||
cr.AllowTarget = true;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
await UpdateInternalAsync(guildId, cr);
|
||||
|
||||
return cr;
|
||||
}
|
||||
|
||||
|
||||
public async Task<CustomReaction> DeleteAsync(ulong? guildId, int id)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var toDelete = uow.CustomReactions.GetById(id);
|
||||
|
||||
if (toDelete is null)
|
||||
return null;
|
||||
|
||||
if ((toDelete.IsGlobal() && guildId == null) || (guildId == toDelete.GuildId))
|
||||
{
|
||||
uow.CustomReactions.Remove(toDelete);
|
||||
await uow.SaveChangesAsync();
|
||||
await DeleteInternalAsync(guildId, id);
|
||||
return toDelete;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public CustomReaction[] GetCustomReactionsFor(ulong? maybeGuildId)
|
||||
{
|
||||
if (maybeGuildId is ulong guildId)
|
||||
{
|
||||
return _newGuildReactions.TryGetValue(guildId, out var crs)
|
||||
? crs
|
||||
: Array.Empty<CustomReaction>();
|
||||
}
|
||||
|
||||
return _globalReactions;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool ready;
|
||||
|
||||
private CustomReaction TryGetCustomReaction(IUserMessage umsg)
|
||||
{
|
||||
if (!ready)
|
||||
return null;
|
||||
|
||||
if (!(umsg.Channel is SocketTextChannel channel))
|
||||
return null;
|
||||
|
||||
var content = umsg.Content.Trim().ToLowerInvariant();
|
||||
|
||||
if (_newGuildReactions.TryGetValue(channel.Guild.Id, out var reactions) && reactions.Length > 0)
|
||||
{
|
||||
var cr = MatchCustomReactions(content, reactions);
|
||||
if (!(cr is null))
|
||||
return cr;
|
||||
}
|
||||
|
||||
var localGrs = _globalReactions;
|
||||
|
||||
return MatchCustomReactions(content, localGrs);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private CustomReaction MatchCustomReactions(in ReadOnlySpan<char> content, CustomReaction[] crs)
|
||||
{
|
||||
var result = new List<CustomReaction>(1);
|
||||
for (var i = 0; i < crs.Length; i++)
|
||||
{
|
||||
var cr = crs[i];
|
||||
var trigger = cr.Trigger;
|
||||
if (content.Length > trigger.Length)
|
||||
{
|
||||
// if input is greater than the trigger, it can only work if:
|
||||
// it has CA enabled
|
||||
if (cr.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(cr);
|
||||
}
|
||||
|
||||
// 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 CR has AllowTarget, then the
|
||||
// content has to start with the trigger followed by a space
|
||||
if (cr.AllowTarget && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase)
|
||||
&& content[trigger.Length] == ' ')
|
||||
{
|
||||
result.Add(cr);
|
||||
}
|
||||
}
|
||||
else if (content.Length < cr.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(cr.Trigger))
|
||||
{
|
||||
result.Add(cr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count == 0)
|
||||
return null;
|
||||
|
||||
var cancelled = result.FirstOrDefault(x => x.Response == "-");
|
||||
if (!(cancelled is null))
|
||||
return cancelled;
|
||||
|
||||
return result[_rng.Next(0, result.Count)];
|
||||
}
|
||||
|
||||
public async Task<bool> RunBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg)
|
||||
{
|
||||
// maybe this message is a custom reaction
|
||||
var cr = TryGetCustomReaction(msg);
|
||||
|
||||
if (cr is null || cr.Response == "-")
|
||||
return false;
|
||||
|
||||
if(await _cmdCds.TryBlock(guild, msg.Author, cr.Trigger))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (_gperm.BlockedModules.Contains("ActualCustomReactions"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (guild is SocketGuild sg)
|
||||
{
|
||||
var pc = _perms.GetCacheFor(guild.Id);
|
||||
if (!pc.Permissions.CheckPermissions(msg, cr.Trigger, "ActualCustomReactions",
|
||||
out int index))
|
||||
{
|
||||
if (pc.Verbose)
|
||||
{
|
||||
var returnMsg = _strings.GetText("trigger", sg.Id,
|
||||
index + 1,
|
||||
Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg)));
|
||||
try
|
||||
{
|
||||
await msg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Log.Information(returnMsg);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var sentMsg = await cr.Send(msg, _client, false).ConfigureAwait(false);
|
||||
|
||||
var reactions = cr.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,
|
||||
cr.GuildId);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
if (cr.AutoDeleteTrigger)
|
||||
{
|
||||
try
|
||||
{
|
||||
await msg.DeleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task ResetCrReactions(ulong? maybeGuildId, int id)
|
||||
{
|
||||
CustomReaction cr;
|
||||
using var uow = _db.GetDbContext();
|
||||
cr = uow.CustomReactions.GetById(id);
|
||||
if (cr is null)
|
||||
return;
|
||||
|
||||
cr.Reactions = string.Empty;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private Task UpdateInternalAsync(ulong? maybeGuildId, CustomReaction cr)
|
||||
{
|
||||
if (maybeGuildId is ulong guildId)
|
||||
UpdateInternal(guildId, cr);
|
||||
else
|
||||
return _pubSub.Pub(_gcrEditedKey, cr);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void UpdateInternal(ulong? maybeGuildId, CustomReaction cr)
|
||||
{
|
||||
if (maybeGuildId is ulong guildId)
|
||||
{
|
||||
_newGuildReactions.AddOrUpdate(guildId, new[] {cr},
|
||||
(key, old) =>
|
||||
{
|
||||
var newArray = old.ToArray();
|
||||
for (var i = 0; i < newArray.Length; i++)
|
||||
{
|
||||
if (newArray[i].Id == cr.Id)
|
||||
newArray[i] = cr;
|
||||
}
|
||||
return newArray;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
var crs = _globalReactions;
|
||||
for (var i = 0; i < crs.Length; i++)
|
||||
{
|
||||
if (crs[i].Id == cr.Id)
|
||||
crs[i] = cr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task AddInternalAsync(ulong? maybeGuildId, CustomReaction cr)
|
||||
{
|
||||
// only do this for perf purposes
|
||||
cr.Trigger = cr.Trigger.Replace(MentionPh, _client.CurrentUser.Mention);
|
||||
|
||||
if (maybeGuildId is ulong guildId)
|
||||
{
|
||||
_newGuildReactions.AddOrUpdate(guildId,
|
||||
new[] {cr},
|
||||
(key, old) => old.With(cr));
|
||||
}
|
||||
else
|
||||
{
|
||||
return _pubSub.Pub(_gcrAddedKey, cr);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task DeleteInternalAsync(ulong? maybeGuildId, int id)
|
||||
{
|
||||
if (maybeGuildId is ulong guildId)
|
||||
{
|
||||
_newGuildReactions.AddOrUpdate(guildId,
|
||||
Array.Empty<CustomReaction>(),
|
||||
(key, old) => DeleteInternal(old, id, out _));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
lock (_gcrWriteLock)
|
||||
{
|
||||
var cr = Array.Find(_globalReactions, item => item.Id == id);
|
||||
if (!(cr is null))
|
||||
{
|
||||
return _pubSub.Pub(_gcrDeletedkey, cr.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private CustomReaction[] DeleteInternal(IReadOnlyList<CustomReaction> crs, int id, out CustomReaction deleted)
|
||||
{
|
||||
deleted = null;
|
||||
if (crs is null || crs.Count == 0)
|
||||
return crs as CustomReaction[] ?? crs?.ToArray();
|
||||
|
||||
var newCrs = new CustomReaction[crs.Count - 1];
|
||||
for (int i = 0, k = 0; i < crs.Count; i++, k++)
|
||||
{
|
||||
if (crs[i].Id == id)
|
||||
{
|
||||
deleted = crs[i];
|
||||
k--;
|
||||
continue;
|
||||
}
|
||||
|
||||
newCrs[k] = crs[i];
|
||||
}
|
||||
|
||||
return newCrs;
|
||||
}
|
||||
|
||||
public async Task SetCrReactions(ulong? guildId, int id, IEnumerable<string> emojis)
|
||||
{
|
||||
CustomReaction cr;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
cr = uow.CustomReactions.GetById(id);
|
||||
if (cr is null)
|
||||
return;
|
||||
|
||||
cr.Reactions = string.Join("@@@", emojis);
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await UpdateInternalAsync(guildId, cr);
|
||||
}
|
||||
|
||||
public async Task<(bool Sucess, bool NewValue)> ToggleCrOptionAsync(int id, CrField field)
|
||||
{
|
||||
var newVal = false;
|
||||
CustomReaction cr;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
cr = uow.CustomReactions.GetById(id);
|
||||
if (cr is null)
|
||||
return (false, false);
|
||||
if (field == CrField.AutoDelete)
|
||||
newVal = cr.AutoDeleteTrigger = !cr.AutoDeleteTrigger;
|
||||
else if (field == CrField.ContainsAnywhere)
|
||||
newVal = cr.ContainsAnywhere = !cr.ContainsAnywhere;
|
||||
else if (field == CrField.DmResponse)
|
||||
newVal = cr.DmResponse = !cr.DmResponse;
|
||||
else if (field == CrField.AllowTarget)
|
||||
newVal = cr.AllowTarget = !cr.AllowTarget;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await UpdateInternalAsync(cr.GuildId, cr);
|
||||
|
||||
return (true, newVal);
|
||||
}
|
||||
|
||||
public CustomReaction GetCustomReaction(ulong? guildId, int id)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var cr = uow.CustomReactions.GetById(id);
|
||||
if (cr == null || cr.GuildId != guildId)
|
||||
return null;
|
||||
|
||||
return cr;
|
||||
}
|
||||
|
||||
public int DeleteAllCustomReactions(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var count = uow.CustomReactions.ClearFromGuild(guildId);
|
||||
uow.SaveChanges();
|
||||
|
||||
_newGuildReactions.TryRemove(guildId, out _);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public bool ReactionExists(ulong? guildId, string input)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var cr = uow.CustomReactions.GetByGuildIdAndInput(guildId, input);
|
||||
return cr != null;
|
||||
}
|
||||
|
||||
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
|
||||
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
|
||||
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
|
||||
.WithIndentedSequences()
|
||||
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
|
||||
.DisableAliases()
|
||||
.Build();
|
||||
|
||||
private const string _prependExport =
|
||||
@"# Keys are triggers, Each key has a LIST of custom reactions in the following format:
|
||||
# - res: Response string
|
||||
# react:
|
||||
# - <List
|
||||
# - of
|
||||
# - reactions>
|
||||
# at: Whether custom reaction allows targets (see .h .crat)
|
||||
# ca: Whether custom reaction expects trigger anywhere (see .h .crca)
|
||||
# dm: Whether custom reaction DMs the response (see .h .crdm)
|
||||
# ad: Whether custom reaction automatically deletes triggering message (see .h .crad)
|
||||
|
||||
";
|
||||
public string ExportCrs(ulong? guildId)
|
||||
{
|
||||
var crs = GetCustomReactionsFor(guildId);
|
||||
|
||||
var crsDict = crs
|
||||
.GroupBy(x => x.Trigger)
|
||||
.ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel));
|
||||
|
||||
return _prependExport + _exportSerializer
|
||||
.Serialize(crsDict)
|
||||
.UnescapeUnicodeCodePoints();
|
||||
}
|
||||
|
||||
public async Task<bool> ImportCrsAsync(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;
|
||||
}
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
foreach (var entry in data)
|
||||
{
|
||||
var trigger = entry.Key;
|
||||
await uow._context.CustomReactions.AddRangeAsync(entry.Value
|
||||
.Where(cr => !string.IsNullOrWhiteSpace(cr.Res))
|
||||
.Select(cr => new CustomReaction()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Response = cr.Res,
|
||||
Reactions = cr.React?.JoinWith("@@@"),
|
||||
Trigger = trigger,
|
||||
AllowTarget = cr.At,
|
||||
ContainsAnywhere = cr.Ca,
|
||||
DmResponse = cr.Dm,
|
||||
AutoDeleteTrigger = cr.Ad,
|
||||
}));
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
await TriggerReloadCustomReactions();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user