Files
archived-discord-bot/CompatBot/EventHandlers/UsernameSpoofMonitor.cs

145 lines
5.8 KiB
C#

using CompatApiClient.Utils;
using HomoglyphConverter;
using Microsoft.Extensions.Caching.Memory;
namespace CompatBot.EventHandlers;
internal static class UsernameSpoofMonitor
{
private static readonly Dictionary<string, string> UsernameMapping = new();
private static readonly SemaphoreSlim UsernameLock = new(1, 1);
private static readonly MemoryCache SpoofingReportThrottlingCache = new(new MemoryCacheOptions{ ExpirationScanFrequency = TimeSpan.FromMinutes(10) });
private static readonly TimeSpan SnoozeDuration = TimeSpan.FromHours(1);
public static async Task OnUserUpdated(DiscordClient c, UserUpdatedEventArgs args)
{
if (args.UserBefore.Username == args.UserAfter.Username)
return;
var m = await c.GetMemberAsync(args.UserAfter).ConfigureAwait(false);
if (m is null)
return;
var potentialTargets = GetPotentialVictims(c, m, true, false);
if (!potentialTargets.Any())
return;
if (await IsFlashMobAsync(c, potentialTargets).ConfigureAwait(false))
return;
await c.ReportAsync("🕵️ Potential user impersonation",
$"User {m.GetMentionWithNickname()} has changed their __username__ from " +
$"**{args.UserBefore.Username.Sanitize()}#{args.UserBefore.Discriminator}** to " +
$"**{args.UserAfter.Username.Sanitize()}#{args.UserAfter.Discriminator}**",
potentialTargets,
ReportSeverity.Medium);
}
public static async Task OnMemberUpdated(DiscordClient c, GuildMemberUpdatedEventArgs args)
{
if (args.NicknameBefore == args.NicknameAfter)
return;
var potentialTargets = GetPotentialVictims(c, args.Member, false, true);
if (!potentialTargets.Any())
return;
if (await IsFlashMobAsync(c, potentialTargets).ConfigureAwait(false))
return;
await c.ReportAsync("🕵️ Potential user impersonation",
$"Member {args.Member.GetMentionWithNickname()} has changed their __display name__ from " +
$"**{(args.NicknameBefore ?? args.Member.Username).Sanitize()}** to " +
$"**{args.Member.DisplayName.Sanitize()}**",
potentialTargets,
ReportSeverity.Medium);
}
public static async Task OnMemberAdded(DiscordClient c, GuildMemberAddedEventArgs args)
{
var potentialTargets = GetPotentialVictims(c, args.Member, true, true);
if (!potentialTargets.Any())
return;
if (await IsFlashMobAsync(c, potentialTargets).ConfigureAwait(false))
return;
await c.ReportAsync("🕵️ Potential user impersonation",
$"New member joined the server: {args.Member.GetMentionWithNickname()}",
potentialTargets,
ReportSeverity.Medium);
}
internal static List<DiscordMember> GetPotentialVictims(DiscordClient client, DiscordMember newMember, bool checkUsername, bool checkNickname, List<DiscordMember>? listToCheckAgainst = null)
{
var membersWithRoles = listToCheckAgainst ??
client.Guilds.SelectMany(guild => guild.Value.Members.Values)
.Where(m => m.Roles.Any() || m.IsCurrent)
.OrderByDescending(m => m.Hierarchy)
.ThenByDescending(m => m.JoinedAt)
.ToList();
var newUsername = GetCanonical(newMember.Username);
var newDisplayName = GetCanonical(newMember.DisplayName);
var potentialTargets = new List<DiscordMember>();
foreach (var memberWithRole in membersWithRoles)
if (checkUsername && newUsername == GetCanonical(memberWithRole.Username) && newMember.Id != memberWithRole.Id)
potentialTargets.Add(memberWithRole);
else if (checkNickname && (newDisplayName == GetCanonical(memberWithRole.DisplayName) || newDisplayName == GetCanonical(memberWithRole.Username)) && newMember.Id != memberWithRole.Id)
potentialTargets.Add(memberWithRole);
return potentialTargets;
}
private static async Task<bool> IsFlashMobAsync(DiscordClient client, List<DiscordMember> potentialVictims)
{
if (potentialVictims.Count == 0)
return false;
try
{
var displayName = GetCanonical(potentialVictims[0].DisplayName);
if (SpoofingReportThrottlingCache.TryGetValue(displayName, out string? s) && !string.IsNullOrEmpty(s))
{
SpoofingReportThrottlingCache.Set(displayName, s, SnoozeDuration);
return true;
}
if (potentialVictims.Count > 3)
{
SpoofingReportThrottlingCache.Set(displayName, "y", SnoozeDuration);
var channel = await client.GetChannelAsync(Config.BotLogId).ConfigureAwait(false);
await channel.SendMessageAsync($"`{displayName.Sanitize()}` is a popular member! Snoozing notifications for an hour").ConfigureAwait(false);
return true;
}
}
catch (Exception e)
{
Config.Log.Debug(e);
}
return false;
}
private static string GetCanonical(string name)
{
if (UsernameLock.Wait(0))
try
{
if (UsernameMapping.TryGetValue(name, out var result))
return result;
}
finally
{
UsernameLock.Release();
}
var canonicalName = name.ToCanonicalForm();
if (UsernameLock.Wait(0))
try
{
UsernameMapping[name] = canonicalName;
}
finally
{
UsernameLock.Release();
}
return canonicalName;
}
}