mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
281 lines
14 KiB
C#
281 lines
14 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using CompatApiClient.Utils;
|
|
using CompatBot.EventHandlers;
|
|
using DSharpPlus.Commands.Processors.TextCommands;
|
|
using DSharpPlus.Exceptions;
|
|
|
|
namespace CompatBot.Utils;
|
|
|
|
public static class DiscordClientExtensions
|
|
{
|
|
public static async ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, ulong? guildId, ulong userId)
|
|
{
|
|
try
|
|
{
|
|
var query = client.Guilds.AsEnumerable();
|
|
if (guildId.HasValue)
|
|
query = query.Where(g => g.Key == guildId.Value);
|
|
var guildList = query.ToList();
|
|
var result = guildList.SelectMany(g => g.Value.Members.Values).FirstOrDefault(m => m.Id == userId);
|
|
if (result is not null)
|
|
return result;
|
|
|
|
var fetchTasks = guildList.Select(async g => await g.Value.GetMemberAsync(userId)).ToList();
|
|
foreach (var task in fetchTasks)
|
|
{
|
|
result = await task.ConfigureAwait(false);
|
|
if (result is not null)
|
|
return result;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Error(e, "Failed to fetch member data");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, ulong userId) => GetMemberAsync(client, (ulong?)null, userId);
|
|
public static ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, DiscordUser user) => GetMemberAsync(client, user.Id);
|
|
public static ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, ulong guildId, DiscordUser user) => GetMemberAsync(client, guildId, user.Id);
|
|
public static ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, DiscordGuild? guild, DiscordUser user) => GetMemberAsync(client, guild, user.Id);
|
|
public static ValueTask<DiscordMember?> GetMemberAsync(this DiscordClient client, DiscordGuild? guild, ulong userId)
|
|
=> guild is null ? GetMemberAsync(client, userId) : GetMemberAsync(client, guild.Id, userId);
|
|
|
|
public static ValueTask<string> GetUserNameAsync(this CommandContext ctx, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
|
=> GetUserNameAsync(ctx.Client, ctx.Channel, userId, forDmPurposes, defaultName);
|
|
|
|
public static async ValueTask<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
|
{
|
|
var isPrivate = forDmPurposes ?? channel.IsPrivate;
|
|
if (userId is 0)
|
|
return "";
|
|
|
|
try
|
|
{
|
|
return (await client.GetUserAsync(userId)).Username;
|
|
}
|
|
catch (NotFoundException)
|
|
{
|
|
return isPrivate ? $"@{userId}" : defaultName;
|
|
}
|
|
}
|
|
|
|
public static async ValueTask RemoveReactionAsync(this DiscordMessage message, DiscordEmoji emoji)
|
|
{
|
|
try
|
|
{
|
|
await message.DeleteOwnReactionAsync(emoji).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e);
|
|
}
|
|
}
|
|
|
|
public static async ValueTask ReactWithAsync(this DiscordMessage message, DiscordEmoji emoji, string? fallbackMessage = null, bool? showBoth = null)
|
|
{
|
|
try
|
|
{
|
|
var isDm = message.Channel?.IsPrivate ?? true;
|
|
showBoth ??= isDm;
|
|
var canReact = isDm || (message.Channel?.PermissionsFor(message.Channel.Guild.CurrentMember).HasPermission(DiscordPermission.AddReactions) ?? false);
|
|
if (canReact)
|
|
await message.CreateReactionAsync(emoji).ConfigureAwait(false);
|
|
if ((!canReact || showBoth.Value) && !string.IsNullOrEmpty(fallbackMessage) && message.Channel is not null)
|
|
await message.Channel.SendMessageAsync(fallbackMessage).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e);
|
|
}
|
|
}
|
|
|
|
public static ValueTask RemoveReactionAsync(this TextCommandContext ctx, DiscordEmoji emoji) => RemoveReactionAsync(ctx.Message, emoji);
|
|
|
|
public static ValueTask ReactWithAsync(this TextCommandContext ctx, DiscordEmoji emoji, string? fallbackMessage = null, bool? showBoth = null)
|
|
=> ReactWithAsync(ctx.Message, emoji, fallbackMessage, showBoth ?? (ctx.Prefix == Config.AutoRemoveCommandPrefix));
|
|
|
|
public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null)
|
|
{
|
|
if (timeLimit > DateTime.UtcNow)
|
|
throw new ArgumentException("Time limit can't be set in the future", nameof(timeLimit));
|
|
|
|
var afterTime = timeLimit ?? DateTime.UtcNow.AddSeconds(-30);
|
|
var messages = await channel.GetMessagesBeforeCachedAsync(beforeMessageId, limit).ConfigureAwait(false);
|
|
return messages.TakeWhile(m => m.CreationTimestamp > afterTime).ToList().AsReadOnly();
|
|
}
|
|
|
|
public static async ValueTask<DiscordMessage?> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string? matchedOn, int? filterId, string? context, ReportSeverity severity, string? actionList = null)
|
|
{
|
|
var logChannel = await client.GetChannelAsync(Config.BotLogId).ConfigureAwait(false);
|
|
if (logChannel is null)
|
|
return null;
|
|
|
|
var embedBuilder = await MakeReportTemplateAsync(client, infraction, filterId, message, severity, actionList).ConfigureAwait(false);
|
|
var reportText = string.IsNullOrEmpty(trigger) ? "" : $"Triggered by: `{matchedOn?.Trim(40) ?? trigger}`{Environment.NewLine}";
|
|
if (!string.IsNullOrEmpty(context))
|
|
reportText += $"Triggered in: ```{context.Sanitize()}```{Environment.NewLine}";
|
|
embedBuilder.Description = reportText + embedBuilder.Description;
|
|
var (contents, _) = await message.DownloadAttachmentsAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (contents?.Count > 0)
|
|
return await logChannel.SendMessageAsync(
|
|
new DiscordMessageBuilder()
|
|
.AddEmbed(embedBuilder.Build())
|
|
.AddFiles(contents)
|
|
).ConfigureAwait(false);
|
|
else
|
|
return await logChannel.SendMessageAsync(
|
|
new DiscordMessageBuilder()
|
|
.AddEmbed(embedBuilder.Build())
|
|
).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
if (contents?.Count > 0)
|
|
foreach (var f in contents.Values)
|
|
await f.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
public static async ValueTask<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, IEnumerable<DiscordMember?> reporters, string? comment, ReportSeverity severity)
|
|
{
|
|
var getLogChannelTask = client.GetChannelAsync(Config.BotLogId);
|
|
var embedBuilder = await MakeReportTemplateAsync(client, infraction, null, message, severity).ConfigureAwait(false);
|
|
var reportText = string.IsNullOrEmpty(comment) ? "" : comment.Sanitize() + Environment.NewLine;
|
|
embedBuilder.Description = (reportText + embedBuilder.Description).Trim(EmbedPager.MaxDescriptionLength);
|
|
var mentions = reporters.Where(m => m is not null).Select(GetMentionWithNickname!);
|
|
embedBuilder.AddField("Reporters", string.Join(Environment.NewLine, mentions));
|
|
var logChannel = await getLogChannelTask.ConfigureAwait(false);
|
|
return await logChannel.SendMessageAsync(
|
|
new DiscordMessageBuilder()
|
|
.AddEmbed(embedBuilder.Build())
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
public static async ValueTask<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, string description, ICollection<DiscordMember>? potentialVictims, ReportSeverity severity)
|
|
{
|
|
var result = new DiscordEmbedBuilder
|
|
{
|
|
Title = infraction,
|
|
Color = GetColor(severity),
|
|
Description = description.Trim(EmbedPager.MaxDescriptionLength),
|
|
};
|
|
if (potentialVictims?.Count > 0)
|
|
result.AddField("Potential Targets", string.Join(Environment.NewLine, potentialVictims.Select(GetMentionWithNickname)).Trim(EmbedPager.MaxFieldLength));
|
|
var logChannel = await client.GetChannelAsync(Config.BotLogId).ConfigureAwait(false);
|
|
return await logChannel.SendMessageAsync(
|
|
new DiscordMessageBuilder()
|
|
.AddEmbed(result.Build())
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
public static string GetMentionWithNickname(this DiscordMember member)
|
|
=> string.IsNullOrEmpty(member.Nickname)
|
|
? $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`)"
|
|
: $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`, shown as `{member.Nickname.Sanitize()}`)";
|
|
|
|
public static async ValueTask<string> GetUsernameWithNicknameAsync(this DiscordUser user, DiscordClient client, DiscordGuild? guild = null)
|
|
=> (await client.GetMemberAsync(guild, user).ConfigureAwait(false)).GetUsernameWithNickname()
|
|
?? $"`{user.Username.Sanitize()}#{user.Discriminator}`";
|
|
|
|
public static string? GetUsernameWithNickname(this DiscordMember? member)
|
|
=> member is null
|
|
? null
|
|
: string.IsNullOrEmpty(member.Nickname)
|
|
? $"`{member.Username.Sanitize()}#{member.Discriminator}`"
|
|
: $"`{member.Username.Sanitize()}#{member.Discriminator}` (shown as `{member.Nickname.Sanitize()}`)";
|
|
|
|
[return: NotNullIfNotNull(nameof(fallbackEmoji))]
|
|
public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, string? fallbackEmoji = null)
|
|
=> GetEmoji(client, emojiName, fallbackEmoji == null ? null : DiscordEmoji.FromUnicode(fallbackEmoji));
|
|
|
|
[return: NotNullIfNotNull(nameof(fallbackEmoji))]
|
|
public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, DiscordEmoji? fallbackEmoji)
|
|
{
|
|
if (string.IsNullOrEmpty(emojiName))
|
|
return fallbackEmoji;
|
|
|
|
if (DiscordEmoji.TryFromName(client, emojiName, true, out var result))
|
|
return result;
|
|
|
|
if (DiscordEmoji.TryFromName(client, $":{emojiName}:", true, out result))
|
|
return result;
|
|
|
|
if (DiscordEmoji.TryFromUnicode(emojiName, out result))
|
|
return result;
|
|
return fallbackEmoji;
|
|
}
|
|
|
|
public static Task SendMessageAsync(this DiscordChannel channel, string message, byte[]? attachment, string? filename)
|
|
{
|
|
if (!string.IsNullOrEmpty(filename) && attachment?.Length > 0)
|
|
return channel.SendMessageAsync(new DiscordMessageBuilder().AddFile(filename, new MemoryStream(attachment)).WithContent(message));
|
|
return channel.SendMessageAsync(message);
|
|
}
|
|
|
|
private static async ValueTask<DiscordEmbedBuilder> MakeReportTemplateAsync(DiscordClient client, string infraction, int? filterId, DiscordMessage message, ReportSeverity severity, string? actionList = null)
|
|
{
|
|
var content = message.Content;
|
|
if (message.Channel.IsPrivate)
|
|
severity = ReportSeverity.None;
|
|
var needsAttention = severity > ReportSeverity.Low;
|
|
if (message.Embeds?.Count > 0)
|
|
{
|
|
if (!string.IsNullOrEmpty(content))
|
|
content += Environment.NewLine;
|
|
|
|
var srcEmbed = message.Embeds.First();
|
|
content += $"🔤 {srcEmbed.Title}";
|
|
if (srcEmbed.Fields?.Any() ?? false)
|
|
content += $"{Environment.NewLine}{srcEmbed.Description}{Environment.NewLine}+{srcEmbed.Fields.Count} fields";
|
|
}
|
|
if (message.Attachments?.Count > 0)
|
|
{
|
|
if (!string.IsNullOrEmpty(content))
|
|
content += Environment.NewLine;
|
|
content += string.Join(Environment.NewLine, message.Attachments.Select(a => "📎 " + a.FileName));
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(content))
|
|
content = "🤔 something fishy is going on here, there was no message or attachment";
|
|
DiscordMember? author = null;
|
|
try
|
|
{
|
|
author = await client.GetMemberAsync(message.Author).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e, $"Failed to get the member info for user {message.Author.Id} ({message.Author.Username})");
|
|
}
|
|
var result = new DiscordEmbedBuilder
|
|
{
|
|
Title = infraction,
|
|
Color = GetColor(severity),
|
|
}.AddField("Violator", author is null ? message.Author.Mention : GetMentionWithNickname(author), true)
|
|
.AddField("Channel", message.Channel.IsPrivate ? "Bot's DM" : message.JumpLink.ToString(), true);
|
|
if (filterId is not null)
|
|
result.AddField("Filter #", filterId.ToString(), true);
|
|
result.AddField("Content of the offending item", content.Trim(EmbedPager.MaxFieldLength));
|
|
if (!string.IsNullOrEmpty(actionList))
|
|
result.AddField("Filter Actions", actionList, true);
|
|
if (needsAttention && !message.Channel.IsPrivate)
|
|
result.AddField("Link to the message", message.JumpLink.ToString());
|
|
#if DEBUG
|
|
result.WithFooter("Test bot instance");
|
|
#endif
|
|
return result;
|
|
}
|
|
|
|
private static DiscordColor GetColor(ReportSeverity severity)
|
|
=> severity switch
|
|
{
|
|
ReportSeverity.Low => Config.Colors.LogInfo,
|
|
ReportSeverity.Medium => Config.Colors.LogNotice,
|
|
ReportSeverity.High => Config.Colors.LogAlert,
|
|
_ => Config.Colors.LogUnknown
|
|
};
|
|
} |