mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 08:34:27 -05:00 
			
		
		
		
	- Renamed CustomReaction model to NadekoExpression
- Used structured logging everywhere
This commit is contained in:
		@@ -7,12 +7,12 @@ namespace NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
public static class NadekoExpressionExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static int ClearFromGuild(this DbSet<CustomReaction> exprs, ulong guildId)
 | 
			
		||||
    public static int ClearFromGuild(this DbSet<NadekoExpression> exprs, ulong guildId)
 | 
			
		||||
        => exprs.Delete(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<CustomReaction> ForId(this DbSet<CustomReaction> exprs, ulong id)
 | 
			
		||||
    public static IEnumerable<NadekoExpression> ForId(this DbSet<NadekoExpression> exprs, ulong id)
 | 
			
		||||
        => exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
 | 
			
		||||
 | 
			
		||||
    public static CustomReaction GetByGuildIdAndInput(this DbSet<CustomReaction> exprs, ulong? guildId, string input)
 | 
			
		||||
    public static NadekoExpression GetByGuildIdAndInput(this DbSet<NadekoExpression> exprs, ulong? guildId, string input)
 | 
			
		||||
        => exprs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
public class CustomReaction : DbEntity
 | 
			
		||||
public class NadekoExpression : DbEntity
 | 
			
		||||
{
 | 
			
		||||
    public ulong? GuildId { get; set; }
 | 
			
		||||
    public string Response { get; set; }
 | 
			
		||||
@@ -34,7 +34,7 @@ public class NadekoContext : DbContext
 | 
			
		||||
    public DbSet<Reminder> Reminders { get; set; }
 | 
			
		||||
    public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
 | 
			
		||||
    public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
 | 
			
		||||
    public DbSet<CustomReaction> Expressions { get; set; }
 | 
			
		||||
    public DbSet<NadekoExpression> Expressions { get; set; }
 | 
			
		||||
    public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
 | 
			
		||||
    public DbSet<WaifuUpdate> WaifuUpdates { get; set; }
 | 
			
		||||
    public DbSet<Warning> Warnings { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ClubApplicants");
 | 
			
		||||
                    b.ToTable("ClubApplicants", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b =>
 | 
			
		||||
@@ -44,7 +44,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ClubBans");
 | 
			
		||||
                    b.ToTable("ClubBans", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b =>
 | 
			
		||||
@@ -86,7 +86,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("OwnerId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Clubs");
 | 
			
		||||
                    b.ToTable("Clubs", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b =>
 | 
			
		||||
@@ -151,7 +151,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("DiscordUser");
 | 
			
		||||
                    b.ToTable("DiscordUser", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
 | 
			
		||||
@@ -185,7 +185,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("FollowedStream");
 | 
			
		||||
                    b.ToTable("FollowedStream", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
@@ -214,7 +214,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AntiAltSetting");
 | 
			
		||||
                    b.ToTable("AntiAltSetting", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b =>
 | 
			
		||||
@@ -246,7 +246,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AntiRaidSetting");
 | 
			
		||||
                    b.ToTable("AntiRaidSetting", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b =>
 | 
			
		||||
@@ -268,7 +268,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AntiSpamSettingId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AntiSpamIgnore");
 | 
			
		||||
                    b.ToTable("AntiSpamIgnore", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b =>
 | 
			
		||||
@@ -300,7 +300,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AntiSpamSetting");
 | 
			
		||||
                    b.ToTable("AntiSpamSetting", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoCommand", b =>
 | 
			
		||||
@@ -338,7 +338,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AutoCommands");
 | 
			
		||||
                    b.ToTable("AutoCommands", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b =>
 | 
			
		||||
@@ -366,7 +366,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AutoTranslateChannels");
 | 
			
		||||
                    b.ToTable("AutoTranslateChannels", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b =>
 | 
			
		||||
@@ -394,7 +394,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasAlternateKey("ChannelId", "UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("AutoTranslateUsers");
 | 
			
		||||
                    b.ToTable("AutoTranslateUsers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b =>
 | 
			
		||||
@@ -417,7 +417,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("BanTemplates");
 | 
			
		||||
                    b.ToTable("BanTemplates", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistEntry", b =>
 | 
			
		||||
@@ -437,7 +437,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Blacklist");
 | 
			
		||||
                    b.ToTable("Blacklist", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b =>
 | 
			
		||||
@@ -462,7 +462,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("CommandAlias");
 | 
			
		||||
                    b.ToTable("CommandAlias", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b =>
 | 
			
		||||
@@ -487,7 +487,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("CommandCooldown");
 | 
			
		||||
                    b.ToTable("CommandCooldown", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b =>
 | 
			
		||||
@@ -512,7 +512,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("CurrencyTransactions");
 | 
			
		||||
                    b.ToTable("CurrencyTransactions", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b =>
 | 
			
		||||
@@ -550,7 +550,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Expressions");
 | 
			
		||||
                    b.ToTable("Expressions", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b =>
 | 
			
		||||
@@ -575,7 +575,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("DelMsgOnCmdChannel");
 | 
			
		||||
                    b.ToTable("DelMsgOnCmdChannel", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordPermOverride", b =>
 | 
			
		||||
@@ -601,7 +601,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId", "Command")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("DiscordPermOverrides");
 | 
			
		||||
                    b.ToTable("DiscordPermOverrides", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b =>
 | 
			
		||||
@@ -626,7 +626,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("XpSettingsId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ExcludedItem");
 | 
			
		||||
                    b.ToTable("ExcludedItem", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b =>
 | 
			
		||||
@@ -652,7 +652,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasAlternateKey("GuildConfigId", "Url");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("FeedSub");
 | 
			
		||||
                    b.ToTable("FeedSub", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b =>
 | 
			
		||||
@@ -679,7 +679,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId1");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("FilterChannelId");
 | 
			
		||||
                    b.ToTable("FilterChannelId", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b =>
 | 
			
		||||
@@ -701,7 +701,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("FilteredWord");
 | 
			
		||||
                    b.ToTable("FilteredWord", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b =>
 | 
			
		||||
@@ -723,7 +723,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("FilterLinksChannelId");
 | 
			
		||||
                    b.ToTable("FilterLinksChannelId", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b =>
 | 
			
		||||
@@ -745,7 +745,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("GCChannelId");
 | 
			
		||||
                    b.ToTable("GCChannelId", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b =>
 | 
			
		||||
@@ -771,7 +771,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId", "Number")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("GroupName");
 | 
			
		||||
                    b.ToTable("GroupName", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b =>
 | 
			
		||||
@@ -901,7 +901,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("WarnExpireHours");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("GuildConfigs");
 | 
			
		||||
                    b.ToTable("GuildConfigs", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b =>
 | 
			
		||||
@@ -927,7 +927,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("LogSettingId", "LogItemId", "ItemType")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("IgnoredLogChannels");
 | 
			
		||||
                    b.ToTable("IgnoredLogChannels", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b =>
 | 
			
		||||
@@ -949,7 +949,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("LogSettingId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("IgnoredVoicePresenceCHannels");
 | 
			
		||||
                    b.ToTable("IgnoredVoicePresenceCHannels", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b =>
 | 
			
		||||
@@ -972,7 +972,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("ChannelId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ImageOnlyChannels");
 | 
			
		||||
                    b.ToTable("ImageOnlyChannels", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b =>
 | 
			
		||||
@@ -1037,7 +1037,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("LogSettings");
 | 
			
		||||
                    b.ToTable("LogSettings", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlayerSettings", b =>
 | 
			
		||||
@@ -1071,7 +1071,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("MusicPlayerSettings");
 | 
			
		||||
                    b.ToTable("MusicPlayerSettings", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b =>
 | 
			
		||||
@@ -1094,7 +1094,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("MusicPlaylists");
 | 
			
		||||
                    b.ToTable("MusicPlaylists", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b =>
 | 
			
		||||
@@ -1116,7 +1116,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("MutedUserId");
 | 
			
		||||
                    b.ToTable("MutedUserId", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b =>
 | 
			
		||||
@@ -1138,7 +1138,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("NsfwBlacklistedTags");
 | 
			
		||||
                    b.ToTable("NsfwBlacklistedTags", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
 | 
			
		||||
@@ -1178,7 +1178,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Permissions");
 | 
			
		||||
                    b.ToTable("Permissions", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.PlantedCurrency", b =>
 | 
			
		||||
@@ -1215,7 +1215,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("MessageId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PlantedCurrency");
 | 
			
		||||
                    b.ToTable("PlantedCurrency", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b =>
 | 
			
		||||
@@ -1249,7 +1249,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("MusicPlaylistId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PlaylistSong");
 | 
			
		||||
                    b.ToTable("PlaylistSong", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b =>
 | 
			
		||||
@@ -1275,7 +1275,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Poll");
 | 
			
		||||
                    b.ToTable("Poll", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b =>
 | 
			
		||||
@@ -1300,7 +1300,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PollAnswer");
 | 
			
		||||
                    b.ToTable("PollAnswer", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b =>
 | 
			
		||||
@@ -1325,7 +1325,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PollVote");
 | 
			
		||||
                    b.ToTable("PollVote", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b =>
 | 
			
		||||
@@ -1361,7 +1361,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("Keyword");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Quotes");
 | 
			
		||||
                    b.ToTable("Quotes", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
 | 
			
		||||
@@ -1386,7 +1386,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ReactionRoleMessageId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ReactionRole");
 | 
			
		||||
                    b.ToTable("ReactionRole", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
 | 
			
		||||
@@ -1417,7 +1417,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ReactionRoleMessage");
 | 
			
		||||
                    b.ToTable("ReactionRoleMessage", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
 | 
			
		||||
@@ -1451,7 +1451,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("When");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Reminders");
 | 
			
		||||
                    b.ToTable("Reminders", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b =>
 | 
			
		||||
@@ -1486,7 +1486,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Repeaters");
 | 
			
		||||
                    b.ToTable("Repeaters", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b =>
 | 
			
		||||
@@ -1515,7 +1515,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("PatreonUserId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("RewardedUsers");
 | 
			
		||||
                    b.ToTable("RewardedUsers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.RotatingPlayingStatus", b =>
 | 
			
		||||
@@ -1535,7 +1535,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("RotatingStatus");
 | 
			
		||||
                    b.ToTable("RotatingStatus", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b =>
 | 
			
		||||
@@ -1566,7 +1566,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildId", "RoleId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SelfAssignableRoles");
 | 
			
		||||
                    b.ToTable("SelfAssignableRoles", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
 | 
			
		||||
@@ -1606,7 +1606,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ShopEntry");
 | 
			
		||||
                    b.ToTable("ShopEntry", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b =>
 | 
			
		||||
@@ -1628,7 +1628,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ShopEntryId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("ShopEntryItem");
 | 
			
		||||
                    b.ToTable("ShopEntryItem", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b =>
 | 
			
		||||
@@ -1650,7 +1650,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SlowmodeIgnoredRole");
 | 
			
		||||
                    b.ToTable("SlowmodeIgnoredRole", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b =>
 | 
			
		||||
@@ -1672,7 +1672,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SlowmodeIgnoredUser");
 | 
			
		||||
                    b.ToTable("SlowmodeIgnoredUser", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b =>
 | 
			
		||||
@@ -1697,7 +1697,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("StreamRoleSettingsId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("StreamRoleBlacklistedUser");
 | 
			
		||||
                    b.ToTable("StreamRoleBlacklistedUser", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b =>
 | 
			
		||||
@@ -1729,7 +1729,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("StreamRoleSettings");
 | 
			
		||||
                    b.ToTable("StreamRoleSettings", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b =>
 | 
			
		||||
@@ -1754,7 +1754,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("StreamRoleSettingsId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("StreamRoleWhitelistedUser");
 | 
			
		||||
                    b.ToTable("StreamRoleWhitelistedUser", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b =>
 | 
			
		||||
@@ -1779,7 +1779,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("UnbanTimer");
 | 
			
		||||
                    b.ToTable("UnbanTimer", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b =>
 | 
			
		||||
@@ -1804,7 +1804,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("UnmuteTimer");
 | 
			
		||||
                    b.ToTable("UnmuteTimer", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b =>
 | 
			
		||||
@@ -1832,7 +1832,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("UnroleTimer");
 | 
			
		||||
                    b.ToTable("UnroleTimer", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b =>
 | 
			
		||||
@@ -1877,7 +1877,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("UserId", "GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("UserXpStats");
 | 
			
		||||
                    b.ToTable("UserXpStats", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b =>
 | 
			
		||||
@@ -1902,7 +1902,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("VcRoleInfo");
 | 
			
		||||
                    b.ToTable("VcRoleInfo", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b =>
 | 
			
		||||
@@ -1937,7 +1937,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("WaifuId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("WaifuInfo");
 | 
			
		||||
                    b.ToTable("WaifuInfo", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b =>
 | 
			
		||||
@@ -1962,7 +1962,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("WaifuInfoId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("WaifuItem");
 | 
			
		||||
                    b.ToTable("WaifuItem", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b =>
 | 
			
		||||
@@ -1994,7 +1994,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("WaifuUpdates");
 | 
			
		||||
                    b.ToTable("WaifuUpdates", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b =>
 | 
			
		||||
@@ -2037,7 +2037,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Warnings");
 | 
			
		||||
                    b.ToTable("Warnings", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b =>
 | 
			
		||||
@@ -2068,7 +2068,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildConfigId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("WarningPunishment");
 | 
			
		||||
                    b.ToTable("WarningPunishment", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b =>
 | 
			
		||||
@@ -2093,7 +2093,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("XpSettingsId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("XpCurrencyReward");
 | 
			
		||||
                    b.ToTable("XpCurrencyReward", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b =>
 | 
			
		||||
@@ -2122,7 +2122,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("XpSettingsId", "Level")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("XpRoleReward");
 | 
			
		||||
                    b.ToTable("XpRoleReward", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b =>
 | 
			
		||||
@@ -2145,7 +2145,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.HasIndex("GuildConfigId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("XpSettings");
 | 
			
		||||
                    b.ToTable("XpSettings", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ public partial class Administration
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.ToString());
 | 
			
		||||
                Log.Warning(ex, "Exception in the mute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -124,7 +124,7 @@ public partial class Administration
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.ToString());
 | 
			
		||||
                Log.Warning(ex, "Exception in the chatmute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -148,7 +148,7 @@ public partial class Administration
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.ToString());
 | 
			
		||||
                Log.Warning(ex, "Error in chatmute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -221,7 +221,6 @@ public class ProtectionService : INService
 | 
			
		||||
                if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
 | 
			
		||||
                    if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
 | 
			
		||||
                    {
 | 
			
		||||
                        stats.Dispose();
 | 
			
		||||
                        var settings = spamSettings.AntiSpamSettings;
 | 
			
		||||
                        await PunishUsers(settings.Action,
 | 
			
		||||
                            ProtectionType.Spamming,
 | 
			
		||||
@@ -319,10 +318,8 @@ public class ProtectionService : INService
 | 
			
		||||
 | 
			
		||||
    public bool TryStopAntiSpam(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_antiSpamGuilds.TryRemove(guildId, out var removed))
 | 
			
		||||
        if (_antiSpamGuilds.TryRemove(guildId, out _))
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var (_, val) in removed.UserStats) val.Dispose();
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigsForId(guildId,
 | 
			
		||||
                set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +1,65 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public sealed class UserSpamStats : IDisposable
 | 
			
		||||
public sealed class UserSpamStats
 | 
			
		||||
{
 | 
			
		||||
    public int Count
 | 
			
		||||
        => timers.Count;
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            lock (_applyLock)
 | 
			
		||||
            {
 | 
			
		||||
                Cleanup();
 | 
			
		||||
                Log.Information("{Count}",_messageTracker.Count.ToString());
 | 
			
		||||
                return _messageTracker.Count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string LastMessage { get; set; }
 | 
			
		||||
    private string lastMessage;
 | 
			
		||||
 | 
			
		||||
    private ConcurrentQueue<Timer> timers;
 | 
			
		||||
    private readonly Queue<DateTime> _messageTracker;
 | 
			
		||||
 | 
			
		||||
    private readonly object _applyLock = new();
 | 
			
		||||
 | 
			
		||||
    private readonly TimeSpan _maxTime = TimeSpan.FromMinutes(30);
 | 
			
		||||
 | 
			
		||||
    public UserSpamStats(IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        LastMessage = msg.Content.ToUpperInvariant();
 | 
			
		||||
        timers = new();
 | 
			
		||||
        lastMessage = msg.Content.ToUpperInvariant();
 | 
			
		||||
        _messageTracker = new();
 | 
			
		||||
 | 
			
		||||
        ApplyNextMessage(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ApplyNextMessage(IUserMessage message)
 | 
			
		||||
    {
 | 
			
		||||
        var upperMsg = message.Content.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
        lock (_applyLock)
 | 
			
		||||
        {
 | 
			
		||||
            var upperMsg = message.Content.ToUpperInvariant();
 | 
			
		||||
            if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any()))
 | 
			
		||||
            if (upperMsg != lastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any()))
 | 
			
		||||
            {
 | 
			
		||||
                LastMessage = upperMsg;
 | 
			
		||||
                while (timers.TryDequeue(out var old))
 | 
			
		||||
                    old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                // if it's a new message, reset spam counter
 | 
			
		||||
                lastMessage = upperMsg;
 | 
			
		||||
                _messageTracker.Clear();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var t = new Timer(_ =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (timers.TryDequeue(out var old))
 | 
			
		||||
                        old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                },
 | 
			
		||||
                null,
 | 
			
		||||
                TimeSpan.FromMinutes(30),
 | 
			
		||||
                TimeSpan.FromMinutes(30));
 | 
			
		||||
            timers.Enqueue(t);
 | 
			
		||||
            _messageTracker.Enqueue(DateTime.UtcNow);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    private void Cleanup()
 | 
			
		||||
    {
 | 
			
		||||
        while (timers.TryDequeue(out var old))
 | 
			
		||||
            old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
        lock (_applyLock)
 | 
			
		||||
        {
 | 
			
		||||
            while (_messageTracker.TryPeek(out var dateTime))
 | 
			
		||||
            {
 | 
			
		||||
                if (DateTime.UtcNow - dateTime < _maxTime)
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                _messageTracker.Dequeue();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,7 +18,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
    private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
 | 
			
		||||
        new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> _autoCommands = new();
 | 
			
		||||
    private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
 | 
			
		||||
 | 
			
		||||
    private readonly IImageCache _imgs;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
@@ -75,12 +75,12 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
                if (server.OwnerId != _client.CurrentUser.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    await server.LeaveAsync();
 | 
			
		||||
                    Log.Information($"Left server {server.Name} [{server.Id}]");
 | 
			
		||||
                    Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await server.DeleteAsync();
 | 
			
		||||
                    Log.Information($"Deleted server {server.Name} [{server.Id}]");
 | 
			
		||||
                    Log.Information("Deleted server {Name} [{Id}]", server.Name, server.Id);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
@@ -89,7 +89,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        _autoCommands = uow.AutoCommands.AsNoTracking()
 | 
			
		||||
        autoCommands = uow.AutoCommands.AsNoTracking()
 | 
			
		||||
                           .Where(x => x.Interval >= 5)
 | 
			
		||||
                           .AsEnumerable()
 | 
			
		||||
                           .GroupBy(x => x.GuildId)
 | 
			
		||||
@@ -145,7 +145,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
 | 
			
		||||
        if (cmd.Interval >= 5)
 | 
			
		||||
        {
 | 
			
		||||
            var autos = _autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
 | 
			
		||||
            var autos = autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
 | 
			
		||||
            autos.AddOrUpdate(cmd.Id,
 | 
			
		||||
                _ => TimerFromAutoCommand(cmd),
 | 
			
		||||
                (_, old) =>
 | 
			
		||||
@@ -185,9 +185,9 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
 | 
			
		||||
        if (!ownerChannels.Any())
 | 
			
		||||
            Log.Warning(
 | 
			
		||||
                "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server.");
 | 
			
		||||
                "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server");
 | 
			
		||||
        else
 | 
			
		||||
            Log.Information($"Created {ownerChannels.Count} out of {_creds.OwnerIds.Count} owner message channels.");
 | 
			
		||||
            Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels", ownerChannels.Count, _creds.OwnerIds.Count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LeaveGuild(string guildStr)
 | 
			
		||||
@@ -220,7 +220,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("Can't contact owner with id {0}", ownerCh.Recipient.Id);
 | 
			
		||||
                        Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id);
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
@@ -262,7 +262,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
        if (cmd is not null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.Remove(cmd);
 | 
			
		||||
            if (_autoCommands.TryGetValue(cmd.GuildId, out var autos))
 | 
			
		||||
            if (autoCommands.TryGetValue(cmd.GuildId, out var autos))
 | 
			
		||||
                if (autos.TryRemove(cmd.Id, out var timer))
 | 
			
		||||
                    timer.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,7 @@ public partial class Administration
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.Message);
 | 
			
		||||
                Log.Warning(ex, "Exception occured while warning a user");
 | 
			
		||||
                var errorEmbed = _eb.Create().WithErrorColor().WithDescription(GetText(strs.cant_apply_punishment));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed) errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
@@ -565,7 +565,7 @@ public partial class Administration
 | 
			
		||||
        {
 | 
			
		||||
            var bans = await ctx.Guild.GetBansAsync();
 | 
			
		||||
 | 
			
		||||
            var bun = bans.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == user.ToLowerInvariant());
 | 
			
		||||
            var bun = bans.FirstOrDefault(x => x.User.ToString()!.ToLowerInvariant() == user.ToLowerInvariant());
 | 
			
		||||
 | 
			
		||||
            if (bun is null)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,9 @@ public class UserPunishService : INService
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning($"Can't find role {roleId.Value} on server {guild.Id} to apply punishment.");
 | 
			
		||||
                    Log.Warning("Can't find role {RoleId} on server {GuildId} to apply punishment",
 | 
			
		||||
                        roleId.Value,
 | 
			
		||||
                        guild.Id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
@@ -217,7 +219,9 @@ WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND
 | 
			
		||||
	AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));");
 | 
			
		||||
 | 
			
		||||
        if (cleared > 0 || deleted > 0)
 | 
			
		||||
            Log.Information($"Cleared {cleared} warnings and deleted {deleted} warnings due to expiry.");
 | 
			
		||||
            Log.Information("Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry",
 | 
			
		||||
                cleared,
 | 
			
		||||
                deleted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task CheckWarnExpiresAsync(ulong guildId)
 | 
			
		||||
@@ -305,7 +309,8 @@ WHERE GuildId={guildId}
 | 
			
		||||
        IRole role = null)
 | 
			
		||||
    {
 | 
			
		||||
        // these 3 don't make sense with time
 | 
			
		||||
        if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles && time is not null)
 | 
			
		||||
        if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
 | 
			
		||||
            && time is not null)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49)))
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ public class ExportedExpr
 | 
			
		||||
    public bool Ca { get; set; }
 | 
			
		||||
    public string[] React;
 | 
			
		||||
 | 
			
		||||
    public static ExportedExpr FromModel(CustomReaction cr)
 | 
			
		||||
    public static ExportedExpr FromModel(NadekoExpression cr)
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            Res = cr.Response,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ public static class NadekoExpressionExtensions
 | 
			
		||||
        => str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
 | 
			
		||||
 | 
			
		||||
    public static async Task<IUserMessage> Send(
 | 
			
		||||
        this CustomReaction cr,
 | 
			
		||||
        this NadekoExpression cr,
 | 
			
		||||
        IUserMessage ctx,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        bool sanitize)
 | 
			
		||||
 
 | 
			
		||||
@@ -46,17 +46,17 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private readonly object _gexprWriteLock = new();
 | 
			
		||||
 | 
			
		||||
    private readonly TypedKey<CustomReaction> _gexprAddedKey = new("gexpr.added");
 | 
			
		||||
    private readonly TypedKey<NadekoExpression> _gexprAddedKey = new("gexpr.added");
 | 
			
		||||
    private readonly TypedKey<int> _gexprDeletedkey = new("gexpr.deleted");
 | 
			
		||||
    private readonly TypedKey<CustomReaction> _gexprEditedKey = new("gexpr.edited");
 | 
			
		||||
    private readonly TypedKey<NadekoExpression> _gexprEditedKey = new("gexpr.edited");
 | 
			
		||||
    private readonly TypedKey<bool> _exprsReloadedKey = new("exprs.reloaded");
 | 
			
		||||
 | 
			
		||||
    // it is perfectly fine to have global expressions as an array
 | 
			
		||||
    // 1. expressions are almost never added (compared to how many times they are being looped through)
 | 
			
		||||
    // 2. only need write locks for this as we'll rebuild+replace the array on every edit
 | 
			
		||||
    // 3. there's never many of them (at most a thousand, usually < 100)
 | 
			
		||||
    private CustomReaction[] globalReactions;
 | 
			
		||||
    private ConcurrentDictionary<ulong, CustomReaction[]> newGuildReactions;
 | 
			
		||||
    private NadekoExpression[] globalReactions;
 | 
			
		||||
    private ConcurrentDictionary<ulong, NadekoExpression[]> newGuildReactions;
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
@@ -140,7 +140,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        ready = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private CustomReaction TryGetExpression(IUserMessage umsg)
 | 
			
		||||
    private NadekoExpression TryGetExpression(IUserMessage umsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (!ready)
 | 
			
		||||
            return null;
 | 
			
		||||
@@ -163,9 +163,9 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private CustomReaction MatchExpressions(in ReadOnlySpan<char> content, CustomReaction[] exprs)
 | 
			
		||||
    private NadekoExpression MatchExpressions(in ReadOnlySpan<char> content, NadekoExpression[] exprs)
 | 
			
		||||
    {
 | 
			
		||||
        var result = new List<CustomReaction>(1);
 | 
			
		||||
        var result = new List<NadekoExpression>(1);
 | 
			
		||||
        for (var i = 0; i < exprs.Length; i++)
 | 
			
		||||
        {
 | 
			
		||||
            var expr = exprs[i];
 | 
			
		||||
@@ -248,19 +248,19 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
                {
 | 
			
		||||
                    if (pc.Verbose)
 | 
			
		||||
                    {
 | 
			
		||||
                        var returnMsg = _strings.GetText(strs.perm_prevent(index + 1,
 | 
			
		||||
                        var permissionMessage = _strings.GetText(strs.perm_prevent(index + 1,
 | 
			
		||||
                                Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg))),
 | 
			
		||||
                            sg.Id);
 | 
			
		||||
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await msg.Channel.SendErrorAsync(_eb, returnMsg);
 | 
			
		||||
                            await msg.Channel.SendErrorAsync(_eb, permissionMessage);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        Log.Information(returnMsg);
 | 
			
		||||
                        Log.Information("{PermissionMessage}", permissionMessage);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
@@ -307,7 +307,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex.Message);
 | 
			
		||||
            Log.Warning(ex, "Error in Expression RunBehavior: {ErrorMessage}", ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -315,7 +315,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    public async Task ResetExprReactions(ulong? maybeGuildId, int id)
 | 
			
		||||
    {
 | 
			
		||||
        CustomReaction expr;
 | 
			
		||||
        NadekoExpression expr;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        expr = uow.Expressions.GetById(id);
 | 
			
		||||
        if (expr is null)
 | 
			
		||||
@@ -326,7 +326,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task UpdateInternalAsync(ulong? maybeGuildId, CustomReaction expr)
 | 
			
		||||
    private Task UpdateInternalAsync(ulong? maybeGuildId, NadekoExpression expr)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
            UpdateInternal(guildId, expr);
 | 
			
		||||
@@ -336,7 +336,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void UpdateInternal(ulong? maybeGuildId, CustomReaction expr)
 | 
			
		||||
    private void UpdateInternal(ulong? maybeGuildId, NadekoExpression expr)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
            newGuildReactions.AddOrUpdate(guildId,
 | 
			
		||||
@@ -359,7 +359,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task AddInternalAsync(ulong? maybeGuildId, CustomReaction expr)
 | 
			
		||||
    private Task AddInternalAsync(ulong? maybeGuildId, NadekoExpression expr)
 | 
			
		||||
    {
 | 
			
		||||
        // only do this for perf purposes
 | 
			
		||||
        expr.Trigger = expr.Trigger.Replace(MENTION_PH, _client.CurrentUser.Mention);
 | 
			
		||||
@@ -377,7 +377,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
        {
 | 
			
		||||
            newGuildReactions.AddOrUpdate(guildId,
 | 
			
		||||
                Array.Empty<CustomReaction>(),
 | 
			
		||||
                Array.Empty<NadekoExpression>(),
 | 
			
		||||
                (key, old) => DeleteInternal(old, id, out _));
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
@@ -392,16 +392,16 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private CustomReaction[] DeleteInternal(
 | 
			
		||||
        IReadOnlyList<CustomReaction> exprs,
 | 
			
		||||
    private NadekoExpression[] DeleteInternal(
 | 
			
		||||
        IReadOnlyList<NadekoExpression> exprs,
 | 
			
		||||
        int id,
 | 
			
		||||
        out CustomReaction deleted)
 | 
			
		||||
        out NadekoExpression deleted)
 | 
			
		||||
    {
 | 
			
		||||
        deleted = null;
 | 
			
		||||
        if (exprs is null || exprs.Count == 0)
 | 
			
		||||
            return exprs as CustomReaction[] ?? exprs?.ToArray();
 | 
			
		||||
            return exprs as NadekoExpression[] ?? exprs?.ToArray();
 | 
			
		||||
 | 
			
		||||
        var newExprs = new CustomReaction[exprs.Count - 1];
 | 
			
		||||
        var newExprs = new NadekoExpression[exprs.Count - 1];
 | 
			
		||||
        for (int i = 0, k = 0; i < exprs.Count; i++, k++)
 | 
			
		||||
        {
 | 
			
		||||
            if (exprs[i].Id == id)
 | 
			
		||||
@@ -419,7 +419,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    public async Task SetExprReactions(ulong? guildId, int id, IEnumerable<string> emojis)
 | 
			
		||||
    {
 | 
			
		||||
        CustomReaction expr;
 | 
			
		||||
        NadekoExpression expr;
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            expr = uow.Expressions.GetById(id);
 | 
			
		||||
@@ -437,7 +437,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    public async Task<(bool Sucess, bool NewValue)> ToggleExprOptionAsync(int id, ExprField field)
 | 
			
		||||
    {
 | 
			
		||||
        var newVal = false;
 | 
			
		||||
        CustomReaction expr;
 | 
			
		||||
        NadekoExpression expr;
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            expr = uow.Expressions.GetById(id);
 | 
			
		||||
@@ -460,7 +460,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return (true, newVal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CustomReaction GetExpression(ulong? guildId, int id)
 | 
			
		||||
    public NadekoExpression GetExpression(ulong? guildId, int id)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var expr = uow.Expressions.GetById(id);
 | 
			
		||||
@@ -516,7 +516,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        {
 | 
			
		||||
            var trigger = entry.Key;
 | 
			
		||||
            await uow.Expressions.AddRangeAsync(entry.Value.Where(expr => !string.IsNullOrWhiteSpace(expr.Res))
 | 
			
		||||
                                                         .Select(expr => new CustomReaction
 | 
			
		||||
                                                         .Select(expr => new NadekoExpression
 | 
			
		||||
                                                         {
 | 
			
		||||
                                                             GuildId = guildId,
 | 
			
		||||
                                                             Response = expr.Res,
 | 
			
		||||
@@ -542,11 +542,11 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    private ValueTask OnExprsShouldReload(bool _)
 | 
			
		||||
        => new(ReloadInternal(_bot.GetCurrentGuildIds()));
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnGexprAdded(CustomReaction c)
 | 
			
		||||
    private ValueTask OnGexprAdded(NadekoExpression c)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
            var newGlobalReactions = new CustomReaction[globalReactions.Length + 1];
 | 
			
		||||
            var newGlobalReactions = new NadekoExpression[globalReactions.Length + 1];
 | 
			
		||||
            Array.Copy(globalReactions, newGlobalReactions, globalReactions.Length);
 | 
			
		||||
            newGlobalReactions[globalReactions.Length] = c;
 | 
			
		||||
            globalReactions = newGlobalReactions;
 | 
			
		||||
@@ -555,7 +555,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnGexprEdited(CustomReaction c)
 | 
			
		||||
    private ValueTask OnGexprEdited(NadekoExpression c)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
@@ -611,10 +611,10 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    #region Basic Operations
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomReaction> AddAsync(ulong? guildId, string key, string message)
 | 
			
		||||
    public async Task<NadekoExpression> AddAsync(ulong? guildId, string key, string message)
 | 
			
		||||
    {
 | 
			
		||||
        key = key.ToLowerInvariant();
 | 
			
		||||
        var expr = new CustomReaction { GuildId = guildId, Trigger = key, Response = message };
 | 
			
		||||
        var expr = new NadekoExpression { GuildId = guildId, Trigger = key, Response = message };
 | 
			
		||||
 | 
			
		||||
        if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            expr.AllowTarget = true;
 | 
			
		||||
@@ -630,7 +630,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return expr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomReaction> EditAsync(ulong? guildId, int id, string message)
 | 
			
		||||
    public async Task<NadekoExpression> EditAsync(ulong? guildId, int id, string message)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var expr = uow.Expressions.GetById(id);
 | 
			
		||||
@@ -656,7 +656,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomReaction> DeleteAsync(ulong? guildId, int id)
 | 
			
		||||
    public async Task<NadekoExpression> DeleteAsync(ulong? guildId, int id)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var toDelete = uow.Expressions.GetById(id);
 | 
			
		||||
@@ -676,10 +676,10 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    public CustomReaction[] GetExpressionsFor(ulong? maybeGuildId)
 | 
			
		||||
    public NadekoExpression[] GetExpressionsFor(ulong? maybeGuildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeGuildId is { } guildId)
 | 
			
		||||
            return newGuildReactions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<CustomReaction>();
 | 
			
		||||
            return newGuildReactions.TryGetValue(guildId, out var exprs) ? exprs : Array.Empty<NadekoExpression>();
 | 
			
		||||
 | 
			
		||||
        return globalReactions;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -56,8 +56,9 @@ public partial class Gambling
 | 
			
		||||
                if (await bj.Join(ctx.User, amount))
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync(strs.bj_joined);
 | 
			
		||||
                else
 | 
			
		||||
                    Log.Information(
 | 
			
		||||
                        $"{ctx.User} can't join a blackjack game as it's in " + bj.State + " state already.");
 | 
			
		||||
                    Log.Information("{User} can't join a blackjack game as it's in {BlackjackState} state already",
 | 
			
		||||
                        ctx.User,
 | 
			
		||||
                        bj.State);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Message.DeleteAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ public class Blackjack
 | 
			
		||||
    private readonly ICurrencyService _cs;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    private readonly SemaphoreSlim locker = new(1, 1);
 | 
			
		||||
    private readonly SemaphoreSlim _locker = new(1, 1);
 | 
			
		||||
 | 
			
		||||
    public Blackjack(ICurrencyService cs, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
@@ -45,14 +45,14 @@ public class Blackjack
 | 
			
		||||
        {
 | 
			
		||||
            //wait for players to join
 | 
			
		||||
            await Task.Delay(20000);
 | 
			
		||||
            await locker.WaitAsync();
 | 
			
		||||
            await _locker.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                State = GameState.Playing;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
                _locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await PrintState();
 | 
			
		||||
@@ -79,7 +79,7 @@ public class Blackjack
 | 
			
		||||
            foreach (var usr in Players.Where(x => !x.Done))
 | 
			
		||||
                while (!usr.Done)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information($"Waiting for {usr.DiscordUser}'s move");
 | 
			
		||||
                    Log.Information("Waiting for {DiscordUser}'s move", usr.DiscordUser);
 | 
			
		||||
                    await PromptUserMove(usr);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -115,7 +115,7 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Join(IUser user, long bet)
 | 
			
		||||
    {
 | 
			
		||||
        await locker.WaitAsync();
 | 
			
		||||
        await _locker.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (State != GameState.Starting)
 | 
			
		||||
@@ -132,7 +132,7 @@ public class Blackjack
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            locker.Release();
 | 
			
		||||
            _locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -148,7 +148,7 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Stand(User u)
 | 
			
		||||
    {
 | 
			
		||||
        await locker.WaitAsync();
 | 
			
		||||
        await _locker.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (State != GameState.Playing)
 | 
			
		||||
@@ -163,7 +163,7 @@ public class Blackjack
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            locker.Release();
 | 
			
		||||
            _locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -229,7 +229,7 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Double(User u)
 | 
			
		||||
    {
 | 
			
		||||
        await locker.WaitAsync();
 | 
			
		||||
        await _locker.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (State != GameState.Playing)
 | 
			
		||||
@@ -260,7 +260,7 @@ public class Blackjack
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            locker.Release();
 | 
			
		||||
            _locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -276,7 +276,7 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Hit(User u)
 | 
			
		||||
    {
 | 
			
		||||
        await locker.WaitAsync();
 | 
			
		||||
        await _locker.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (State != GameState.Playing)
 | 
			
		||||
@@ -300,7 +300,7 @@ public class Blackjack
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            locker.Release();
 | 
			
		||||
            _locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,9 +51,12 @@ public class GamblingService : INService
 | 
			
		||||
                    if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% "
 | 
			
		||||
                                    + $"| max: {maxDecay} "
 | 
			
		||||
                                    + $"| threshold: {config.Decay.MinThreshold}");
 | 
			
		||||
                    Log.Information(@"Decaying users' currency - decay: {ConfigDecayPercent}% 
 | 
			
		||||
                                    | max: {MaxDecay} 
 | 
			
		||||
                                    | threshold: {DecayMinTreshold}",
 | 
			
		||||
                        config.Decay.Percent * 100,
 | 
			
		||||
                        maxDecay,
 | 
			
		||||
                        config.Decay.MinThreshold);
 | 
			
		||||
 | 
			
		||||
                    if (maxDecay == 0)
 | 
			
		||||
                        maxDecay = int.MaxValue;
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ public class ChatterBotService : IEarlyBehavior
 | 
			
		||||
                    try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg); }
 | 
			
		||||
                    catch { }
 | 
			
		||||
 | 
			
		||||
                    Log.Information(returnMsg);
 | 
			
		||||
                    Log.Information("{PermissionMessage}", returnMsg);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
@@ -125,11 +125,19 @@ public class ChatterBotService : IEarlyBehavior
 | 
			
		||||
            var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message);
 | 
			
		||||
            if (cleverbotExecuted)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information($@"CleverBot Executed
 | 
			
		||||
Server: {guild.Name} [{guild.Id}]
 | 
			
		||||
Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}]
 | 
			
		||||
UserId: {usrMsg.Author} [{usrMsg.Author.Id}]
 | 
			
		||||
Message: {usrMsg.Content}");
 | 
			
		||||
                Log.Information(@"CleverBot Executed
 | 
			
		||||
Server: {GuildName} [{GuildId}]
 | 
			
		||||
Channel: {ChannelName} [{ChannelId}]
 | 
			
		||||
UserId: {Author} [{AuthorId}]
 | 
			
		||||
Message: {Content}",
 | 
			
		||||
                    guild.Name,
 | 
			
		||||
                    guild.Id,
 | 
			
		||||
                    usrMsg.Channel?.Name,
 | 
			
		||||
                    usrMsg.Channel?.Id,
 | 
			
		||||
                    usrMsg.Author,
 | 
			
		||||
                    usrMsg.Author.Id,
 | 
			
		||||
                    usrMsg.Content);
 | 
			
		||||
                
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -31,8 +31,7 @@ public class OfficialCleverbotSession : IChatterBotSession
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Unexpected cleverbot response received: ");
 | 
			
		||||
            Log.Warning(dataString);
 | 
			
		||||
            Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ public class TypingGame
 | 
			
		||||
    public ITextChannel Channel { get; }
 | 
			
		||||
    public string CurrentSentence { get; private set; }
 | 
			
		||||
    public bool IsActive { get; private set; }
 | 
			
		||||
    private readonly Stopwatch sw;
 | 
			
		||||
    private readonly List<ulong> finishedUserIds;
 | 
			
		||||
    private readonly Stopwatch _sw;
 | 
			
		||||
    private readonly List<ulong> _finishedUserIds;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly GamesService _games;
 | 
			
		||||
    private readonly string _prefix;
 | 
			
		||||
@@ -35,25 +35,24 @@ public class TypingGame
 | 
			
		||||
 | 
			
		||||
        Channel = channel;
 | 
			
		||||
        IsActive = false;
 | 
			
		||||
        sw = new();
 | 
			
		||||
        finishedUserIds = new();
 | 
			
		||||
        _sw = new();
 | 
			
		||||
        _finishedUserIds = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Stop()
 | 
			
		||||
    {
 | 
			
		||||
        if (!IsActive) return false;
 | 
			
		||||
        _client.MessageReceived -= AnswerReceived;
 | 
			
		||||
        finishedUserIds.Clear();
 | 
			
		||||
        _finishedUserIds.Clear();
 | 
			
		||||
        IsActive = false;
 | 
			
		||||
        sw.Stop();
 | 
			
		||||
        sw.Reset();
 | 
			
		||||
        _sw.Stop();
 | 
			
		||||
        _sw.Reset();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await Channel.SendConfirmAsync(_eb, "Typing contest stopped.");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex.ToString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
@@ -88,7 +87,7 @@ public class TypingGame
 | 
			
		||||
            {
 | 
			
		||||
                m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture);
 | 
			
		||||
            });
 | 
			
		||||
            sw.Start();
 | 
			
		||||
            _sw.Start();
 | 
			
		||||
            HandleAnswers();
 | 
			
		||||
 | 
			
		||||
            while (i > 0)
 | 
			
		||||
@@ -133,19 +132,19 @@ public class TypingGame
 | 
			
		||||
 | 
			
		||||
                var distance = CurrentSentence.LevenshteinDistance(guess);
 | 
			
		||||
                var decision = Judge(distance, guess.Length);
 | 
			
		||||
                if (decision && !finishedUserIds.Contains(msg.Author.Id))
 | 
			
		||||
                if (decision && !_finishedUserIds.Contains(msg.Author.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    var elapsed = sw.Elapsed;
 | 
			
		||||
                    var elapsed = _sw.Elapsed;
 | 
			
		||||
                    var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60;
 | 
			
		||||
                    finishedUserIds.Add(msg.Author.Id);
 | 
			
		||||
                    _finishedUserIds.Add(msg.Author.Id);
 | 
			
		||||
                    await Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                                .WithOkColor()
 | 
			
		||||
                                                .WithTitle($"{msg.Author} finished the race!")
 | 
			
		||||
                                                .AddField("Place", $"#{finishedUserIds.Count}", true)
 | 
			
		||||
                                                .AddField("Place", $"#{_finishedUserIds.Count}", true)
 | 
			
		||||
                                                .AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
 | 
			
		||||
                                                .AddField("Errors", distance.ToString(), true));
 | 
			
		||||
 | 
			
		||||
                    if (finishedUserIds.Count % 4 == 0)
 | 
			
		||||
                    if (_finishedUserIds.Count % 4 == 0)
 | 
			
		||||
                        await Channel.SendConfirmAsync(_eb,
 | 
			
		||||
                            ":exclamation: A lot of people finished, here is the text for those still typing:"
 | 
			
		||||
                            + $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**");
 | 
			
		||||
@@ -153,7 +152,7 @@ public class TypingGame
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.ToString());
 | 
			
		||||
                Log.Warning(ex, "Error receiving typing game answer: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
 
 | 
			
		||||
@@ -264,7 +264,7 @@ public class TriviaGame
 | 
			
		||||
                    embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
 | 
			
		||||
                await Channel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex) { Log.Warning(ex.ToString()); }
 | 
			
		||||
            catch (Exception ex) { Log.Warning(ex, "Exception in a potential guess"); }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,10 @@ namespace NadekoBot.Modules.Music.Resolvers;
 | 
			
		||||
 | 
			
		||||
public class RadioResolver : IRadioResolver
 | 
			
		||||
{
 | 
			
		||||
    private readonly Regex plsRegex = new("File1=(?<url>.*?)\\n", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex m3uRegex = new("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
 | 
			
		||||
    private readonly Regex asxRegex = new("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex xspfRegex = new("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex _plsRegex = new("File1=(?<url>.*?)\\n", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex _m3URegex = new("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
 | 
			
		||||
    private readonly Regex _asxRegex = new("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex _xspfRegex = new("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo> ResolveByQueryAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
@@ -46,13 +46,13 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
            //Regex.Match(query)
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = plsRegex.Match(file);
 | 
			
		||||
                var m = _plsRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"Failed reading .pls:\n{file}");
 | 
			
		||||
                Log.Warning("Failed reading .pls:\n{PlsFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -64,13 +64,13 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
                */
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = m3uRegex.Match(file);
 | 
			
		||||
                var m = _m3URegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"Failed reading .m3u:\n{file}");
 | 
			
		||||
                Log.Warning("Failed reading .m3u:\n{M3uFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -78,13 +78,13 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
            //<ref href="http://armitunes.com:8000"/>
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = asxRegex.Match(file);
 | 
			
		||||
                var m = _asxRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"Failed reading .asx:\n{file}");
 | 
			
		||||
                Log.Warning("Failed reading .asx:\n{AsxFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -97,13 +97,13 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
                */
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = xspfRegex.Match(file);
 | 
			
		||||
                var m = _xspfRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"Failed reading .xspf:\n{file}");
 | 
			
		||||
                Log.Warning("Failed reading .xspf:\n{XspfFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,8 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
        WordFilteringServers.TryRemove(guildId);
 | 
			
		||||
        ServerFilteredWords.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
        foreach (var c in gc.FilterWordsChannelIds) WordFilteringChannels.TryRemove(c.ChannelId);
 | 
			
		||||
        foreach (var c in gc.FilterWordsChannelIds)
 | 
			
		||||
            WordFilteringChannels.TryRemove(c.ChannelId);
 | 
			
		||||
 | 
			
		||||
        gc.FilterWords = false;
 | 
			
		||||
        gc.FilteredWords.Clear();
 | 
			
		||||
@@ -131,7 +132,9 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
        var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>();
 | 
			
		||||
        var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' ');
 | 
			
		||||
        if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var word in wordsInMessage)
 | 
			
		||||
            {
 | 
			
		||||
                if (filteredChannelWords.Contains(word) || filteredServerWords.Contains(word))
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel",
 | 
			
		||||
@@ -145,12 +148,15 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (HttpException ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("I do not have permission to filter words in channel with id " + usrMsg.Channel.Id,
 | 
			
		||||
                            ex);
 | 
			
		||||
                        Log.Warning(ex,
 | 
			
		||||
                            "I do not have permission to filter words in channel with id {Id}",
 | 
			
		||||
                            usrMsg.Channel.Id);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
@@ -177,7 +183,9 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("I do not have permission to filter invites in channel with id " + usrMsg.Channel.Id, ex);
 | 
			
		||||
                Log.Warning(ex,
 | 
			
		||||
                    "I do not have permission to filter invites in channel with id {Id}",
 | 
			
		||||
                    usrMsg.Channel.Id);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -207,7 +215,7 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("I do not have permission to filter links in channel with id " + usrMsg.Channel.Id, ex);
 | 
			
		||||
                Log.Warning(ex, "I do not have permission to filter links in channel with id {Id}", usrMsg.Channel.Id);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
{
 | 
			
		||||
    private static readonly ConcurrentDictionary<string, string> cachedShortenedLinks = new();
 | 
			
		||||
    private static readonly ConcurrentDictionary<string, string> _cachedShortenedLinks = new();
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private readonly IGoogleApiService _google;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
@@ -141,7 +141,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        var eb = _eb.Create()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText(strs.time_new))
 | 
			
		||||
                    .WithDescription(Format.Code(data.Time.ToString()))
 | 
			
		||||
                    .WithDescription(Format.Code(data.Time.ToString(Culture)))
 | 
			
		||||
                    .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
 | 
			
		||||
                    .AddField(GetText(strs.timezone), data.TimeZoneName, true);
 | 
			
		||||
 | 
			
		||||
@@ -282,20 +282,20 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        query = query.Trim();
 | 
			
		||||
        if (!cachedShortenedLinks.TryGetValue(query, out var shortLink))
 | 
			
		||||
        if (!_cachedShortenedLinks.TryGetValue(query, out var shortLink))
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var _http = _httpFactory.CreateClient();
 | 
			
		||||
                using var http = _httpFactory.CreateClient();
 | 
			
		||||
                using var req = new HttpRequestMessage(HttpMethod.Post, "https://goolnk.com/api/v1/shorten");
 | 
			
		||||
                var formData = new MultipartFormDataContent { { new StringContent(query), "url" } };
 | 
			
		||||
                req.Content = formData;
 | 
			
		||||
 | 
			
		||||
                using var res = await _http.SendAsync(req);
 | 
			
		||||
                using var res = await http.SendAsync(req);
 | 
			
		||||
                var content = await res.Content.ReadAsStringAsync();
 | 
			
		||||
                var data = JsonConvert.DeserializeObject<ShortenData>(content);
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(data?.ResultUrl))
 | 
			
		||||
                    cachedShortenedLinks.TryAdd(query, data.ResultUrl);
 | 
			
		||||
                    _cachedShortenedLinks.TryAdd(query, data.ResultUrl);
 | 
			
		||||
                else
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
@@ -406,7 +406,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Hearthstone([Leftover] string name)
 | 
			
		||||
    {
 | 
			
		||||
        var arg = name;
 | 
			
		||||
        if (!await ValidateQuery(name))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@@ -496,7 +495,8 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
                            .Where(x => x.Senses is not null
 | 
			
		||||
                                        && x.Senses.Count > 0
 | 
			
		||||
                                        && x.Senses[0].Definition is not null)
 | 
			
		||||
                            .Select(x => (Sense: x.Senses[0], x.PartOfSpeech));
 | 
			
		||||
                            .Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
 | 
			
		||||
                            .ToList();
 | 
			
		||||
 | 
			
		||||
            if (!datas.Any())
 | 
			
		||||
            {
 | 
			
		||||
@@ -505,17 +505,17 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var col = datas.Select(data => (
 | 
			
		||||
                               Definition: data.Sense.Definition is string
 | 
			
		||||
                                   ? data.Sense.Definition.ToString()
 | 
			
		||||
                                   : ((JArray)JToken.Parse(data.Sense.Definition.ToString())).First.ToString(),
 | 
			
		||||
                               Example: data.Sense.Examples is null || data.Sense.Examples.Count == 0
 | 
			
		||||
            var col = datas.Select(x => (
 | 
			
		||||
                               Definition: x.Sense.Definition is string
 | 
			
		||||
                                   ? x.Sense.Definition.ToString()
 | 
			
		||||
                                   : ((JArray)JToken.Parse(x.Sense.Definition.ToString())).First.ToString(),
 | 
			
		||||
                               Example: x.Sense.Examples is null || x.Sense.Examples.Count == 0
 | 
			
		||||
                                   ? string.Empty
 | 
			
		||||
                                   : data.Sense.Examples[0].Text, Word: word,
 | 
			
		||||
                               WordType: string.IsNullOrWhiteSpace(data.PartOfSpeech) ? "-" : data.PartOfSpeech))
 | 
			
		||||
                                   : x.Sense.Examples[0].Text, Word: word,
 | 
			
		||||
                               WordType: string.IsNullOrWhiteSpace(x.PartOfSpeech) ? "-" : x.PartOfSpeech))
 | 
			
		||||
                           .ToList();
 | 
			
		||||
 | 
			
		||||
            Log.Information($"Sending {col.Count} definition for: {word}");
 | 
			
		||||
            Log.Information("Sending {Count} definition for: {Word}", col.Count, word);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(0,
 | 
			
		||||
                page =>
 | 
			
		||||
@@ -547,8 +547,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        var response = await http.GetStringAsync("https://catfact.ninja/fact");
 | 
			
		||||
        if (response is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var fact = JObject.Parse(response)["fact"].ToString();
 | 
			
		||||
        await SendConfirmAsync("🐈" + GetText(strs.catfact), fact);
 | 
			
		||||
@@ -563,9 +561,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
            usr = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
        var av = usr.RealAvatarUrl();
 | 
			
		||||
        if (av is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={av}");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -690,7 +685,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            var res = await http.GetStringAsync("https://bible-api.com/" + book + " " + chapterAndVerse);
 | 
			
		||||
            var res = await http.GetStringAsync($"https://bible-api.com/{book} {chapterAndVerse}");
 | 
			
		||||
 | 
			
		||||
            obj = JsonConvert.DeserializeObject<BibleVerses>(res);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -68,12 +68,12 @@ public class SearchesService : INService
 | 
			
		||||
        if (File.Exists("data/wowjokes.json"))
 | 
			
		||||
            WowJokes = JsonConvert.DeserializeObject<List<WoWJoke>>(File.ReadAllText("data/wowjokes.json"));
 | 
			
		||||
        else
 | 
			
		||||
            Log.Warning("data/wowjokes.json is missing. WOW Jokes are not loaded.");
 | 
			
		||||
            Log.Warning("data/wowjokes.json is missing. WOW Jokes are not loaded");
 | 
			
		||||
 | 
			
		||||
        if (File.Exists("data/magicitems.json"))
 | 
			
		||||
            MagicItems = JsonConvert.DeserializeObject<List<MagicItem>>(File.ReadAllText("data/magicitems.json"));
 | 
			
		||||
        else
 | 
			
		||||
            Log.Warning("data/magicitems.json is missing. Magic items are not loaded.");
 | 
			
		||||
            Log.Warning("data/magicitems.json is missing. Magic items are not loaded");
 | 
			
		||||
 | 
			
		||||
        if (File.Exists("data/yomama.txt"))
 | 
			
		||||
        {
 | 
			
		||||
@@ -165,14 +165,14 @@ public class SearchesService : INService
 | 
			
		||||
                                                 + "appid=42cd627dd60debf25a5739e50a217d74&"
 | 
			
		||||
                                                 + "units=metric");
 | 
			
		||||
 | 
			
		||||
            if (data is null)
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(data))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return JsonConvert.DeserializeObject<WeatherData>(data);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex.Message);
 | 
			
		||||
            Log.Warning(ex, "Error getting weather data");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -196,7 +196,7 @@ public class SearchesService : INService
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var _http = _httpFactory.CreateClient();
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            var res = await _cache.GetOrAddCachedDataAsync($"geo_{query}",
 | 
			
		||||
                _ =>
 | 
			
		||||
                {
 | 
			
		||||
@@ -207,7 +207,7 @@ public class SearchesService : INService
 | 
			
		||||
                              + $"q={Uri.EscapeDataString(query)}&"
 | 
			
		||||
                              + "format=json";
 | 
			
		||||
 | 
			
		||||
                    var res = _http.GetStringAsync(url);
 | 
			
		||||
                    var res = http.GetStringAsync(url);
 | 
			
		||||
                    return res;
 | 
			
		||||
                },
 | 
			
		||||
                "",
 | 
			
		||||
@@ -227,7 +227,7 @@ public class SearchesService : INService
 | 
			
		||||
                + $"key={_creds.TimezoneDbApiKey}&format=json&"
 | 
			
		||||
                + "by=position&"
 | 
			
		||||
                + $"lat={geoData.Lat}&lng={geoData.Lon}");
 | 
			
		||||
            using var geoRes = await _http.SendAsync(req);
 | 
			
		||||
            using var geoRes = await http.SendAsync(req);
 | 
			
		||||
            var resString = await geoRes.Content.ReadAsStringAsync();
 | 
			
		||||
            var timeObj = JsonConvert.DeserializeObject<TimeZoneResult>(resString);
 | 
			
		||||
 | 
			
		||||
@@ -403,7 +403,7 @@ public class SearchesService : INService
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex.Message);
 | 
			
		||||
            Log.Error(ex, "Error getting Hearthstone Card: {ErrorMessage}", ex.Message);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ public sealed class StreamNotificationService : INService
 | 
			
		||||
    private readonly Dictionary<StreamDataKey, Dictionary<ulong, HashSet<FollowedStream>>> _shardTrackedStreams;
 | 
			
		||||
    private readonly ConcurrentHashSet<ulong> _offlineNotificationServers;
 | 
			
		||||
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private readonly IPubSub _pubSub;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    private readonly Timer _notifCleanupTimer;
 | 
			
		||||
@@ -49,7 +48,6 @@ public sealed class StreamNotificationService : INService
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _pubSub = pubSub;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _streamTracker = new(httpFactory, redis, creds.RedisKey(), client.ShardId == 0);
 | 
			
		||||
@@ -123,8 +121,12 @@ public sealed class StreamNotificationService : INService
 | 
			
		||||
                        using var uow = _db.GetDbContext();
 | 
			
		||||
                        foreach (var kvp in deleteGroups)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Information($"Deleting {kvp.Value.Count} {kvp.Key} streams because "
 | 
			
		||||
                                            + $"they've been erroring for more than {errorLimit}: {string.Join(", ", kvp.Value)}");
 | 
			
		||||
                            Log.Information(
 | 
			
		||||
                                "Deleting {StreamCount} {Platform} streams because they've been erroring for more than {ErrorLimit}: {RemovedList}",
 | 
			
		||||
                                kvp.Value.Count,
 | 
			
		||||
                                kvp.Key,
 | 
			
		||||
                                errorLimit,
 | 
			
		||||
                                string.Join(", ", kvp.Value));
 | 
			
		||||
 | 
			
		||||
                            var toDelete = uow.Set<FollowedStream>()
 | 
			
		||||
                                              .AsQueryable()
 | 
			
		||||
@@ -140,8 +142,7 @@ public sealed class StreamNotificationService : INService
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Error("Error cleaning up FollowedStreams");
 | 
			
		||||
                        Log.Error(ex.ToString());
 | 
			
		||||
                        Log.Error(ex, "Error cleaning up FollowedStreams");
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                null,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								src/NadekoBot/Modules/Utility/Patreon/PatreonRefreshData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/NadekoBot/Modules/Utility/Patreon/PatreonRefreshData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility;
 | 
			
		||||
 | 
			
		||||
public sealed class PatreonRefreshData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("access_token")]
 | 
			
		||||
    public string AccessToken { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("refresh_token")]
 | 
			
		||||
    public string RefreshToken { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("expires_in")]
 | 
			
		||||
    public long ExpiresIn { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("scope")]
 | 
			
		||||
    public string Scope { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("token_type")]
 | 
			
		||||
    public string TokenType { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,19 +6,18 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility.Services;
 | 
			
		||||
namespace NadekoBot.Modules.Utility;
 | 
			
		||||
 | 
			
		||||
public class PatreonRewardsService : INService
 | 
			
		||||
{
 | 
			
		||||
    public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
 | 
			
		||||
 | 
			
		||||
    public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
 | 
			
		||||
    private readonly SemaphoreSlim getPledgesLocker = new(1, 1);
 | 
			
		||||
    private readonly SemaphoreSlim _getPledgesLocker = new(1, 1);
 | 
			
		||||
 | 
			
		||||
    private readonly Timer _updater;
 | 
			
		||||
    private readonly SemaphoreSlim claimLockJustInCase = new(1, 1);
 | 
			
		||||
    private readonly SemaphoreSlim _claimLockJustInCase = new(1, 1);
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly ICurrencyService _currency;
 | 
			
		||||
    private readonly GamblingConfigService _gamblingConfigService;
 | 
			
		||||
@@ -128,7 +127,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LastUpdate = DateTime.UtcNow;
 | 
			
		||||
        await getPledgesLocker.WaitAsync();
 | 
			
		||||
        await _getPledgesLocker.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var members = new List<PatreonMember>();
 | 
			
		||||
@@ -154,7 +153,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
 | 
			
		||||
                    members.AddRange(data.Data);
 | 
			
		||||
                    users.AddRange(data.Included);
 | 
			
		||||
                } while (!string.IsNullOrWhiteSpace(page = data?.Links?.Next));
 | 
			
		||||
                } while (!string.IsNullOrWhiteSpace(page = data.Links?.Next));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var userData = members.Join(users,
 | 
			
		||||
@@ -185,13 +184,13 @@ public class PatreonRewardsService : INService
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            getPledgesLocker.Release();
 | 
			
		||||
            _getPledgesLocker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<int> ClaimReward(ulong userId, string patreonUserId, int cents)
 | 
			
		||||
    {
 | 
			
		||||
        await claimLockJustInCase.WaitAsync();
 | 
			
		||||
        await _claimLockJustInCase.WaitAsync();
 | 
			
		||||
        var settings = _gamblingConfigService.Data;
 | 
			
		||||
        var now = DateTime.UtcNow;
 | 
			
		||||
        try
 | 
			
		||||
@@ -213,7 +212,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
 | 
			
		||||
                await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, true);
 | 
			
		||||
 | 
			
		||||
                Log.Information($"Sending new currency reward to {userId}");
 | 
			
		||||
                Log.Information("Sending new currency reward to {UserId}", userId);
 | 
			
		||||
                await SendMessageToUser(userId,
 | 
			
		||||
                    "Thank you for your pledge! " + $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !");
 | 
			
		||||
                return eligibleFor;
 | 
			
		||||
@@ -228,7 +227,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
 | 
			
		||||
                await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, true);
 | 
			
		||||
 | 
			
		||||
                Log.Information($"Sending recurring currency reward to {userId}");
 | 
			
		||||
                Log.Information("Sending recurring currency reward to {UserId}", userId);
 | 
			
		||||
                await SendMessageToUser(userId,
 | 
			
		||||
                    "Thank you for your continued support! "
 | 
			
		||||
                    + $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!");
 | 
			
		||||
@@ -246,7 +245,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
 | 
			
		||||
                await _currency.AddAsync(userId, "Patreon reward - update", toAward, true);
 | 
			
		||||
 | 
			
		||||
                Log.Information($"Sending updated currency reward to {userId}");
 | 
			
		||||
                Log.Information("Sending updated currency reward to {UserId}", userId);
 | 
			
		||||
                await SendMessageToUser(userId,
 | 
			
		||||
                    "Thank you for increasing your pledge! "
 | 
			
		||||
                    + $"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
 | 
			
		||||
@@ -257,7 +256,7 @@ public class PatreonRewardsService : INService
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            claimLockJustInCase.Release();
 | 
			
		||||
            _claimLockJustInCase.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -276,23 +275,4 @@ public class PatreonRewardsService : INService
 | 
			
		||||
            // ignored
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private sealed class PatreonRefreshData
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("access_token")]
 | 
			
		||||
        public string AccessToken { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("refresh_token")]
 | 
			
		||||
        public string RefreshToken { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("expires_in")]
 | 
			
		||||
        public long ExpiresIn { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("scope")]
 | 
			
		||||
        public string Scope { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("token_type")]
 | 
			
		||||
        public string TokenType { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,6 @@
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="AngleSharp" Version="0.16.1" />
 | 
			
		||||
    <PackageReference Include="AWSSDK.S3" Version="3.7.7.5" />
 | 
			
		||||
<!--    <PackageReference Include="Cloneable" Version="1.3.0" />-->
 | 
			
		||||
    <PackageReference Include="CodeHollow.FeedReader" Version="1.2.2" />
 | 
			
		||||
    <PackageReference Include="CommandLineParser" Version="2.8.0" />
 | 
			
		||||
    <PackageReference Include="Discord.Net" Version="3.0.0" />
 | 
			
		||||
 
 | 
			
		||||
@@ -15,14 +15,14 @@ public interface IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
{
 | 
			
		||||
    private const string _credsFileName = "creds.yml";
 | 
			
		||||
    private const string _credsExampleFileName = "creds_example.yml";
 | 
			
		||||
    private const string CREDS_FILE_NAME = "creds.yml";
 | 
			
		||||
    private const string CREDS_EXAMPLE_FILE_NAME = "creds_example.yml";
 | 
			
		||||
 | 
			
		||||
    private string CredsPath
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), _credsFileName);
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
 | 
			
		||||
 | 
			
		||||
    private string CredsExamplePath
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), _credsExampleFileName);
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
 | 
			
		||||
 | 
			
		||||
    private string OldCredsJsonPath
 | 
			
		||||
        => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
 | 
			
		||||
@@ -37,7 +37,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
    private readonly IConfigurationRoot _config;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private readonly object reloadLock = new();
 | 
			
		||||
    private readonly object _reloadLock = new();
 | 
			
		||||
 | 
			
		||||
    public BotCredsProvider(int? totalShards = null)
 | 
			
		||||
    {
 | 
			
		||||
@@ -47,9 +47,10 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
        MigrateCredentials();
 | 
			
		||||
 | 
			
		||||
        if (!File.Exists(CredsPath))
 | 
			
		||||
            Log.Warning($"{CredsPath} is missing. "
 | 
			
		||||
                        + "Attempting to load creds from environment variables prefixed with 'NadekoBot_'. "
 | 
			
		||||
                        + $"Example is in {CredsExamplePath}");
 | 
			
		||||
            Log.Warning(
 | 
			
		||||
                "{CredsPath} is missing. Attempting to load creds from environment variables prefixed with 'NadekoBot_'. Example is in {CredsExamplePath}",
 | 
			
		||||
                CredsPath,
 | 
			
		||||
                CredsExamplePath);
 | 
			
		||||
 | 
			
		||||
        _config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
 | 
			
		||||
                                            .AddEnvironmentVariables("NadekoBot_")
 | 
			
		||||
@@ -62,15 +63,14 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
    public void Reload()
 | 
			
		||||
    {
 | 
			
		||||
        lock (reloadLock)
 | 
			
		||||
        lock (_reloadLock)
 | 
			
		||||
        {
 | 
			
		||||
            _creds.OwnerIds.Clear();
 | 
			
		||||
            _config.Bind(_creds);
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(_creds.Token))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error("Token is missing from creds.yml or Environment variables.\n"
 | 
			
		||||
                          + "Add it and restart the program.");
 | 
			
		||||
                Log.Error("Token is missing from creds.yml or Environment variables.\nAdd it and restart the program");
 | 
			
		||||
                Helpers.ReadErrorAndExit(5);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -96,13 +96,13 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
 | 
			
		||||
    public void ModifyCredsFile(Action<Creds> func)
 | 
			
		||||
    {
 | 
			
		||||
        var ymlData = File.ReadAllText(_credsFileName);
 | 
			
		||||
        var ymlData = File.ReadAllText(CREDS_FILE_NAME);
 | 
			
		||||
        var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
 | 
			
		||||
 | 
			
		||||
        func(creds);
 | 
			
		||||
 | 
			
		||||
        ymlData = Yaml.Serializer.Serialize(creds);
 | 
			
		||||
        File.WriteAllText(_credsFileName, ymlData);
 | 
			
		||||
        File.WriteAllText(CREDS_FILE_NAME, ymlData);
 | 
			
		||||
 | 
			
		||||
        Reload();
 | 
			
		||||
    }
 | 
			
		||||
@@ -145,13 +145,13 @@ public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
                "Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (File.Exists(_credsFileName))
 | 
			
		||||
        if (File.Exists(CREDS_FILE_NAME))
 | 
			
		||||
        {
 | 
			
		||||
            var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(_credsFileName));
 | 
			
		||||
            var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
 | 
			
		||||
            if (creds.Version <= 1)
 | 
			
		||||
            {
 | 
			
		||||
                creds.Version = 2;
 | 
			
		||||
                File.WriteAllText(_credsFileName, Yaml.Serializer.Serialize(creds));
 | 
			
		||||
                File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ public class YtdlOperation
 | 
			
		||||
        {
 | 
			
		||||
            using var process = CreateProcess(args);
 | 
			
		||||
 | 
			
		||||
            Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
 | 
			
		||||
            Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 | 
			
		||||
            process.Start();
 | 
			
		||||
 | 
			
		||||
            var str = await process.StandardOutput.ReadToEndAsync();
 | 
			
		||||
@@ -63,7 +63,7 @@ public class YtdlOperation
 | 
			
		||||
    {
 | 
			
		||||
        using var process = CreateProcess(args);
 | 
			
		||||
 | 
			
		||||
        Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
 | 
			
		||||
        Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 | 
			
		||||
        process.Start();
 | 
			
		||||
 | 
			
		||||
        string line;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user