mirror of
https://github.com/RPCS3/discord-bot.git
synced 2024-12-13 22:08:37 +00:00
153 lines
6.6 KiB
C#
153 lines
6.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CompatApiClient.Utils;
|
|
using CompatBot.Utils;
|
|
using CompatBot.Utils.Extensions;
|
|
using DSharpPlus;
|
|
using DSharpPlus.Entities;
|
|
using DSharpPlus.EventArgs;
|
|
using HomoglyphConverter;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
namespace CompatBot.EventHandlers
|
|
{
|
|
internal static class UsernameSpoofMonitor
|
|
{
|
|
private static readonly Dictionary<string, string> UsernameMapping = new Dictionary<string, string>();
|
|
private static readonly SemaphoreSlim UsernameLock = new SemaphoreSlim(1, 1);
|
|
private static readonly MemoryCache SpoofingReportThrottlingCache = new MemoryCache(new MemoryCacheOptions{ ExpirationScanFrequency = TimeSpan.FromMinutes(10) });
|
|
private static readonly TimeSpan SnoozeDuration = TimeSpan.FromHours(1);
|
|
|
|
public static async Task OnUserUpdated(UserUpdateEventArgs args)
|
|
{
|
|
if (args.UserBefore.Username == args.UserAfter.Username)
|
|
return;
|
|
|
|
var potentialTargets = GetPotentialVictims(args.Client, args.Client.GetMember(args.UserAfter), true, false);
|
|
if (!potentialTargets.Any())
|
|
return;
|
|
|
|
if (await IsFlashmobAsync(args.Client, potentialTargets).ConfigureAwait(false))
|
|
return;
|
|
|
|
var m = args.Client.GetMember(args.UserAfter);
|
|
await args.Client.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(GuildMemberUpdateEventArgs args)
|
|
{
|
|
if (args.NicknameBefore == args.NicknameAfter)
|
|
return;
|
|
|
|
var potentialTargets = GetPotentialVictims(args.Client, args.Member, false, true);
|
|
if (!potentialTargets.Any())
|
|
return;
|
|
|
|
if (await IsFlashmobAsync(args.Client, potentialTargets).ConfigureAwait(false))
|
|
return;
|
|
|
|
await args.Client.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(GuildMemberAddEventArgs args)
|
|
{
|
|
var potentialTargets = GetPotentialVictims(args.Client, args.Member, true, true);
|
|
if (!potentialTargets.Any())
|
|
return;
|
|
|
|
if (await IsFlashmobAsync(args.Client, potentialTargets).ConfigureAwait(false))
|
|
return;
|
|
|
|
await args.Client.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)
|
|
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)
|
|
{
|
|
string result;
|
|
if (UsernameLock.Wait(0))
|
|
try
|
|
{
|
|
if (UsernameMapping.TryGetValue(name, out result))
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
UsernameLock.Release();
|
|
}
|
|
result = name.ToCanonicalForm();
|
|
if (UsernameLock.Wait(0))
|
|
try
|
|
{
|
|
UsernameMapping[name] = result;
|
|
}
|
|
finally
|
|
{
|
|
UsernameLock.Release();
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|