We no longer blindly dequeue 5 embeds, we check their length too. The webhook executor will now send up to 10 embeds OR embeds totaling less than 6000 characters, whichever is less. Embeds longer than 6000 characters are discarded to prevent errors. We also check for an empty request body in SendLogAsync and bail to prevent 400s.
177 lines
6 KiB
C#
177 lines
6 KiB
C#
using System.Drawing;
|
|
using Catalogger.Backend.Cache.InMemoryCache;
|
|
using Humanizer;
|
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
|
using Remora.Discord.API.Abstractions.Objects;
|
|
using Remora.Discord.Commands.Contexts;
|
|
using Remora.Discord.Commands.Extensions;
|
|
using Remora.Discord.Commands.Services;
|
|
using Remora.Rest.Core;
|
|
using Remora.Results;
|
|
|
|
namespace Catalogger.Backend.Extensions;
|
|
|
|
public static class DiscordExtensions
|
|
{
|
|
public static string Tag(this IPartialUser user)
|
|
{
|
|
var discriminator = user.Discriminator.OrDefault();
|
|
return discriminator == 0
|
|
? user.Username.Value
|
|
: $"{user.Username.Value}#{discriminator:0000}";
|
|
}
|
|
|
|
public static string AvatarUrl(this IUser user, int size = 256)
|
|
{
|
|
if (user.Avatar != null)
|
|
{
|
|
var ext = user.Avatar.HasGif ? ".gif" : ".webp";
|
|
return $"https://cdn.discordapp.com/avatars/{user.ID}/{user.Avatar.Value}{ext}?size={size}";
|
|
}
|
|
|
|
var avatarIndex =
|
|
user.Discriminator == 0 ? (int)((user.ID.Value >> 22) % 6) : user.Discriminator % 5;
|
|
return $"https://cdn.discordapp.com/embed/avatars/{avatarIndex}.png?size={size}";
|
|
}
|
|
|
|
public static string? AvatarUrl(this IGuildMemberUpdate member, int size = 256) =>
|
|
GuildAvatarUrl(
|
|
member.GuildID,
|
|
member.User.ID,
|
|
member.Avatar.OrDefault()?.Value,
|
|
isAnimated: member.Avatar.OrDefault()?.HasGif,
|
|
size
|
|
);
|
|
|
|
public static string? AvatarUrl(this IGuildMember member, Snowflake guildId, int size = 256) =>
|
|
GuildAvatarUrl(
|
|
guildId,
|
|
member.User.GetOrThrow().ID,
|
|
member.Avatar.OrDefault()?.Value,
|
|
isAnimated: member.Avatar.OrDefault()?.HasGif,
|
|
size
|
|
);
|
|
|
|
private static string? GuildAvatarUrl(
|
|
Snowflake guildId,
|
|
Snowflake userId,
|
|
string? hash,
|
|
bool? isAnimated,
|
|
int size = 256
|
|
)
|
|
{
|
|
if (hash == null)
|
|
return null;
|
|
|
|
var ext = isAnimated == true ? ".gif" : ".webp";
|
|
|
|
return $"https://cdn.discordapp.com/guilds/{guildId}/users/{userId}/avatars/{hash}{ext}?size={size}";
|
|
}
|
|
|
|
public static string? IconUrl(this IGuild guild, int size = 256)
|
|
{
|
|
if (guild.Icon == null)
|
|
return null;
|
|
|
|
var ext = guild.Icon.HasGif ? ".gif" : ".webp";
|
|
|
|
return $"https://cdn.discordapp.com/icons/{guild.ID}/{guild.Icon.Value}{ext}?size={size}";
|
|
}
|
|
|
|
public static ulong ToUlong(this Snowflake snowflake) => snowflake.Value;
|
|
|
|
public static ulong ToUlong(this Optional<Snowflake> snowflake)
|
|
{
|
|
if (!snowflake.IsDefined())
|
|
throw new Exception("ToUlong called on an undefined Snowflake");
|
|
return snowflake.Value.Value;
|
|
}
|
|
|
|
public static string ToPrettyString(this Color color)
|
|
{
|
|
var r = color.R.ToString("X2");
|
|
var g = color.G.ToString("X2");
|
|
var b = color.B.ToString("X2");
|
|
|
|
return $"#{r}{g}{b}";
|
|
}
|
|
|
|
public static bool Is(this Optional<Snowflake> s1, Snowflake s2) =>
|
|
s1.IsDefined(out var value) && value == s2;
|
|
|
|
public static bool Is(this Optional<Snowflake> s1, ulong s2) =>
|
|
s1.IsDefined(out var value) && value == s2;
|
|
|
|
public static T GetOrThrow<T>(this Result<T> result)
|
|
{
|
|
if (result.Error != null)
|
|
throw new DiscordRestException(result.Error.Message);
|
|
return result.Entity;
|
|
}
|
|
|
|
public static T GetOrThrow<T>(this Optional<T> optional) =>
|
|
optional.OrThrow(() => new CataloggerError("Optional<T> was unset"));
|
|
|
|
public static async Task<T> GetOrThrow<T>(this Task<Result<T>> result) =>
|
|
(await result).GetOrThrow();
|
|
|
|
public static string ToPrettyString(this IDiscordPermissionSet permissionSet) =>
|
|
string.Join(
|
|
", ",
|
|
permissionSet.GetPermissions().Select(p => p.Humanize(LetterCasing.Title))
|
|
);
|
|
|
|
public static (Snowflake, Snowflake) GetUserAndGuild(
|
|
this ContextInjectionService contextInjectionService
|
|
)
|
|
{
|
|
if (contextInjectionService.Context is not IInteractionCommandContext ctx)
|
|
throw new CataloggerError("No context");
|
|
if (!ctx.TryGetUserID(out var userId))
|
|
throw new CataloggerError("No user ID in context");
|
|
if (!ctx.TryGetGuildID(out var guildId))
|
|
throw new CataloggerError("No guild ID in context");
|
|
return (userId, guildId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts a list of roles by their position in the Discord interface.
|
|
/// </summary>
|
|
/// <param name="roles">The list of guild roles to filter.</param>
|
|
/// <param name="filterByIds">An optional list of role IDs to return, from a member object or similar.
|
|
/// If null, the entire list is returned.</param>
|
|
/// <returns></returns>
|
|
public static IEnumerable<IRole> Sorted(
|
|
this IEnumerable<IRole> roles,
|
|
IEnumerable<Snowflake>? filterByIds = null
|
|
)
|
|
{
|
|
var sorted = roles.OrderByDescending(r => r.Position);
|
|
return filterByIds != null ? sorted.Where(r => filterByIds.Contains(r.ID)) : sorted;
|
|
}
|
|
|
|
public static async Task<string> TryFormatModeratorAsync(
|
|
this UserCache userCache,
|
|
AuditLogCache.ActionData actionData
|
|
)
|
|
{
|
|
var moderator = await userCache.GetUserAsync(actionData.ModeratorId);
|
|
return moderator != null
|
|
? $"{moderator.Tag()} <@{moderator.ID}>"
|
|
: $"*(unknown user {actionData.ModeratorId}) <@{actionData.ModeratorId}>*";
|
|
}
|
|
|
|
public static int TextLength(this IEmbed embed)
|
|
{
|
|
var length = OptionalStringLength(embed.Description) + OptionalStringLength(embed.Title);
|
|
var fieldLength = (embed.Fields.OrDefault() ?? [])
|
|
.Select(f => f.Name.Length + f.Value.Length)
|
|
.Sum();
|
|
|
|
return length + fieldLength;
|
|
}
|
|
|
|
private static int OptionalStringLength(Optional<string> s) => s.OrDefault()?.Length ?? 0;
|
|
|
|
public class DiscordRestException(string message) : Exception(message);
|
|
}
|