Merge pull request 'v6' () from EllieBotDevs/elliebot:v6 into v6

Reviewed-on: 
This commit is contained in:
Toastie 2025-03-20 21:42:26 +00:00
commit 4642607ccc
10 changed files with 199 additions and 144 deletions
CHANGELOG.md
src/EllieBot
EllieBot.csproj
Modules
Administration
Permissions/Filter
Searches
StreamNotification
_common/StreamNotifications
Utility

View file

@ -2,6 +2,34 @@
*a,c,f,r,o* *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 ## [6.0.9] - 19.03.2025
### Changed ### Changed

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>6.0.9</Version> <Version>6.0.12</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -107,12 +107,12 @@ public class AdministrationService : IEService, IReadyExecutor
.UpdateWithOutputAsync(x => new() .UpdateWithOutputAsync(x => new()
{ {
DeleteMessageOnCommand = !x.DeleteMessageOnCommand DeleteMessageOnCommand = !x.DeleteMessageOnCommand
}); }, (old, newVal) => newVal);
if (conf.Length == 0) if (conf.Length == 0)
return false; return false;
var val = conf[0].Inserted.DeleteMessageOnCommand; var val = conf[0].DeleteMessageOnCommand;
if (val) if (val)
deleteMessagesOnCommand.Add(guildId); deleteMessagesOnCommand.Add(guildId);

View file

