mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Begun reorganizing
This commit is contained in:
		@@ -1,46 +0,0 @@
 | 
			
		||||
using OneOf;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public static class BotCacheExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static async ValueTask<T?> GetOrDefaultAsync<T>(this IBotCache cache, TypedKey<T> key)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await cache.GetAsync(key);
 | 
			
		||||
        if (result.TryGetValue(out var val))
 | 
			
		||||
            return val;
 | 
			
		||||
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private static TypedKey<byte[]> GetImgKey(Uri uri)
 | 
			
		||||
        => new($"image:{uri}");
 | 
			
		||||
 | 
			
		||||
    public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data)
 | 
			
		||||
        => c.SetImageDataAsync(new Uri(key), data);
 | 
			
		||||
    public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data)
 | 
			
		||||
        => await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48));
 | 
			
		||||
 | 
			
		||||
    public static async ValueTask<OneOf<byte[], None>> GetImageDataAsync(this IBotCache c, Uri key)
 | 
			
		||||
        => await c.GetAsync(GetImgKey(key));
 | 
			
		||||
 | 
			
		||||
    public static async Task<TimeSpan?> GetRatelimitAsync(
 | 
			
		||||
        this IBotCache c,
 | 
			
		||||
        TypedKey<long> key,
 | 
			
		||||
        TimeSpan length)
 | 
			
		||||
    {
 | 
			
		||||
        var now = DateTime.UtcNow;
 | 
			
		||||
        var nowB = now.ToBinary();
 | 
			
		||||
 | 
			
		||||
        var cachedValue = await c.GetOrAddAsync(key,
 | 
			
		||||
            () => Task.FromResult(now.ToBinary()),
 | 
			
		||||
            expiry: length);
 | 
			
		||||
 | 
			
		||||
        if (cachedValue == nowB)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var diff = now - DateTime.FromBinary(cachedValue);
 | 
			
		||||
        return length - diff;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,47 +0,0 @@
 | 
			
		||||
using OneOf;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public interface IBotCache
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds an item to the cache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="key">Key to add</param>
 | 
			
		||||
    /// <param name="value">Value to add to the cache</param>
 | 
			
		||||
    /// <param name="expiry">Optional expiry</param>
 | 
			
		||||
    /// <param name="overwrite">Whether old value should be overwritten</param>
 | 
			
		||||
    /// <typeparam name="T">Type of the value</typeparam>
 | 
			
		||||
    /// <returns>Returns whether add was sucessful. Always true unless ovewrite = false</returns>
 | 
			
		||||
    ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true);
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get an element from the cache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="key">Key</param>
 | 
			
		||||
    /// <typeparam name="T">Type of the value</typeparam>
 | 
			
		||||
    /// <returns>Either a value or <see cref="None"/></returns>
 | 
			
		||||
    ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key);
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Remove a key from the cache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="key">Key to remove</param>
 | 
			
		||||
    /// <typeparam name="T">Type of the value</typeparam>
 | 
			
		||||
    /// <returns>Whether there was item</returns>
 | 
			
		||||
    ValueTask<bool> RemoveAsync<T>(TypedKey<T> key);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get the key if it exists or add a new one
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="key">Key to get and potentially add</param>
 | 
			
		||||
    /// <param name="createFactory">Value creation factory</param>
 | 
			
		||||
    /// <param name="expiry">Optional expiry</param>
 | 
			
		||||
    /// <typeparam name="T">Type of the value</typeparam>
 | 
			
		||||
    /// <returns>The retrieved or newly added value</returns>
 | 
			
		||||
    ValueTask<T?> GetOrAddAsync<T>(
 | 
			
		||||
        TypedKey<T> key,
 | 
			
		||||
        Func<Task<T?>> createFactory,
 | 
			
		||||
        TimeSpan? expiry = null);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using OneOf;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
 | 
			
		||||
