From f7dd09d43ee4863c98a43f4e3ee2a892f68dd0c3 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 29 May 2023 22:49:25 +0500 Subject: [PATCH] =?UTF-8?q?Remora.Discord=20part=204=20out=20of=20?= =?UTF-8?q?=E2=88=9E=20(well=20that=20was=20painful)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Octol1ttle --- Boyfriend.cs | 10 +++- EventResponders.cs | 125 +++++++++++++++++++++++++++++++++++---- Extensions.cs | 21 +++++++ InteractionResponders.cs | 28 +++++++++ Messages.Designer.cs | 24 ++++++++ Messages.resx | 12 ++++ Messages.ru.resx | 12 ++++ Messages.tt-ru.resx | 12 ++++ 8 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 InteractionResponders.cs diff --git a/Boyfriend.cs b/Boyfriend.cs index 3ac5a18..f36889e 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -7,9 +7,11 @@ using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; using Remora.Discord.Caching.Extensions; using Remora.Discord.Caching.Services; +using Remora.Discord.Commands.Extensions; using Remora.Discord.Gateway; using Remora.Discord.Gateway.Extensions; using Remora.Discord.Hosting.Extensions; +using Remora.Discord.Interactivity.Extensions; using Remora.Rest.Core; namespace Boyfriend; @@ -62,7 +64,13 @@ public class Boyfriend { services.AddTransient(); services.Configure( - options => options.Intents |= GatewayIntents.MessageContents | GatewayIntents.GuildMembers); + options => options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildScheduledEvents); + + services.AddDiscordCommands(); + services.AddInteractivity(); + services.AddInteractionGroup(); } ).ConfigureLogging( c => c.AddConsole() diff --git a/EventResponders.cs b/EventResponders.cs index fdeeb9b..1518520 100644 --- a/EventResponders.cs +++ b/EventResponders.cs @@ -5,11 +5,14 @@ using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Discord.Caching; using Remora.Discord.Caching.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Discord.Gateway.Responders; +using Remora.Discord.Interactivity; +using Remora.Rest.Core; using Remora.Results; // ReSharper disable UnusedType.Global @@ -50,11 +53,10 @@ public class GuildCreateResponder : IResponder { .WithCurrentTimestamp() .WithColour(Color.Aqua) .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); return (Result)await _channelApi.CreateMessageAsync( - channel, embeds: new[] { built }!, ct: ct); + channel, embeds: new[] { built }, ct: ct); } } @@ -62,12 +64,15 @@ public class MessageDeletedResponder : IResponder { private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestUserAPI _userApi; public MessageDeletedResponder( - IDiscordRestAuditLogAPI auditLogApi, CacheService cacheService, IDiscordRestChannelAPI channelApi) { + IDiscordRestAuditLogAPI auditLogApi, CacheService cacheService, IDiscordRestChannelAPI channelApi, + IDiscordRestUserAPI userApi) { _auditLogApi = auditLogApi; _cacheService = cacheService; _channelApi = channelApi; + _userApi = userApi; } public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { @@ -92,12 +97,12 @@ public class MessageDeletedResponder : IResponder { var user = message.Author; if (options.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - var userResult = await _cacheService.TryGetValueAsync( - new KeyHelpers.UserCacheKey(auditLog.UserID!.Value), ct); + var userResult = await auditLog.UserID!.Value.TryGetUserAsync(_cacheService, _userApi, ct); if (!userResult.IsDefined(out user)) return Result.FromError(userResult); } Messages.Culture = guildId.GetGuildCulture(); + var embed = new EmbedBuilder() .WithSmallTitle( message.Author, @@ -127,23 +132,32 @@ public class MessageEditedResponder : IResponder { } public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + return Result.FromSuccess(); + if (!gatewayEvent.Content.IsDefined(out var newContent)) + return Result.FromSuccess(); + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); if (!gatewayEvent.ID.IsDefined(out var messageId)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); - if (!gatewayEvent.Content.IsDefined(out var newContent)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.Content))); if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EditedTimestamp))); + var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); var messageResult = await _cacheService.TryGetValueAsync( - new KeyHelpers.MessageCacheKey(channelId, messageId), ct); + cacheKey, ct); if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); if (string.IsNullOrWhiteSpace(message.Content) || string.IsNullOrWhiteSpace(newContent) || message.Content == newContent) return Result.FromSuccess(); + await _cacheService.EvictAsync(cacheKey, ct); + var newMessageResult = await _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + if (!newMessageResult.IsDefined(out var newMessage)) return Result.FromError(newMessageResult); + // No need to await the recache since we don't depend on it + _ = _cacheService.CacheAsync(cacheKey, newMessage, ct); + var logChannelResult = guildId.GetConfigChannel("PrivateFeedbackChannel"); if (!logChannelResult.IsDefined(out var logChannel)) return Result.FromSuccess(); @@ -154,12 +168,12 @@ public class MessageEditedResponder : IResponder { var diff = new SideBySideDiffBuilder(Differ.Instance).BuildDiffModel(message.Content, newContent, true, true); Messages.Culture = guildId.GetGuildCulture(); + var embed = new EmbedBuilder() .WithSmallTitle( message.Author, - string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), - $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") - .WithDescription($"{Mention.Channel(message.ChannelID)}\n{diff.AsMarkdown()}") + string.Format(Messages.CachedMessageEdited, message.Author.GetTag())) + .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") .WithUserFooter(currentUser) .WithTimestamp(timestamp.Value) .WithColour(Color.Gold) @@ -210,3 +224,90 @@ public class GuildMemberAddResponder : IResponder { channel, embeds: new[] { built }, allowedMentions: Boyfriend.NoMentions, ct: ct); } } + +public class GuildScheduledEventCreateResponder : IResponder { + private readonly CacheService _cacheService; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestUserAPI _userApi; + + public GuildScheduledEventCreateResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, IDiscordRestUserAPI userApi) { + _cacheService = cacheService; + _channelApi = channelApi; + _userApi = userApi; + } + + public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) { + var channelResult = gatewayEvent.GuildID.GetConfigChannel("EventNotificationChannel"); + if (!channelResult.IsDefined(out var channel)) return Result.FromSuccess(); + + var currentUserResult = await _cacheService.TryGetValueAsync( + new KeyHelpers.CurrentUserCacheKey(), ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + if (!gatewayEvent.CreatorID.IsDefined(out var creatorId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.CreatorID))); + var creatorResult = await creatorId.Value.TryGetUserAsync(_cacheService, _userApi, ct); + if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult); + + Messages.Culture = gatewayEvent.GuildID.GetGuildCulture(); + + string embedDescription; + var eventDescription = gatewayEvent.Description is { HasValue: true, Value: not null } + ? gatewayEvent.Description.Value + : string.Empty; + switch (gatewayEvent.EntityType) { + case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: + if (!gatewayEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.LocalEventCreatedDescription, + Markdown.Timestamp(gatewayEvent.ScheduledStartTime), + Mention.Channel(channelId) + ))}"; + break; + case GuildScheduledEventEntityType.External: + if (!gatewayEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EntityMetadata))); + if (!gatewayEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.ExternalEventCreatedDescription, + Markdown.Timestamp(gatewayEvent.ScheduledStartTime), + Markdown.Timestamp(endTime), + Markdown.InlineCode(location) + ))}"; + break; + default: + return Result.FromError(new ArgumentOutOfRangeError(nameof(gatewayEvent.EntityType))); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(creator, string.Format(Messages.EventCreatedTitle, creator.GetTag())) + .WithTitle(gatewayEvent.Name) + .WithDescription(embedDescription) + .WithEventCover(gatewayEvent.ID, gatewayEvent.Image) + .WithUserFooter(currentUser) + .WithCurrentTimestamp() + .WithColour(Color.Gray) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + var button = new ButtonComponent( + ButtonComponentStyle.Primary, + Messages.EventDetailsButton, + new PartialEmoji(Name: "📋"), + CustomIDHelpers.CreateButtonIDWithState( + "scheduled-event-details", $"{gatewayEvent.GuildID}:{gatewayEvent.ID}") + ); + + return (Result)await _channelApi.CreateMessageAsync( + channel, embeds: new[] { built }, components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + } +} diff --git a/Extensions.cs b/Extensions.cs index 7c760e9..cc402df 100644 --- a/Extensions.cs +++ b/Extensions.cs @@ -4,7 +4,10 @@ using DiffPlex.DiffBuilder.Model; using Microsoft.Extensions.Configuration; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; +using Remora.Discord.Caching; +using Remora.Discord.Caching.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; @@ -41,6 +44,16 @@ public static class Extensions { return value is not null ? CultureInfoCache[value] : CultureInfoCache["en"]; } + public static async Task> TryGetUserAsync( + this Snowflake userId, CacheService cacheService, IDiscordRestUserAPI userApi, CancellationToken ct) { + var cachedUserResult = await cacheService.TryGetValueAsync( + new KeyHelpers.UserCacheKey(userId), ct); + + if (cachedUserResult.IsDefined(out var cachedUser)) return Result.FromSuccess(cachedUser); + + return await userApi.GetUserAsync(userId, ct); + } + public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) { var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrl = avatarUrlResult.IsSuccess @@ -81,6 +94,14 @@ public static class Extensions { return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); } + 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; + } + public static string SanitizeForBlockCode(this string s) { return s.Replace("```", "​`​`​`​"); } diff --git a/InteractionResponders.cs b/InteractionResponders.cs new file mode 100644 index 0000000..e554668 --- /dev/null +++ b/InteractionResponders.cs @@ -0,0 +1,28 @@ +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Feedback.Messages; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Interactivity; +using Remora.Results; + +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global + +namespace Boyfriend; + +public class InteractionResponders : InteractionGroup { + private readonly FeedbackService _feedbackService; + + public InteractionResponders(FeedbackService feedbackService) { + _feedbackService = feedbackService; + } + + [Button("scheduled-event-details")] + public async Task OnStatefulButtonClicked(string? state = null) { + if (state is null) return Result.FromError(new ArgumentNullError(nameof(state))); + + var idArray = state.Split(':'); + return (Result)await _feedbackService.SendContextualAsync( + $"https://discord.com/events/{idArray[0]}/{idArray[1]}", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + } +} diff --git a/Messages.Designer.cs b/Messages.Designer.cs index b6dd8dc..b018355 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -764,5 +764,29 @@ namespace Boyfriend { return ResourceManager.GetString("IssuedBy", resourceCulture); } } + + internal static string EventCreatedTitle { + get { + return ResourceManager.GetString("EventCreatedTitle", resourceCulture); + } + } + + internal static string LocalEventCreatedDescription { + get { + return ResourceManager.GetString("LocalEventCreatedDescription", resourceCulture); + } + } + + internal static string ExternalEventCreatedDescription { + get { + return ResourceManager.GetString("ExternalEventCreatedDescription", resourceCulture); + } + } + + internal static string EventDetailsButton { + get { + return ResourceManager.GetString("EventDetailsButton", resourceCulture); + } + } } } diff --git a/Messages.resx b/Messages.resx index 1d41ed5..a666d6e 100644 --- a/Messages.resx +++ b/Messages.resx @@ -481,4 +481,16 @@ Issued by + + {0} has created a new event: + + + The event will start at {0} in {1} + + + The event will start at {0} until {1} in {2} + + + Event details + diff --git a/Messages.ru.resx b/Messages.ru.resx index 64575a7..281b4a0 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -481,4 +481,16 @@ Ответственный + + {0} создаёт новое событие: + + + Событие пройдёт в {0} на {1} + + + Событие пройдёт с {0} до {1} на {2} + + + Подробнее о событии + diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index 38c581f..a454020 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -481,4 +481,16 @@ ответственный + + {0} создает новое событие: + + + движуха произойдет в {0} на {1} + + + движуха будет происходить с {0} до {1} на {2} + + + побольше о движухе +