discord-bot/CompatBot/Commands/Moderation.Audit.cs
13xforever 77ff167b89 Limit full member list processing to one thread at a time
Also return results of spoofing checks as an attachments instead of spaming the channel
2018-11-03 16:32:05 +05:00

177 lines
8.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.EventHandlers;
using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
namespace CompatBot.Commands
{
internal sealed partial class Moderation
{
[Group("audit"), RequiresBotModRole]
[Description("Commands to audit server things")]
public sealed class Audit: BaseCommandModuleCustom
{
private static readonly SemaphoreSlim CheckLock = new SemaphoreSlim(1, 1);
[Command("spoofing"), Aliases("impersonation"), RequireDirectMessage]
[Description("Checks every user on the server for name spoofing")]
public Task Spoofing(CommandContext ctx)
{
SpoofingCheck(ctx);
return Task.CompletedTask;
}
[Command("members"), Aliases("users"), RequireDirectMessage]
[Description("Dumps server member information, including usernames, nicknames, and roles")]
public async Task Members(CommandContext ctx)
{
if (!CheckLock.Wait(0))
{
await ctx.RespondAsync("Another check is already in progress").ConfigureAwait(false);
return;
}
try
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
var members = GetMembers(ctx.Client);
using (var compressedResult = new MemoryStream())
{
using (var memoryStream = new MemoryStream())
{
using (var writer = new StreamWriter(memoryStream, new UTF8Encoding(false), 4096, true))
{
foreach (var member in members)
writer.WriteLine($"{member.Username}\t{member.Nickname}\t{member.JoinedAt:O}\t{(string.Join(',', member.Roles.Select(r => r.Name)))}");
writer.Flush();
}
memoryStream.Seek(0, SeekOrigin.Begin);
if (memoryStream.Length <= Config.AttachmentSizeLimit)
{
await ctx.RespondWithFileAsync("names.txt", memoryStream).ConfigureAwait(false);
return;
}
using (var gzip = new GZipStream(compressedResult, CompressionLevel.Optimal, true))
{
memoryStream.CopyTo(gzip);
gzip.Flush();
}
}
compressedResult.Seek(0, SeekOrigin.Begin);
if (compressedResult.Length <= Config.AttachmentSizeLimit)
await ctx.RespondWithFileAsync("names.txt.gz", compressedResult).ConfigureAwait(false);
else
await ctx.RespondAsync($"Dump is too large: {compressedResult.Length} bytes").ConfigureAwait(false);
}
}
catch (Exception e)
{
Config.Log.Warn(e, "Failed to dump guild members");
await ctx.ReactWithAsync(Config.Reactions.Failure, "Failed to dump guild members").ConfigureAwait(false);
}
finally
{
CheckLock.Release(1);
}
}
private List<DiscordMember> GetMembers(DiscordClient client)
{
//owner -> white name
//newbs -> veterans
return client.Guilds.Select(g => g.Value.GetAllMembersAsync().ConfigureAwait(false))
.SelectMany(l => l.GetAwaiter().GetResult())
.OrderByDescending(m => m.Hierarchy)
.ThenByDescending(m => m.JoinedAt)
.ToList();
}
private async void SpoofingCheck(CommandContext ctx)
{
if (!CheckLock.Wait(0))
{
await ctx.RespondAsync("Another check is already in progress").ConfigureAwait(false);
return;
}
try
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
var members = GetMembers(ctx.Client);
if (members.Count < 2)
return;
var result = new StringBuilder("List of potential impersonators → victims:").AppendLine();
var headerLength = result.Length;
var checkedMembers = new List<DiscordMember>(members.Count) {members[0]};
for (var i = 1; i < members.Count; i++)
{
var member = members[i];
var victims = UsernameSpoofMonitor.GetPotentialVictims(ctx.Client, member, true, true, checkedMembers);
if (victims.Any())
result.Append(member.GetMentionWithNickname()).Append(" → ").AppendLine(string.Join(", ", victims.Select(m => m.GetMentionWithNickname())));
checkedMembers.Add(member);
}
using (var compressedStream = new MemoryStream())
{
using (var uncompressedStream = new MemoryStream())
{
using (var writer = new StreamWriter(uncompressedStream, new UTF8Encoding(false), 4096, true))
{
writer.Write(result.ToString());
writer.Flush();
}
uncompressedStream.Seek(0, SeekOrigin.Begin);
if (result.Length <= headerLength)
{
await ctx.RespondAsync("No potential name spoofing was detected").ConfigureAwait(false);
return;
}
if (uncompressedStream.Length <= Config.AttachmentSizeLimit)
{
await ctx.RespondWithFileAsync("spoofing_check_results.txt", uncompressedStream).ConfigureAwait(false);
return;
}
using (var gzip = new GZipStream(compressedStream, CompressionLevel.Optimal, true))
{
uncompressedStream.CopyTo(gzip);
gzip.Flush();
}
compressedStream.Seek(0, SeekOrigin.Begin);
if (compressedStream.Length <= Config.AttachmentSizeLimit)
await ctx.RespondWithFileAsync("spoofing_check_results.txt.gz", compressedStream).ConfigureAwait(false);
else
await ctx.RespondAsync($"Dump is too large: {compressedStream.Length} bytes").ConfigureAwait(false);
}
}
}
catch (Exception e)
{
Config.Log.Error(e);
//should be extra careful, as async void will run on a thread pull, and will terminate the whole application with an uncaught exception
try { await ctx.ReactWithAsync(Config.Reactions.Failure, "(X_X)").ConfigureAwait(false); } catch { }
}
finally
{
CheckLock.Release(1);
}
}
}
}
}