// ReSharper disable InconsistentlySynchronizedField
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class MemoryBotCache : IBotCache
 | 
			
		||||
{
 | 
			
		||||
    // needed for overwrites and Delete return value
 | 
			
		||||
    private readonly object _cacheLock = new object();
 | 
			
		||||
    private readonly MemoryCache _cache;
 | 
			
		||||
 | 
			
		||||
    public MemoryBotCache()
 | 
			
		||||
    {
 | 
			
		||||
        _cache = new MemoryCache(new MemoryCacheOptions());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (overwrite)
 | 
			
		||||
        {
 | 
			
		||||
            using var item = _cache.CreateEntry(key.Key);
 | 
			
		||||
            item.Value = value;
 | 
			
		||||
            item.AbsoluteExpirationRelativeToNow = expiry;
 | 
			
		||||
            return new(true);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        lock (_cacheLock)
 | 
			
		||||
        {
 | 
			
		||||
            if (_cache.TryGetValue(key.Key, out var old) && old is not null)
 | 
			
		||||
                return new(false);
 | 
			
		||||
            
 | 
			
		||||
            using var item = _cache.CreateEntry(key.Key);
 | 
			
		||||
            item.Value = value;
 | 
			
		||||
            item.AbsoluteExpirationRelativeToNow = expiry;
 | 
			
		||||
            return new(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<T?> GetOrAddAsync<T>(
 | 
			
		||||
        TypedKey<T> key,
 | 
			
		||||
        Func<Task<T?>> createFactory,
 | 
			
		||||
        TimeSpan? expiry = null)
 | 
			
		||||
        => await _cache.GetOrCreateAsync(key.Key,
 | 
			
		||||
            async ce =>
 | 
			
		||||
            {
 | 
			
		||||
                ce.AbsoluteExpirationRelativeToNow = expiry;
 | 
			
		||||
                var val = await createFactory();
 | 
			
		||||
                return val;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_cache.TryGetValue(key.Key, out var val) || val is null)
 | 
			
		||||
            return new(new None());
 | 
			
		||||
 | 
			
		||||
        return new((T)val);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_cacheLock)
 | 
			
		||||
        {
 | 
			
		||||
            var toReturn = _cache.TryGetValue(key.Key, out var old ) && old is not null;
 | 
			
		||||
            _cache.Remove(key.Key);
 | 
			
		||||
            return new(toReturn);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
using OneOf;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public static class OneOfExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static bool TryGetValue<T>(this OneOf<T, None> oneOf, out T value)
 | 
			
		||||
        => oneOf.TryPickT0(out value, out _);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,80 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class EventPubSub : IPubSub
 | 
			
		||||
{
 | 
			
		||||
    private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions = new();
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
        where TData : notnull
 | 
			
		||||
    {
 | 
			
		||||
        Func<object, ValueTask> localAction = obj => action((TData)obj);
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_actions.TryGetValue(key.Key, out var keyActions))
 | 
			
		||||
            {
 | 
			
		||||
                keyActions = new();
 | 
			
		||||
                _actions[key.Key] = keyActions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!keyActions.TryGetValue(action, out var sameActions))
 | 
			
		||||
            {
 | 
			
		||||
                sameActions = new();
 | 
			
		||||
                keyActions[action] = sameActions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sameActions.Add(localAction);
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
        where TData : notnull
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (_actions.TryGetValue(key.Key, out var actions))
 | 
			
		||||
                // if this class ever gets used, this needs to be properly implemented
 | 
			
		||||
                // 1. ignore all valuetasks which are completed
 | 
			
		||||
                // 2. run all other tasks in parallel
 | 
			
		||||
                return actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()).WhenAll();
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            // get subscriptions for this action
 | 
			
		||||
            if (_actions.TryGetValue(key.Key, out var actions))
 | 
			
		||||
                // get subscriptions which have the same action hash code
 | 
			
		||||
                // note: having this as a list allows for multiple subscriptions of
 | 
			
		||||
                //       the same insance's/static method
 | 
			
		||||
            {
 | 
			
		||||
                if (actions.TryGetValue(action, out var sameActions))
 | 
			
		||||
                {
 | 
			
		||||
                    // remove last subscription
 | 
			
		||||
                    sameActions.RemoveAt(sameActions.Count - 1);
 | 
			
		||||
 | 
			
		||||
                    // if the last subscription was the only subscription
 | 
			
		||||
                    // we can safely remove this action's dictionary entry
 | 
			
		||||
                    if (sameActions.Count == 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        actions.Remove(action);
 | 
			
		||||
 | 
			
		||||
                        // if our dictionary has no more elements after 
 | 
			
		||||
                        // removing the entry
 | 
			
		||||
                        // it's safe to remove it from the key's subscriptions
 | 
			
		||||
                        if (actions.Count == 0)
 | 
			
		||||
                            _actions.Remove(key.Key);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public interface IPubSub
 | 
			
		||||
{
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
        where TData : notnull;
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
        where TData : notnull;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public interface ISeria
 | 
			
		||||
{
 | 
			
		||||
    byte[] Serialize<T>(T data);
 | 
			
		||||
    T? Deserialize<T>(byte[]? data);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public readonly struct TypedKey<TData>
 | 
			
		||||
{
 | 
			
		||||
    public string Key { get; }
 | 
			
		||||
 | 
			
		||||
    public TypedKey(in string key)
 | 
			
		||||
        => Key = key;
 | 
			
		||||
 | 
			
		||||
    public static implicit operator TypedKey<TData>(in string input)
 | 
			
		||||
        => new(input);
 | 
			
		||||
 | 
			
		||||
    public static implicit operator string(in TypedKey<TData> input)
 | 
			
		||||
        => input.Key;
 | 
			
		||||
 | 
			
		||||
    public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
 | 
			
		||||
        => left.Key == right.Key;
 | 
			
		||||
 | 
			
		||||
    public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
 | 
			
		||||
        => !(left == right);
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object? obj)
 | 
			
		||||
        => obj is TypedKey<TData> o && o == this;
 | 
			
		||||
 | 
			
		||||
    public override int GetHashCode()
 | 
			
		||||
        => Key?.GetHashCode() ?? 0;
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
        => Key;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
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(NadekoExpression 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()
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
public enum ExprField
 | 
			
		||||
{
 | 
			
		||||
    AutoDelete,
 | 
			
		||||
    DmResponse,
 | 
			
		||||
    AllowTarget,
 | 
			
		||||
    ContainsAnywhere,
 | 
			
		||||
    Message
 | 
			
		||||
}
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
public static class NadekoExpressionExtensions
 | 
			
		||||
{
 | 
			
		||||
    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 NadekoExpression 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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,393 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
[Name("Expressions")]
 | 
			
		||||
public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
 | 
			
		||||
{
 | 
			
		||||
    public enum All
 | 
			
		||||
    {
 | 
			
		||||
        All
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private readonly IHttpClientFactory _clientFactory;
 | 
			
		||||
 | 
			
		||||
    public NadekoExpressions(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_NADEKO
 | 
			
		||||
    [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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,754 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using YamlDotNet.Serialization;
 | 
			
		||||
using YamlDotNet.Serialization.NamingConventions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
public sealed class NadekoExpressionsService : 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<NadekoExpression> _gexprAddedKey = new("gexpr.added");
 | 
			
		||||
    private readonly TypedKey<int> _gexprDeletedkey = new("gexpr.deleted");
 | 
			
		||||
    private readonly TypedKey<NadekoExpression> _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 NadekoExpression[] globalExpressions = Array.Empty<NadekoExpression>();
 | 
			
		||||
    private ConcurrentDictionary<ulong, NadekoExpression[]> newguildExpressions = new();
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly PermissionService _perms;
 | 
			
		||||
    private readonly CommandHandler _cmd;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly Bot _bot;
 | 
			
		||||
    private readonly GlobalPermissionService _gperm;
 | 
			
		||||
    private readonly CmdCdService _cmdCds;
 | 
			
		||||
    private readonly IPubSub _pubSub;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
    private bool ready;
 | 
			
		||||
    private ConcurrentHashSet<ulong> _disabledGlobalExpressionGuilds;
 | 
			
		||||
 | 
			
		||||
    public NadekoExpressionsService(
 | 
			
		||||
        PermissionService perms,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        Bot bot,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        CommandHandler cmd,
 | 
			
		||||
        GlobalPermissionService gperm,
 | 
			
		||||
        CmdCdService cmdCds,
 | 
			
		||||
        IPubSub pubSub,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _perms = perms;
 | 
			
		||||
        _cmd = cmd;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _bot = bot;
 | 
			
		||||
        _gperm = gperm;
 | 
			
		||||
        _cmdCds = cmdCds;
 | 
			
		||||
        _pubSub = pubSub;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
        _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, _bot.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, _bot.Mention);
 | 
			
		||||
                    return x;
 | 
			
		||||
                })
 | 
			
		||||
                .ToArray();
 | 
			
		||||
 | 
			
		||||
            globalExpressions = globalItems;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ready = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NadekoExpression 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 NadekoExpression MatchExpressions(in ReadOnlySpan<char> content, NadekoExpression[] exprs)
 | 
			
		||||
    {
 | 
			
		||||
        var result = new List<NadekoExpression>(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;
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
        NadekoExpression 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, NadekoExpression expr)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
            UpdateInternal(guildId, expr);
 | 
			
		||||
        else
 | 
			
		||||
            return _pubSub.Pub(_gexprEditedKey, expr);
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void UpdateInternal(ulong? maybeGuildId, NadekoExpression 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, NadekoExpression 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<NadekoExpression>(),
 | 
			
		||||
                (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 NadekoExpression[] DeleteInternal(
 | 
			
		||||
        IReadOnlyList<NadekoExpression> exprs,
 | 
			
		||||
        int id,
 | 
			
		||||
        out NadekoExpression deleted)
 | 
			
		||||
    {
 | 
			
		||||
        deleted = null;
 | 
			
		||||
        if (exprs is null || exprs.Count == 0)
 | 
			
		||||
            return exprs as NadekoExpression[] ?? exprs?.ToArray();
 | 
			
		||||
 | 
			
		||||
        var newExprs = new NadekoExpression[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)
 | 
			
		||||
    {
 | 
			
		||||
        NadekoExpression 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;
 | 
			
		||||
        NadekoExpression 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 NadekoExpression 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 NadekoExpression
 | 
			
		||||
                {
 | 
			
		||||
                    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 Task OnReadyAsync()
 | 
			
		||||
        => ReloadInternal(_bot.GetCurrentGuildIds());
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnExprsShouldReload(bool _)
 | 
			
		||||
        => new(ReloadInternal(_bot.GetCurrentGuildIds()));
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnGexprAdded(NadekoExpression c)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
            var newGlobalReactions = new NadekoExpression[globalExpressions.Length + 1];
 | 
			
		||||
            Array.Copy(globalExpressions, newGlobalReactions, globalExpressions.Length);
 | 
			
		||||
            newGlobalReactions[globalExpressions.Length] = c;
 | 
			
		||||
            globalExpressions = newGlobalReactions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnGexprEdited(NadekoExpression 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<NadekoExpression> AddAsync(ulong? guildId, string key, string message)
 | 
			
		||||
    {
 | 
			
		||||
        key = key.ToLowerInvariant();
 | 
			
		||||
        var expr = new NadekoExpression
 | 
			
		||||
        {
 | 
			
		||||
            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<NadekoExpression> 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<NadekoExpression> 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 NadekoExpression[] GetExpressionsFor(ulong? maybeGuildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
            return newguildExpressions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<NadekoExpression>();
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -108,6 +108,7 @@
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Econ\Nadeko.Econ.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer" />
 | 
			
		||||
        <ProjectReference Include="..\NadekoBot.Modules.Expresssions\NadekoBot.Modules.Expresssions.csproj" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
@@ -139,6 +140,9 @@
 | 
			
		||||
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <Folder Include="Modules\Expressions" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup Condition=" '$(Version)' == '' ">
 | 
			
		||||
        <VersionPrefix Condition=" '$(VersionPrefix)' == '' ">5.0.0</VersionPrefix>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user