using System.Net; using System.Text; using DiffPlex.DiffBuilder.Model; using Microsoft.Extensions.Logging; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; namespace Octobot; public static class Extensions { /// /// Adds a footer representing that an action was performed by a . /// /// The builder to add the footer to. /// The user that performed the action whose tag and avatar to use. /// The builder with the added footer. public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) { var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrl = avatarUrlResult.IsSuccess ? avatarUrlResult.Entity.AbsoluteUri : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri; return builder.WithFooter( new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl)); } /// /// Adds a title using the author field, making it smaller than using the title field. /// /// The builder to add the small title to. /// The text of the small title. /// The user whose avatar to use in the small title. /// The builder with the added small title in the author field. public static EmbedBuilder WithSmallTitle( this EmbedBuilder builder, string text, IUser? avatarSource = null) { Uri? avatarUrl = null; if (avatarSource is not null) { var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); avatarUrl = avatarUrlResult.IsSuccess ? avatarUrlResult.Entity : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; } builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri); return builder; } /// /// Adds a user avatar in the thumbnail field. /// /// The builder to add the thumbnail to. /// The user whose avatar to use in the thumbnail field. /// The builder with the added avatar in the thumbnail field. public static EmbedBuilder WithLargeAvatar( this EmbedBuilder builder, IUser avatarSource) { var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256); var avatarUrl = avatarUrlResult.IsSuccess ? avatarUrlResult.Entity : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity; return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri); } /// /// Adds a footer representing that the action was performed in the . /// /// The builder to add the footer to. /// The guild whose name and icon to use. /// The builder with the added footer. public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) { var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); var iconUrl = iconUrlResult.IsSuccess ? iconUrlResult.Entity.AbsoluteUri : default(Optional); return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); } /// /// Adds a title representing that the action happened in the . /// /// The builder to add the title to. /// The guild whose name and icon to use. /// The builder with the added title. public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) { var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); var iconUrl = iconUrlResult.IsSuccess ? iconUrlResult.Entity.AbsoluteUri : null; builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl); return builder; } /// /// Adds a scheduled event's cover image. /// /// The builder to add the image to. /// The ID of the scheduled event whose image to use. /// The Optional containing the image hash. /// The builder with the added cover image. public static EmbedBuilder WithEventCover( this EmbedBuilder builder, Snowflake eventId, Optional imageHashOptional) { if (!imageHashOptional.IsDefined(out var imageHash)) { return builder; } var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024); return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; } /// /// Sanitizes a string for use in by inserting zero-width spaces in between /// symbols used to format the string with block code. /// /// The string to sanitize. /// The sanitized string that can be safely used in . private static string SanitizeForBlockCode(this string s) { return s.Replace("```", "​`​`​`​"); } /// /// Sanitizes a string (see ) and formats the string to use Markdown Block Code /// formatting with a specified /// language for syntax highlighting. /// /// The string to sanitize and format. /// /// /// The sanitized string formatted to use Markdown Block Code with a specified /// language for syntax highlighting. /// public static string InBlockCode(this string s, string language = "") { s = s.SanitizeForBlockCode(); return $"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith("`", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(s) ? " " : "")}```"; } public static string Localized(this string key) { return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; } /// /// Encodes a string to allow its transmission in request headers. /// /// Used when encountering "Request headers must contain only ASCII characters". /// The string to encode. /// An encoded string with spaces kept intact. public static string EncodeHeader(this string s) { return WebUtility.UrlEncode(s).Replace('+', ' '); } public static string AsMarkdown(this DiffPaneModel model) { var builder = new StringBuilder(); foreach (var line in model.Lines) { if (line.Type is ChangeType.Deleted) { builder.Append("-- "); } if (line.Type is ChangeType.Inserted) { builder.Append("++ "); } if (line.Type is not ChangeType.Imaginary) { builder.AppendLine(line.Text); } } return InBlockCode(builder.ToString(), "diff"); } public static string GetTag(this IUser user) { return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}"; } public static Snowflake ToSnowflake(this ulong id) { return DiscordSnowflake.New(id); } public static TResult? MaxOrDefault( this IEnumerable source, Func selector) { var list = source.ToList(); return list.Any() ? list.Max(selector) : default; } public static bool TryGetContextIDs( this ICommandContext context, out Snowflake guildId, out Snowflake channelId, out Snowflake userId) { channelId = default; userId = default; return context.TryGetGuildID(out guildId) && context.TryGetChannelID(out channelId) && context.TryGetUserID(out userId); } /// /// Checks whether this Snowflake has any value set. /// /// The Snowflake to check. /// true if the Snowflake has no value set or it's set to 0, false otherwise. public static bool Empty(this Snowflake snowflake) { return snowflake.Value is 0; } /// /// Checks whether this snowflake is empty (see ) or it's equal to /// /// /// The Snowflake to check for emptiness /// The Snowflake to check for equality with . /// /// true if is empty or is equal to , false /// otherwise. /// /// public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake) { return snowflake.Empty() || snowflake == anotherSnowflake; } public static async Task SendContextualEmbedResultAsync( this FeedbackService feedback, Result embedResult, CancellationToken ct = default) { if (!embedResult.IsDefined(out var embed)) { return Result.FromError(embedResult); } return (Result)await feedback.SendContextualEmbedAsync(embed, ct: ct); } /// /// Checks if the has failed due to an error that has resulted from neither invalid user /// input nor the execution environment and logs the error using the provided . /// /// /// This has special behavior for - its exception will be passed to the /// /// /// The logger to use. /// The Result whose error check. /// The message to use if this result has failed. public static void LogResult(this ILogger logger, IResult result, string? message = "") { if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) { return; } if (result.Error is ExceptionError exe) { logger.LogError(exe.Exception, "{ErrorMessage}", message); return; } logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); } public static void AddIfFailed(this List list, Result result) { if (!result.IsSuccess) { list.Add(result); } } /// /// Return an appropriate result for a list of failed results. The list must only contain failed results. /// /// The list of failed results. /// /// A successful result if the list is empty, the only Result in the list, or /// containing all results from the list. /// /// public static Result AggregateErrors(this List list) { return list.Count switch { 0 => Result.FromSuccess(), 1 => list[0], _ => new AggregateError(list.Cast().ToArray()) }; } public static Result TryGetExternalEventData(this IGuildScheduledEvent scheduledEvent, out DateTimeOffset endTime, out string? location) { endTime = default; location = default; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); } if (!metadata.Location.IsDefined(out location)) { return new ArgumentNullError(nameof(metadata.Location)); } return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) ? Result.FromSuccess() : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); } }