mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Merge branch 'v4' into v5
This commit is contained in:
		@@ -63,6 +63,9 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
 | 
			
		||||
    [Comment("""Official cleverbot api key.""")]
 | 
			
		||||
    public string CleverbotApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Official GPT-3 api key.")]
 | 
			
		||||
    public string Gpt3ApiKey { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    [Comment("""
 | 
			
		||||
        Which cache implementation should bot use.
 | 
			
		||||
@@ -144,7 +147,7 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
 | 
			
		||||
    public Creds()
 | 
			
		||||
    {
 | 
			
		||||
        Version = 6;
 | 
			
		||||
        Version = 7;
 | 
			
		||||
        Token = string.Empty;
 | 
			
		||||
        UsePrivilegedIntents = true;
 | 
			
		||||
        OwnerIds = new List<ulong>();
 | 
			
		||||
@@ -154,6 +157,7 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
        Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
 | 
			
		||||
        BotListToken = string.Empty;
 | 
			
		||||
        CleverbotApiKey = string.Empty;
 | 
			
		||||
        Gpt3ApiKey = string.Empty;
 | 
			
		||||
        BotCache = BotCacheImplemenation.Memory;
 | 
			
		||||
        RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
 | 
			
		||||
        Db = new()
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ public interface IBotCredentials
 | 
			
		||||
    int TotalShards { get; }
 | 
			
		||||
    Creds.PatreonSettings Patreon { get; }
 | 
			
		||||
    string CleverbotApiKey { get; }
 | 
			
		||||
    string Gpt3ApiKey { get; }
 | 
			
		||||
    RestartConfig RestartCommand { get; }
 | 
			
		||||
    Creds.VotesSettings Votes { get; }
 | 
			
		||||
    string BotListToken { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -472,6 +472,9 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
 | 
			
		||||
            pb.WithIsMultiple(paramData.IsParams)
 | 
			
		||||
              .WithIsOptional(paramData.IsOptional)
 | 
			
		||||
              .WithIsRemainder(paramData.IsLeftover);
 | 
			
		||||
 | 
			
		||||
            if (paramData.IsOptional)
 | 
			
		||||
                pb.WithDefault(paramData.DefaultValue);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
@@ -800,6 +803,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
 | 
			
		||||
 | 
			
		||||
                var leftoverAttribute = pi.GetCustomAttribute<leftoverAttribute>(true);
 | 
			
		||||
                var hasDefaultValue = pi.HasDefaultValue;
 | 
			
		||||
                var defaultValue = pi.DefaultValue;
 | 
			
		||||
                var isLeftover = leftoverAttribute != null;
 | 
			
		||||
                var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;
 | 
			
		||||
                var paramType = pi.ParameterType;
 | 
			
		||||
@@ -838,7 +842,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                canInject = false;
 | 
			
		||||
                canInject = false;                                                                                           
 | 
			
		||||
 | 
			
		||||
                if (isParams)
 | 
			
		||||
                {
 | 
			
		||||
@@ -859,7 +863,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
 | 
			
		||||
                    throw new ArgumentException("Leftover attribute error.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, isLeftover, isParams));
 | 
			
		||||
                cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, defaultValue, isLeftover, isParams));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ public sealed record ParamData(
 | 
			
		||||
    Type Type,
 | 
			
		||||
    string Name,
 | 
			
		||||
    bool IsOptional,
 | 
			
		||||
    object? DefaultValue,
 | 
			
		||||
    bool IsLeftover,
 | 
			
		||||
    bool IsParams
 | 
			
		||||
);
 | 
			
		||||
@@ -46,9 +46,7 @@ public sealed class CommandOrExprTypeReader : NadekoTypeReader<CommandOrExprInfo
 | 
			
		||||
 | 
			
		||||
    public override async ValueTask<TypeReaderResult<CommandOrExprInfo>> ReadAsync(ICommandContext ctx, string input)
 | 
			
		||||
    {
 | 
			
		||||
        input = input.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
        if (_exprs.ExpressionExists(ctx.Guild?.Id, input) || _exprs.ExpressionExists(null, input))
 | 
			
		||||
        if (_exprs.ExpressionExists(ctx.Guild?.Id, input))
 | 
			
		||||
            return TypeReaderResult.FromSuccess(new CommandOrExprInfo(input, CommandOrExprInfo.Type.Custom));
 | 
			
		||||
 | 
			
		||||
        var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,4 @@ public static class NadekoExpressionExtensions
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<NadekoExpression> ForId(this DbSet<NadekoExpression> exprs, ulong id)
 | 
			
		||||
        => exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
 | 
			
		||||
 | 
			
		||||
    public static NadekoExpression GetByGuildIdAndInput(
 | 
			
		||||
        this DbSet<NadekoExpression> exprs,
 | 
			
		||||
        ulong? guildId,
 | 
			
		||||
        string input)
 | 
			
		||||
        => exprs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
 | 
			
		||||
}
 | 
			
		||||
@@ -356,24 +356,24 @@ public class MuteService : INService
 | 
			
		||||
 | 
			
		||||
    public async Task TimedBan(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        TimeSpan after,
 | 
			
		||||
        string reason,
 | 
			
		||||
        int pruneDays)
 | 
			
		||||
    {
 | 
			
		||||
        await guild.AddBanAsync(user.Id, pruneDays, reason);
 | 
			
		||||
        await guild.AddBanAsync(userId, pruneDays, reason);
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
            config.UnbanTimer.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UserId = userId,
 | 
			
		||||
                UnbanAt = DateTime.UtcNow + after
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StartUn_Timer(guild.Id, user.Id, after, TimerType.Ban); // start the timer
 | 
			
		||||
        StartUn_Timer(guild.Id, userId, after, TimerType.Ban); // start the timer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task TimedRole(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,9 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
    #if !GLOBAL_NADEKO
 | 
			
		||||
    , INService // don't load this service on global nadeko
 | 
			
		||||
    #endif
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
        , INService // don't load this service on global nadeko
 | 
			
		||||
#endif
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentDictionary<ulong, LogSetting> GuildLogSettings { get; }
 | 
			
		||||
 | 
			
		||||
@@ -49,15 +49,15 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        _prot = prot;
 | 
			
		||||
        _tz = tz;
 | 
			
		||||
        _punishService = punishService;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        using (var uow = db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
            var configs = uow.LogSettings.AsQueryable()
 | 
			
		||||
                             .AsNoTracking()
 | 
			
		||||
                             .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
                             .Include(ls => ls.LogIgnores)
 | 
			
		||||
                             .ToList();
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
                .Include(ls => ls.LogIgnores)
 | 
			
		||||
                .ToList();
 | 
			
		||||
 | 
			
		||||
            GuildLogSettings = configs.ToDictionary(ls => ls.GuildId).ToConcurrent();
 | 
			
		||||
        }
 | 
			
		||||
@@ -73,12 +73,13 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated;
 | 
			
		||||
        _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS;
 | 
			
		||||
        _client.GuildMemberUpdated += _client_GuildUserUpdated;
 | 
			
		||||
        _client.PresenceUpdated += _client_PresenceUpdated;
 | 
			
		||||
        _client.UserUpdated += _client_UserUpdated;
 | 
			
		||||
        _client.ChannelCreated += _client_ChannelCreated;
 | 
			
		||||
        _client.ChannelDestroyed += _client_ChannelDestroyed;
 | 
			
		||||
        _client.ChannelUpdated += _client_ChannelUpdated;
 | 
			
		||||
        _client.RoleDeleted += _client_RoleDeleted;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        _client.ThreadCreated += _client_ThreadCreated;
 | 
			
		||||
        _client.ThreadDeleted += _client_ThreadDeleted;
 | 
			
		||||
 | 
			
		||||
@@ -86,10 +87,63 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        _mute.UserUnmuted += MuteCommands_UserUnmuted;
 | 
			
		||||
 | 
			
		||||
        _prot.OnAntiProtectionTriggered += TriggeredAntiProtection;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        _punishService.OnUserWarned += PunishServiceOnOnUserWarned;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task _client_PresenceUpdated(SocketUser user, SocketPresence? before, SocketPresence? after)
 | 
			
		||||
    {
 | 
			
		||||
        if (user is not SocketGuildUser gu)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!GuildLogSettings.TryGetValue(gu.Guild.Id, out var logSetting)
 | 
			
		||||
            || before is null
 | 
			
		||||
            || after is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        ITextChannel? logChannel;
 | 
			
		||||
 | 
			
		||||
        if (!user.IsBot
 | 
			
		||||
            && logSetting.LogUserPresenceId is not null
 | 
			
		||||
            && (logChannel =
 | 
			
		||||
                await TryGetLogChannel(gu.Guild, logSetting, LogType.UserPresence)) is not null)
 | 
			
		||||
        {
 | 
			
		||||
            if (before.Status != after.Status)
 | 
			
		||||
            {
 | 
			
		||||
                var str = "🎭"
 | 
			
		||||
                          + Format.Code(PrettyCurrentTime(gu.Guild))
 | 
			
		||||
                          + GetText(logChannel.Guild,
 | 
			
		||||
                              strs.user_status_change("👤" + Format.Bold(gu.Username),
 | 
			
		||||
                                  Format.Bold(after.Status.ToString())));
 | 
			
		||||
                PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                    new List<string>
 | 
			
		||||
                    {
 | 
			
		||||
                        str
 | 
			
		||||
                    },
 | 
			
		||||
                    (_, list) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(str);
 | 
			
		||||
                        return list;
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
            else if (before.Activities.FirstOrDefault()?.Name != after.Activities.FirstOrDefault()?.Name)
 | 
			
		||||
            {
 | 
			
		||||
                var str =
 | 
			
		||||
                    $"👾`{PrettyCurrentTime(gu.Guild)}`👤__**{gu.Username}**__ is now playing **{after.Activities.FirstOrDefault()?.Name ?? "-"}**.";
 | 
			
		||||
                PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                    new List<string>
 | 
			
		||||
                    {
 | 
			
		||||
                        str
 | 
			
		||||
                    },
 | 
			
		||||
                    (_, list) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(str);
 | 
			
		||||
                        return list;
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task _client_ThreadDeleted(Cacheable<SocketThreadChannel, ulong> sch)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
@@ -139,7 +193,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                ITextChannel? logChannel;
 | 
			
		||||
                if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ThreadCreated)) is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                var title = GetText(logChannel.Guild, strs.thread_created);
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(_eb.Create()
 | 
			
		||||
@@ -177,22 +231,24 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                var keys = PresenceUpdates.Keys.ToList();
 | 
			
		||||
 | 
			
		||||
                await keys.Select(key =>
 | 
			
		||||
                          {
 | 
			
		||||
                              if (!((SocketGuild)key.Guild).CurrentUser.GetPermissions(key).SendMessages)
 | 
			
		||||
                                  return Task.CompletedTask;
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!((SocketGuild)key.Guild).CurrentUser.GetPermissions(key).SendMessages)
 | 
			
		||||
                            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                              if (PresenceUpdates.TryRemove(key, out var msgs))
 | 
			
		||||
                              {
 | 
			
		||||
                                  var title = GetText(key.Guild, strs.presence_updates);
 | 
			
		||||
                                  var desc = string.Join(Environment.NewLine, msgs);
 | 
			
		||||
                                  return key.SendConfirmAsync(_eb, title, desc.TrimTo(2048)!);
 | 
			
		||||
                              }
 | 
			
		||||
                        if (PresenceUpdates.TryRemove(key, out var msgs))
 | 
			
		||||
                        {
 | 
			
		||||
                            var title = GetText(key.Guild, strs.presence_updates);
 | 
			
		||||
                            var desc = string.Join(Environment.NewLine, msgs);
 | 
			
		||||
                            return key.SendConfirmAsync(_eb, title, desc.TrimTo(2048)!);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                              return Task.CompletedTask;
 | 
			
		||||
                          })
 | 
			
		||||
                          .WhenAll();
 | 
			
		||||
                        return Task.CompletedTask;
 | 
			
		||||
                    })
 | 
			
		||||
                    .WhenAll();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -255,35 +311,35 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
            logSetting.UserLeftId = logSetting.UserBannedId = logSetting.UserUnbannedId = logSetting.UserUpdatedId =
 | 
			
		||||
                logSetting.ChannelCreatedId = logSetting.ChannelDestroyedId = logSetting.ChannelUpdatedId =
 | 
			
		||||
                    logSetting.LogUserPresenceId = logSetting.LogVoicePresenceId = logSetting.UserMutedId =
 | 
			
		||||
                        logSetting.LogVoicePresenceTTSId = value ? channelId : null;
 | 
			
		||||
                        logSetting.LogVoicePresenceTTSId = logSetting.ThreadCreatedId = logSetting.ThreadDeletedId
 | 
			
		||||
                            = logSetting.LogWarnsId = value ? channelId : null;
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        GuildLogSettings.AddOrUpdate(guildId, _ => logSetting, (_, _) => logSetting);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private async Task PunishServiceOnOnUserWarned(Warning arg)
 | 
			
		||||
    {
 | 
			
		||||
        if (!GuildLogSettings.TryGetValue(arg.GuildId, out var logSetting) || logSetting.LogWarnsId is null)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        var g = _client.GetGuild(arg.GuildId);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        ITextChannel? logChannel;
 | 
			
		||||
        if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserWarned)) is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithOkColor()
 | 
			
		||||
                       .WithTitle($"⚠️ User Warned")
 | 
			
		||||
                       .WithDescription($"<@{arg.UserId}> | {arg.UserId}")
 | 
			
		||||
                       .AddField("Mod", arg.Moderator)
 | 
			
		||||
                       .AddField("Reason", string.IsNullOrWhiteSpace(arg.Reason) ? "-" : arg.Reason, true)
 | 
			
		||||
                       .WithFooter(CurrentTime(g));
 | 
			
		||||
            .WithOkColor()
 | 
			
		||||
            .WithTitle($"⚠️ User Warned")
 | 
			
		||||
            .WithDescription($"<@{arg.UserId}> | {arg.UserId}")
 | 
			
		||||
            .AddField("Mod", arg.Moderator)
 | 
			
		||||
            .AddField("Reason", string.IsNullOrWhiteSpace(arg.Reason) ? "-" : arg.Reason, true)
 | 
			
		||||
            .WithFooter(CurrentTime(g));
 | 
			
		||||
 | 
			
		||||
        await logChannel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserUpdated(SocketUser before, SocketUser uAfter)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
@@ -307,18 +363,18 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                if (before.Username != after.Username)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithTitle("👥 " + GetText(g, strs.username_changed))
 | 
			
		||||
                         .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}")
 | 
			
		||||
                         .AddField("Old Name", $"{before.Username}", true)
 | 
			
		||||
                         .AddField("New Name", $"{after.Username}", true)
 | 
			
		||||
                         .WithFooter(CurrentTime(g))
 | 
			
		||||
                         .WithOkColor();
 | 
			
		||||
                        .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}")
 | 
			
		||||
                        .AddField("Old Name", $"{before.Username}", true)
 | 
			
		||||
                        .AddField("New Name", $"{after.Username}", true)
 | 
			
		||||
                        .WithFooter(CurrentTime(g))
 | 
			
		||||
                        .WithOkColor();
 | 
			
		||||
                }
 | 
			
		||||
                else if (before.AvatarId != after.AvatarId)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithTitle("👥" + GetText(g, strs.avatar_changed))
 | 
			
		||||
                         .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}")
 | 
			
		||||
                         .WithFooter(CurrentTime(g))
 | 
			
		||||
                         .WithOkColor();
 | 
			
		||||
                        .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}")
 | 
			
		||||
                        .WithFooter(CurrentTime(g))
 | 
			
		||||
                        .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                    var bav = before.RealAvatarUrl();
 | 
			
		||||
                    if (bav.IsAbsoluteUri)
 | 
			
		||||
@@ -482,10 +538,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithAuthor(mutes)
 | 
			
		||||
                               .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}")
 | 
			
		||||
                               .WithFooter(CurrentTime(usr.Guild))
 | 
			
		||||
                               .WithOkColor();
 | 
			
		||||
                    .WithAuthor(mutes)
 | 
			
		||||
                    .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}")
 | 
			
		||||
                    .WithFooter(CurrentTime(usr.Guild))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
@@ -529,10 +585,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithAuthor(mutes)
 | 
			
		||||
                               .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}")
 | 
			
		||||
                               .WithFooter($"{CurrentTime(usr.Guild)}")
 | 
			
		||||
                               .WithOkColor();
 | 
			
		||||
                    .WithAuthor(mutes)
 | 
			
		||||
                    .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}")
 | 
			
		||||
                    .WithFooter($"{CurrentTime(usr.Guild)}")
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
                    embed.WithDescription(reason);
 | 
			
		||||
@@ -583,11 +639,11 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithAuthor($"🛡 Anti-{protection}")
 | 
			
		||||
                               .WithTitle(GetText(logChannel.Guild, strs.users) + " " + punishment)
 | 
			
		||||
                               .WithDescription(string.Join("\n", users.Select(u => u.ToString())))
 | 
			
		||||
                               .WithFooter(CurrentTime(logChannel.Guild))
 | 
			
		||||
                               .WithOkColor();
 | 
			
		||||
                    .WithAuthor($"🛡 Anti-{protection}")
 | 
			
		||||
                    .WithTitle(GetText(logChannel.Guild, strs.users) + " " + punishment)
 | 
			
		||||
                    .WithDescription(string.Join("\n", users.Select(u => u.ToString())))
 | 
			
		||||
                    .WithFooter(CurrentTime(logChannel.Guild))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
@@ -636,16 +692,16 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                    && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    var embed = _eb.Create()
 | 
			
		||||
                                   .WithOkColor()
 | 
			
		||||
                                   .WithFooter(CurrentTime(before.Guild))
 | 
			
		||||
                                   .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}");
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithFooter(CurrentTime(before.Guild))
 | 
			
		||||
                        .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}");
 | 
			
		||||
                    if (before.Nickname != after.Nickname)
 | 
			
		||||
                    {
 | 
			
		||||
                        embed.WithAuthor("👥 " + GetText(logChannel.Guild, strs.nick_change))
 | 
			
		||||
                             .AddField(GetText(logChannel.Guild, strs.old_nick),
 | 
			
		||||
                                 $"{before.Nickname}#{before.Discriminator}")
 | 
			
		||||
                             .AddField(GetText(logChannel.Guild, strs.new_nick),
 | 
			
		||||
                                 $"{after.Nickname}#{after.Discriminator}");
 | 
			
		||||
                            .AddField(GetText(logChannel.Guild, strs.old_nick),
 | 
			
		||||
                                $"{before.Nickname}#{before.Discriminator}")
 | 
			
		||||
                            .AddField(GetText(logChannel.Guild, strs.new_nick),
 | 
			
		||||
                                $"{after.Nickname}#{after.Discriminator}");
 | 
			
		||||
 | 
			
		||||
                        await logChannel.EmbedAsync(embed);
 | 
			
		||||
                    }
 | 
			
		||||
@@ -655,7 +711,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                        {
 | 
			
		||||
                            var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name);
 | 
			
		||||
                            embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_add))
 | 
			
		||||
                                 .WithDescription(string.Join(", ", diffRoles).SanitizeMentions());
 | 
			
		||||
                                .WithDescription(string.Join(", ", diffRoles).SanitizeMentions());
 | 
			
		||||
 | 
			
		||||
                            await logChannel.EmbedAsync(embed);
 | 
			
		||||
                        }
 | 
			
		||||
@@ -663,59 +719,19 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                        {
 | 
			
		||||
                            await Task.Delay(1000);
 | 
			
		||||
                            var diffRoles = before.Roles.Where(r => !after.Roles.Contains(r) && !IsRoleDeleted(r.Id))
 | 
			
		||||
                                                  .Select(r => r.Name)
 | 
			
		||||
                                                  .ToList();
 | 
			
		||||
                                .Select(r => r.Name)
 | 
			
		||||
                                .ToList();
 | 
			
		||||
 | 
			
		||||
                            if (diffRoles.Any())
 | 
			
		||||
                            {
 | 
			
		||||
                                embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_rem))
 | 
			
		||||
                                     .WithDescription(string.Join(", ", diffRoles).SanitizeMentions());
 | 
			
		||||
                                    .WithDescription(string.Join(", ", diffRoles).SanitizeMentions());
 | 
			
		||||
 | 
			
		||||
                                await logChannel.EmbedAsync(embed);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!before.IsBot
 | 
			
		||||
                    && logSetting.LogUserPresenceId is not null
 | 
			
		||||
                    && (logChannel =
 | 
			
		||||
                        await TryGetLogChannel(before.Guild, logSetting, LogType.UserPresence)) is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (before.Status != after.Status)
 | 
			
		||||
                    {
 | 
			
		||||
                        var str = "🎭"
 | 
			
		||||
                                  + Format.Code(PrettyCurrentTime(after.Guild))
 | 
			
		||||
                                  + GetText(logChannel.Guild,
 | 
			
		||||
                                      strs.user_status_change("👤" + Format.Bold(after.Username),
 | 
			
		||||
                                          Format.Bold(after.Status.ToString())));
 | 
			
		||||
                        PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                            new List<string>
 | 
			
		||||
                            {
 | 
			
		||||
                                str
 | 
			
		||||
                            },
 | 
			
		||||
                            (_, list) =>
 | 
			
		||||
                            {
 | 
			
		||||
                                list.Add(str);
 | 
			
		||||
                                return list;
 | 
			
		||||
                            });
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (before.Activities.FirstOrDefault()?.Name != after.Activities.FirstOrDefault()?.Name)
 | 
			
		||||
                    {
 | 
			
		||||
                        var str =
 | 
			
		||||
                            $"👾`{PrettyCurrentTime(after.Guild)}`👤__**{after.Username}**__ is now playing **{after.Activities.FirstOrDefault()?.Name ?? "-"}**.";
 | 
			
		||||
                        PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                            new List<string>
 | 
			
		||||
                            {
 | 
			
		||||
                                str
 | 
			
		||||
                            },
 | 
			
		||||
                            (_, list) =>
 | 
			
		||||
                            {
 | 
			
		||||
                                list.Add(str);
 | 
			
		||||
                                return list;
 | 
			
		||||
                            });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
@@ -754,15 +770,15 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                if (before.Name != after.Name)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_name_change))
 | 
			
		||||
                         .WithDescription($"{after} | {after.Id}")
 | 
			
		||||
                         .AddField(GetText(logChannel.Guild, strs.ch_old_name), before.Name);
 | 
			
		||||
                        .WithDescription($"{after} | {after.Id}")
 | 
			
		||||
                        .AddField(GetText(logChannel.Guild, strs.ch_old_name), before.Name);
 | 
			
		||||
                }
 | 
			
		||||
                else if (beforeTextChannel?.Topic != afterTextChannel?.Topic)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_topic_change))
 | 
			
		||||
                         .WithDescription($"{after} | {after.Id}")
 | 
			
		||||
                         .AddField(GetText(logChannel.Guild, strs.old_topic), beforeTextChannel?.Topic ?? "-")
 | 
			
		||||
                         .AddField(GetText(logChannel.Guild, strs.new_topic), afterTextChannel?.Topic ?? "-");
 | 
			
		||||
                        .WithDescription($"{after} | {after.Id}")
 | 
			
		||||
                        .AddField(GetText(logChannel.Guild, strs.old_topic), beforeTextChannel?.Topic ?? "-")
 | 
			
		||||
                        .AddField(GetText(logChannel.Guild, strs.new_topic), afterTextChannel?.Topic ?? "-");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                    return;
 | 
			
		||||
@@ -795,7 +811,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                ITextChannel? logChannel;
 | 
			
		||||
                if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelDestroyed)) is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                string title;
 | 
			
		||||
                if (ch is IVoiceChannel)
 | 
			
		||||
                    title = GetText(logChannel.Guild, strs.voice_chan_destroyed);
 | 
			
		||||
@@ -803,10 +819,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                    title = GetText(logChannel.Guild, strs.text_chan_destroyed);
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                               .WithOkColor()
 | 
			
		||||
                                               .WithTitle("🆕 " + title)
 | 
			
		||||
                                               .WithDescription($"{ch.Name} | {ch.Id}")
 | 
			
		||||
                                               .WithFooter(CurrentTime(ch.Guild)));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("🆕 " + title)
 | 
			
		||||
                    .WithDescription($"{ch.Name} | {ch.Id}")
 | 
			
		||||
                    .WithFooter(CurrentTime(ch.Guild)));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
@@ -815,7 +831,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private Task _client_ChannelCreated(IChannel ich)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
@@ -839,10 +855,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                    title = GetText(logChannel.Guild, strs.text_chan_created);
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                               .WithOkColor()
 | 
			
		||||
                                               .WithTitle("🆕 " + title)
 | 
			
		||||
                                               .WithDescription($"{ch.Name} | {ch.Id}")
 | 
			
		||||
                                               .WithFooter(CurrentTime(ch.Guild)));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("🆕 " + title)
 | 
			
		||||
                    .WithDescription($"{ch.Name} | {ch.Id}")
 | 
			
		||||
                    .WithFooter(CurrentTime(ch.Guild)));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
@@ -942,11 +958,11 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserLeft)) is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("❌ " + GetText(logChannel.Guild, strs.user_left))
 | 
			
		||||
                               .WithDescription(usr.ToString())
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("❌ " + GetText(logChannel.Guild, strs.user_left))
 | 
			
		||||
                    .WithDescription(usr.ToString())
 | 
			
		||||
                    .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                    .WithFooter(CurrentTime(guild));
 | 
			
		||||
 | 
			
		||||
                if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute))
 | 
			
		||||
                    embed.WithThumbnailUrl(usr.GetAvatarUrl());
 | 
			
		||||
@@ -975,17 +991,17 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("✅ " + GetText(logChannel.Guild, strs.user_joined))
 | 
			
		||||
                               .WithDescription($"{usr.Mention} `{usr}`")
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.joined_server),
 | 
			
		||||
                                   $"{usr.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}",
 | 
			
		||||
                                   true)
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.joined_discord),
 | 
			
		||||
                                   $"{usr.CreatedAt:dd.MM.yyyy HH:mm}",
 | 
			
		||||
                                   true)
 | 
			
		||||
                               .WithFooter(CurrentTime(usr.Guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("✅ " + GetText(logChannel.Guild, strs.user_joined))
 | 
			
		||||
                    .WithDescription($"{usr.Mention} `{usr}`")
 | 
			
		||||
                    .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                    .AddField(GetText(logChannel.Guild, strs.joined_server),
 | 
			
		||||
                        $"{usr.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}",
 | 
			
		||||
                        true)
 | 
			
		||||
                    .AddField(GetText(logChannel.Guild, strs.joined_discord),
 | 
			
		||||
                        $"{usr.CreatedAt:dd.MM.yyyy HH:mm}",
 | 
			
		||||
                        true)
 | 
			
		||||
                    .WithFooter(CurrentTime(usr.Guild));
 | 
			
		||||
 | 
			
		||||
                if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute))
 | 
			
		||||
                    embed.WithThumbnailUrl(usr.GetAvatarUrl());
 | 
			
		||||
@@ -1016,11 +1032,11 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned)) is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned))
 | 
			
		||||
                               .WithDescription(usr.ToString()!)
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned))
 | 
			
		||||
                    .WithDescription(usr.ToString()!)
 | 
			
		||||
                    .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                    .WithFooter(CurrentTime(guild));
 | 
			
		||||
 | 
			
		||||
                if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute))
 | 
			
		||||
                    embed.WithThumbnailUrl(usr.GetAvatarUrl());
 | 
			
		||||
@@ -1060,16 +1076,15 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned))
 | 
			
		||||
                               .WithDescription(usr.ToString()!)
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .AddField("Reason", string.IsNullOrWhiteSpace(reason) ? "-" : reason)
 | 
			
		||||
                               .WithFooter(CurrentTime(guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned))
 | 
			
		||||
                    .WithDescription(usr.ToString()!)
 | 
			
		||||
                    .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                    .AddField("Reason", string.IsNullOrWhiteSpace(reason) ? "-" : reason)
 | 
			
		||||
                    .WithFooter(CurrentTime(guild));
 | 
			
		||||
 | 
			
		||||
                var avatarUrl = usr.GetAvatarUrl();
 | 
			
		||||
 | 
			
		||||
@@ -1115,14 +1130,14 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
                var resolvedMessage = msg.Resolve(TagHandling.FullName);
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("🗑 "
 | 
			
		||||
                                          + GetText(logChannel.Guild, strs.msg_del(((ITextChannel)msg.Channel).Name)))
 | 
			
		||||
                               .WithDescription(msg.Author.ToString()!)
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.content),
 | 
			
		||||
                                   string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage)
 | 
			
		||||
                               .AddField("Id", msg.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(channel.Guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("🗑 "
 | 
			
		||||
                               + GetText(logChannel.Guild, strs.msg_del(((ITextChannel)msg.Channel).Name)))
 | 
			
		||||
                    .WithDescription(msg.Author.ToString()!)
 | 
			
		||||
                    .AddField(GetText(logChannel.Guild, strs.content),
 | 
			
		||||
                        string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage)
 | 
			
		||||
                    .AddField("Id", msg.Id.ToString())
 | 
			
		||||
                    .WithFooter(CurrentTime(channel.Guild));
 | 
			
		||||
                if (msg.Attachments.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    embed.AddField(GetText(logChannel.Guild, strs.attachments),
 | 
			
		||||
@@ -1175,19 +1190,19 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("📝 "
 | 
			
		||||
                                          + GetText(logChannel.Guild,
 | 
			
		||||
                                              strs.msg_update(((ITextChannel)after.Channel).Name)))
 | 
			
		||||
                               .WithDescription(after.Author.ToString()!)
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.old_msg),
 | 
			
		||||
                                   string.IsNullOrWhiteSpace(before.Content)
 | 
			
		||||
                                       ? "-"
 | 
			
		||||
                                       : before.Resolve(TagHandling.FullName))
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.new_msg),
 | 
			
		||||
                                   string.IsNullOrWhiteSpace(after.Content) ? "-" : after.Resolve(TagHandling.FullName))
 | 
			
		||||
                               .AddField("Id", after.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(channel.Guild));
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("📝 "
 | 
			
		||||
                               + GetText(logChannel.Guild,
 | 
			
		||||
                                   strs.msg_update(((ITextChannel)after.Channel).Name)))
 | 
			
		||||
                    .WithDescription(after.Author.ToString()!)
 | 
			
		||||
                    .AddField(GetText(logChannel.Guild, strs.old_msg),
 | 
			
		||||
                        string.IsNullOrWhiteSpace(before.Content)
 | 
			
		||||
                            ? "-"
 | 
			
		||||
                            : before.Resolve(TagHandling.FullName))
 | 
			
		||||
                    .AddField(GetText(logChannel.Guild, strs.new_msg),
 | 
			
		||||
                        string.IsNullOrWhiteSpace(after.Content) ? "-" : after.Resolve(TagHandling.FullName))
 | 
			
		||||
                    .AddField("Id", after.Id.ToString())
 | 
			
		||||
                    .WithFooter(CurrentTime(channel.Guild));
 | 
			
		||||
 | 
			
		||||
                await logChannel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -402,12 +402,21 @@ public partial class Administration
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
 | 
			
		||||
        public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
 | 
			
		||||
            => Ban(time, user.Id, msg);
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, user.Id);
 | 
			
		||||
            var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (guildUser is not null && !await CheckRoleHierarchy(guildUser))
 | 
			
		||||
                return;
 | 
			
		||||
@@ -429,13 +438,14 @@ public partial class Administration
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var user = await ctx.Client.GetUserAsync(userId);
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
 | 
			
		||||
            await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle("⛔️ " + GetText(strs.banned_user))
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true)
 | 
			
		||||
                            .AddField(GetText(strs.username),  user?.ToString() ?? userId.ToString(), true)
 | 
			
		||||
                            .AddField("ID", userId.ToString(), true)
 | 
			
		||||
                            .AddField(GetText(strs.duration),
 | 
			
		||||
                                time.Time.Humanize(3, minUnit: TimeUnit.Minute, culture: Culture),
 | 
			
		||||
                                true);
 | 
			
		||||
 
 | 
			
		||||
@@ -157,7 +157,7 @@ public class UserPunishService : INService, IReadyExecutor
 | 
			
		||||
                if (minutes == 0)
 | 
			
		||||
                    await guild.AddBanAsync(user, reason: reason, pruneDays: banPrune);
 | 
			
		||||
                else
 | 
			
		||||
                    await _mute.TimedBan(user.Guild, user, TimeSpan.FromMinutes(minutes), reason, banPrune);
 | 
			
		||||
                    await _mute.TimedBan(user.Guild, user.Id, TimeSpan.FromMinutes(minutes), reason, banPrune);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.Softban:
 | 
			
		||||
                banPrune = await GetBanPruneAsync(user.GuildId) ?? 7;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,14 +36,14 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
            """;
 | 
			
		||||
 | 
			
		||||
    private static readonly ISerializer _exportSerializer = new SerializerBuilder()
 | 
			
		||||
                                                            .WithEventEmitter(args
 | 
			
		||||
                                                                => new MultilineScalarFlowStyleEmitter(args))
 | 
			
		||||
                                                            .WithNamingConvention(CamelCaseNamingConvention.Instance)
 | 
			
		||||
                                                            .WithIndentedSequences()
 | 
			
		||||
                                                            .ConfigureDefaultValuesHandling(DefaultValuesHandling
 | 
			
		||||
                                                                .OmitDefaults)
 | 
			
		||||
                                                            .DisableAliases()
 | 
			
		||||
                                                            .Build();
 | 
			
		||||
        .WithEventEmitter(args
 | 
			
		||||
            => new MultilineScalarFlowStyleEmitter(args))
 | 
			
		||||
        .WithNamingConvention(CamelCaseNamingConvention.Instance)
 | 
			
		||||
        .WithIndentedSequences()
 | 
			
		||||
        .ConfigureDefaultValuesHandling(DefaultValuesHandling
 | 
			
		||||
            .OmitDefaults)
 | 
			
		||||
        .DisableAliases()
 | 
			
		||||
        .Build();
 | 
			
		||||
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => 0;
 | 
			
		||||
@@ -59,8 +59,8 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
    // 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;
 | 
			
		||||
    private ConcurrentDictionary<ulong, NadekoExpression[]> newguildExpressions;
 | 
			
		||||
    private NadekoExpression[] globalExpressions = Array.Empty<NadekoExpression>();
 | 
			
		||||
    private ConcurrentDictionary<ulong, NadekoExpression[]> newguildExpressions = new();
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
@@ -114,20 +114,20 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var guildItems = await uow.Expressions.AsNoTracking()
 | 
			
		||||
                                  .Where(x => allGuildIds.Contains(x.GuildId.Value))
 | 
			
		||||
                                  .ToListAsync();
 | 
			
		||||
            .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();
 | 
			
		||||
            .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
 | 
			
		||||
        _disabledGlobalExpressionGuilds = new(await uow.GuildConfigs
 | 
			
		||||
            .Where(x => x.DisableGlobalExpressions)
 | 
			
		||||
            .Select(x => x.GuildId)
 | 
			
		||||
            .ToListAsyncLinqToDB());
 | 
			
		||||
@@ -135,14 +135,14 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
        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();
 | 
			
		||||
                .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;
 | 
			
		||||
        }
 | 
			
		||||
@@ -169,7 +169,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
        if (_disabledGlobalExpressionGuilds.Contains(channel.Guild.Id))
 | 
			
		||||
            return null;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        var localGrs = globalExpressions;
 | 
			
		||||
 | 
			
		||||
        return MatchExpressions(content, localGrs);
 | 
			
		||||
@@ -468,7 +468,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
        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)
 | 
			
		||||
@@ -511,9 +511,25 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    public bool ExpressionExists(ulong? guildId, string input)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var expr = uow.Expressions.GetByGuildIdAndInput(guildId, input);
 | 
			
		||||
        return expr is not null;
 | 
			
		||||
        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)
 | 
			
		||||