@ -27,6 +27,7 @@ public class ProtectionService : IReadyExecutor, IEService
private readonly UserPunishService _punishService; private readonly UserPunishService _punishService;
private readonly INotifySubscriber _notifySub; private readonly INotifySubscriber _notifySub;
private readonly ShardData _shardData; private readonly ShardData _shardData;
private readonly Channel<PunishQueueItem> _punishUserQueue = private readonly Channel<PunishQueueItem> _punishUserQueue =
Channel.CreateUnbounded<PunishQueueItem>(new() Channel.CreateUnbounded<PunishQueueItem>(new()
{ {
@ -176,10 +177,7 @@ public class ProtectionService : IReadyExecutor, IEService
try try
{ {
if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
|| spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new() || spamSettings.AntiSpamSettings.IgnoredChannels.Any(x => x.ChannelId == channel.Id))
{
ChannelId = channel.Id
}))
return; return;
var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
@ -275,23 +273,25 @@ public class ProtectionService : IReadyExecutor, IEService
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<AntiRaidSetting>() await uow.GetTable<AntiRaidSetting>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
Action = action, Action = action,
Seconds = seconds, Seconds = seconds,
UserThreshold = userThreshold, UserThreshold = userThreshold,
PunishDuration = minutesDuration PunishDuration = minutesDuration
}, _ => new() },
{ _ => new()
Action = action, {
Seconds = seconds, Action = action,
UserThreshold = userThreshold, Seconds = seconds,
PunishDuration = minutesDuration UserThreshold = userThreshold,
}, () => new() PunishDuration = minutesDuration
{ },
GuildId = guildId () => new()
}); {
GuildId = guildId
});
return stats; return stats;
@ -364,23 +364,25 @@ public class ProtectionService : IReadyExecutor, IEService
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<AntiSpamSetting>() await uow.GetTable<AntiSpamSetting>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
Action = stats.AntiSpamSettings.Action, Action = stats.AntiSpamSettings.Action,
MessageThreshold = stats.AntiSpamSettings.MessageThreshold, MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
MuteTime = stats.AntiSpamSettings.MuteTime, MuteTime = stats.AntiSpamSettings.MuteTime,
RoleId = stats.AntiSpamSettings.RoleId RoleId = stats.AntiSpamSettings.RoleId
}, (old) => new() },
{ (old) => new()
GuildId = guildId, {
Action = stats.AntiSpamSettings.Action, GuildId = guildId,
MessageThreshold = stats.AntiSpamSettings.MessageThreshold, Action = stats.AntiSpamSettings.Action,
MuteTime = stats.AntiSpamSettings.MuteTime, MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
RoleId = stats.AntiSpamSettings.RoleId MuteTime = stats.AntiSpamSettings.MuteTime,
}, () => new() RoleId = stats.AntiSpamSettings.RoleId
{ },
GuildId = guildId () => new()
}); {
GuildId = guildId
});
return stats; return stats;
} }
@ -405,7 +407,7 @@ public class ProtectionService : IReadyExecutor, IEService
if (spam.IgnoredChannels.All(x => x.ChannelId != channelId)) if (spam.IgnoredChannels.All(x => x.ChannelId != channelId))
{ {
if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) 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); spam.IgnoredChannels.Add(obj);
added = true; added = true;
@ -417,7 +419,7 @@ public class ProtectionService : IReadyExecutor, IEService
uow.Set<AntiSpamIgnore>().Remove(toRemove); uow.Set<AntiSpamIgnore>().Remove(toRemove);
if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) 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; added = false;
} }
@ -462,22 +464,24 @@ public class ProtectionService : IReadyExecutor, IEService
await uow.GetTable<AntiAltSetting>() await uow.GetTable<AntiAltSetting>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
Action = action, Action = action,
ActionDurationMinutes = actionDurationMinutes, ActionDurationMinutes = actionDurationMinutes,
MinAge = TimeSpan.FromMinutes(minAgeMinutes), MinAge = TimeSpan.FromMinutes(minAgeMinutes),
RoleId = roleId RoleId = roleId
}, _ => new() },
{ _ => new()
Action = action, {
ActionDurationMinutes = actionDurationMinutes, Action = action,
MinAge = TimeSpan.FromMinutes(minAgeMinutes), ActionDurationMinutes = actionDurationMinutes,
RoleId = roleId MinAge = TimeSpan.FromMinutes(minAgeMinutes),
}, () => new() RoleId = roleId
{ },
GuildId = guildId () => new()
}); {
GuildId = guildId
});
_antiAltGuilds[guildId] = new(new() _antiAltGuilds[guildId] = new(new()
{ {

View file

@ -52,12 +52,12 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var confs = await uow.GetTable<GuildFilterConfig>() var confs = await uow.GetTable<GuildFilterConfig>()
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId)) .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
.LoadWith(x => x.FilterInvitesChannelIds) .LoadWith(x => x.FilterInvitesChannelIds)
.LoadWith(x => x.FilterWordsChannelIds) .LoadWith(x => x.FilterWordsChannelIds)
.LoadWith(x => x.FilterLinksChannelIds) .LoadWith(x => x.FilterLinksChannelIds)
.LoadWith(x => x.FilteredWords) .LoadWith(x => x.FilteredWords)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
foreach (var conf in confs) foreach (var conf in confs)
{ {
@ -97,7 +97,7 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var fc = uow.FilterConfigForId(guildId, var fc = uow.FilterConfigForId(guildId,
set => set.Include(x => x.FilteredWords) set => set.Include(x => x.FilteredWords)
.Include(x => x.FilterWordsChannelIds)); .Include(x => x.FilterWordsChannelIds));
WordFilteringServers.TryRemove(guildId); WordFilteringServers.TryRemove(guildId);
ServerFilteredWords.TryRemove(guildId, out _); ServerFilteredWords.TryRemove(guildId, out _);
@ -140,7 +140,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
var filteredChannelWords = var filteredChannelWords =
FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>(); FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>();
var filteredServerWords = FilteredWordsForServer(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) if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0)
{ {
foreach (var word in wordsInMessage) foreach (var word in wordsInMessage)
@ -183,7 +184,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
return false; return false;
if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id)) 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", Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel",
usrMsg.Author.ToString(), usrMsg.Author.ToString(),
@ -219,7 +221,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
return false; return false;
if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) || LinkFilteringServers.Contains(guild.Id)) 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", Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel",
usrMsg.Author.ToString(), usrMsg.Author.ToString(),
@ -246,10 +249,10 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var conf = await uow.GetTable<GuildFilterConfig>() var conf = await uow.GetTable<GuildFilterConfig>()
.Where(fi => fi.GuildId == guildId) .Where(fi => fi.GuildId == guildId)
.LoadWith(x => x.FilterInvitesChannelIds) .LoadWith(x => x.FilterInvitesChannelIds)
.LoadWith(x => x.FilterLinksChannelIds) .LoadWith(x => x.FilterLinksChannelIds)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
return new() return new()
{ {

View file

@ -48,7 +48,10 @@ public partial class Searches
return; 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] [Cmd]

View file

@ -63,20 +63,19 @@ public class NotifChecker
.ToDictionary(x => x.Key.Name, x => x.Value)); .ToDictionary(x => x.Key.Name, x => x.Value));
var newStreamData = await oldStreamDataDict var newStreamData = await oldStreamDataDict
.Select(x => .Select(async x =>
{ {
// get all stream data for the streams of this type // get all stream data for the streams of this type
if (_streamProviders.TryGetValue(x.Key, if (_streamProviders.TryGetValue(x.Key,
out var provider)) out var provider))
{ {
return provider.GetStreamDataAsync(x.Value return await provider.GetStreamDataAsync(x.Value
.Select(entry => entry.Key) .Select(entry => entry.Key)
.ToList()); .ToList());
} }
// this means there's no provider for this stream data, (and there was before?) // this means there's no provider for this stream data, (and there was before?)
return Task.FromResult<IReadOnlyCollection<StreamData>>( return [];
new List<StreamData>());
}) })
.WhenAll(); .WhenAll();

