mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-20 00:43: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>
|
<ItemGroup>
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.1"/>
|
<PackageReference Include="DiffPlex" Version="1.7.1"/>
|
||||||
<PackageReference Include="Humanizer.Core.ru" Version="2.14.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="Microsoft.Extensions.Hosting" Version="7.0.1"/>
|
||||||
<PackageReference Include="Remora.Discord" Version="2023.3.0"/>
|
<PackageReference Include="Remora.Discord" Version="2023.3.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -64,15 +64,19 @@ public class Boyfriend {
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
|
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
|
||||||
|
// Init
|
||||||
.AddDiscordCaching()
|
.AddDiscordCaching()
|
||||||
.AddDiscordCommands(true)
|
.AddDiscordCommands(true)
|
||||||
|
.AddInteractivity()
|
||||||
|
// Slash command event handlers
|
||||||
.AddPreparationErrorEvent<ErrorLoggingPreparationErrorEvent>()
|
.AddPreparationErrorEvent<ErrorLoggingPreparationErrorEvent>()
|
||||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||||
.AddInteractivity()
|
|
||||||
.AddInteractionGroup<InteractionResponders>()
|
.AddInteractionGroup<InteractionResponders>()
|
||||||
|
// Services
|
||||||
.AddSingleton<GuildDataService>()
|
.AddSingleton<GuildDataService>()
|
||||||
.AddSingleton<UtilityService>()
|
.AddSingleton<UtilityService>()
|
||||||
.AddHostedService<GuildUpdateService>()
|
.AddHostedService<GuildUpdateService>()
|
||||||
|
// Slash commands
|
||||||
.AddCommandTree()
|
.AddCommandTree()
|
||||||
.WithCommandGroup<AboutCommandGroup>()
|
.WithCommandGroup<AboutCommandGroup>()
|
||||||
.WithCommandGroup<BanCommandGroup>()
|
.WithCommandGroup<BanCommandGroup>()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -12,9 +11,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Extensions.Formatting;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Extensions.Formatting;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
@ -16,14 +16,12 @@ using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the command to clear messages in a channel: /clear.
|
/// Handles the command to clear messages in a channel: /clear.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class ClearCommandGroup : CommandGroup {
|
public class ClearCommandGroup : CommandGroup {
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
|
@ -54,6 +52,7 @@ public class ClearCommandGroup : CommandGroup {
|
||||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.ManageMessages)]
|
[RequireBotDiscordPermissions(DiscordPermission.ManageMessages)]
|
||||||
[Description("Remove multiple messages")]
|
[Description("Remove multiple messages")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> ClearMessagesAsync(
|
public async Task<Result> ClearMessagesAsync(
|
||||||
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
|
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
|
||||||
int amount) {
|
int amount) {
|
||||||
|
|
|
@ -3,8 +3,6 @@ using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Services;
|
using Remora.Discord.Commands.Services;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -13,9 +12,6 @@ using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Extensions.Formatting;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Gateway;
|
using Remora.Discord.Gateway;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Extensions.Formatting;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.Data.Options;
|
using Boyfriend.Data.Options;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Boyfriend.Services;
|
using Boyfriend.Services;
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
|
@ -13,9 +12,6 @@ using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Extensions.Formatting;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend.Commands;
|
namespace Boyfriend.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -77,8 +73,7 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
foreach (var option in AllOptions) {
|
foreach (var option in AllOptions) {
|
||||||
builder.Append(Markdown.InlineCode(option.Name))
|
builder.Append(Markdown.InlineCode(option.Name))
|
||||||
.Append(": ");
|
.Append(": ");
|
||||||
var something = option.GetAsObject(cfg);
|
builder.AppendLine(option.Display(cfg));
|
||||||
builder.AppendLine(Markdown.InlineCode(something.ToString()!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
|
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
|
||||||
|
@ -131,7 +126,7 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
|
|
||||||
builder.Append(Markdown.InlineCode(option.Name))
|
builder.Append(Markdown.InlineCode(option.Name))
|
||||||
.Append($" {Messages.SettingIsNow} ")
|
.Append($" {Messages.SettingIsNow} ")
|
||||||
.Append(Markdown.InlineCode(option.GetAsObject(cfg).ToString()!));
|
.Append(Markdown.InlineCode(option.Display(cfg)));
|
||||||
|
|
||||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
|
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
|
||||||
.WithDescription(builder.ToString())
|
.WithDescription(builder.ToString())
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Boyfriend.Data.Options;
|
using Boyfriend.Data.Options;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
||||||
namespace Boyfriend.Data;
|
namespace Boyfriend.Data;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
namespace Boyfriend.Data.Options;
|
namespace Boyfriend.Data.Options;
|
||||||
|
@ -7,6 +6,10 @@ namespace Boyfriend.Data.Options;
|
||||||
public class BoolOption : Option<bool> {
|
public class BoolOption : Option<bool> {
|
||||||
public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { }
|
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) {
|
public override Result Set(JsonNode settings, string from) {
|
||||||
if (!TryParseBool(from, out var value))
|
if (!TryParseBool(from, out var value))
|
||||||
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
|
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
|
||||||
|
|
|
@ -5,6 +5,6 @@ namespace Boyfriend.Data.Options;
|
||||||
|
|
||||||
public interface IOption {
|
public interface IOption {
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
object GetAsObject(JsonNode settings);
|
string Display(JsonNode settings);
|
||||||
Result Set(JsonNode settings, string from);
|
Result Set(JsonNode settings, string from);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.locale;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
namespace Boyfriend.Data.Options;
|
namespace Boyfriend.Data.Options;
|
||||||
|
@ -15,6 +15,10 @@ public class LanguageOption : Option<CultureInfo> {
|
||||||
|
|
||||||
public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { }
|
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 />
|
/// <inheritdoc />
|
||||||
public override CultureInfo Get(JsonNode settings) {
|
public override CultureInfo Get(JsonNode settings) {
|
||||||
var property = settings[Name];
|
var property = settings[Name];
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
namespace Boyfriend.Data.Options;
|
namespace Boyfriend.Data.Options;
|
||||||
|
@ -7,7 +8,8 @@ namespace Boyfriend.Data.Options;
|
||||||
/// Represents an per-guild option.
|
/// Represents an per-guild option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the option.</typeparam>
|
/// <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;
|
internal readonly T DefaultValue;
|
||||||
|
|
||||||
public Option(string name, T defaultValue) {
|
public Option(string name, T defaultValue) {
|
||||||
|
@ -17,8 +19,8 @@ public class Option<T> : IOption {
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public object GetAsObject(JsonNode settings) {
|
public virtual string Display(JsonNode settings) {
|
||||||
return Get(settings)!;
|
return Markdown.InlineCode(Get(settings).ToString()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.locale;
|
using Remora.Discord.Extensions.Formatting;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
|
@ -8,6 +8,10 @@ namespace Boyfriend.Data.Options;
|
||||||
public class SnowflakeOption : Option<Snowflake> {
|
public class SnowflakeOption : Option<Snowflake> {
|
||||||
public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { }
|
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) {
|
public override Snowflake Get(JsonNode settings) {
|
||||||
var property = settings[Name];
|
var property = settings[Name];
|
||||||
return property != null ? property.GetValue<ulong>().ToSnowflake() : DefaultValue;
|
return property != null ? property.GetValue<ulong>().ToSnowflake() : DefaultValue;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Remora.Commands.Parsers;
|
using Remora.Commands.Parsers;
|
||||||
using Remora.Results;
|
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.Diagnostics.CodeAnalysis;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.locale;
|
|
||||||
using DiffPlex.DiffBuilder.Model;
|
using DiffPlex.DiffBuilder.Model;
|
||||||
using Remora.Discord.API;
|
using Remora.Discord.API;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
|
@ -4,9 +4,6 @@ using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Discord.Interactivity;
|
using Remora.Discord.Interactivity;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
// ReSharper disable ClassNeverInstantiated.Global
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Boyfriend;
|
namespace Boyfriend;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace Boyfriend.locale {
|
namespace Boyfriend {
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[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 System.Text.Json.Nodes;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Boyfriend.locale;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
Loading…
Add table
Reference in a new issue