mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-19 16:33:36 +03:00
Split responders into separate class files
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
fe4793f5f9
commit
6017a46f38
30 changed files with 434 additions and 393 deletions
|
@ -21,6 +21,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="DiffPlex" Version="1.7.1"/>
|
||||
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1"/>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1"/>
|
||||
<PackageReference Include="Remora.Discord" Version="2023.3.0"/>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -64,15 +64,19 @@ public class Boyfriend {
|
|||
});
|
||||
|
||||
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
|
||||
// Init
|
||||
.AddDiscordCaching()
|
||||
.AddDiscordCommands(true)
|
||||
.AddInteractivity()
|
||||
// Slash command event handlers
|
||||
.AddPreparationErrorEvent<ErrorLoggingPreparationErrorEvent>()
|
||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||
.AddInteractivity()
|
||||
.AddInteractionGroup<InteractionResponders>()
|
||||
// Services
|
||||
.AddSingleton<GuildDataService>()
|
||||
.AddSingleton<UtilityService>()
|
||||
.AddHostedService<GuildUpdateService>()
|
||||
// Slash commands
|
||||
.AddCommandTree()
|
||||
.WithCommandGroup<AboutCommandGroup>()
|
||||
.WithCommandGroup<BanCommandGroup>()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -12,9 +11,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
@ -16,14 +16,12 @@ using Remora.Discord.Extensions.Formatting;
|
|||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the command to clear messages in a channel: /clear.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ClearCommandGroup : CommandGroup {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly ICommandContext _context;
|
||||
|
@ -54,6 +52,7 @@ public class ClearCommandGroup : CommandGroup {
|
|||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.ManageMessages)]
|
||||
[Description("Remove multiple messages")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ClearMessagesAsync(
|
||||
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
|
||||
int amount) {
|
||||
|
|
|
@ -3,8 +3,6 @@ using Remora.Discord.Commands.Contexts;
|
|||
using Remora.Discord.Commands.Services;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.ComponentModel;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -13,9 +12,6 @@ using Remora.Discord.Commands.Feedback.Services;
|
|||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.ComponentModel;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Gateway;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.ComponentModel;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
using System.Text;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.Data.Options;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -13,9 +12,6 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend.Commands;
|
||||
|
||||
/// <summary>
|
||||
|
@ -77,8 +73,7 @@ public class SettingsCommandGroup : CommandGroup {
|
|||
foreach (var option in AllOptions) {
|
||||
builder.Append(Markdown.InlineCode(option.Name))
|
||||
.Append(": ");
|
||||
var something = option.GetAsObject(cfg);
|
||||
builder.AppendLine(Markdown.InlineCode(something.ToString()!));
|
||||
builder.AppendLine(option.Display(cfg));
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
|
||||
|
@ -131,7 +126,7 @@ public class SettingsCommandGroup : CommandGroup {
|
|||
|
||||
builder.Append(Markdown.InlineCode(option.Name))
|
||||
.Append($" {Messages.SettingIsNow} ")
|
||||
.Append(Markdown.InlineCode(option.GetAsObject(cfg).ToString()!));
|
||||
.Append(Markdown.InlineCode(option.Display(cfg)));
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
|
||||
.WithDescription(builder.ToString())
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using Boyfriend.Data.Options;
|
||||
using Boyfriend.locale;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
||||
namespace Boyfriend.Data;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Boyfriend.locale;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Data.Options;
|
||||
|
@ -7,6 +6,10 @@ namespace Boyfriend.Data.Options;
|
|||
public class BoolOption : Option<bool> {
|
||||
public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { }
|
||||
|
||||
public override string Display(JsonNode settings) {
|
||||
return Get(settings) ? Messages.Yes : Messages.No;
|
||||
}
|
||||
|
||||
public override Result Set(JsonNode settings, string from) {
|
||||
if (!TryParseBool(from, out var value))
|
||||
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
|
||||
|
|
|
@ -5,6 +5,6 @@ namespace Boyfriend.Data.Options;
|
|||
|
||||
public interface IOption {
|
||||
string Name { get; }
|
||||
object GetAsObject(JsonNode settings);
|
||||
Result Set(JsonNode settings, string from);
|
||||
string Display(JsonNode settings);
|
||||
Result Set(JsonNode settings, string from);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Boyfriend.locale;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Data.Options;
|
||||
|
@ -15,6 +15,10 @@ public class LanguageOption : Option<CultureInfo> {
|
|||
|
||||
public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { }
|
||||
|
||||
public override string Display(JsonNode settings) {
|
||||
return Markdown.InlineCode(settings[Name]?.GetValue<string>() ?? "en");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CultureInfo Get(JsonNode settings) {
|
||||
var property = settings[Name];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Data.Options;
|
||||
|
@ -7,7 +8,8 @@ namespace Boyfriend.Data.Options;
|
|||
/// Represents an per-guild option.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the option.</typeparam>
|
||||
public class Option<T> : IOption {
|
||||
public class Option<T> : IOption
|
||||
where T : notnull {
|
||||
internal readonly T DefaultValue;
|
||||
|
||||
public Option(string name, T defaultValue) {
|
||||
|
@ -17,8 +19,8 @@ public class Option<T> : IOption {
|
|||
|
||||
public string Name { get; }
|
||||
|
||||
public object GetAsObject(JsonNode settings) {
|
||||
return Get(settings)!;
|
||||
public virtual string Display(JsonNode settings) {
|
||||
return Markdown.InlineCode(Get(settings).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Boyfriend.locale;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
|
@ -8,6 +8,10 @@ namespace Boyfriend.Data.Options;
|
|||
public class SnowflakeOption : Option<Snowflake> {
|
||||
public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { }
|
||||
|
||||
public override string Display(JsonNode settings) {
|
||||
return Name.EndsWith("Channel") ? Mention.Channel(Get(settings)) : Mention.Role(Get(settings));
|
||||
}
|
||||
|
||||
public override Snowflake Get(JsonNode settings) {
|
||||
var property = settings[Name];
|
||||
return property != null ? property.GetValue<ulong>().ToSnowflake() : DefaultValue;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Boyfriend.locale;
|
||||
using Remora.Commands.Parsers;
|
||||
using Remora.Results;
|
||||
|
||||
|
|
|
@ -1,339 +0,0 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Boyfriend.Services;
|
||||
using DiffPlex.DiffBuilder;
|
||||
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.Caching;
|
||||
using Remora.Discord.Caching.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable UnusedType.Global
|
||||
|
||||
namespace Boyfriend;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a <see cref="Messages.Ready" /> message to a guild that has just initialized if that guild
|
||||
/// has <see cref="GuildSettings.ReceiveStartupMessages" /> enabled
|
||||
/// </summary>
|
||||
public class GuildCreateResponder : IResponder<IGuildCreate> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly ILogger<GuildCreateResponder> _logger;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public GuildCreateResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger<GuildCreateResponder> logger,
|
||||
IDiscordRestUserAPI userApi) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_logger = logger;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild isn't IAvailableGuild
|
||||
|
||||
var guild = gatewayEvent.Guild.AsT0;
|
||||
_logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
|
||||
|
||||
var cfg = await _dataService.GetSettings(guild.ID, ct);
|
||||
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||
return Result.FromSuccess();
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
return Result.FromSuccess();
|
||||
|
||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var i = Random.Shared.Next(1, 4);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser)
|
||||
.WithTitle($"Beep{i}".Localized())
|
||||
.WithDescription(Messages.Ready)
|
||||
.WithCurrentTimestamp()
|
||||
.WithColour(ColorsList.Blue)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles logging the contents of a deleted message and the user who deleted the message
|
||||
/// to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
|
||||
/// </summary>
|
||||
public class MessageDeletedResponder : IResponder<IMessageDelete> {
|
||||
private readonly IDiscordRestAuditLogAPI _auditLogApi;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public MessageDeletedResponder(
|
||||
IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi,
|
||||
GuildDataService dataService, IDiscordRestUserAPI userApi) {
|
||||
_auditLogApi = auditLogApi;
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
|
||||
|
||||
var cfg = await _dataService.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess();
|
||||
|
||||
var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
|
||||
if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
|
||||
if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess();
|
||||
|
||||
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
|
||||
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
|
||||
if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult);
|
||||
|
||||
var auditLog = auditLogPage.AuditLogEntries.Single();
|
||||
if (!auditLog.Options.IsDefined(out var options))
|
||||
return Result.FromError(new ArgumentNullError(nameof(auditLog.Options)));
|
||||
|
||||
var user = message.Author;
|
||||
if (options.ChannelID == gatewayEvent.ChannelID
|
||||
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) {
|
||||
var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct);
|
||||
if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(
|
||||
string.Format(
|
||||
Messages.CachedMessageDeleted,
|
||||
message.Author.GetTag()), message.Author)
|
||||
.WithDescription(
|
||||
$"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}")
|
||||
.WithActionFooter(user)
|
||||
.WithTimestamp(message.Timestamp)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles logging the difference between an edited message's old and new content
|
||||
/// to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
|
||||
/// </summary>
|
||||
public class MessageEditedResponder : IResponder<IMessageUpdate> {
|
||||
private readonly CacheService _cacheService;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public MessageEditedResponder(
|
||||
CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
|
||||
IDiscordRestUserAPI userApi) {
|
||||
_cacheService = cacheService;
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId))
|
||||
return Result.FromSuccess();
|
||||
var cfg = await _dataService.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
return Result.FromSuccess();
|
||||
if (!gatewayEvent.Content.IsDefined(out var newContent))
|
||||
return Result.FromSuccess();
|
||||
if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
|
||||
return Result.FromSuccess(); // The message wasn't actually edited
|
||||
|
||||
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)));
|
||||
|
||||
var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
|
||||
var messageResult = await _cacheService.TryGetValueAsync<IMessage>(
|
||||
cacheKey, ct);
|
||||
if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
|
||||
if (message.Content == newContent) return Result.FromSuccess();
|
||||
|
||||
// Custom event responders are called earlier than responders responsible for message caching
|
||||
// This means that subsequent edit logs may contain the wrong content
|
||||
// We can work around this by evicting the message from the cache
|
||||
await _cacheService.EvictAsync<IMessage>(cacheKey, ct);
|
||||
// However, since we evicted the message, subsequent edits won't have a cached instance to work with
|
||||
// Getting the message will put it back in the cache, resolving all issues
|
||||
// We don't need to await this since the result is not needed
|
||||
// NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages
|
||||
// NOTE: Awaiting this might not even solve this if the same responder is called asynchronously
|
||||
_ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
|
||||
|
||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
||||
|
||||
var diff = InlineDiffBuilder.Diff(message.Content, newContent);
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
|
||||
.WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}")
|
||||
.WithUserFooter(currentUser)
|
||||
.WithTimestamp(timestamp.Value)
|
||||
.WithColour(ColorsList.Yellow)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a guild's <see cref="GuildSettings.WelcomeMessage" /> if one is set.
|
||||
/// If <see cref="GuildSettings.ReturnRolesOnRejoin"/> is enabled, roles will be returned.
|
||||
/// </summary>
|
||||
/// <seealso cref="GuildSettings.WelcomeMessage" />
|
||||
public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
|
||||
public GuildMemberAddResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_guildApi = guildApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.User.IsDefined(out var user))
|
||||
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User)));
|
||||
var data = await _dataService.GetData(gatewayEvent.GuildID, ct);
|
||||
var cfg = data.Settings;
|
||||
if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||
|| GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
|
||||
return Result.FromSuccess();
|
||||
if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) {
|
||||
var result = await _guildApi.ModifyGuildMemberAsync(
|
||||
gatewayEvent.GuildID, user.ID,
|
||||
roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct);
|
||||
if (!result.IsSuccess) return Result.FromError(result.Error);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset"
|
||||
? Messages.DefaultWelcomeMessage
|
||||
: GuildSettings.WelcomeMessage.Get(cfg);
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
|
||||
if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user)
|
||||
.WithGuildFooter(guild)
|
||||
.WithTimestamp(gatewayEvent.JoinedAt)
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a notification when a scheduled event has been cancelled
|
||||
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
|
||||
/// </summary>
|
||||
public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEventDelete> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
|
||||
public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) {
|
||||
var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct);
|
||||
guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value);
|
||||
|
||||
if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty())
|
||||
return Result.FromSuccess();
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name))
|
||||
.WithDescription(":(")
|
||||
.WithColour(ColorsList.Red)
|
||||
.WithCurrentTimestamp()
|
||||
.Build();
|
||||
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles updating <see cref="MemberData.Roles" /> when a guild member is updated.
|
||||
/// </summary>
|
||||
public class GuildMemberUpdateResponder : IResponder<IGuildMemberUpdate> {
|
||||
private readonly GuildDataService _dataService;
|
||||
|
||||
public GuildMemberUpdateResponder(GuildDataService dataService) {
|
||||
_dataService = dataService;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) {
|
||||
var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct);
|
||||
memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value);
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending replies to easter egg messages.
|
||||
/// </summary>
|
||||
public class MessageCreateResponder : IResponder<IMessageCreate> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
|
||||
public MessageCreateResponder(IDiscordRestChannelAPI channelApi) {
|
||||
_channelApi = channelApi;
|
||||
}
|
||||
|
||||
public Task<Result> RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) {
|
||||
_ = _channelApi.CreateMessageAsync(
|
||||
gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch {
|
||||
"whoami" => "`nobody`",
|
||||
"сука !!" => "`root`",
|
||||
"воооо" => "`removing /...`",
|
||||
"пон" =>
|
||||
"https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg",
|
||||
"++++" => "#",
|
||||
"осу" => "https://github.com/ppy/osu",
|
||||
_ => default(Optional<string>)
|
||||
});
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Boyfriend.locale;
|
||||
using DiffPlex.DiffBuilder.Model;
|
||||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
|
|
@ -4,9 +4,6 @@ using Remora.Discord.Commands.Feedback.Services;
|
|||
using Remora.Discord.Interactivity;
|
||||
using Remora.Results;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Boyfriend;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Boyfriend.locale {
|
||||
namespace Boyfriend {
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
63
src/Responders/GuildLoadedResponder.cs
Normal file
63
src/Responders/GuildLoadedResponder.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.API.Gateway.Events;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a <see cref="Ready" /> message to a guild that has just initialized if that guild
|
||||
/// has <see cref="GuildSettings.ReceiveStartupMessages" /> enabled
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class GuildLoadedResponder : IResponder<IGuildCreate> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly ILogger<GuildLoadedResponder> _logger;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public GuildLoadedResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger<GuildLoadedResponder> logger,
|
||||
IDiscordRestUserAPI userApi) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_logger = logger;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild is not IAvailableGuild
|
||||
|
||||
var guild = gatewayEvent.Guild.AsT0;
|
||||
_logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
|
||||
|
||||
var cfg = await _dataService.GetSettings(guild.ID, ct);
|
||||
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||
return Result.FromSuccess();
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
return Result.FromSuccess();
|
||||
|
||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var i = Random.Shared.Next(1, 4);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser)
|
||||
.WithTitle($"Beep{i}".Localized())
|
||||
.WithDescription(Messages.Ready)
|
||||
.WithCurrentTimestamp()
|
||||
.WithColour(ColorsList.Blue)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct);
|
||||
}
|
||||
}
|
65
src/Responders/GuildMemberJoinedResponder.cs
Normal file
65
src/Responders/GuildMemberJoinedResponder.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a guild's <see cref="GuildSettings.WelcomeMessage" /> if one is set.
|
||||
/// If <see cref="GuildSettings.ReturnRolesOnRejoin" /> is enabled, roles will be returned.
|
||||
/// </summary>
|
||||
/// <seealso cref="GuildSettings.WelcomeMessage" />
|
||||
[UsedImplicitly]
|
||||
public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
|
||||
public GuildMemberJoinedResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_guildApi = guildApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.User.IsDefined(out var user))
|
||||
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User)));
|
||||
var data = await _dataService.GetData(gatewayEvent.GuildID, ct);
|
||||
var cfg = data.Settings;
|
||||
if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||
|| GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
|
||||
return Result.FromSuccess();
|
||||
if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) {
|
||||
var result = await _guildApi.ModifyGuildMemberAsync(
|
||||
gatewayEvent.GuildID, user.ID,
|
||||
roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct);
|
||||
if (!result.IsSuccess) return Result.FromError(result.Error);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset"
|
||||
? Messages.DefaultWelcomeMessage
|
||||
: GuildSettings.WelcomeMessage.Get(cfg);
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
|
||||
if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user)
|
||||
.WithGuildFooter(guild)
|
||||
.WithTimestamp(gatewayEvent.JoinedAt)
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
26
src/Responders/GuildMemberRolesUpdatedResponder.cs
Normal file
26
src/Responders/GuildMemberRolesUpdatedResponder.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles updating <see cref="MemberData.Roles" /> when a guild member is updated.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class GuildMemberUpdateResponder : IResponder<IGuildMemberUpdate> {
|
||||
private readonly GuildDataService _dataService;
|
||||
|
||||
public GuildMemberUpdateResponder(GuildDataService dataService) {
|
||||
_dataService = dataService;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) {
|
||||
var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct);
|
||||
memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value);
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
}
|
78
src/Responders/MessageDeletedResponder.cs
Normal file
78
src/Responders/MessageDeletedResponder.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles logging the contents of a deleted message and the user who deleted the message
|
||||
/// to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class MessageDeletedResponder : IResponder<IMessageDelete> {
|
||||
private readonly IDiscordRestAuditLogAPI _auditLogApi;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public MessageDeletedResponder(
|
||||
IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi,
|
||||
GuildDataService dataService, IDiscordRestUserAPI userApi) {
|
||||
_auditLogApi = auditLogApi;
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess();
|
||||
|
||||
var cfg = await _dataService.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess();
|
||||
|
||||
var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
|
||||
if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
|
||||
if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess();
|
||||
|
||||
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
|
||||
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
|
||||
if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult);
|
||||
|
||||
var auditLog = auditLogPage.AuditLogEntries.Single();
|
||||
if (!auditLog.Options.IsDefined(out var options))
|
||||
return Result.FromError(new ArgumentNullError(nameof(auditLog.Options)));
|
||||
|
||||
var user = message.Author;
|
||||
if (options.ChannelID == gatewayEvent.ChannelID
|
||||
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) {
|
||||
var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct);
|
||||
if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(
|
||||
string.Format(
|
||||
Messages.CachedMessageDeleted,
|
||||
message.Author.GetTag()), message.Author)
|
||||
.WithDescription(
|
||||
$"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}")
|
||||
.WithActionFooter(user)
|
||||
.WithTimestamp(message.Timestamp)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
89
src/Responders/MessageEditedResponder.cs
Normal file
89
src/Responders/MessageEditedResponder.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using DiffPlex.DiffBuilder;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Caching;
|
||||
using Remora.Discord.Caching.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles logging the difference between an edited message's old and new content
|
||||
/// to a guild's <see cref="GuildSettings.PrivateFeedbackChannel" /> if one is set.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class MessageEditedResponder : IResponder<IMessageUpdate> {
|
||||
private readonly CacheService _cacheService;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public MessageEditedResponder(
|
||||
CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
|
||||
IDiscordRestUserAPI userApi) {
|
||||
_cacheService = cacheService;
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) {
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId))
|
||||
return Result.FromSuccess();
|
||||
var cfg = await _dataService.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
return Result.FromSuccess();
|
||||
if (!gatewayEvent.Content.IsDefined(out var newContent))
|
||||
return Result.FromSuccess();
|
||||
if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
|
||||
return Result.FromSuccess(); // The message wasn't actually edited
|
||||
|
||||
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)));
|
||||
|
||||
var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
|
||||
var messageResult = await _cacheService.TryGetValueAsync<IMessage>(
|
||||
cacheKey, ct);
|
||||
if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult);
|
||||
if (message.Content == newContent) return Result.FromSuccess();
|
||||
|
||||
// Custom event responders are called earlier than responders responsible for message caching
|
||||
// This means that subsequent edit logs may contain the wrong content
|
||||
// We can work around this by evicting the message from the cache
|
||||
await _cacheService.EvictAsync<IMessage>(cacheKey, ct);
|
||||
// However, since we evicted the message, subsequent edits won't have a cached instance to work with
|
||||
// Getting the message will put it back in the cache, resolving all issues
|
||||
// We don't need to await this since the result is not needed
|
||||
// NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages
|
||||
// NOTE: Awaiting this might not even solve this if the same responder is called asynchronously
|
||||
_ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
|
||||
|
||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
||||
|
||||
var diff = InlineDiffBuilder.Diff(message.Content, newContent);
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
|
||||
.WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}")
|
||||
.WithUserFooter(currentUser)
|
||||
.WithTimestamp(timestamp.Value)
|
||||
.WithColour(ColorsList.Yellow)
|
||||
.Build();
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built },
|
||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
35
src/Responders/MessageReceivedResponder.cs
Normal file
35
src/Responders/MessageReceivedResponder.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending replies to easter egg messages.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class MessageCreateResponder : IResponder<IMessageCreate> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
|
||||
public MessageCreateResponder(IDiscordRestChannelAPI channelApi) {
|
||||
_channelApi = channelApi;
|
||||
}
|
||||
|
||||
public Task<Result> RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) {
|
||||
_ = _channelApi.CreateMessageAsync(
|
||||
gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch {
|
||||
"whoami" => "`nobody`",
|
||||
"сука !!" => "`root`",
|
||||
"воооо" => "`removing /...`",
|
||||
"пон" =>
|
||||
"https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg",
|
||||
"++++" => "#",
|
||||
"осу" => "https://github.com/ppy/osu",
|
||||
_ => default(Optional<string>)
|
||||
});
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
}
|
||||
}
|
45
src/Responders/ScheduledEventCancelledResponder.cs
Normal file
45
src/Responders/ScheduledEventCancelledResponder.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Boyfriend.Data;
|
||||
using Boyfriend.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Boyfriend.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a notification when a scheduled event has been cancelled
|
||||
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEventDelete> {
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly GuildDataService _dataService;
|
||||
|
||||
public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) {
|
||||
var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct);
|
||||
guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value);
|
||||
|
||||
if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty())
|
||||
return Result.FromSuccess();
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name))
|
||||
.WithDescription(":(")
|
||||
.WithColour(ColorsList.Red)
|
||||
.WithCurrentTimestamp()
|
||||
.Build();
|
||||
|
||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Boyfriend.Data;
|
||||
using Boyfriend.locale;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
|
Loading…
Add table
Reference in a new issue