View file

@ -1,12 +1,7 @@
using System.Net; using EllieBot.Db.Models;
using EllieBot.Db.Models;
using EllieBot.Services;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Xml.Linq;
using AngleSharp.Browser;
namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers; 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> /// <returns><see cref="StreamData"/> of the channel. Null if none found</returns>
public override async Task<StreamData?> GetStreamDataAsync(string channelId) public override async Task<StreamData?> GetStreamDataAsync(string channelId)
{ {
var instances = _scs.Data.InvidiousInstances; try
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); 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; Log.Warning(ex, "Unable to get stream data for a youtube channel {ChannelId}", channelId);
} _failingStreams.TryAdd(channelId, DateTime.UtcNow);
if (vid is null)
return 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,
};
} }
/// <summary> /// <summary>

View file

@ -17,7 +17,7 @@ public class LiveChannelService(
IReplacementService repSvc, IReplacementService repSvc,
ShardData shardData) : IReadyExecutor, IEService 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(); private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new();

View file

@ -1,4 +1,5 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -36,10 +37,10 @@ public class VerboseErrorsService : IReadyExecutor, IEService
try try
{ {
var embed = _hs.GetCommandHelp(cmd, channel.Guild) var embed = _hs.GetCommandHelp(cmd, channel.Guild)
.WithTitle("Command Error") .WithTitle("Command Error")
.WithDescription(reason) .WithDescription(reason)
.WithFooter("Admin may disable verbose errors via `.ve` command") .WithFooter("Admin may disable verbose errors via `.ve` command")
.WithErrorColor(); .WithErrorColor();
await _sender.Response(channel).Embed(embed).SendAsync(); 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) public async Task<bool> ToggleVerboseErrors(ulong guildId, bool? maybeEnabled = null)
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
var isEnabled = ctx.GetTable<GuildConfig>() var current = await ctx.GetTable<GuildConfig>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.Select(x => x.VerboseErrors) .Select(x => x.VerboseErrors)
.FirstOrDefault(); .FirstOrDefaultAsync();
if (isEnabled) // This doesn't need to be duplicated inside the using block var newState = maybeEnabled ?? !current;
await ctx.GetTable<GuildConfig>()
.Where(x => x.GuildId == guildId)
.Set(x => x.VerboseErrors, newState)
.UpdateAsync();
if (newState)
{ {
_guildsDisabled.TryRemove(guildId); _guildsDisabled.TryRemove(guildId);
} }
@ -68,15 +82,15 @@ public class VerboseErrorsService : IReadyExecutor, IEService
_guildsDisabled.Add(guildId); _guildsDisabled.Add(guildId);
} }
return isEnabled; return newState;
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
var disabledOn = ctx.GetTable<GuildConfig>() var disabledOn = ctx.GetTable<GuildConfig>()
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) && !x.VerboseErrors) .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) && !x.VerboseErrors)
.Select(x => x.GuildId); .Select(x => x.GuildId);
foreach (var guildId in disabledOn) foreach (var guildId in disabledOn)
_guildsDisabled.Add(guildId); _guildsDisabled.Add(guildId);