diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd6640..226b262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ *a,c,f,r,o* +## [6.0.12] - 20.03.2025 + +### Fixed +- `.antispamignore` fixed for the last time hopefully + - protection commands are some of the oldest commands, and they might get overhauled in future updates + - please report if you find any other weird issue with them + +## [6.0.11] - 20.03.2025 + +### Changed +- wordfilter, invitefilter and linkfilter will now properly detect forwarded messages, as forwards were used to circumvent filtering. + +### Fixed +- `.dmc` fixed +- Fixed .streamremove - now showing proper youtube name when removing instead of channel id + +## [6.0.10] - 20.03.2025 + +### Changed + +- Live channels `.lcha` is limited to 1 for now. It will be reverted back to 5 in a couple of days at most as some things need to be implemented. + +### Fixed + +- `.antispam` won't break if you have thread channels in the server anymore +- `.ve` now works properly +- selfhosters: `.yml` parsing errors will now tell you which .yml file is causing the issue and why. + ## [6.0.9] - 19.03.2025 ### Changed diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index 3cf7abb..1af72b5 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -4,7 +4,7 @@ <Nullable>enable</Nullable> <ImplicitUsings>true</ImplicitUsings> <SatelliteResourceLanguages>en</SatelliteResourceLanguages> - <Version>6.0.9</Version> + <Version>6.0.12</Version> <!-- Output/build --> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> diff --git a/src/EllieBot/Modules/Administration/AdministrationService.cs b/src/EllieBot/Modules/Administration/AdministrationService.cs index 526eb69..d87cacd 100644 --- a/src/EllieBot/Modules/Administration/AdministrationService.cs +++ b/src/EllieBot/Modules/Administration/AdministrationService.cs @@ -107,12 +107,12 @@ public class AdministrationService : IEService, IReadyExecutor .UpdateWithOutputAsync(x => new() { DeleteMessageOnCommand = !x.DeleteMessageOnCommand - }); + }, (old, newVal) => newVal); if (conf.Length == 0) return false; - var val = conf[0].Inserted.DeleteMessageOnCommand; + var val = conf[0].DeleteMessageOnCommand; if (val) deleteMessagesOnCommand.Add(guildId); diff --git a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs index d97442d..f850006 100644 --- a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs +++ b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs @@ -27,6 +27,7 @@ public class ProtectionService : IReadyExecutor, IEService private readonly UserPunishService _punishService; private readonly INotifySubscriber _notifySub; private readonly ShardData _shardData; + private readonly Channel<PunishQueueItem> _punishUserQueue = Channel.CreateUnbounded<PunishQueueItem>(new() { @@ -176,10 +177,7 @@ public class ProtectionService : IReadyExecutor, IEService try { if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) - || spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new() - { - ChannelId = channel.Id - })) + || spamSettings.AntiSpamSettings.IgnoredChannels.Any(x => x.ChannelId == channel.Id)) return; var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, @@ -275,23 +273,25 @@ public class ProtectionService : IReadyExecutor, IEService await using var uow = _db.GetDbContext(); await uow.GetTable<AntiRaidSetting>() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - Action = action, - Seconds = seconds, - UserThreshold = userThreshold, - PunishDuration = minutesDuration - }, _ => new() - { - Action = action, - Seconds = seconds, - UserThreshold = userThreshold, - PunishDuration = minutesDuration - }, () => new() - { - GuildId = guildId - }); + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + Action = action, + Seconds = seconds, + UserThreshold = userThreshold, + PunishDuration = minutesDuration + }, + _ => new() + { + Action = action, + Seconds = seconds, + UserThreshold = userThreshold, + PunishDuration = minutesDuration + }, + () => new() + { + GuildId = guildId + }); return stats; @@ -364,23 +364,25 @@ public class ProtectionService : IReadyExecutor, IEService await using var uow = _db.GetDbContext(); await uow.GetTable<AntiSpamSetting>() .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - Action = stats.AntiSpamSettings.Action, - MessageThreshold = stats.AntiSpamSettings.MessageThreshold, - MuteTime = stats.AntiSpamSettings.MuteTime, - RoleId = stats.AntiSpamSettings.RoleId - }, (old) => new() - { - GuildId = guildId, - Action = stats.AntiSpamSettings.Action, - MessageThreshold = stats.AntiSpamSettings.MessageThreshold, - MuteTime = stats.AntiSpamSettings.MuteTime, - RoleId = stats.AntiSpamSettings.RoleId - }, () => new() - { - GuildId = guildId - }); + { + GuildId = guildId, + Action = stats.AntiSpamSettings.Action, + MessageThreshold = stats.AntiSpamSettings.MessageThreshold, + MuteTime = stats.AntiSpamSettings.MuteTime, + RoleId = stats.AntiSpamSettings.RoleId + }, + (old) => new() + { + GuildId = guildId, + Action = stats.AntiSpamSettings.Action, + MessageThreshold = stats.AntiSpamSettings.MessageThreshold, + MuteTime = stats.AntiSpamSettings.MuteTime, + RoleId = stats.AntiSpamSettings.RoleId + }, + () => new() + { + GuildId = guildId + }); return stats; } @@ -405,7 +407,7 @@ public class ProtectionService : IReadyExecutor, IEService if (spam.IgnoredChannels.All(x => x.ChannelId != channelId)) { if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache + temp.AntiSpamSettings.IgnoredChannels.Add(obj); spam.IgnoredChannels.Add(obj); added = true; @@ -417,7 +419,7 @@ public class ProtectionService : IReadyExecutor, IEService uow.Set<AntiSpamIgnore>().Remove(toRemove); if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache + temp.AntiSpamSettings.IgnoredChannels.RemoveAll(x => x.ChannelId == channelId); added = false; } @@ -462,22 +464,24 @@ public class ProtectionService : IReadyExecutor, IEService await uow.GetTable<AntiAltSetting>() .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - Action = action, - ActionDurationMinutes = actionDurationMinutes, - MinAge = TimeSpan.FromMinutes(minAgeMinutes), - RoleId = roleId - }, _ => new() - { - Action = action, - ActionDurationMinutes = actionDurationMinutes, - MinAge = TimeSpan.FromMinutes(minAgeMinutes), - RoleId = roleId - }, () => new() - { - GuildId = guildId - }); + { + GuildId = guildId, + Action = action, + ActionDurationMinutes = actionDurationMinutes, + MinAge = TimeSpan.FromMinutes(minAgeMinutes), + RoleId = roleId + }, + _ => new() + { + Action = action, + ActionDurationMinutes = actionDurationMinutes, + MinAge = TimeSpan.FromMinutes(minAgeMinutes), + RoleId = roleId + }, + () => new() + { + GuildId = guildId + }); _antiAltGuilds[guildId] = new(new() { diff --git a/src/EllieBot/Modules/Permissions/Filter/FilterService.cs b/src/EllieBot/Modules/Permissions/Filter/FilterService.cs index f596f1b..27c80b7 100644 --- a/src/EllieBot/Modules/Permissions/Filter/FilterService.cs +++ b/src/EllieBot/Modules/Permissions/Filter/FilterService.cs @@ -52,12 +52,12 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor await using var uow = _db.GetDbContext(); var confs = await uow.GetTable<GuildFilterConfig>() - .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId)) - .LoadWith(x => x.FilterInvitesChannelIds) - .LoadWith(x => x.FilterWordsChannelIds) - .LoadWith(x => x.FilterLinksChannelIds) - .LoadWith(x => x.FilteredWords) - .ToListAsyncLinqToDB(); + .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId)) + .LoadWith(x => x.FilterInvitesChannelIds) + .LoadWith(x => x.FilterWordsChannelIds) + .LoadWith(x => x.FilterLinksChannelIds) + .LoadWith(x => x.FilteredWords) + .ToListAsyncLinqToDB(); foreach (var conf in confs) { @@ -97,7 +97,7 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor await using var uow = _db.GetDbContext(); var fc = uow.FilterConfigForId(guildId, set => set.Include(x => x.FilteredWords) - .Include(x => x.FilterWordsChannelIds)); + .Include(x => x.FilterWordsChannelIds)); WordFilteringServers.TryRemove(guildId); ServerFilteredWords.TryRemove(guildId, out _); @@ -140,7 +140,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>(); var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>(); - var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); + var wordsInMessage = (usrMsg.Content + " " + usrMsg.ForwardedMessages.FirstOrDefault().Message?.Content) + .ToLowerInvariant().Split(' '); if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) { foreach (var word in wordsInMessage) @@ -183,7 +184,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor return false; if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id)) - && usrMsg.Content.IsDiscordInvite()) + && (usrMsg.Content.IsDiscordInvite() || + usrMsg.ForwardedMessages.Any(x => x.Message?.Content.IsDiscordInvite() ?? false))) { Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel", usrMsg.Author.ToString(), @@ -219,7 +221,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor return false; if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) || LinkFilteringServers.Contains(guild.Id)) - && usrMsg.Content.TryGetUrlPath(out _)) + && (usrMsg.Content.TryGetUrlPath(out _) || + usrMsg.ForwardedMessages.Any(x => x.Message?.Content.TryGetUrlPath(out _) ?? false))) { Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel", usrMsg.Author.ToString(), @@ -246,10 +249,10 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor await using var uow = _db.GetDbContext(); var conf = await uow.GetTable<GuildFilterConfig>() - .Where(fi => fi.GuildId == guildId) - .LoadWith(x => x.FilterInvitesChannelIds) - .LoadWith(x => x.FilterLinksChannelIds) - .FirstOrDefaultAsyncLinqToDB(); + .Where(fi => fi.GuildId == guildId) + .LoadWith(x => x.FilterInvitesChannelIds) + .LoadWith(x => x.FilterLinksChannelIds) + .FirstOrDefaultAsyncLinqToDB(); return new() { diff --git a/src/EllieBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs b/src/EllieBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs index 48fdd5b..2c14537 100644 --- a/src/EllieBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs +++ b/src/EllieBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs @@ -48,7 +48,10 @@ public partial class Searches return; } - await Response().Confirm(strs.stream_removed(Format.Bold(fs.Username), fs.Type)).SendAsync(); + await Response() + .Confirm(strs.stream_removed( + Format.Bold(string.IsNullOrWhiteSpace(fs.PrettyName) ? fs.Username : fs.PrettyName), + fs.Type)).SendAsync(); } [Cmd] diff --git a/src/EllieBot/Modules/Searches/_common/StreamNotifications/NotifChecker.cs b/src/EllieBot/Modules/Searches/_common/StreamNotifications/NotifChecker.cs index 3ddebec..2c68d5a 100644 --- a/src/EllieBot/Modules/Searches/_common/StreamNotifications/NotifChecker.cs +++ b/src/EllieBot/Modules/Searches/_common/StreamNotifications/NotifChecker.cs @@ -63,20 +63,19 @@ public class NotifChecker .ToDictionary(x => x.Key.Name, x => x.Value)); var newStreamData = await oldStreamDataDict - .Select(x => + .Select(async x => { // get all stream data for the streams of this type if (_streamProviders.TryGetValue(x.Key, out var provider)) { - return provider.GetStreamDataAsync(x.Value + return await provider.GetStreamDataAsync(x.Value .Select(entry => entry.Key) .ToList()); } // this means there's no provider for this stream data, (and there was before?) - return Task.FromResult<IReadOnlyCollection<StreamData>>( - new List<StreamData>()); + return []; }) .WhenAll(); diff --git a/src/EllieBot/Modules/Searches/_common/StreamNotifications/Providers/YouTubeProvider.cs b/src/EllieBot/Modules/Searches/_common/StreamNotifications/Providers/YouTubeProvider.cs index 0983e64..aa761cd 100644 --- a/src/EllieBot/Modules/Searches/_common/StreamNotifications/Providers/YouTubeProvider.cs +++ b/src/EllieBot/Modules/Searches/_common/StreamNotifications/Providers/YouTubeProvider.cs @@ -1,12 +1,7 @@ -using System.Net; -using EllieBot.Db.Models; -using EllieBot.Services; +using EllieBot.Db.Models; using System.Text.RegularExpressions; using System.Net.Http.Json; -using System.Text.Json; using System.Text.Json.Serialization; -using System.Xml.Linq; -using AngleSharp.Browser; namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers; @@ -113,56 +108,65 @@ public sealed partial class YouTubeProvider : Provider /// <returns><see cref="StreamData"/> of the channel. Null if none found</returns> public override async Task<StreamData?> GetStreamDataAsync(string channelId) { - var instances = _scs.Data.InvidiousInstances; - - if (instances is not { Count: > 0 }) - return null; - - var invInstance = instances[_rng.Next(0, instances.Count)]; - var client = _httpFactory.CreateClient(); - client.BaseAddress = new Uri(invInstance); - - var channel = await client.GetFromJsonAsync<InvidiousChannelResponse>($"/api/v1/channels/{channelId}"); - if (channel is null) - return null; - - var response = - await client.GetFromJsonAsync<InvChannelStreamsResponse>($"/api/v1/channels/{channelId}/streams"); - if (response is null) - return null; - - var vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming && x.LengthSeconds == 0); - var isLive = false; - if (vid is null) + try { - vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming); + var instances = _scs.Data.InvidiousInstances; + + if (instances is not { Count: > 0 }) + return null; + + var invInstance = instances[_rng.Next(0, instances.Count)]; + var client = _httpFactory.CreateClient(); + client.BaseAddress = new Uri(invInstance); + + var channel = await client.GetFromJsonAsync<InvidiousChannelResponse>($"/api/v1/channels/{channelId}"); + if (channel is null) + return null; + + var response = + await client.GetFromJsonAsync<InvChannelStreamsResponse>($"/api/v1/channels/{channelId}/streams"); + if (response is null) + return null; + + var vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming && x.LengthSeconds == 0); + var isLive = false; + if (vid is null) + { + vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming); + } + else + { + isLive = true; + } + + if (vid is null) + return null; + + var avatarUrl = channel?.AuthorThumbnails?.Select(x => x.Url).LastOrDefault(); + + return new StreamData() + { + Game = "Livestream", + Name = vid.Author, + Preview = vid.Thumbnails + .Skip(1) + .Select(x => "https://i.ytimg.com/" + x.Url) + .FirstOrDefault(), + Title = vid.Title, + Viewers = vid.ViewCount, + AvatarUrl = avatarUrl, + IsLive = isLive, + StreamType = FollowedStream.FType.Youtube, + StreamUrl = "https://youtube.com/watch?v=" + vid.VideoId, + UniqueName = vid.AuthorId, + }; } - else + catch (Exception ex) { - isLive = true; - } - - if (vid is null) + Log.Warning(ex, "Unable to get stream data for a youtube channel {ChannelId}", channelId); + _failingStreams.TryAdd(channelId, DateTime.UtcNow); return null; - - var avatarUrl = channel?.AuthorThumbnails?.Select(x => x.Url).LastOrDefault(); - - return new StreamData() - { - Game = "Livestream", - Name = vid.Author, - Preview = vid.Thumbnails - .Skip(1) - .Select(x => "https://i.ytimg.com/" + x.Url) - .FirstOrDefault(), - Title = vid.Title, - Viewers = vid.ViewCount, - AvatarUrl = avatarUrl, - IsLive = isLive, - StreamType = FollowedStream.FType.Youtube, - StreamUrl = "https://youtube.com/watch?v=" + vid.VideoId, - UniqueName = vid.AuthorId, - }; + } } /// <summary> diff --git a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs index e2df525..f43db76 100644 --- a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs +++ b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs @@ -17,7 +17,7 @@ public class LiveChannelService( IReplacementService repSvc, ShardData shardData) : IReadyExecutor, IEService { - public const int MAX_LIVECHANNELS = 5; + public const int MAX_LIVECHANNELS = 1; private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new(); diff --git a/src/EllieBot/Modules/Utility/VerboseErrorsService.cs b/src/EllieBot/Modules/Utility/VerboseErrorsService.cs index 748c7c2..7420500 100644 --- a/src/EllieBot/Modules/Utility/VerboseErrorsService.cs +++ b/src/EllieBot/Modules/Utility/VerboseErrorsService.cs @@ -1,4 +1,5 @@ #nullable disable +using LinqToDB; using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; @@ -36,10 +37,10 @@ public class VerboseErrorsService : IReadyExecutor, IEService try { var embed = _hs.GetCommandHelp(cmd, channel.Guild) - .WithTitle("Command Error") - .WithDescription(reason) - .WithFooter("Admin may disable verbose errors via `.ve` command") - .WithErrorColor(); + .WithTitle("Command Error") + .WithDescription(reason) + .WithFooter("Admin may disable verbose errors via `.ve` command") + .WithErrorColor(); await _sender.Response(channel).Embed(embed).SendAsync(); } @@ -50,16 +51,29 @@ public class VerboseErrorsService : IReadyExecutor, IEService } } + /// <summary> + /// Toggles or sets verbose errors for the specified guild. + /// </summary> + /// <param name="guildId">The ID of the guild to toggle verbose errors for.</param> + /// <param name="maybeEnabled">If specified, sets to this value; otherwise toggles current value.</param> + /// <returns>Returns the new state of verbose errors (true = enabled, false = disabled).</returns> public async Task<bool> ToggleVerboseErrors(ulong guildId, bool? maybeEnabled = null) { await using var ctx = _db.GetDbContext(); - var isEnabled = ctx.GetTable<GuildConfig>() - .Where(x => x.GuildId == guildId) - .Select(x => x.VerboseErrors) - .FirstOrDefault(); - - if (isEnabled) // This doesn't need to be duplicated inside the using block + var current = await ctx.GetTable<GuildConfig>() + .Where(x => x.GuildId == guildId) + .Select(x => x.VerboseErrors) + .FirstOrDefaultAsync(); + + var newState = maybeEnabled ?? !current; + + await ctx.GetTable<GuildConfig>() + .Where(x => x.GuildId == guildId) + .Set(x => x.VerboseErrors, newState) + .UpdateAsync(); + + if (newState) { _guildsDisabled.TryRemove(guildId); } @@ -68,15 +82,15 @@ public class VerboseErrorsService : IReadyExecutor, IEService _guildsDisabled.Add(guildId); } - return isEnabled; + return newState; } public async Task OnReadyAsync() { await using var ctx = _db.GetDbContext(); var disabledOn = ctx.GetTable<GuildConfig>() - .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) && !x.VerboseErrors) - .Select(x => x.GuildId); + .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) && !x.VerboseErrors) + .Select(x => x.GuildId); foreach (var guildId in disabledOn) _guildsDisabled.Add(guildId);