@@ -544,17 +560,17 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
        {
 | 
			
		||||
            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
 | 
			
		||||
                                                     }));
 | 
			
		||||
                .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();
 | 
			
		||||
@@ -727,12 +743,12 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Modules.Games.Common;
 | 
			
		||||
using NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
using NadekoBot.Modules.Permissions;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
@@ -27,6 +28,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly IPatronageService _ps;
 | 
			
		||||
    private readonly CmdCdService _ccs;
 | 
			
		||||
    private readonly GamesConfigService _gcs;
 | 
			
		||||
 | 
			
		||||
    public ChatterBotService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
@@ -38,7 +40,8 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
        IBotCredentials creds,
 | 
			
		||||
        IEmbedBuilderService eb,
 | 
			
		||||
        IPatronageService ps,
 | 
			
		||||
        CmdCdService cmdCdService)
 | 
			
		||||
        CmdCdService cmdCdService,
 | 
			
		||||
        GamesConfigService gcs)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _perms = perms;
 | 
			
		||||
@@ -49,6 +52,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
        _ps = ps;
 | 
			
		||||
        _ccs = cmdCdService;
 | 
			
		||||
        _gcs = gcs;
 | 
			
		||||
 | 
			
		||||
        _flKey = new FeatureLimitKey()
 | 
			
		||||
        {
 | 
			
		||||
@@ -64,11 +68,26 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
 | 
			
		||||
    public IChatterBotSession CreateSession()
 | 
			
		||||
    {
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
 | 
			
		||||
            return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
 | 
			
		||||
        switch (_gcs.Data.ChatBot)
 | 
			
		||||
        {
 | 
			
		||||
            case ChatBotImplementation.Cleverbot:
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
 | 
			
		||||
                    return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
 | 
			
		||||
 | 
			
		||||
        Log.Information("Cleverbot will not work as the api key is missing.");
 | 
			
		||||
        return null;
 | 
			
		||||
                Log.Information("Cleverbot will not work as the api key is missing.");
 | 
			
		||||
                return null;
 | 
			
		||||
            case ChatBotImplementation.Gpt3:
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
 | 
			
		||||
                    return new OfficialGpt3Session(_creds.Gpt3ApiKey,
 | 
			
		||||
                        _gcs.Data.ChatGpt.Model,
 | 
			
		||||
                        _gcs.Data.ChatGpt.MaxTokens,
 | 
			
		||||
                        _httpFactory);
 | 
			
		||||
 | 
			
		||||
                Log.Information("Gpt3 will not work as the api key is missing.");
 | 
			
		||||
                return null;
 | 
			
		||||
            default:
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
 | 
			
		||||
@@ -102,7 +121,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is not SocketGuild sg)
 | 
			
		||||
            return false;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var message = PrepareMessage(usrMsg, out var cbs);
 | 
			
		||||
@@ -147,7 +166,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                uint? monthly = quota.Quota is int mVal and >= 0
 | 
			
		||||
                    ? (uint)mVal
 | 
			
		||||
                    : null;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId,
 | 
			
		||||
                    sg.OwnerId == usrMsg.Author.Id,
 | 
			
		||||
                    FeatureType.Limit,
 | 
			
		||||
@@ -155,7 +174,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                    null,
 | 
			
		||||
                    daily,
 | 
			
		||||
                    monthly);
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                if (maybeLimit.TryPickT1(out var ql, out var counters))
 | 
			
		||||
                {
 | 
			
		||||
                    if (ql.Quota == 0)
 | 
			
		||||
@@ -166,7 +185,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                            "In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/nadekobot) on patreon.",
 | 
			
		||||
                            footer:
 | 
			
		||||
                            "You may disable the cleverbot feature, and this message via '.cleverbot' command");
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@@ -174,7 +193,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                        null!,
 | 
			
		||||
                        $"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.",
 | 
			
		||||
                        footer: "You may wait for the quota reset or .");
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -185,7 +204,7 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                title: null,
 | 
			
		||||
                response.SanitizeMentions(true)
 | 
			
		||||
                // , footer: counter > 0 ? counter.ToString() : null
 | 
			
		||||
                );
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            Log.Information("""
 | 
			
		||||
                CleverBot Executed
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
 | 
			
		||||
public class ChatterBotResponse
 | 
			
		||||
{
 | 
			
		||||
    public string Convo_id { get; set; }
 | 
			
		||||
    public string BotSay { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
 | 
			
		||||
public class Gpt3Response
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("choices")]
 | 
			
		||||
    public Choice[] Choices { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class Choice
 | 
			
		||||
{
 | 
			
		||||
    public string Text { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class Gpt3ApiRequest
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("model")]
 | 
			
		||||
    public string Model { get; init; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("prompt")]
 | 
			
		||||
    public string Prompt { get; init; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("temperature")]
 | 
			
		||||
    public int Temperature { get; init; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("max_tokens")]
 | 
			
		||||
    public int MaxTokens { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
 | 
			
		||||
public class OfficialGpt3Session : IChatterBotSession
 | 
			
		||||
{
 | 
			
		||||
    private string Uri
 | 
			
		||||
        => $"https://api.openai.com/v1/completions";
 | 
			
		||||
 | 
			
		||||
    private readonly string _apiKey;
 | 
			
		||||
    private readonly string _model;
 | 
			
		||||
    private readonly int _maxTokens;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
    public OfficialGpt3Session(
 | 
			
		||||
        string apiKey,
 | 
			
		||||
        Gpt3Model model,
 | 
			
		||||
        int maxTokens,
 | 
			
		||||
        IHttpClientFactory factory)
 | 
			
		||||
    {
 | 
			
		||||
        _apiKey = apiKey;
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
        switch (model)
 | 
			
		||||
        {
 | 
			
		||||
            case Gpt3Model.Ada001:
 | 
			
		||||
                _model = "text-ada-001";
 | 
			
		||||
                break;
 | 
			
		||||
            case Gpt3Model.Babbage001:
 | 
			
		||||
                _model = "text-babbage-001";
 | 
			
		||||
                break;
 | 
			
		||||
            case Gpt3Model.Curie001:
 | 
			
		||||
                _model = "text-curie-001";
 | 
			
		||||
                break;
 | 
			
		||||
            case Gpt3Model.Davinci003:
 | 
			
		||||
                _model = "text-davinci-003";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _maxTokens = maxTokens;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string> Think(string input)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
 | 
			
		||||
        var data = await http.PostAsJsonAsync(Uri, new Gpt3ApiRequest()
 | 
			
		||||
        {
 | 
			
		||||
            Model = _model,
 | 
			
		||||
            Prompt = input,
 | 
			
		||||
            MaxTokens = _maxTokens,
 | 
			
		||||
            Temperature = 1,
 | 
			
		||||
        });
 | 
			
		||||
        var dataString = await data.Content.ReadAsStringAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var response = JsonConvert.DeserializeObject<Gpt3Response>(dataString);
 | 
			
		||||
 | 
			
		||||
            return response?.Choices[0]?.Text;
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace NadekoBot.Modules.Games.Common;
 | 
			
		||||
public sealed partial class GamesConfig : ICloneable<GamesConfig>
 | 
			
		||||
{
 | 
			
		||||
    [Comment("DO NOT CHANGE")]
 | 
			
		||||
    public int Version { get; set; }
 | 
			
		||||
    public int Version { get; set; } = 2;
 | 
			
		||||
 | 
			
		||||
    [Comment("Hangman related settings (.hangman command)")]
 | 
			
		||||
    public HangmanConfig Hangman { get; set; } = new()
 | 
			
		||||
@@ -95,6 +95,27 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
 | 
			
		||||
            Name = "Unicorn"
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Which chatbot API should bot use.
 | 
			
		||||
'cleverbot' - bot will use Cleverbot API. 
 | 
			
		||||
'gpt3' - bot will use GPT-3 API")]
 | 
			
		||||
    public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3;
 | 
			
		||||
 | 
			
		||||
    public ChatGptConfig ChatGpt { get; set; } = new();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Cloneable]
 | 
			
		||||
public sealed partial class ChatGptConfig
 | 
			
		||||
{
 | 
			
		||||
    [Comment(@"Which GPT-3 Model should bot use.
 | 
			
		||||
'ada001' - cheapest and fastest 
 | 
			
		||||
'babbage001' - 2nd option
 | 
			
		||||
'curie001' - 3rd option
 | 
			
		||||
'davinci003' - Most expensive, slowest")]
 | 
			
		||||
    public Gpt3Model Model { get; set; } = Gpt3Model.Ada001;
 | 
			
		||||
 | 
			
		||||
    [Comment(@"The maximum number of tokens to use per GPT-3 API call")]
 | 
			
		||||
    public int MaxTokens { get; set; } = 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Cloneable]
 | 
			
		||||
@@ -122,4 +143,18 @@ public sealed partial class RaceAnimal
 | 
			
		||||
{
 | 
			
		||||
    public string Icon { get; set; }
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum ChatBotImplementation
 | 
			
		||||
{
 | 
			
		||||
    Cleverbot,
 | 
			
		||||
    Gpt3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum Gpt3Model
 | 
			
		||||
{
 | 
			
		||||
    Ada001,
 | 
			
		||||
    Babbage001,
 | 
			
		||||
    Curie001,
 | 
			
		||||
    Davinci003
 | 
			
		||||
}
 | 
			
		||||
@@ -28,6 +28,20 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
 | 
			
		||||
            long.TryParse,
 | 
			
		||||
            ConfigPrinters.ToString,
 | 
			
		||||
            val => val >= 0);
 | 
			
		||||
        
 | 
			
		||||
        AddParsedProp("chatbot",
 | 
			
		||||
            gs => gs.ChatBot,
 | 
			
		||||
            ConfigParsers.InsensitiveEnum,
 | 
			
		||||
            ConfigPrinters.ToString);
 | 
			
		||||
        AddParsedProp("gpt.model",
 | 
			
		||||
            gs => gs.ChatGpt.Model,
 | 
			
		||||
            ConfigParsers.InsensitiveEnum,
 | 
			
		||||
            ConfigPrinters.ToString);
 | 
			
		||||
        AddParsedProp("gpt.max_tokens",
 | 
			
		||||
            gs => gs.ChatGpt.MaxTokens,
 | 
			
		||||
            int.TryParse,
 | 
			
		||||
            ConfigPrinters.ToString,
 | 
			
		||||
            val => val > 0);
 | 
			
		||||
 | 
			
		||||
        Migrate();
 | 
			
		||||
    }
 | 
			
		||||
@@ -45,5 +59,14 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (data.Version < 2)
 | 
			
		||||
        {
 | 
			
		||||
            ModifyConfig(c =>
 | 
			
		||||
            {
 | 
			
		||||
                c.Version = 2;
 | 
			
		||||
                c.ChatBot = ChatBotImplementation.Cleverbot;  
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,6 @@ public class GamesService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    //channelId, game
 | 
			
		||||
    public ConcurrentDictionary<ulong, AcrophobiaGame> AcrophobiaGames { get; } = new();
 | 
			
		||||
    public ConcurrentDictionary<ulong, TriviaGame> RunningTrivias { get; } = new();
 | 
			
		||||
    public Dictionary<ulong, TicTacToe> TicTacToeGames { get; } = new();
 | 
			
		||||
    public ConcurrentDictionary<ulong, TypingGame> RunningContests { get; } = new();
 | 
			
		||||
    public ConcurrentDictionary<ulong, NunchiGame> NunchiGames { get; } = new();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
@@ -27,10 +28,11 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
 | 
			
		||||
    private readonly IGoogleApiService _google;
 | 
			
		||||
 | 
			
		||||
    public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google)
 | 
			
		||||
    public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google, SearchesConfigService scs)
 | 
			
		||||
    {
 | 
			
		||||
        _trackCacher = trackCacher;
 | 
			
		||||
        _google = google;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        _ytdlPlaylistOperation = new("-4 "
 | 
			
		||||
                                     + "--geo-bypass "
 | 
			
		||||
@@ -44,7 +46,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
                                     + "--no-check-certificate "
 | 
			
		||||
                                     + "-i "
 | 
			
		||||
                                     + "--yes-playlist "
 | 
			
		||||
                                     + "-- \"{0}\"");
 | 
			
		||||
                                     + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
 | 
			
		||||
        _ytdlIdOperation = new("-4 "
 | 
			
		||||
                               + "--geo-bypass "
 | 
			
		||||
@@ -56,7 +58,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
                               + "--get-thumbnail "
 | 
			
		||||
                               + "--get-duration "
 | 
			
		||||
                               + "--no-check-certificate "
 | 
			
		||||
                               + "-- \"{0}\"");
 | 
			
		||||
                               + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
 | 
			
		||||
        _ytdlSearchOperation = new("-4 "
 | 
			
		||||
                                   + "--geo-bypass "
 | 
			
		||||
@@ -69,7 +71,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
                                   + "--get-duration "
 | 
			
		||||
                                   + "--no-check-certificate "
 | 
			
		||||
                                   + "--default-search "
 | 
			
		||||
                                   + "\"ytsearch:\" -- \"{0}\"");
 | 
			
		||||
                                   + "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private YtTrackData ResolveYtdlData(string ytdlOutputString)
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,6 @@ public interface ISearchImagesService
 | 
			
		||||
    ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag);
 | 
			
		||||
    ValueTask<string[]> GetBlacklistedTags(ulong guildId);
 | 
			
		||||
    Task<UrlReply> Butts();
 | 
			
		||||
    Task<Gallery> GetNhentaiByIdAsync(uint id);
 | 
			
		||||
    Task<Gallery> GetNhentaiBySearchAsync(string search);
 | 
			
		||||
    // Task<Gallery> GetNhentaiByIdAsync(uint id);
 | 
			
		||||
    // Task<Gallery> GetNhentaiBySearchAsync(string search);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
public interface INhentaiService
 | 
			
		||||
{
 | 
			
		||||
    Task<Gallery?> GetAsync(uint id);
 | 
			
		||||
    Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search);
 | 
			
		||||
}
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
//
 | 
			
		||||
// public interface INhentaiService
 | 
			
		||||
// {
 | 
			
		||||
//     Task<Gallery?> GetAsync(uint id);
 | 
			
		||||
//     Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search);
 | 
			
		||||
// }
 | 
			
		||||
@@ -1,115 +1,115 @@
 | 
			
		||||
using AngleSharp.Html.Dom;
 | 
			
		||||
using AngleSharp.Html.Parser;
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
public sealed class NhentaiScraperService : INhentaiService, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
    private static readonly HtmlParser _htmlParser = new(new()
 | 
			
		||||
    {
 | 
			
		||||
        IsScripting = false,
 | 
			
		||||
        IsEmbedded = false,
 | 
			
		||||
        IsSupportingProcessingInstructions = false,
 | 
			
		||||
        IsKeepingSourceReferences = false,
 | 
			
		||||
        IsNotSupportingFrames = true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    public NhentaiScraperService(IHttpClientFactory httpFactory)
 | 
			
		||||
    {
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private HttpClient GetHttpClient()
 | 
			
		||||
    {
 | 
			
		||||
        var http = _httpFactory.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36");
 | 
			
		||||
        http.DefaultRequestHeaders.Add("Cookie", "cf_clearance=I5pR71P4wJkRBFTLFjBndI.GwfKwT.Gx06uS8XNmRJo-1657214595-0-150; csrftoken=WMWRLtsQtBVQYvYkbqXKJHI9T1JwWCdd3tNhoxHn7aHLUYHAqe60XFUKAoWsJtda");
 | 
			
		||||
        return http;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<Gallery?> GetAsync(uint id)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = GetHttpClient();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var url = $"https://nhentai.net/g/{id}/";
 | 
			
		||||
            var strRes = await http.GetStringAsync(url);
 | 
			
		||||
            var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
 | 
			
		||||
            var title = doc.QuerySelector("#info .title")?.TextContent;
 | 
			
		||||
            var fullTitle = doc.QuerySelector("meta[itemprop=\"name\"]")?.Attributes["content"]?.Value
 | 
			
		||||
                            ?? title;
 | 
			
		||||
            var thumb = (doc.QuerySelector("#cover a img") as IHtmlImageElement)?.Dataset["src"];
 | 
			
		||||
 | 
			
		||||
            var tagsElem = doc.QuerySelector("#tags");
 | 
			
		||||
            
 | 
			
		||||
            var pageCount = tagsElem?.QuerySelector("a.tag[href^=\"/search/?q=pages\"] span")?.TextContent;
 | 
			
		||||
            var likes = doc.QuerySelector(".buttons .btn-disabled.btn.tooltip span span")?.TextContent?.Trim('(', ')');
 | 
			
		||||
            var uploadedAt = (tagsElem?.QuerySelector(".tag-container .tags time.nobold") as IHtmlTimeElement)?.DateTime;
 | 
			
		||||
 | 
			
		||||
            var tags = tagsElem?.QuerySelectorAll(".tag-container .tags > a.tag[href^=\"/tag\"]")
 | 
			
		||||
                .Cast<IHtmlAnchorElement>()
 | 
			
		||||
                .Select(x => new Tag()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = x.QuerySelector("span:first-child")?.TextContent,
 | 
			
		||||
                    Url = $"https://nhentai.net{x.PathName}"
 | 
			
		||||
                })
 | 
			
		||||
                .ToArray();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(fullTitle))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            if (!int.TryParse(pageCount, out var pc))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            if (!int.TryParse(likes, out var lc))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            if (!DateTime.TryParse(uploadedAt, out var ua))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return new Gallery(id,
 | 
			
		||||
                url,
 | 
			
		||||
                fullTitle,
 | 
			
		||||
                title,
 | 
			
		||||
                thumb,
 | 
			
		||||
                pc,
 | 
			
		||||
                lc,
 | 
			
		||||
                ua,
 | 
			
		||||
                tags);
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpRequestException)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Nhentai with id {NhentaiId} not found", id);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = GetHttpClient();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var url = $"https://nhentai.net/search/?q={Uri.EscapeDataString(search)}&sort=popular-today";
 | 
			
		||||
            var strRes = await http.GetStringAsync(url);
 | 
			
		||||
            var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
 | 
			
		||||
            var elems = doc.QuerySelectorAll(".container .gallery a")
 | 
			
		||||
                .Cast<IHtmlAnchorElement>()
 | 
			
		||||
                .Where(x => x.PathName.StartsWith("/g/"))
 | 
			
		||||
                .Select(x => x.PathName[3..^1])
 | 
			
		||||
                .Select(uint.Parse)
 | 
			
		||||
                .ToArray();
 | 
			
		||||
            
 | 
			
		||||
            return elems;
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpRequestException)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Nhentai search for {NhentaiSearch} failed", search);
 | 
			
		||||
            return Array.Empty<uint>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
// using AngleSharp.Html.Dom;
 | 
			
		||||
// using AngleSharp.Html.Parser;
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class NhentaiScraperService : INhentaiService, INService
 | 
			
		||||
// {
 | 
			
		||||
//     private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
//
 | 
			
		||||
//     private static readonly HtmlParser _htmlParser = new(new()
 | 
			
		||||
//     {
 | 
			
		||||
//         IsScripting = false,
 | 
			
		||||
//         IsEmbedded = false,
 | 
			
		||||
//         IsSupportingProcessingInstructions = false,
 | 
			
		||||
//         IsKeepingSourceReferences = false,
 | 
			
		||||
//         IsNotSupportingFrames = true
 | 
			
		||||
//     });
 | 
			
		||||
//
 | 
			
		||||
//     public NhentaiScraperService(IHttpClientFactory httpFactory)
 | 
			
		||||
//     {
 | 
			
		||||
//         _httpFactory = httpFactory;
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     private HttpClient GetHttpClient()
 | 
			
		||||
//     {
 | 
			
		||||
//         var http = _httpFactory.CreateClient();
 | 
			
		||||
//         http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36");
 | 
			
		||||
//         http.DefaultRequestHeaders.Add("Cookie", "cf_clearance=I5pR71P4wJkRBFTLFjBndI.GwfKwT.Gx06uS8XNmRJo-1657214595-0-150; csrftoken=WMWRLtsQtBVQYvYkbqXKJHI9T1JwWCdd3tNhoxHn7aHLUYHAqe60XFUKAoWsJtda");
 | 
			
		||||
//         return http;
 | 
			
		||||
//     }
 | 
			
		||||
//     
 | 
			
		||||
//     public async Task<Gallery?> GetAsync(uint id)
 | 
			
		||||
//     {
 | 
			
		||||
//         using var http = GetHttpClient();
 | 
			
		||||
//         try
 | 
			
		||||
//         {
 | 
			
		||||
//             var url = $"https://nhentai.net/g/{id}/";
 | 
			
		||||
//             var strRes = await http.GetStringAsync(url);
 | 
			
		||||
//             var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
//
 | 
			
		||||
//             var title = doc.QuerySelector("#info .title")?.TextContent;
 | 
			
		||||
//             var fullTitle = doc.QuerySelector("meta[itemprop=\"name\"]")?.Attributes["content"]?.Value
 | 
			
		||||
//                             ?? title;
 | 
			
		||||
//             var thumb = (doc.QuerySelector("#cover a img") as IHtmlImageElement)?.Dataset["src"];
 | 
			
		||||
//
 | 
			
		||||
//             var tagsElem = doc.QuerySelector("#tags");
 | 
			
		||||
//             
 | 
			
		||||
//             var pageCount = tagsElem?.QuerySelector("a.tag[href^=\"/search/?q=pages\"] span")?.TextContent;
 | 
			
		||||
//             var likes = doc.QuerySelector(".buttons .btn-disabled.btn.tooltip span span")?.TextContent?.Trim('(', ')');
 | 
			
		||||
//             var uploadedAt = (tagsElem?.QuerySelector(".tag-container .tags time.nobold") as IHtmlTimeElement)?.DateTime;
 | 
			
		||||
//
 | 
			
		||||
//             var tags = tagsElem?.QuerySelectorAll(".tag-container .tags > a.tag[href^=\"/tag\"]")
 | 
			
		||||
//                 .Cast<IHtmlAnchorElement>()
 | 
			
		||||
//                 .Select(x => new Tag()
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Name = x.QuerySelector("span:first-child")?.TextContent,
 | 
			
		||||
//                     Url = $"https://nhentai.net{x.PathName}"
 | 
			
		||||
//                 })
 | 
			
		||||
//                 .ToArray();
 | 
			
		||||
//
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(fullTitle))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!int.TryParse(pageCount, out var pc))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!int.TryParse(likes, out var lc))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!DateTime.TryParse(uploadedAt, out var ua))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             return new Gallery(id,
 | 
			
		||||
//                 url,
 | 
			
		||||
//                 fullTitle,
 | 
			
		||||
//                 title,
 | 
			
		||||
//                 thumb,
 | 
			
		||||
//                 pc,
 | 
			
		||||
//                 lc,
 | 
			
		||||
//                 ua,
 | 
			
		||||
//                 tags);
 | 
			
		||||
//         }
 | 
			
		||||
//         catch (HttpRequestException)
 | 
			
		||||
//         {
 | 
			
		||||
//             Log.Warning("Nhentai with id {NhentaiId} not found", id);
 | 
			
		||||
//             return null;
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     public async Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search)
 | 
			
		||||
//     {
 | 
			
		||||
//         using var http = GetHttpClient();
 | 
			
		||||
//         try
 | 
			
		||||
//         {
 | 
			
		||||
//             var url = $"https://nhentai.net/search/?q={Uri.EscapeDataString(search)}&sort=popular-today";
 | 
			
		||||
//             var strRes = await http.GetStringAsync(url);
 | 
			
		||||
//             var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
//
 | 
			
		||||
//             var elems = doc.QuerySelectorAll(".container .gallery a")
 | 
			
		||||
//                 .Cast<IHtmlAnchorElement>()
 | 
			
		||||
//                 .Where(x => x.PathName.StartsWith("/g/"))
 | 
			
		||||
//                 .Select(x => x.PathName[3..^1])
 | 
			
		||||
//                 .Select(uint.Parse)
 | 
			
		||||
//                 .ToArray();
 | 
			
		||||
//             
 | 
			
		||||
//             return elems;
 | 
			
		||||
//         }
 | 
			
		||||
//         catch (HttpRequestException)
 | 
			
		||||
//         {
 | 
			
		||||
//             Log.Warning("Nhentai search for {NhentaiSearch} failed", search);
 | 
			
		||||
//             return Array.Empty<uint>();
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
@@ -360,67 +360,65 @@ public partial class NSFW : NadekoModule<ISearchImagesService>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task Nhentai(uint id)
 | 
			
		||||
    {
 | 
			
		||||
        var g = await _service.GetNhentaiByIdAsync(id);
 | 
			
		||||
 | 
			
		||||
        if (g is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public async Task Nhentai([Leftover] string query)
 | 
			
		||||
    {
 | 
			
		||||
        var g = await _service.GetNhentaiBySearchAsync(query);
 | 
			
		||||
 | 
			
		||||
        if (g is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task SendNhentaiGalleryInternalAsync(Gallery g)
 | 
			
		||||
    {
 | 
			
		||||
        var count = 0;
 | 
			
		||||
        var tagString = g.Tags.Shuffle()
 | 
			
		||||
                         .Select(tag => $"[{tag.Name}]({tag.Url})")
 | 
			
		||||
                         .TakeWhile(tag => (count += tag.Length) < 1000)
 | 
			
		||||
                         .Join(" ");
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
            .WithTitle(g.Title)
 | 
			
		||||
            .WithDescription(g.FullTitle)
 | 
			
		||||
            .WithImageUrl(g.Thumbnail)
 | 
			
		||||
            .WithUrl(g.Url)
 | 
			
		||||
            .AddField(GetText(strs.favorites), g.Likes, true)
 | 
			
		||||
            .AddField(GetText(strs.pages), g.PageCount, true)
 | 
			
		||||
            .AddField(GetText(strs.tags),
 | 
			
		||||
                string.IsNullOrWhiteSpace(tagString)
 | 
			
		||||
                    ? "?"
 | 
			
		||||
                    : tagString,
 | 
			
		||||
                true)
 | 
			
		||||
            .WithFooter(g.UploadedAt.ToString("f"))
 | 
			
		||||
            .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
    // [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    // [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    // [Priority(1)]
 | 
			
		||||
    // public async Task Nhentai(uint id)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var g = await _service.GetNhentaiByIdAsync(id);
 | 
			
		||||
    //
 | 
			
		||||
    //     if (g is null)
 | 
			
		||||
    //     {
 | 
			
		||||
    //         await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
    //         return;
 | 
			
		||||
    //     }
 | 
			
		||||
    //
 | 
			
		||||
    //     await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // [Cmd]
 | 
			
		||||
    // [RequireContext(ContextType.Guild)]
 | 
			
		||||
    // [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    // [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    // [Priority(0)]
 | 
			
		||||
    // public async Task Nhentai([Leftover] string query)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var g = await _service.GetNhentaiBySearchAsync(query);
 | 
			
		||||
    //
 | 
			
		||||
    //     if (g is null)
 | 
			
		||||
    //     {
 | 
			
		||||
    //         await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
    //         return;
 | 
			
		||||
    //     }
 | 
			
		||||
    //
 | 
			
		||||
    //     await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // private async Task SendNhentaiGalleryInternalAsync(Gallery g)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var count = 0;
 | 
			
		||||
    //     var tagString = g.Tags.Shuffle()
 | 
			
		||||
    //                      .Select(tag => $"[{tag.Name}]({tag.Url})")
 | 
			
		||||
    //                      .TakeWhile(tag => (count += tag.Length) < 1000)
 | 
			
		||||
    //                      .Join(" ");
 | 
			
		||||
    //
 | 
			
		||||
    //     var embed = _eb.Create()
 | 
			
		||||
    //         .WithTitle(g.Title)
 | 
			
		||||
    //         .WithDescription(g.FullTitle)
 | 
			
		||||
    //         .WithImageUrl(g.Thumbnail)
 | 
			
		||||
    //         .WithUrl(g.Url)
 | 
			
		||||
    //         .AddField(GetText(strs.favorites), g.Likes, true)
 | 
			
		||||
    //         .AddField(GetText(strs.pages), g.PageCount, true)
 | 
			
		||||
    //         .AddField(GetText(strs.tags),
 | 
			
		||||
    //             string.IsNullOrWhiteSpace(tagString)
 | 
			
		||||
    //                 ? "?"
 | 
			
		||||
    //                 : tagString,
 | 
			
		||||
    //             true)
 | 
			
		||||
    //         .WithFooter(g.UploadedAt.ToString("f"))
 | 
			
		||||
    //         .WithOkColor();
 | 
			
		||||
    //
 | 
			
		||||
    //     await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    private async Task InternalDapiCommand(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
 
 | 
			
		||||
@@ -19,17 +19,15 @@ public class SearchImagesService : ISearchImagesService, INService
 | 
			
		||||
    private readonly SearchImageCacher _cache;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly INhentaiService _nh;
 | 
			
		||||
 | 
			
		||||
    private readonly object _taglock = new();
 | 
			
		||||
 | 
			
		||||
    public SearchImagesService(
 | 
			
		||||
        DbService db,
 | 
			
		||||
        SearchImageCacher cacher,
 | 
			
		||||
        IHttpClientFactory httpFactory,
 | 
			
		||||
        INhentaiService nh)
 | 
			
		||||
        IHttpClientFactory httpFactory
 | 
			
		||||
        )
 | 
			
		||||
    {
 | 
			
		||||
        _nh = nh;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
        _cache = cacher;
 | 
			
		||||
@@ -277,6 +275,7 @@ public class SearchImagesService : ISearchImagesService, INService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    #region Nhentai
 | 
			
		||||
 | 
			
		||||
    public Task<Gallery?> GetNhentaiByIdAsync(uint id)
 | 
			
		||||
@@ -294,4 +293,5 @@ public class SearchImagesService : ISearchImagesService, INService
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    */
 | 
			
		||||
}
 | 
			
		||||
@@ -39,7 +39,7 @@ public partial class Searches
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task MagicItem()
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.WowJokes.Any())
 | 
			
		||||
            if (!_service.MagicItems.Any())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.magicitems_not_loaded);
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
@@ -199,7 +199,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        if (!await ValidateQuery(ffs))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var shortenedUrl = await _google.ShortenUrl($"https://lmgtfy.com/?q={Uri.EscapeDataString(ffs)}");
 | 
			
		||||
        var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(ffs)}");
 | 
			
		||||
        await SendConfirmAsync($"<{shortenedUrl}>");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -325,7 +325,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
                            return _eb.Create()
 | 
			
		||||
                                      .WithOkColor()
 | 
			
		||||
                                      .WithUrl(item.Permalink)
 | 
			
		||||
                                      .WithAuthor(item.Word)
 | 
			
		||||
                                      .WithTitle(item.Word)
 | 
			
		||||
                                      .WithDescription(item.Definition);
 | 
			
		||||
                        },
 | 
			
		||||
                        items.Length,
 | 
			
		||||
@@ -612,4 +612,4 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        [JsonProperty("result_url")]
 | 
			
		||||
        public string ResultUrl { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
 | 
			
		||||
        
 | 
			
		||||
        - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
 | 
			
		||||
        """)]
 | 
			
		||||
    public YoutubeSearcher YtProvider { get; set; } = YoutubeSearcher.Ytdl;
 | 
			
		||||
    public YoutubeSearcher YtProvider { get; set; } = YoutubeSearcher.Ytdlp;
 | 
			
		||||
 | 
			
		||||
    [Comment("""
 | 
			
		||||
        Set the searx instance urls in case you want to use 'searx' for either img or web search.
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,7 @@
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
 | 
			
		||||
 | 
			
		||||
        <PackageReference Include="MorseCode.ITask" Version="2.0.3" />
 | 
			
		||||
        <PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
 | 
			
		||||
        
 | 
			
		||||
@@ -78,13 +79,14 @@
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        <!-- Db-related packages -->
 | 
			
		||||
        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
 | 
			
		||||
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
 | 
			
		||||
            <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        
 | 
			
		||||
  
 | 
			
		||||
        <PackageReference Include="linq2db.EntityFrameworkCore" Version="7.1.0" />
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />        
 | 
			
		||||
@@ -93,7 +95,7 @@
 | 
			
		||||
        
 | 
			
		||||
        <!-- Used by stream notifications -->
 | 
			
		||||
        <PackageReference Include="TwitchLib.Api" Version="3.4.1" />
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        <!-- Uncomment to check for disposable issues -->
 | 
			
		||||
<!--        <PackageReference Include="IDisposableAnalyzers" Version="4.0.2">-->
 | 
			
		||||
<!--            <PrivateAssets>all</PrivateAssets>-->
 | 
			
		||||
@@ -101,6 +103,7 @@
 | 
			
		||||
<!--        </PackageReference>-->
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
 | 
			
		||||
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
@@ -134,7 +137,7 @@
 | 
			
		||||
        <Version Condition=" '$(VersionSuffix)' != '' ">$(VersionPrefix).$(VersionSuffix)</Version>
 | 
			
		||||
        <Version Condition=" '$(Version)' == '' ">$(VersionPrefix)</Version>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' ">
 | 
			
		||||
        <!-- Define trace doesn't seem to affect the build at all so I had to remove $(DefineConstants)-->
 | 
			
		||||
        <DefineTrace>false</DefineTrace>
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@ public sealed class GamblingTxTracker : ITxTracker, INService, IReadyExecutor
 | 
			
		||||
    public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx
 | 
			
		||||
            .GetTable<GamblingStats>()
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
        return await ctx.Set<GamblingStats>()
 | 
			
		||||
            .ToListAsyncEF();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -34,19 +34,19 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
    public BotCredsProvider(int? totalShards = null, string credPath = null)
 | 
			
		||||
    {
 | 
			
		||||
        _totalShards = totalShards;
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(credPath))
 | 
			
		||||
        {
 | 
			
		||||
            CredsPath = credPath;
 | 
			
		||||
            CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
 | 
			
		||||
            CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
 | 
			
		||||
        _totalShards = totalShards;
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(credPath))
 | 
			
		||||
        {
 | 
			
		||||
            CredsPath = credPath;
 | 
			
		||||
            CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
 | 
			
		||||
            CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!File.Exists(CredsExamplePath))
 | 
			
		||||
@@ -69,8 +69,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
        _config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
 | 
			
		||||
                                            .AddEnvironmentVariables("NadekoBot_")
 | 
			
		||||
                                            .Build();
 | 
			
		||||
 | 
			
		||||
                                            .Build();
 | 
			
		||||
 | 
			
		||||
        _changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
 | 
			
		||||
        Reload();
 | 
			
		||||
    }
 | 
			
		||||
@@ -131,14 +131,14 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
        ymlData = Yaml.Serializer.Serialize(creds);
 | 
			
		||||
        File.WriteAllText(CREDS_FILE_NAME, ymlData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string OldCredsJsonPath
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
 | 
			
		||||
 | 
			
		||||
    private string OldCredsJsonBackupPath
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
 | 
			
		||||
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
 | 
			
		||||
 | 
			
		||||
    private void MigrateCredentials()
 | 
			
		||||
    {
 | 
			
		||||
        if (File.Exists(OldCredsJsonPath))
 | 
			
		||||
@@ -177,15 +177,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
            Log.Warning(
 | 
			
		||||
                "Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (File.Exists(CREDS_FILE_NAME))
 | 
			
		||||
        {
 | 
			
		||||
            var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
 | 
			
		||||
            if (creds.Version <= 5)
 | 
			
		||||
            {
 | 
			
		||||
                creds.Version = 6;
 | 
			
		||||
                creds.BotCache = BotCacheImplemenation.Redis;
 | 
			
		||||
            }
 | 
			
		||||
            if (creds.Version <= 6)
 | 
			
		||||
            {
 | 
			
		||||
                creds.Version = 7;
 | 
			
		||||
                File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    public const string BOT_VERSION = "4.3.11";
 | 
			
		||||
    public const string BOT_VERSION = "4.3.13";
 | 
			
		||||
 | 
			
		||||
    public string Author
 | 
			
		||||
        => "Kwoth#2452";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# DO NOT CHANGE
 | 
			
		||||
version: 6
 | 
			
		||||
version: 7
 | 
			
		||||
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
 | 
			
		||||
token: ''
 | 
			
		||||
# List of Ids of the users who have bot owner permissions
 | 
			
		||||
@@ -56,6 +56,8 @@ patreon:
 | 
			
		||||
botListToken: ''
 | 
			
		||||
# Official cleverbot api key.
 | 
			
		||||
cleverbotApiKey: ''
 | 
			
		||||
# Official GPT-3 api key.
 | 
			
		||||
gpt3ApiKey: ''
 | 
			
		||||
# Which cache implementation should bot use.
 | 
			
		||||
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
 | 
			
		||||
# 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
 | 
			
		||||
 
 | 
			
		||||
@@ -664,8 +664,6 @@ avatar:
 | 
			
		||||
  - av
 | 
			
		||||
hentai:
 | 
			
		||||
  - hentai
 | 
			
		||||
nhentai:
 | 
			
		||||
  - nhentai
 | 
			
		||||
danbooru:
 | 
			
		||||
  - danbooru
 | 
			
		||||
derpibooru:
 | 
			
		||||
@@ -807,6 +805,7 @@ hentaibomb:
 | 
			
		||||
  - hentaibomb
 | 
			
		||||
cleverbot:
 | 
			
		||||
  - cleverbot
 | 
			
		||||
  - chatgpt
 | 
			
		||||
shorten:
 | 
			
		||||
  - shorten
 | 
			
		||||
wikia:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# DO NOT CHANGE
 | 
			
		||||
version: 1
 | 
			
		||||
version: 2
 | 
			
		||||
# Hangman related settings (.hangman command)
 | 
			
		||||
hangman:
 | 
			
		||||
# The amount of currency awarded to the winner of a hangman game
 | 
			
		||||
@@ -8,8 +8,8 @@ hangman:
 | 
			
		||||
trivia:
 | 
			
		||||
# The amount of currency awarded to the winner of the trivia game.
 | 
			
		||||
  currencyReward: 0
 | 
			
		||||
  # Users won't be able to start trivia games which have 
 | 
			
		||||
# a smaller win requirement than the one specified by this setting.
 | 
			
		||||
  # Users won't be able to start trivia games which have
 | 
			
		||||
  # a smaller win requirement than the one specified by this setting.
 | 
			
		||||
  minimumWinReq: 1
 | 
			
		||||
# List of responses for the .8ball command. A random one will be selected every time
 | 
			
		||||
eightBallResponses:
 | 
			
		||||
@@ -54,3 +54,17 @@ raceAnimals:
 | 
			
		||||
    name: Crab
 | 
			
		||||
  - icon: "🦄"
 | 
			
		||||
    name: Unicorn
 | 
			
		||||
# Which chatbot API should bot use.
 | 
			
		||||
# 'cleverbot' - bot will use Cleverbot API.
 | 
			
		||||
# 'gpt3' - bot will use GPT-3 API
 | 
			
		||||
chatBot: gpt3
 | 
			
		||||
 | 
			
		||||
chatGpt:
 | 
			
		||||
  # Which GPT-3 Model should bot use.
 | 
			
		||||
  # 'ada001' - cheapest and fastest
 | 
			
		||||
  # 'babbage001' - 2nd option
 | 
			
		||||
  # 'curie001' - 3rd option
 | 
			
		||||
  # 'davinci003' - Most expensive, slowest
 | 
			
		||||
  model: davinci003
 | 
			
		||||
  # The maximum number of tokens to use per GPT-3 API call
 | 
			
		||||
  maxTokens: 100
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ imgSearchEngine: Google
 | 
			
		||||
# - `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
 | 
			
		||||
# 
 | 
			
		||||
# - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
 | 
			
		||||
ytProvider: Ytdl
 | 
			
		||||
ytProvider: Ytdlp
 | 
			
		||||
# Set the searx instance urls in case you want to use 'searx' for either img or web search.
 | 
			
		||||
# Nadeko will use a random one for each request.
 | 
			
		||||
# Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`
 | 
			
		||||
 
 | 
			
		||||
@@ -1117,11 +1117,6 @@ hentai:
 | 
			
		||||
  desc: "Shows a hentai image from a random website (gelbooru, danbooru, konachan or yandere) with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags. Only 1 tag allowed."
 | 
			
		||||
  args:
 | 
			
		||||
    - "yuri"
 | 
			
		||||
nhentai:
 | 
			
		||||
  desc: "Shows basic information about a hentai with the specified id, or a valid nhentai search query."
 | 
			
		||||
  args:
 | 
			
		||||
    - "273426"
 | 
			
		||||
    - "cute girl"
 | 
			
		||||
autohentai:
 | 
			
		||||
  desc: "Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tag groups. Random group will be chosen every time the image is sent. Max 2 tags per group. 20 seconds minimum. Provide no parameters to disable."
 | 
			
		||||
  args:
 | 
			
		||||
@@ -1377,7 +1372,7 @@ listservers:
 | 
			
		||||
  args:
 | 
			
		||||
    - "3"
 | 
			
		||||
cleverbot:
 | 
			
		||||
  desc: "Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Expressions starting with %bot.mention% won't work if cleverbot is enabled."
 | 
			
		||||
  desc: "Toggles cleverbot/chatgpt session. When enabled, the bot will reply to messages starting with bot mention in the server. Expressions starting with %bot.mention% won't work if cleverbot/chatgpt is enabled."
 | 
			
		||||
  args:
 | 
			
		||||
    - ""
 | 
			
		||||
shorten:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user