547 lines
No EOL
16 KiB
C#
547 lines
No EOL
16 KiB
C#
#nullable disable
|
|
using LinqToDB;
|
|
using LinqToDB.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
using EllieBot.Db.Models;
|
|
using System.Threading.Channels;
|
|
|
|
namespace EllieBot.Modules.Administration.Services;
|
|
|
|
public class ProtectionService : IReadyExecutor, IEService
|
|
{
|
|
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = static delegate
|
|
{
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
private readonly ConcurrentDictionary<ulong, AntiRaidStats> _antiRaidGuilds = new();
|
|
|
|
private readonly ConcurrentDictionary<ulong, AntiSpamStats> _antiSpamGuilds = new();
|
|
|
|
private readonly ConcurrentDictionary<ulong, AntiAltStats> _antiAltGuilds = new();
|
|
|
|
private readonly DiscordSocketClient _client;
|
|
private readonly MuteService _mute;
|
|
private readonly DbService _db;
|
|
private readonly UserPunishService _punishService;
|
|
private readonly INotifySubscriber _notifySub;
|
|
private readonly ShardData _shardData;
|
|
|
|
private readonly Channel<PunishQueueItem> _punishUserQueue =
|
|
Channel.CreateUnbounded<PunishQueueItem>(new()
|
|
{
|
|
SingleReader = true,
|
|
SingleWriter = false
|
|
});
|
|
|
|
public ProtectionService(
|
|
DiscordSocketClient client,
|
|
MuteService mute,
|
|
DbService db,
|
|
UserPunishService punishService,
|
|
INotifySubscriber notifySub,
|
|
ShardData shardData)
|
|
{
|
|
_client = client;
|
|
_mute = mute;
|
|
_db = db;
|
|
_punishService = punishService;
|
|
_notifySub = notifySub;
|
|
_shardData = shardData;
|
|
|
|
_client.MessageReceived += HandleAntiSpam;
|
|
_client.UserJoined += HandleUserJoined;
|
|
|
|
_client.LeftGuild += _client_LeftGuild;
|
|
}
|
|
|
|
private async Task RunQueue()
|
|
{
|
|
while (true)
|
|
{
|
|
var item = await _punishUserQueue.Reader.ReadAsync();
|
|
|
|
var muteTime = item.MuteTime;
|
|
var gu = item.User;
|
|
try
|
|
{
|
|
await _punishService.ApplyPunishment(gu.Guild,
|
|
gu,
|
|
_client.CurrentUser,
|
|
item.Action,
|
|
muteTime,
|
|
item.RoleId,
|
|
$"{item.Type} Protection");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error in punish queue: {Message}", ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
private Task _client_LeftGuild(SocketGuild guild)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
await TryStopAntiRaidAsync(guild.Id);
|
|
await TryStopAntiSpamAsync(guild.Id);
|
|
await TryStopAntiAltAsync(guild.Id);
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task HandleUserJoined(SocketGuildUser user)
|
|
{
|
|
if (user.IsBot)
|
|
return Task.CompletedTask;
|
|
|
|
_antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats);
|
|
_antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts);
|
|
|
|
if (maybeStats is null && maybeAlts is null)
|
|
return Task.CompletedTask;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
if (maybeAlts is { } alts)
|
|
{
|
|
if (user.CreatedAt != default)
|
|
{
|
|
var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime;
|
|
if (diff < alts.MinAge)
|
|
{
|
|
alts.Increment();
|
|
|
|
await PunishUsers(alts.Action,
|
|
ProtectionType.Alting,
|
|
alts.ActionDurationMinutes,
|
|
alts.RoleId,
|
|
user);
|
|
|
|
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
|
|
ProtectionType.Alting,
|
|
user.Id));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
if (maybeStats is not { } stats || !stats.RaidUsers.Add(user))
|
|
return;
|
|
|
|
++stats.UsersCount;
|
|
|
|
if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold)
|
|
{
|
|
var users = stats.RaidUsers.ToArray();
|
|
stats.RaidUsers.Clear();
|
|
var settings = stats.AntiRaidSettings;
|
|
|
|
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
|
|
await _notifySub.NotifyAsync(
|
|
new ProtectionNotifyModel(user.Guild.Id, ProtectionType.Raiding, users[0].Id)
|
|
);
|
|
}
|
|
|
|
await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
|
|
|
|
stats.RaidUsers.TryRemove(user);
|
|
--stats.UsersCount;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task HandleAntiSpam(SocketMessage arg)
|
|
{
|
|
if (arg is not SocketUserMessage msg || msg.Author.IsBot)
|
|
return Task.CompletedTask;
|
|
|
|
if (msg.Channel is not ITextChannel channel)
|
|
return Task.CompletedTask;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
|
|
|| spamSettings.AntiSpamSettings.IgnoredChannels.Any(x => x.ChannelId == channel.Id))
|
|
return;
|
|
|
|
var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
|
|
_ => new(msg),
|
|
(_, old) =>
|
|
{
|
|
old.ApplyNextMessage(msg);
|
|
return old;
|
|
});
|
|
|
|
if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
|
|
{
|
|
if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
|
|
{
|
|
var settings = spamSettings.AntiSpamSettings;
|
|
await PunishUsers(settings.Action,
|
|
ProtectionType.Spamming,
|
|
settings.MuteTime,
|
|
settings.RoleId,
|
|
(IGuildUser)msg.Author);
|
|
|
|
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
|
|
ProtectionType.Spamming,
|
|
msg.Author.Id));
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task PunishUsers(
|
|
PunishmentAction action,
|
|
ProtectionType pt,
|
|
int muteTime,
|
|
ulong? roleId,
|
|
params IGuildUser[] gus)
|
|
{
|
|
Log.Information("[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild",
|
|
pt,
|
|
gus.Length,
|
|
action,
|
|
gus[0].Guild.Name);
|
|
|
|
foreach (var gu in gus)
|
|
{
|
|
await _punishUserQueue.Writer.WriteAsync(new()
|
|
{
|
|
Action = action,
|
|
Type = pt,
|
|
User = gu,
|
|
MuteTime = muteTime,
|
|
RoleId = roleId
|
|
});
|
|
}
|
|
|
|
_ = OnAntiProtectionTriggered(action, pt, gus);
|
|
}
|
|
|
|
public async Task<AntiRaidStats> StartAntiRaidAsync(
|
|
ulong guildId,
|
|
int userThreshold,
|
|
int seconds,
|
|
PunishmentAction action,
|
|
int minutesDuration)
|
|
{
|
|
var g = _client.GetGuild(guildId);
|
|
await _mute.GetMuteRole(g);
|
|
|
|
if (action == PunishmentAction.AddRole)
|
|
return null;
|
|
|
|
if (!IsDurationAllowed(action))
|
|
minutesDuration = 0;
|
|
|
|
var stats = new AntiRaidStats
|
|
{
|
|
AntiRaidSettings = new()
|
|
{
|
|
Action = action,
|
|
Seconds = seconds,
|
|
UserThreshold = userThreshold,
|
|
PunishDuration = minutesDuration
|
|
}
|
|
};
|
|
|
|
_antiRaidGuilds.AddOrUpdate(guildId, stats, (_, _) => stats);
|
|
|
|
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
|
|
});
|
|
|
|
|
|
return stats;
|
|
}
|
|
|
|
public async Task<bool> TryStopAntiRaidAsync(ulong guildId)
|
|
{
|
|
if (_antiRaidGuilds.TryRemove(guildId, out _))
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
await uow.GetTable<AntiRaidSetting>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.DeleteAsync();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async Task<bool> TryStopAntiSpamAsync(ulong guildId)
|
|
{
|
|
if (_antiSpamGuilds.TryRemove(guildId, out _))
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
await uow.GetTable<AntiSpamSetting>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.DeleteAsync();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async Task<AntiSpamStats> StartAntiSpamAsync(
|
|
ulong guildId,
|
|
int messageCount,
|
|
PunishmentAction action,
|
|
int punishDurationMinutes,
|
|
ulong? roleId)
|
|
{
|
|
var g = _client.GetGuild(guildId);
|
|
|
|
if (action == PunishmentAction.Mute)
|
|
await _mute.GetMuteRole(g);
|
|
|
|
if (!IsDurationAllowed(action))
|
|
punishDurationMinutes = 0;
|
|
|
|
var stats = new AntiSpamStats
|
|
{
|
|
AntiSpamSettings = new()
|
|
{
|
|
Action = action,
|
|
MessageThreshold = messageCount,
|
|
MuteTime = punishDurationMinutes,
|
|
RoleId = roleId
|
|
}
|
|
};
|
|
|
|
_antiSpamGuilds.AddOrUpdate(guildId,
|
|
stats,
|
|
(_, old) =>
|
|
{
|
|
stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels;
|
|
return stats;
|
|
});
|
|
|
|
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
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
public async Task<bool?> AntiSpamIgnoreAsync(ulong guildId, ulong channelId)
|
|
{
|
|
var obj = new AntiSpamIgnore
|
|
{
|
|
ChannelId = channelId
|
|
};
|
|
|
|
await using var uow = _db.GetDbContext();
|
|
var spam = await uow.Set<AntiSpamSetting>()
|
|
.Include(x => x.IgnoredChannels)
|
|
.Where(x => x.GuildId == guildId)
|
|
.FirstOrDefaultAsyncEF();
|
|
|
|
if (spam is null)
|
|
return null;
|
|
|
|
var added = false;
|
|
if (spam.IgnoredChannels.All(x => x.ChannelId != channelId))
|
|
{
|
|
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
|
|
temp.AntiSpamSettings.IgnoredChannels.Add(obj);
|
|
|
|
spam.IgnoredChannels.Add(obj);
|
|
added = true;
|
|
}
|
|
else
|
|
{
|
|
var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId);
|
|
|
|
uow.Set<AntiSpamIgnore>().Remove(toRemove);
|
|
|
|
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
|
|
temp.AntiSpamSettings.IgnoredChannels.RemoveAll(x => x.ChannelId == channelId);
|
|
|
|
added = false;
|
|
}
|
|
|
|
await uow.SaveChangesAsync();
|
|
return added;
|
|
}
|
|
|
|
public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId)
|
|
{
|
|
_antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats);
|
|
_antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats);
|
|
_antiAltGuilds.TryGetValue(guildId, out var antiAltStats);
|
|
|
|
return (antiSpamStats, antiRaidStats, antiAltStats);
|
|
}
|
|
|
|
public bool IsDurationAllowed(PunishmentAction action)
|
|
{
|
|
switch (action)
|
|
{
|
|
case PunishmentAction.Ban:
|
|
case PunishmentAction.Mute:
|
|
case PunishmentAction.ChatMute:
|
|
case PunishmentAction.VoiceMute:
|
|
case PunishmentAction.AddRole:
|
|
case PunishmentAction.TimeOut:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task StartAntiAltAsync(
|
|
ulong guildId,
|
|
int minAgeMinutes,
|
|
PunishmentAction action,
|
|
int actionDurationMinutes = 0,
|
|
ulong? roleId = null)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
|
|
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
|
|
});
|
|
|
|
_antiAltGuilds[guildId] = new(new()
|
|
{
|
|
GuildId = guildId,
|
|
Action = action,
|
|
ActionDurationMinutes = actionDurationMinutes,
|
|
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
|
|
RoleId = roleId
|
|
});
|
|
}
|
|
|
|
public async Task<bool> TryStopAntiAltAsync(ulong guildId)
|
|
{
|
|
if (!_antiAltGuilds.TryRemove(guildId, out _))
|
|
return false;
|
|
|
|
await using var uow = _db.GetDbContext();
|
|
await uow.GetTable<AntiAltSetting>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.DeleteAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task OnReadyAsync()
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
|
|
var gids = _client.GetGuildIds();
|
|
var configs = await uow.Set<AntiAltSetting>()
|
|
.Where(x => gids.Contains(x.GuildId))
|
|
.ToListAsyncEF();
|
|
|
|
foreach (var config in configs)
|
|
_antiAltGuilds[config.GuildId] = new(config);
|
|
|
|
var raidConfigs = await uow.GetTable<AntiRaidSetting>()
|
|
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
|
.ToListAsyncLinqToDB();
|
|
|
|
foreach (var config in raidConfigs)
|
|
{
|
|
_antiRaidGuilds[config.GuildId] = new()
|
|
{
|
|
AntiRaidSettings = config,
|
|
};
|
|
}
|
|
|
|
var spamConfigs = await uow.GetTable<AntiSpamSetting>()
|
|
.AsNoTracking()
|
|
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
|
.ToListAsyncLinqToDB();
|
|
|
|
foreach (var config in spamConfigs)
|
|
{
|
|
_antiSpamGuilds[config.GuildId] = new()
|
|
{
|
|
AntiSpamSettings = config,
|
|
};
|
|
}
|
|
|
|
await RunQueue();
|
|
}
|
|
} |