forked from TeamInklings/Octobot
The Milestone Commit (#48)
mctaylors: - updated readme 7 times (and only adding new logo from /about) - [removed](aeeb3d4399
) bot footer from created event embed on the second try - [changed](4b9b91d9e4
) cdn from discord to upload.systems Octol1ttle: - Guild settings code has been overhauled. Instead of instances of a `GuildConfiguration` class being (de-)serialized when used with listing and setting options provided by reflection, there are now multiple `Option` classes responsible for the type of option they are storing. The classes support getting a value, validating and setting values with Results, and getting a user-friendly representation of these values. This makes use of polymorphism, providing clean and easier to use and refactor code. - Gateway event responders have been split into their own separate files, which should make it easier to find and modify responders when needed. - Warning suppressions regarding unused and never instantiated classes have been replaced by `[ImplicitUse]` annotations provided by `JetBrains.Annotations`. This avoids hiding real issues and provides a better way to suppress false warnings while being explicit. - It is no longer possible to execute some slash commands if they are run without the correct permissions - Dependencies are now more explicitly defined neroduckale: - Made easter eggs case-insensitive --------- Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com> Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com> Co-authored-by: Octol1ttle <l1ttleofficial@outlook.com> Co-authored-by: nrdk <neroduck@vk.com>
This commit is contained in:
parent
3eb17b96c5
commit
c6dd3727c3
39 changed files with 912 additions and 658 deletions
6
.github/workflows/resharper.yml
vendored
6
.github/workflows/resharper.yml
vendored
|
@ -4,10 +4,12 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
merge_group:
|
||||||
|
types: [checks_requested]
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
inspect-code:
|
inspect-code:
|
||||||
|
|
|
@ -21,8 +21,12 @@
|
||||||
<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.Caching" Version="36.0.0"/>
|
||||||
|
<PackageReference Include="Remora.Discord.Extensions" Version="5.3.1"/>
|
||||||
|
<PackageReference Include="Remora.Discord.Hosting" Version="6.0.6"/>
|
||||||
|
<PackageReference Include="Remora.Discord.Interactivity" Version="4.5.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="locale\Messages.resx">
|
<EmbeddedResource Update="locale\Messages.resx">
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
<picture>
|
<p align="center">
|
||||||
|
<img src="https://cdn.upload.systems/uploads/v40uV9K1.png" alt="Boyfriend logo" width="75%"/>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png">
|
</p>
|
||||||
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/95250141/206895340-3415d97d-91fd-4fb6-8c17-4e1bf340e1df.png">
|
|
||||||
|
|
||||||
<img alt="Boyfriend Logo" src="https://user-images.githubusercontent.com/95250141/206895339-ef5510c8-8b30-4887-b89c-5dc14a24b18a.png">
|
|
||||||
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend)
|
![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend)
|
||||||
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master)
|
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master)
|
||||||
|
|
BIN
docs/assets/boyfriend.png
Normal file
BIN
docs/assets/boyfriend.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 KiB |
|
@ -56,23 +56,28 @@ public class Boyfriend {
|
||||||
| GatewayIntents.GuildMembers
|
| GatewayIntents.GuildMembers
|
||||||
| GatewayIntents.GuildScheduledEvents);
|
| GatewayIntents.GuildScheduledEvents);
|
||||||
services.Configure<CacheSettings>(
|
services.Configure<CacheSettings>(
|
||||||
settings => {
|
cSettings => {
|
||||||
settings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
|
cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
|
||||||
settings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
|
cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
|
||||||
settings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7));
|
cSettings.SetAbsoluteExpiration<IMessage>(TimeSpan.FromDays(7));
|
||||||
settings.SetSlidingExpiration<IMessage>(TimeSpan.FromDays(7));
|
cSettings.SetSlidingExpiration<IMessage>(TimeSpan.FromDays(7));
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
|
services.AddTransient<IConfigurationBuilder, ConfigurationBuilder>()
|
||||||
|
// Init
|
||||||
.AddDiscordCaching()
|
.AddDiscordCaching()
|
||||||
.AddDiscordCommands(true)
|
.AddDiscordCommands(true)
|
||||||
.AddPreparationErrorEvent<ErrorLoggingPreparationErrorEvent>()
|
// Interactions
|
||||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
|
||||||
.AddInteractivity()
|
.AddInteractivity()
|
||||||
.AddInteractionGroup<InteractionResponders>()
|
.AddInteractionGroup<InteractionResponders>()
|
||||||
|
// Slash command event handlers
|
||||||
|
.AddPreparationErrorEvent<ErrorLoggingPreparationErrorEvent>()
|
||||||
|
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||||
|
// 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,6 +1,8 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Boyfriend.Data;
|
||||||
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.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
@ -10,14 +12,12 @@ 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>
|
||||||
/// Handles the command to show information about this bot: /about.
|
/// Handles the command to show information about this bot: /about.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class AboutCommandGroup : CommandGroup {
|
public class AboutCommandGroup : CommandGroup {
|
||||||
private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" };
|
private static readonly string[] Developers = { "Octol1ttle", "mctaylors", "neroduckale" };
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
|
@ -42,6 +42,7 @@ public class AboutCommandGroup : CommandGroup {
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("about")]
|
[Command("about")]
|
||||||
[Description("Shows Boyfriend's developers")]
|
[Description("Shows Boyfriend's developers")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> SendAboutBotAsync() {
|
public async Task<Result> SendAboutBotAsync() {
|
||||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||||
return Result.FromError(
|
return Result.FromError(
|
||||||
|
@ -51,8 +52,8 @@ public class AboutCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers));
|
var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers));
|
||||||
foreach (var dev in Developers)
|
foreach (var dev in Developers)
|
||||||
|
@ -65,8 +66,7 @@ public class AboutCommandGroup : CommandGroup {
|
||||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser)
|
var embed = new EmbedBuilder().WithSmallTitle(Messages.AboutBot, currentUser)
|
||||||
.WithDescription(builder.ToString())
|
.WithDescription(builder.ToString())
|
||||||
.WithColour(ColorsList.Cyan)
|
.WithColour(ColorsList.Cyan)
|
||||||
.WithImageUrl(
|
.WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png")
|
||||||
"https://media.discordapp.net/attachments/837385840946053181/1125009665592393738/boyfriend.png")
|
|
||||||
.Build();
|
.Build();
|
||||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Boyfriend.Data;
|
||||||
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;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
using Remora.Discord.API.Objects;
|
using Remora.Discord.API.Objects;
|
||||||
|
using Remora.Discord.Commands.Attributes;
|
||||||
using Remora.Discord.Commands.Conditions;
|
using Remora.Discord.Commands.Conditions;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
using Remora.Discord.Commands.Feedback.Services;
|
||||||
|
@ -13,14 +16,12 @@ 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>
|
||||||
/// Handles commands related to ban management: /ban and /unban.
|
/// Handles commands related to ban management: /ban and /unban.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class BanCommandGroup : CommandGroup {
|
public class BanCommandGroup : CommandGroup {
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
|
@ -58,10 +59,13 @@ public class BanCommandGroup : CommandGroup {
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <seealso cref="UnbanUserAsync" />
|
/// <seealso cref="UnbanUserAsync" />
|
||||||
[Command("ban", "бан")]
|
[Command("ban", "бан")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||||
[Description("Ban user")]
|
[Description("Ban user")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> BanUserAsync(
|
public async Task<Result> BanUserAsync(
|
||||||
[Description("User to ban")] IUser target,
|
[Description("User to ban")] IUser target,
|
||||||
[Description("Ban reason")] string reason,
|
[Description("Ban reason")] string reason,
|
||||||
|
@ -76,8 +80,8 @@ public class BanCommandGroup : CommandGroup {
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
||||||
var cfg = data.Configuration;
|
var cfg = data.Settings;
|
||||||
Messages.Culture = data.Culture;
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
||||||
if (existingBanResult.IsDefined()) {
|
if (existingBanResult.IsDefined()) {
|
||||||
|
@ -145,8 +149,10 @@ public class BanCommandGroup : CommandGroup {
|
||||||
string.Format(Messages.UserBanned, target.GetTag()), target)
|
string.Format(Messages.UserBanned, target.GetTag()), target)
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||||
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
|
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
|
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
|
||||||
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
||||||
string.Format(Messages.UserBanned, target.GetTag()), target)
|
string.Format(Messages.UserBanned, target.GetTag()), target)
|
||||||
.WithDescription(description)
|
.WithDescription(description)
|
||||||
|
@ -160,14 +166,14 @@ public class BanCommandGroup : CommandGroup {
|
||||||
|
|
||||||
var builtArray = new[] { logBuilt };
|
var builtArray = new[] { logBuilt };
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PublicFeedbackChannel != channelId.Value)
|
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
|
||||||
&& cfg.PrivateFeedbackChannel != channelId.Value)
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,10 +199,13 @@ public class BanCommandGroup : CommandGroup {
|
||||||
/// <seealso cref="BanUserAsync" />
|
/// <seealso cref="BanUserAsync" />
|
||||||
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
|
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
|
||||||
[Command("unban")]
|
[Command("unban")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||||
[Description("Unban user")]
|
[Description("Unban user")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> UnbanUserAsync(
|
public async Task<Result> UnbanUserAsync(
|
||||||
[Description("User to unban")] IUser target,
|
[Description("User to unban")] IUser target,
|
||||||
[Description("Unban reason")] string reason) {
|
[Description("Unban reason")] string reason) {
|
||||||
|
@ -209,8 +218,8 @@ public class BanCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
||||||
if (!existingBanResult.IsDefined()) {
|
if (!existingBanResult.IsDefined()) {
|
||||||
|
@ -238,8 +247,10 @@ public class BanCommandGroup : CommandGroup {
|
||||||
string.Format(Messages.UserUnbanned, target.GetTag()), target)
|
string.Format(Messages.UserUnbanned, target.GetTag()), target)
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||||
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
|
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
|
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
|
||||||
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
||||||
string.Format(Messages.UserUnbanned, target.GetTag()), target)
|
string.Format(Messages.UserUnbanned, target.GetTag()), target)
|
||||||
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
||||||
|
@ -254,14 +265,14 @@ public class BanCommandGroup : CommandGroup {
|
||||||
var builtArray = new[] { logBuilt };
|
var builtArray = new[] { logBuilt };
|
||||||
|
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PublicFeedbackChannel != channelId.Value)
|
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
|
||||||
&& cfg.PrivateFeedbackChannel != channelId.Value)
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Boyfriend.Data;
|
||||||
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;
|
||||||
|
@ -14,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;
|
||||||
|
@ -48,10 +48,13 @@ public class ClearCommandGroup : CommandGroup {
|
||||||
/// were cleared and vice-versa.
|
/// were cleared and vice-versa.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("clear", "очистить")]
|
[Command("clear", "очистить")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[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) {
|
||||||
|
@ -64,8 +67,8 @@ public class ClearCommandGroup : CommandGroup {
|
||||||
if (!messagesResult.IsDefined(out var messages))
|
if (!messagesResult.IsDefined(out var messages))
|
||||||
return Result.FromError(messagesResult);
|
return Result.FromError(messagesResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var idList = new List<Snowflake>(messages.Count);
|
var idList = new List<Snowflake>(messages.Count);
|
||||||
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine();
|
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine();
|
||||||
|
@ -93,7 +96,8 @@ public class ClearCommandGroup : CommandGroup {
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var title = string.Format(Messages.MessagesCleared, amount.ToString());
|
var title = string.Format(Messages.MessagesCleared, amount.ToString());
|
||||||
if (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value) {
|
if (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) {
|
||||||
var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser)
|
var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser)
|
||||||
.WithDescription(description)
|
.WithDescription(description)
|
||||||
.WithActionFooter(user)
|
.WithActionFooter(user)
|
||||||
|
@ -105,9 +109,9 @@ public class ClearCommandGroup : CommandGroup {
|
||||||
return Result.FromError(logEmbed);
|
return Result.FromError(logEmbed);
|
||||||
|
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PrivateFeedbackChannel != channelId.Value)
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { logBuilt },
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { logBuilt },
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
|
using Remora.Discord.Commands.Extensions;
|
||||||
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>
|
||||||
/// Handles error logging for slash commands that couldn't be successfully prepared.
|
/// Handles error logging for slash commands that couldn't be successfully prepared.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent {
|
public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent {
|
||||||
private readonly ILogger<ErrorLoggingPreparationErrorEvent> _logger;
|
private readonly ILogger<ErrorLoggingPreparationErrorEvent> _logger;
|
||||||
|
|
||||||
|
@ -27,8 +28,11 @@ public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent {
|
||||||
/// <returns>A result which has succeeded.</returns>
|
/// <returns>A result which has succeeded.</returns>
|
||||||
public Task<Result> PreparationFailed(
|
public Task<Result> PreparationFailed(
|
||||||
IOperationContext context, IResult preparationResult, CancellationToken ct = default) {
|
IOperationContext context, IResult preparationResult, CancellationToken ct = default) {
|
||||||
if (!preparationResult.IsSuccess)
|
if (!preparationResult.IsSuccess && !preparationResult.Error.IsUserOrEnvironmentError()) {
|
||||||
_logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message);
|
_logger.LogWarning("Error in slash command preparation.\n{ErrorMessage}", preparationResult.Error.Message);
|
||||||
|
if (preparationResult.Error is ExceptionError exerr)
|
||||||
|
_logger.LogError(exerr.Exception, "An exception has been thrown");
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult(Result.FromSuccess());
|
return Task.FromResult(Result.FromSuccess());
|
||||||
}
|
}
|
||||||
|
@ -37,6 +41,7 @@ public class ErrorLoggingPreparationErrorEvent : IPreparationErrorEvent {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles error logging for slash command groups.
|
/// Handles error logging for slash command groups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent {
|
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent {
|
||||||
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
|
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
|
||||||
|
|
||||||
|
@ -54,8 +59,11 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent {
|
||||||
/// <returns>A result which has succeeded.</returns>
|
/// <returns>A result which has succeeded.</returns>
|
||||||
public Task<Result> AfterExecutionAsync(
|
public Task<Result> AfterExecutionAsync(
|
||||||
ICommandContext context, IResult commandResult, CancellationToken ct = default) {
|
ICommandContext context, IResult commandResult, CancellationToken ct = default) {
|
||||||
if (!commandResult.IsSuccess)
|
if (!commandResult.IsSuccess && !commandResult.Error.IsUserOrEnvironmentError()) {
|
||||||
_logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message);
|
_logger.LogWarning("Error in slash command execution.\n{ErrorMessage}", commandResult.Error.Message);
|
||||||
|
if (commandResult.Error is ExceptionError exerr)
|
||||||
|
_logger.LogError(exerr.Exception, "An exception has been thrown");
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult(Result.FromSuccess());
|
return Task.FromResult(Result.FromSuccess());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using Boyfriend.Data;
|
||||||
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;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
using Remora.Discord.API.Objects;
|
using Remora.Discord.API.Objects;
|
||||||
|
using Remora.Discord.Commands.Attributes;
|
||||||
using Remora.Discord.Commands.Conditions;
|
using Remora.Discord.Commands.Conditions;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
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>
|
||||||
/// Handles the command to kick members of a guild: /kick.
|
/// Handles the command to kick members of a guild: /kick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class KickCommandGroup : CommandGroup {
|
public class KickCommandGroup : CommandGroup {
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
|
@ -54,10 +55,13 @@ public class KickCommandGroup : CommandGroup {
|
||||||
/// was kicked and vice-versa.
|
/// was kicked and vice-versa.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("kick", "кик")]
|
[Command("kick", "кик")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.KickMembers)]
|
[RequireDiscordPermission(DiscordPermission.KickMembers)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
||||||
[Description("Kick member")]
|
[Description("Kick member")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> KickUserAsync(
|
public async Task<Result> KickUserAsync(
|
||||||
[Description("Member to kick")] IUser target,
|
[Description("Member to kick")] IUser target,
|
||||||
[Description("Kick reason")] string reason) {
|
[Description("Kick reason")] string reason) {
|
||||||
|
@ -71,8 +75,8 @@ public class KickCommandGroup : CommandGroup {
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
||||||
var cfg = data.Configuration;
|
var cfg = data.Settings;
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
|
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
|
||||||
if (!memberResult.IsSuccess) {
|
if (!memberResult.IsSuccess) {
|
||||||
|
@ -129,8 +133,10 @@ public class KickCommandGroup : CommandGroup {
|
||||||
string.Format(Messages.UserKicked, target.GetTag()), target)
|
string.Format(Messages.UserKicked, target.GetTag()), target)
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||||
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
|
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
|
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
|
||||||
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
||||||
string.Format(Messages.UserKicked, target.GetTag()), target)
|
string.Format(Messages.UserKicked, target.GetTag()), target)
|
||||||
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
||||||
|
@ -144,14 +150,14 @@ public class KickCommandGroup : CommandGroup {
|
||||||
|
|
||||||
var builtArray = new[] { logBuilt };
|
var builtArray = new[] { logBuilt };
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PublicFeedbackChannel != channelId.Value)
|
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
|
||||||
&& cfg.PrivateFeedbackChannel != channelId.Value)
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Boyfriend.Data;
|
||||||
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;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
using Remora.Discord.API.Objects;
|
using Remora.Discord.API.Objects;
|
||||||
|
using Remora.Discord.Commands.Attributes;
|
||||||
using Remora.Discord.Commands.Conditions;
|
using Remora.Discord.Commands.Conditions;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
using Remora.Discord.Commands.Feedback.Services;
|
||||||
|
@ -13,14 +16,12 @@ 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>
|
||||||
/// Handles commands related to mute management: /mute and /unmute.
|
/// Handles commands related to mute management: /mute and /unmute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class MuteCommandGroup : CommandGroup {
|
public class MuteCommandGroup : CommandGroup {
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
|
@ -58,10 +59,13 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <seealso cref="UnmuteUserAsync" />
|
/// <seealso cref="UnmuteUserAsync" />
|
||||||
[Command("mute", "мут")]
|
[Command("mute", "мут")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||||
[Description("Mute member")]
|
[Description("Mute member")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> MuteUserAsync(
|
public async Task<Result> MuteUserAsync(
|
||||||
[Description("Member to mute")] IUser target,
|
[Description("Member to mute")] IUser target,
|
||||||
[Description("Mute reason")] string reason,
|
[Description("Mute reason")] string reason,
|
||||||
|
@ -93,8 +97,8 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
return Result.FromError(interactionResult);
|
return Result.FromError(interactionResult);
|
||||||
|
|
||||||
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
||||||
var cfg = data.Configuration;
|
var cfg = data.Settings;
|
||||||
Messages.Culture = data.Culture;
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
Result<Embed> responseEmbed;
|
Result<Embed> responseEmbed;
|
||||||
if (interactionResult.Entity is not null) {
|
if (interactionResult.Entity is not null) {
|
||||||
|
@ -116,8 +120,10 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
string.Format(Messages.UserMuted, target.GetTag()), target)
|
string.Format(Messages.UserMuted, target.GetTag()), target)
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||||
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
|
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
|
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
|
||||||
var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
|
var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
|
||||||
.Append(
|
.Append(
|
||||||
string.Format(
|
string.Format(
|
||||||
|
@ -136,14 +142,14 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
|
|
||||||
var builtArray = new[] { logBuilt };
|
var builtArray = new[] { logBuilt };
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PublicFeedbackChannel != channelId.Value)
|
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
|
||||||
&& cfg.PrivateFeedbackChannel != channelId.Value)
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,10 +175,13 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
/// <seealso cref="MuteUserAsync" />
|
/// <seealso cref="MuteUserAsync" />
|
||||||
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
|
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
|
||||||
[Command("unmute", "размут")]
|
[Command("unmute", "размут")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||||
[Description("Unmute member")]
|
[Description("Unmute member")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> UnmuteUserAsync(
|
public async Task<Result> UnmuteUserAsync(
|
||||||
[Description("Member to unmute")] IUser target,
|
[Description("Member to unmute")] IUser target,
|
||||||
[Description("Unmute reason")] string reason) {
|
[Description("Unmute reason")] string reason) {
|
||||||
|
@ -185,8 +194,8 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
|
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
|
||||||
if (!memberResult.IsSuccess) {
|
if (!memberResult.IsSuccess) {
|
||||||
|
@ -220,8 +229,10 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||||
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
|
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
|
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||||
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
|
||||||
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
var logEmbed = new EmbedBuilder().WithSmallTitle(
|
||||||
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
||||||
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
|
||||||
|
@ -236,14 +247,14 @@ public class MuteCommandGroup : CommandGroup {
|
||||||
var builtArray = new[] { logBuilt };
|
var builtArray = new[] { logBuilt };
|
||||||
|
|
||||||
// Not awaiting to reduce response time
|
// Not awaiting to reduce response time
|
||||||
if (cfg.PublicFeedbackChannel != channelId.Value)
|
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
|
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
|
||||||
&& cfg.PrivateFeedbackChannel != channelId.Value)
|
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
|
||||||
_ = _channelApi.CreateMessageAsync(
|
_ = _channelApi.CreateMessageAsync(
|
||||||
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
|
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
|
||||||
ct: CancellationToken);
|
ct: CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using Boyfriend.Data;
|
||||||
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.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
@ -9,14 +11,12 @@ 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>
|
||||||
/// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping
|
/// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class PingCommandGroup : CommandGroup {
|
public class PingCommandGroup : CommandGroup {
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly DiscordGatewayClient _client;
|
private readonly DiscordGatewayClient _client;
|
||||||
|
@ -44,6 +44,7 @@ public class PingCommandGroup : CommandGroup {
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("ping", "пинг")]
|
[Command("ping", "пинг")]
|
||||||
[Description("Get bot latency")]
|
[Description("Get bot latency")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> SendPingAsync() {
|
public async Task<Result> SendPingAsync() {
|
||||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _))
|
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _))
|
||||||
return Result.FromError(
|
return Result.FromError(
|
||||||
|
@ -53,8 +54,8 @@ public class PingCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var latency = _client.Latency.TotalMilliseconds;
|
var latency = _client.Latency.TotalMilliseconds;
|
||||||
if (latency is 0) {
|
if (latency is 0) {
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
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.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
using Remora.Discord.Commands.Attributes;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
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>
|
||||||
/// Handles the command to manage reminders: /remind
|
/// Handles the command to manage reminders: /remind
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class RemindCommandGroup : CommandGroup {
|
public class RemindCommandGroup : CommandGroup {
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
private readonly GuildDataService _dataService;
|
private readonly GuildDataService _dataService;
|
||||||
|
@ -40,7 +40,9 @@ public class RemindCommandGroup : CommandGroup {
|
||||||
/// <param name="message">The text of the reminder.</param>
|
/// <param name="message">The text of the reminder.</param>
|
||||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||||
[Command("remind")]
|
[Command("remind")]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
[Description("Create a reminder")]
|
[Description("Create a reminder")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> AddReminderAsync(
|
public async Task<Result> AddReminderAsync(
|
||||||
[Description("After what period of time mention the reminder")]
|
[Description("After what period of time mention the reminder")]
|
||||||
TimeSpan @in,
|
TimeSpan @in,
|
||||||
|
@ -57,8 +59,8 @@ public class RemindCommandGroup : CommandGroup {
|
||||||
|
|
||||||
(await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add(
|
(await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add(
|
||||||
new Reminder {
|
new Reminder {
|
||||||
RemindAt = remindAt,
|
At = remindAt,
|
||||||
Channel = channelId.Value,
|
Channel = channelId.Value.Value,
|
||||||
Text = message
|
Text = message
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,44 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
|
using Boyfriend.Data.Options;
|
||||||
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.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
using Remora.Discord.Commands.Attributes;
|
||||||
|
using Remora.Discord.Commands.Conditions;
|
||||||
using Remora.Discord.Commands.Contexts;
|
using Remora.Discord.Commands.Contexts;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
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>
|
||||||
/// Handles the commands to list and modify per-guild settings: /settings and /settings list.
|
/// Handles the commands to list and modify per-guild settings: /settings and /settings list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class SettingsCommandGroup : CommandGroup {
|
public class SettingsCommandGroup : CommandGroup {
|
||||||
|
private static readonly IOption[] AllOptions = {
|
||||||
|
GuildSettings.Language,
|
||||||
|
GuildSettings.WelcomeMessage,
|
||||||
|
GuildSettings.ReceiveStartupMessages,
|
||||||
|
GuildSettings.RemoveRolesOnMute,
|
||||||
|
GuildSettings.ReturnRolesOnRejoin,
|
||||||
|
GuildSettings.AutoStartEvents,
|
||||||
|
GuildSettings.PublicFeedbackChannel,
|
||||||
|
GuildSettings.PrivateFeedbackChannel,
|
||||||
|
GuildSettings.EventNotificationChannel,
|
||||||
|
GuildSettings.DefaultRole,
|
||||||
|
GuildSettings.MuteRole,
|
||||||
|
GuildSettings.EventNotificationRole,
|
||||||
|
GuildSettings.EventEarlyNotificationOffset
|
||||||
|
};
|
||||||
|
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
private readonly GuildDataService _dataService;
|
private readonly GuildDataService _dataService;
|
||||||
private readonly FeedbackService _feedbackService;
|
private readonly FeedbackService _feedbackService;
|
||||||
|
@ -36,13 +54,18 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A slash command that lists current per-guild settings.
|
/// A slash command that lists current per-guild GuildSettings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A feedback sending result which may or may not have succeeded.
|
/// A feedback sending result which may or may not have succeeded.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("settingslist")]
|
[Command("settingslist")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
|
[RequireContext(ChannelContext.Guild)]
|
||||||
|
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||||
[Description("Shows settings list for this server")]
|
[Description("Shows settings list for this server")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> ListSettingsAsync() {
|
public async Task<Result> ListSettingsAsync() {
|
||||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||||
return Result.FromError(
|
return Result.FromError(
|
||||||
|
@ -52,19 +75,15 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
foreach (var setting in typeof(GuildConfiguration).GetProperties()) {
|
foreach (var option in AllOptions) {
|
||||||
builder.Append(Markdown.InlineCode(setting.Name))
|
builder.Append(Markdown.InlineCode(option.Name))
|
||||||
.Append(": ");
|
.Append(": ");
|
||||||
var something = setting.GetValue(cfg);
|
builder.AppendLine(option.Display(cfg));
|
||||||
if (something!.GetType() == typeof(List<GuildConfiguration.NotificationReceiver>)) {
|
|
||||||
var list = (something as List<GuildConfiguration.NotificationReceiver>);
|
|
||||||
builder.AppendLine(string.Join(", ", list!.Select(v => Markdown.InlineCode(v.ToString()))));
|
|
||||||
} else { builder.AppendLine(Markdown.InlineCode(something.ToString()!)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
|
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser)
|
||||||
|
@ -77,13 +96,18 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A slash command that modifies per-guild settings.
|
/// A slash command that modifies per-guild GuildSettings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="setting">The setting to modify.</param>
|
/// <param name="setting">The setting to modify.</param>
|
||||||
/// <param name="value">The new value of the setting.</param>
|
/// <param name="value">The new value of the setting.</param>
|
||||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||||
[Command("settings")]
|
[Command("settings")]
|
||||||
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||||
|
[DiscordDefaultDMPermission(false)]
|
||||||
|
[RequireContext(ChannelContext.Guild)]
|
||||||
|
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||||
[Description("Change settings for this server")]
|
[Description("Change settings for this server")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> EditSettingsAsync(
|
public async Task<Result> EditSettingsAsync(
|
||||||
[Description("The setting whose value you want to change")]
|
[Description("The setting whose value you want to change")]
|
||||||
string setting,
|
string setting,
|
||||||
|
@ -96,40 +120,16 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
if (!currentUserResult.IsDefined(out var currentUser))
|
if (!currentUserResult.IsDefined(out var currentUser))
|
||||||
return Result.FromError(currentUserResult);
|
return Result.FromError(currentUserResult);
|
||||||
|
|
||||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
|
||||||
Messages.Culture = cfg.GetCulture();
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
|
|
||||||
PropertyInfo? property = null;
|
var option = AllOptions.Single(
|
||||||
|
o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
try {
|
var setResult = option.Set(cfg, value);
|
||||||
foreach (var prop in typeof(GuildConfiguration).GetProperties())
|
if (!setResult.IsSuccess) {
|
||||||
if (string.Equals(setting, prop.Name, StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
property = prop;
|
|
||||||
if (property == null || !property.CanWrite)
|
|
||||||
throw new ApplicationException(Messages.SettingDoesntExist);
|
|
||||||
var type = property.PropertyType;
|
|
||||||
|
|
||||||
if (value is "reset" or "default") { property.SetValue(cfg, null); } else if (type == typeof(string)) {
|
|
||||||
if (setting == "language" && value is not ("ru" or "en" or "mctaylors-ru"))
|
|
||||||
throw new ApplicationException(Messages.LanguageNotSupported);
|
|
||||||
property.SetValue(cfg, value);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (type == typeof(bool))
|
|
||||||
property.SetValue(cfg, Convert.ToBoolean(value));
|
|
||||||
|
|
||||||
if (type == typeof(ulong)) {
|
|
||||||
var id = Convert.ToUInt64(value);
|
|
||||||
|
|
||||||
property.SetValue(cfg, id);
|
|
||||||
}
|
|
||||||
} catch (Exception e) when (e is FormatException or OverflowException) {
|
|
||||||
throw new ApplicationException(Messages.InvalidSettingValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser)
|
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser)
|
||||||
.WithDescription(e.Message)
|
.WithDescription(setResult.Error.Message)
|
||||||
.WithColour(ColorsList.Red)
|
.WithColour(ColorsList.Red)
|
||||||
.Build();
|
.Build();
|
||||||
if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed);
|
if (!failedEmbed.IsDefined(out var failedBuilt)) return Result.FromError(failedEmbed);
|
||||||
|
@ -139,9 +139,9 @@ public class SettingsCommandGroup : CommandGroup {
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.Append(Markdown.InlineCode(setting))
|
builder.Append(Markdown.InlineCode(option.Name))
|
||||||
.Append($" {Messages.SettingIsNow} ")
|
.Append($" {Messages.SettingIsNow} ")
|
||||||
.Append(Markdown.InlineCode(value));
|
.Append(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,90 +0,0 @@
|
||||||
using System.Globalization;
|
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
|
||||||
|
|
||||||
namespace Boyfriend.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores per-guild settings that can be set by a member
|
|
||||||
/// with <see cref="DiscordPermission.ManageGuild" /> using the /settings command
|
|
||||||
/// </summary>
|
|
||||||
public class GuildConfiguration {
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a scheduled event notification receiver.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Used to selectively mention guild members when a scheduled event has started or is about to start.
|
|
||||||
/// </remarks>
|
|
||||||
public enum NotificationReceiver {
|
|
||||||
Interested,
|
|
||||||
Role
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
|
|
||||||
{ "en", new CultureInfo("en-US") },
|
|
||||||
{ "ru", new CultureInfo("ru-RU") },
|
|
||||||
{ "mctaylors-ru", new CultureInfo("tt-RU") }
|
|
||||||
};
|
|
||||||
|
|
||||||
public string Language { get; set; } = "en";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>No message will be sent if set to "off", "disable" or "disabled".</item>
|
|
||||||
/// <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset"</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
/// <seealso cref="GuildMemberAddResponder" />
|
|
||||||
public string WelcomeMessage { get; set; } = "default";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls whether or not the <see cref="Messages.Ready" /> message should be sent
|
|
||||||
/// in <see cref="PrivateFeedbackChannel" /> on startup.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="GuildCreateResponder" />
|
|
||||||
public bool ReceiveStartupMessages { get; set; }
|
|
||||||
|
|
||||||
public bool RemoveRolesOnMute { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Roles will not be returned if the member left the guild because of /ban or /kick.</remarks>
|
|
||||||
public bool ReturnRolesOnRejoin { get; set; }
|
|
||||||
|
|
||||||
public bool AutoStartEvents { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls what channel should all public messages be sent to.
|
|
||||||
/// </summary>
|
|
||||||
public ulong PublicFeedbackChannel { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls what channel should all private, moderator-only messages be sent to.
|
|
||||||
/// </summary>
|
|
||||||
public ulong PrivateFeedbackChannel { get; set; }
|
|
||||||
|
|
||||||
public ulong EventNotificationChannel { get; set; }
|
|
||||||
public ulong DefaultRole { get; set; }
|
|
||||||
public ulong MuteRole { get; set; }
|
|
||||||
public ulong EventNotificationRole { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls what guild members should be mentioned when a scheduled event has started or is about to start.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="NotificationReceiver" />
|
|
||||||
public List<NotificationReceiver> EventStartedReceivers { get; set; }
|
|
||||||
= new() { NotificationReceiver.Interested, NotificationReceiver.Role };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls the amount of time before a scheduled event to send a reminder in <see cref="EventNotificationChannel" />.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero;
|
|
||||||
|
|
||||||
// Do not convert this to a property, else serialization will be attempted
|
|
||||||
public CultureInfo GetCulture() {
|
|
||||||
return CultureInfoCache[Language];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Globalization;
|
using System.Text.Json.Nodes;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
|
|
||||||
namespace Boyfriend.Data;
|
namespace Boyfriend.Data;
|
||||||
|
@ -8,29 +8,26 @@ namespace Boyfriend.Data;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This information is stored on disk as a JSON file.</remarks>
|
/// <remarks>This information is stored on disk as a JSON file.</remarks>
|
||||||
public class GuildData {
|
public class GuildData {
|
||||||
public readonly GuildConfiguration Configuration;
|
|
||||||
public readonly string ConfigurationPath;
|
|
||||||
|
|
||||||
public readonly Dictionary<ulong, MemberData> MemberData;
|
public readonly Dictionary<ulong, MemberData> MemberData;
|
||||||
public readonly string MemberDataPath;
|
public readonly string MemberDataPath;
|
||||||
|
|
||||||
public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents;
|
public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents;
|
||||||
public readonly string ScheduledEventsPath;
|
public readonly string ScheduledEventsPath;
|
||||||
|
public readonly JsonNode Settings;
|
||||||
|
public readonly string SettingsPath;
|
||||||
|
|
||||||
public GuildData(
|
public GuildData(
|
||||||
GuildConfiguration configuration, string configurationPath,
|
JsonNode settings, string settingsPath,
|
||||||
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
||||||
Dictionary<ulong, MemberData> memberData, string memberDataPath) {
|
Dictionary<ulong, MemberData> memberData, string memberDataPath) {
|
||||||
Configuration = configuration;
|
Settings = settings;
|
||||||
ConfigurationPath = configurationPath;
|
SettingsPath = settingsPath;
|
||||||
ScheduledEvents = scheduledEvents;
|
ScheduledEvents = scheduledEvents;
|
||||||
ScheduledEventsPath = scheduledEventsPath;
|
ScheduledEventsPath = scheduledEventsPath;
|
||||||
MemberData = memberData;
|
MemberData = memberData;
|
||||||
MemberDataPath = memberDataPath;
|
MemberDataPath = memberDataPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CultureInfo Culture => Configuration.GetCulture();
|
|
||||||
|
|
||||||
public MemberData GetMemberData(Snowflake userId) {
|
public MemberData GetMemberData(Snowflake userId) {
|
||||||
if (MemberData.TryGetValue(userId.Value, out var existing)) return existing;
|
if (MemberData.TryGetValue(userId.Value, out var existing)) return existing;
|
||||||
|
|
||||||
|
|
63
src/Data/GuildSettings.cs
Normal file
63
src/Data/GuildSettings.cs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
using Boyfriend.Data.Options;
|
||||||
|
using Boyfriend.Responders;
|
||||||
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
||||||
|
namespace Boyfriend.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all per-guild settings that can be set by a member
|
||||||
|
/// with <see cref="DiscordPermission.ManageGuild" /> using the /settings command
|
||||||
|
/// </summary>
|
||||||
|
public static class GuildSettings {
|
||||||
|
public static readonly LanguageOption Language = new("Language", "en");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>No message will be sent if set to "off", "disable" or "disabled".</item>
|
||||||
|
/// <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset"</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="GuildMemberJoinedResponder" />
|
||||||
|
public static readonly Option<string> WelcomeMessage = new("WelcomeMessage", "default");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether or not the <see cref="Messages.Ready" /> message should be sent
|
||||||
|
/// in <see cref="PrivateFeedbackChannel" /> on startup.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="GuildLoadedResponder" />
|
||||||
|
public static readonly BoolOption ReceiveStartupMessages = new("ReceiveStartupMessages", false);
|
||||||
|
|
||||||
|
public static readonly BoolOption RemoveRolesOnMute = new("RemoveRolesOnMute", false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether or not a guild member's roles are returned if he/she leaves and then joins back.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Roles will not be returned if the member left the guild because of /ban or /kick.</remarks>
|
||||||
|
public static readonly BoolOption ReturnRolesOnRejoin = new("ReturnRolesOnRejoin", false);
|
||||||
|
|
||||||
|
public static readonly BoolOption AutoStartEvents = new("AutoStartEvents", false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls what channel should all public messages be sent to.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly SnowflakeOption PublicFeedbackChannel = new("PublicFeedbackChannel");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls what channel should all private, moderator-only messages be sent to.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel");
|
||||||
|
|
||||||
|
public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
|
||||||
|
public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
|
||||||
|
public static readonly SnowflakeOption MuteRole = new("MuteRole");
|
||||||
|
public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the amount of time before a scheduled event to send a reminder in <see cref="EventNotificationChannel" />.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly TimeSpanOption EventEarlyNotificationOffset = new(
|
||||||
|
"EventEarlyNotificationOffset", TimeSpan.Zero);
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
using Remora.Rest.Core;
|
|
||||||
|
|
||||||
namespace Boyfriend.Data;
|
namespace Boyfriend.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -13,6 +11,6 @@ public class MemberData {
|
||||||
|
|
||||||
public ulong Id { get; }
|
public ulong Id { get; }
|
||||||
public DateTimeOffset? BannedUntil { get; set; }
|
public DateTimeOffset? BannedUntil { get; set; }
|
||||||
public List<Snowflake> Roles { get; set; } = new();
|
public List<ulong> Roles { get; set; } = new();
|
||||||
public List<Reminder> Reminders { get; } = new();
|
public List<Reminder> Reminders { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
34
src/Data/Options/BoolOption.cs
Normal file
34
src/Data/Options/BoolOption.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
settings[Name] = value;
|
||||||
|
return Result.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseBool(string from, out bool value) {
|
||||||
|
value = false;
|
||||||
|
switch (from) {
|
||||||
|
case "1" or "y" or "yes" or "д" or "да":
|
||||||
|
value = true;
|
||||||
|
return true;
|
||||||
|
case "0" or "n" or "no" or "н" or "не" or "нет":
|
||||||
|
value = false;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/Data/Options/IOption.cs
Normal file
10
src/Data/Options/IOption.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
namespace Boyfriend.Data.Options;
|
||||||
|
|
||||||
|
public interface IOption {
|
||||||
|
string Name { get; }
|
||||||
|
string Display(JsonNode settings);
|
||||||
|
Result Set(JsonNode settings, string from);
|
||||||
|
}
|
35
src/Data/Options/LanguageOption.cs
Normal file
35
src/Data/Options/LanguageOption.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Discord.Extensions.Formatting;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
namespace Boyfriend.Data.Options;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class LanguageOption : Option<CultureInfo> {
|
||||||
|
private static readonly Dictionary<string, CultureInfo> CultureInfoCache = new() {
|
||||||
|
{ "en", new CultureInfo("en-US") },
|
||||||
|
{ "ru", new CultureInfo("ru-RU") },
|
||||||
|
{ "mctaylors-ru", new CultureInfo("tt-RU") }
|
||||||
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
return property != null ? CultureInfoCache[property.GetValue<string>()] : DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Result Set(JsonNode settings, string from) {
|
||||||
|
if (!CultureInfoCache.ContainsKey(from.ToLowerInvariant()))
|
||||||
|
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported));
|
||||||
|
|
||||||
|
return base.Set(settings, from.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
}
|
46
src/Data/Options/Option.cs
Normal file
46
src/Data/Options/Option.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Discord.Extensions.Formatting;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
namespace Boyfriend.Data.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an per-guild option.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the option.</typeparam>
|
||||||
|
public class Option<T> : IOption
|
||||||
|
where T : notnull {
|
||||||
|
internal readonly T DefaultValue;
|
||||||
|
|
||||||
|
public Option(string name, T defaultValue) {
|
||||||
|
Name = name;
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public virtual string Display(JsonNode settings) {
|
||||||
|
return Markdown.InlineCode(Get(settings).ToString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the value of the option from a <see cref="string" /> to the provided JsonNode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The <see cref="JsonNode" /> to set the value to.</param>
|
||||||
|
/// <param name="from">The string from which the new value of the option will be parsed.</param>
|
||||||
|
/// <returns>A value setting result which may or may not have succeeded.</returns>
|
||||||
|
public virtual Result Set(JsonNode settings, string from) {
|
||||||
|
settings[Name] = from;
|
||||||
|
return Result.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the option from the provided <paramref name="settings" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The <see cref="JsonNode" /> to get the value from.</param>
|
||||||
|
/// <returns>The value of the option.</returns>
|
||||||
|
public virtual T Get(JsonNode settings) {
|
||||||
|
var property = settings[Name];
|
||||||
|
return property != null ? property.GetValue<T>() : DefaultValue;
|
||||||
|
}
|
||||||
|
}
|
27
src/Data/Options/SnowflakeOption.cs
Normal file
27
src/Data/Options/SnowflakeOption.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Discord.Extensions.Formatting;
|
||||||
|
using Remora.Rest.Core;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Result Set(JsonNode settings, string from) {
|
||||||
|
if (!ulong.TryParse(from, out var parsed))
|
||||||
|
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
|
||||||
|
|
||||||
|
settings[Name] = parsed;
|
||||||
|
return Result.FromSuccess();
|
||||||
|
}
|
||||||
|
}
|
28
src/Data/Options/TimeSpanOption.cs
Normal file
28
src/Data/Options/TimeSpanOption.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Remora.Commands.Parsers;
|
||||||
|
using Remora.Results;
|
||||||
|
|
||||||
|
namespace Boyfriend.Data.Options;
|
||||||
|
|
||||||
|
public class TimeSpanOption : Option<TimeSpan> {
|
||||||
|
private static readonly TimeSpanParser Parser = new();
|
||||||
|
|
||||||
|
public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { }
|
||||||
|
|
||||||
|
public override TimeSpan Get(JsonNode settings) {
|
||||||
|
var property = settings[Name];
|
||||||
|
return property != null ? ParseTimeSpan(property.GetValue<string>()).Entity : DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Result Set(JsonNode settings, string from) {
|
||||||
|
if (!ParseTimeSpan(from).IsDefined(out var span))
|
||||||
|
return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue));
|
||||||
|
|
||||||
|
settings[Name] = span.ToString();
|
||||||
|
return Result.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result<TimeSpan> ParseTimeSpan(string from) {
|
||||||
|
return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
using Remora.Rest.Core;
|
|
||||||
|
|
||||||
namespace Boyfriend.Data;
|
namespace Boyfriend.Data;
|
||||||
|
|
||||||
public struct Reminder {
|
public struct Reminder {
|
||||||
public DateTimeOffset RemindAt;
|
public DateTimeOffset At;
|
||||||
public string Text;
|
public string Text;
|
||||||
public Snowflake Channel;
|
public ulong Channel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,335 +0,0 @@
|
||||||
using Boyfriend.Data;
|
|
||||||
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="GuildConfiguration.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 guildConfig = await _dataService.GetConfiguration(guild.ID, ct);
|
|
||||||
if (!guildConfig.ReceiveStartupMessages)
|
|
||||||
return Result.FromSuccess();
|
|
||||||
if (guildConfig.PrivateFeedbackChannel is 0)
|
|
||||||
return Result.FromSuccess();
|
|
||||||
|
|
||||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
|
||||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
|
||||||
|
|
||||||
Messages.Culture = guildConfig.GetCulture();
|
|
||||||
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(
|
|
||||||
guildConfig.PrivateFeedbackChannel.ToDiscordSnowflake(), 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="GuildConfiguration.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 guildConfiguration = await _dataService.GetConfiguration(guildId, ct);
|
|
||||||
if (guildConfiguration.PrivateFeedbackChannel is 0) 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 = guildConfiguration.GetCulture();
|
|
||||||
|
|
||||||
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(
|
|
||||||
guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), 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="GuildConfiguration.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 guildConfiguration = await _dataService.GetConfiguration(guildId, ct);
|
|
||||||
if (guildConfiguration.PrivateFeedbackChannel is 0)
|
|
||||||
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 = guildConfiguration.GetCulture();
|
|
||||||
|
|
||||||
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(
|
|
||||||
guildConfiguration.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { built },
|
|
||||||
allowedMentions: Boyfriend.NoMentions, ct: ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles sending a guild's <see cref="GuildConfiguration.WelcomeMessage" /> if one is set.
|
|
||||||
/// If <see cref="GuildConfiguration.ReturnRolesOnRejoin"/> is enabled, roles will be returned.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="GuildConfiguration.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.Configuration;
|
|
||||||
if (cfg.PublicFeedbackChannel is 0 || cfg.WelcomeMessage is "off" or "disable" or "disabled")
|
|
||||||
return Result.FromSuccess();
|
|
||||||
if (cfg.ReturnRolesOnRejoin) {
|
|
||||||
var result = await _guildApi.ModifyGuildMemberAsync(
|
|
||||||
gatewayEvent.GuildID, user.ID, roles: data.GetMemberData(user.ID).Roles, ct: ct);
|
|
||||||
if (!result.IsSuccess) return Result.FromError(result.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
Messages.Culture = data.Culture;
|
|
||||||
var welcomeMessage = cfg.WelcomeMessage is "default" or "reset"
|
|
||||||
? Messages.DefaultWelcomeMessage
|
|
||||||
: cfg.WelcomeMessage;
|
|
||||||
|
|
||||||
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(
|
|
||||||
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), 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="GuildConfiguration.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 (guildData.Configuration.EventNotificationChannel is 0)
|
|
||||||
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(
|
|
||||||
guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), 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();
|
|
||||||
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",
|
|
||||||
"++++" => "#",
|
|
||||||
_ => default(Optional<string>)
|
|
||||||
});
|
|
||||||
return Task.FromResult(Result.FromSuccess());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -170,7 +170,7 @@ public static class Extensions {
|
||||||
return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
|
return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Snowflake ToDiscordSnowflake(this ulong id) {
|
public static Snowflake ToSnowflake(this ulong id) {
|
||||||
return DiscordSnowflake.New(id);
|
return DiscordSnowflake.New(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,4 +190,8 @@ public static class Extensions {
|
||||||
&& context.TryGetChannelID(out channelId)
|
&& context.TryGetChannelID(out channelId)
|
||||||
&& context.TryGetUserID(out userId);
|
&& context.TryGetUserID(out userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool Empty(this Snowflake snowflake) {
|
||||||
|
return snowflake.Value is 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.Commands.Feedback.Messages;
|
using Remora.Discord.Commands.Feedback.Messages;
|
||||||
using Remora.Discord.Commands.Feedback.Services;
|
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>
|
||||||
/// Handles responding to various interactions.
|
/// Handles responding to various interactions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public class InteractionResponders : InteractionGroup {
|
public class InteractionResponders : InteractionGroup {
|
||||||
private readonly FeedbackService _feedbackService;
|
private readonly FeedbackService _feedbackService;
|
||||||
|
|
||||||
|
@ -25,6 +24,7 @@ public class InteractionResponders : InteractionGroup {
|
||||||
/// <param name="state">The ID of the guild and scheduled event, encoded as "guildId:eventId".</param>
|
/// <param name="state">The ID of the guild and scheduled event, encoded as "guildId:eventId".</param>
|
||||||
/// <returns>An ephemeral feedback sending result which may or may not have succeeded.</returns>
|
/// <returns>An ephemeral feedback sending result which may or may not have succeeded.</returns>
|
||||||
[Button("scheduled-event-details")]
|
[Button("scheduled-event-details")]
|
||||||
|
[UsedImplicitly]
|
||||||
public async Task<Result> OnStatefulButtonClicked(string? state = null) {
|
public async Task<Result> OnStatefulButtonClicked(string? state = null) {
|
||||||
if (state is null) return Result.FromError(new ArgumentNullError(nameof(state)));
|
if (state is null) return Result.FromError(new ArgumentNullError(nameof(state)));
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,6 @@
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace Boyfriend {
|
namespace Boyfriend {
|
||||||
using System;
|
|
||||||
|
|
||||||
|
|
||||||
[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);
|
||||||
|
}
|
||||||
|
}
|
34
src/Responders/MessageReceivedResponder.cs
Normal file
34
src/Responders/MessageReceivedResponder.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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.ToLowerInvariant() switch {
|
||||||
|
"whoami" => "`nobody`",
|
||||||
|
"сука !!" => "`root`",
|
||||||
|
"воооо" => "`removing /...`",
|
||||||
|
"пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.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,5 +1,6 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
@ -36,8 +37,8 @@ public class GuildDataService : IHostedService {
|
||||||
private async Task SaveAsync(CancellationToken ct) {
|
private async Task SaveAsync(CancellationToken ct) {
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
foreach (var data in _datas.Values) {
|
foreach (var data in _datas.Values) {
|
||||||
await using var configStream = File.OpenWrite(data.ConfigurationPath);
|
await using var settingsStream = File.OpenWrite(data.SettingsPath);
|
||||||
tasks.Add(JsonSerializer.SerializeAsync(configStream, data.Configuration, cancellationToken: ct));
|
tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct));
|
||||||
|
|
||||||
await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath);
|
await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath);
|
||||||
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
|
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
|
||||||
|
@ -58,17 +59,16 @@ public class GuildDataService : IHostedService {
|
||||||
private async Task<GuildData> InitializeData(Snowflake guildId, CancellationToken ct = default) {
|
private async Task<GuildData> InitializeData(Snowflake guildId, CancellationToken ct = default) {
|
||||||
var idString = $"{guildId}";
|
var idString = $"{guildId}";
|
||||||
var memberDataPath = $"{guildId}/MemberData";
|
var memberDataPath = $"{guildId}/MemberData";
|
||||||
var configurationPath = $"{guildId}/Configuration.json";
|
var settingsPath = $"{guildId}/Settings.json";
|
||||||
var scheduledEventsPath = $"{guildId}/ScheduledEvents.json";
|
var scheduledEventsPath = $"{guildId}/ScheduledEvents.json";
|
||||||
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
|
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
|
||||||
if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath);
|
if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath);
|
||||||
if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct);
|
if (!File.Exists(settingsPath)) await File.WriteAllTextAsync(settingsPath, "{}", ct);
|
||||||
if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
|
if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
|
||||||
|
|
||||||
await using var configurationStream = File.OpenRead(configurationPath);
|
await using var settingsStream = File.OpenRead(settingsPath);
|
||||||
var configuration
|
var jsonSettings
|
||||||
= JsonSerializer.DeserializeAsync<GuildConfiguration>(
|
= JsonNode.Parse(settingsStream);
|
||||||
configurationStream, cancellationToken: ct);
|
|
||||||
|
|
||||||
await using var eventsStream = File.OpenRead(scheduledEventsPath);
|
await using var eventsStream = File.OpenRead(scheduledEventsPath);
|
||||||
var events
|
var events
|
||||||
|
@ -80,23 +80,23 @@ public class GuildDataService : IHostedService {
|
||||||
await using var dataStream = File.OpenRead(dataPath);
|
await using var dataStream = File.OpenRead(dataPath);
|
||||||
var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
|
var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
|
||||||
if (data is null) continue;
|
if (data is null) continue;
|
||||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToDiscordSnowflake(), ct);
|
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct);
|
||||||
if (memberResult.IsSuccess)
|
if (memberResult.IsSuccess)
|
||||||
data.Roles = memberResult.Entity.Roles.ToList();
|
data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value);
|
||||||
|
|
||||||
memberData.Add(data.Id, data);
|
memberData.Add(data.Id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalData = new GuildData(
|
var finalData = new GuildData(
|
||||||
await configuration ?? new GuildConfiguration(), configurationPath,
|
jsonSettings ?? new JsonObject(), settingsPath,
|
||||||
await events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
await events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
||||||
memberData, memberDataPath);
|
memberData, memberDataPath);
|
||||||
while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData);
|
while (!_datas.ContainsKey(guildId)) _datas.TryAdd(guildId, finalData);
|
||||||
return finalData;
|
return finalData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GuildConfiguration> GetConfiguration(Snowflake guildId, CancellationToken ct = default) {
|
public async Task<JsonNode> GetSettings(Snowflake guildId, CancellationToken ct = default) {
|
||||||
return (await GetData(guildId, ct)).Configuration;
|
return (await GetData(guildId, ct)).Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MemberData> GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) {
|
public async Task<MemberData> GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -94,9 +95,9 @@ public class GuildUpdateService : BackgroundService {
|
||||||
/// This method does the following:
|
/// This method does the following:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>Automatically unbans users once their ban period has expired.</item>
|
/// <item>Automatically unbans users once their ban period has expired.</item>
|
||||||
/// <item>Automatically grants members the guild's <see cref="GuildConfiguration.DefaultRole"/> if one is set.</item>
|
/// <item>Automatically grants members the guild's <see cref="GuildSettings.DefaultRole"/> if one is set.</item>
|
||||||
/// <item>Sends reminders about an upcoming scheduled event.</item>
|
/// <item>Sends reminders about an upcoming scheduled event.</item>
|
||||||
/// <item>Automatically starts scheduled events if <see cref="GuildConfiguration.AutoStartEvents"/> is enabled.</item>
|
/// <item>Automatically starts scheduled events if <see cref="GuildSettings.AutoStartEvents"/> is enabled.</item>
|
||||||
/// <item>Sends scheduled event start notifications.</item>
|
/// <item>Sends scheduled event start notifications.</item>
|
||||||
/// <item>Sends scheduled event completion notifications.</item>
|
/// <item>Sends scheduled event completion notifications.</item>
|
||||||
/// <item>Sends reminders to members.</item>
|
/// <item>Sends reminders to members.</item>
|
||||||
|
@ -114,15 +115,15 @@ public class GuildUpdateService : BackgroundService {
|
||||||
/// <param name="ct">The cancellation token for this operation.</param>
|
/// <param name="ct">The cancellation token for this operation.</param>
|
||||||
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
|
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
|
||||||
var data = await _dataService.GetData(guildId, ct);
|
var data = await _dataService.GetData(guildId, ct);
|
||||||
Messages.Culture = data.Culture;
|
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||||
var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake();
|
var defaultRole = GuildSettings.DefaultRole.Get(data.Settings);
|
||||||
|
|
||||||
foreach (var memberData in data.MemberData.Values) {
|
foreach (var memberData in data.MemberData.Values) {
|
||||||
var userId = memberData.Id.ToDiscordSnowflake();
|
var userId = memberData.Id.ToSnowflake();
|
||||||
|
|
||||||
if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake))
|
if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value))
|
||||||
_ = _guildApi.AddGuildMemberRoleAsync(
|
_ = _guildApi.AddGuildMemberRoleAsync(
|
||||||
guildId, userId, defaultRoleSnowflake, ct: ct);
|
guildId, userId, defaultRole, ct: ct);
|
||||||
|
|
||||||
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
|
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
|
||||||
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
||||||
|
@ -139,7 +140,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
|
|
||||||
for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
|
for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
|
||||||
var reminder = memberData.Reminders[i];
|
var reminder = memberData.Reminders[i];
|
||||||
if (DateTimeOffset.UtcNow < reminder.RemindAt) continue;
|
if (DateTimeOffset.UtcNow < reminder.At) continue;
|
||||||
|
|
||||||
var embed = new EmbedBuilder().WithSmallTitle(
|
var embed = new EmbedBuilder().WithSmallTitle(
|
||||||
string.Format(Messages.Reminder, user.GetTag()), user)
|
string.Format(Messages.Reminder, user.GetTag()), user)
|
||||||
|
@ -151,7 +152,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
if (!embed.IsDefined(out var built)) continue;
|
if (!embed.IsDefined(out var built)) continue;
|
||||||
|
|
||||||
var messageResult = await _channelApi.CreateMessageAsync(
|
var messageResult = await _channelApi.CreateMessageAsync(
|
||||||
reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct);
|
reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct);
|
||||||
if (!messageResult.IsSuccess)
|
if (!messageResult.IsSuccess)
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
"Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
|
"Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
|
||||||
|
@ -163,7 +164,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
|
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
|
||||||
if (!eventsResult.IsDefined(out var events)) return;
|
if (!eventsResult.IsDefined(out var events)) return;
|
||||||
|
|
||||||
if (data.Configuration.EventNotificationChannel is 0) return;
|
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return;
|
||||||
|
|
||||||
foreach (var scheduledEvent in events) {
|
foreach (var scheduledEvent in events) {
|
||||||
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
|
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
|
||||||
|
@ -172,7 +173,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
|
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
|
||||||
if (storedEvent.Status == scheduledEvent.Status) {
|
if (storedEvent.Status == scheduledEvent.Status) {
|
||||||
if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
|
if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
|
||||||
if (data.Configuration.AutoStartEvents
|
if (GuildSettings.AutoStartEvents.Get(data.Settings)
|
||||||
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
|
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
|
||||||
var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
|
var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
|
||||||
guildId, scheduledEvent.ID,
|
guildId, scheduledEvent.ID,
|
||||||
|
@ -182,10 +183,11 @@ public class GuildUpdateService : BackgroundService {
|
||||||
"Error in automatic scheduled event start request.\n{ErrorMessage}",
|
"Error in automatic scheduled event start request.\n{ErrorMessage}",
|
||||||
startResult.Error.Message);
|
startResult.Error.Message);
|
||||||
}
|
}
|
||||||
} else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero
|
} else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero
|
||||||
&& !storedEvent.EarlyNotificationSent
|
&& !storedEvent.EarlyNotificationSent
|
||||||
&& DateTimeOffset.UtcNow
|
&& DateTimeOffset.UtcNow
|
||||||
>= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) {
|
>= scheduledEvent.ScheduledStartTime
|
||||||
|
- GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) {
|
||||||
var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
|
var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
|
||||||
if (earlyResult.IsSuccess)
|
if (earlyResult.IsSuccess)
|
||||||
storedEvent.EarlyNotificationSent = true;
|
storedEvent.EarlyNotificationSent = true;
|
||||||
|
@ -203,7 +205,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
|
|
||||||
var result = scheduledEvent.Status switch {
|
var result = scheduledEvent.Status switch {
|
||||||
GuildScheduledEventStatus.Scheduled =>
|
GuildScheduledEventStatus.Scheduled =>
|
||||||
await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct),
|
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
|
||||||
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
|
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
|
||||||
await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
|
await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
|
||||||
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
|
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
|
||||||
|
@ -215,19 +217,17 @@ public class GuildUpdateService : BackgroundService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
|
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole" /> if one is
|
||||||
/// set,
|
/// set,
|
||||||
/// when a scheduled event is created
|
/// when a scheduled event is created
|
||||||
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
|
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scheduledEvent">The scheduled event that has just been created.</param>
|
/// <param name="scheduledEvent">The scheduled event that has just been created.</param>
|
||||||
/// <param name="config">The configuration of the guild containing the scheduled event.</param>
|
/// <param name="settings">The settings of the guild containing the scheduled event.</param>
|
||||||
/// <param name="ct">The cancellation token for this operation.</param>
|
/// <param name="ct">The cancellation token for this operation.</param>
|
||||||
/// <returns>A notification sending result which may or may not have succeeded.</returns>
|
/// <returns>A notification sending result which may or may not have succeeded.</returns>
|
||||||
private async Task<Result> SendScheduledEventCreatedMessage(
|
private async Task<Result> SendScheduledEventCreatedMessage(
|
||||||
IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
|
IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
|
||||||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
|
||||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
|
||||||
|
|
||||||
if (!scheduledEvent.CreatorID.IsDefined(out var creatorId))
|
if (!scheduledEvent.CreatorID.IsDefined(out var creatorId))
|
||||||
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID)));
|
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID)));
|
||||||
|
@ -275,14 +275,13 @@ public class GuildUpdateService : BackgroundService {
|
||||||
.WithTitle(scheduledEvent.Name)
|
.WithTitle(scheduledEvent.Name)
|
||||||
.WithDescription(embedDescription)
|
.WithDescription(embedDescription)
|
||||||
.WithEventCover(scheduledEvent.ID, scheduledEvent.Image)
|
.WithEventCover(scheduledEvent.ID, scheduledEvent.Image)
|
||||||
.WithUserFooter(currentUser)
|
|
||||||
.WithCurrentTimestamp()
|
.WithCurrentTimestamp()
|
||||||
.WithColour(ColorsList.White)
|
.WithColour(ColorsList.White)
|
||||||
.Build();
|
.Build();
|
||||||
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
|
||||||
|
|
||||||
var roleMention = config.EventNotificationRole is not 0
|
var roleMention = !GuildSettings.EventNotificationRole.Get(settings).Empty()
|
||||||
? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake())
|
? Mention.Role(GuildSettings.EventNotificationRole.Get(settings))
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
|
|
||||||
var button = new ButtonComponent(
|
var button = new ButtonComponent(
|
||||||
|
@ -294,14 +293,14 @@ public class GuildUpdateService : BackgroundService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (Result)await _channelApi.CreateMessageAsync(
|
return (Result)await _channelApi.CreateMessageAsync(
|
||||||
config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built },
|
GuildSettings.EventNotificationChannel.Get(settings), roleMention, embeds: new[] { built },
|
||||||
components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
|
components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventStartedReceivers" />s,
|
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers,
|
||||||
/// when a scheduled event is about to start, has started or completed
|
/// when a scheduled event is about to start, has started or completed
|
||||||
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
|
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
|
/// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
|
||||||
/// <param name="data">The data for the guild containing the scheduled event.</param>
|
/// <param name="data">The data for the guild containing the scheduled event.</param>
|
||||||
|
@ -353,7 +352,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentResult = await _utility.GetEventNotificationMentions(
|
var contentResult = await _utility.GetEventNotificationMentions(
|
||||||
scheduledEvent, data.Configuration, ct);
|
scheduledEvent, data.Settings, ct);
|
||||||
if (!contentResult.IsDefined(out content))
|
if (!contentResult.IsDefined(out content))
|
||||||
return Result.FromError(contentResult);
|
return Result.FromError(contentResult);
|
||||||
|
|
||||||
|
@ -383,7 +382,7 @@ public class GuildUpdateService : BackgroundService {
|
||||||
if (!result.IsDefined(out var built)) return Result.FromError(result);
|
if (!result.IsDefined(out var built)) return Result.FromError(result);
|
||||||
|
|
||||||
return (Result)await _channelApi.CreateMessageAsync(
|
return (Result)await _channelApi.CreateMessageAsync(
|
||||||
data.Configuration.EventNotificationChannel.ToDiscordSnowflake(),
|
GuildSettings.EventNotificationChannel.Get(data.Settings),
|
||||||
content ?? default(Optional<string>), embeds: new[] { built }, ct: ct);
|
content ?? default(Optional<string>), embeds: new[] { built }, ct: ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using Boyfriend.Data;
|
using Boyfriend.Data;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
@ -103,38 +104,32 @@ public class UtilityService : IHostedService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the string mentioning all <see cref="GuildConfiguration.NotificationReceiver" />s related to a scheduled
|
/// Gets the string mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers related to a scheduled
|
||||||
/// event.
|
/// event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// If the guild configuration enables <see cref="GuildConfiguration.NotificationReceiver.Role" />, then the
|
|
||||||
/// <see cref="GuildConfiguration.EventNotificationRole" /> will also be mentioned.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="scheduledEvent">
|
/// <param name="scheduledEvent">
|
||||||
/// The scheduled event whose subscribers will be mentioned if the guild configuration enables
|
/// The scheduled event whose subscribers will be mentioned.
|
||||||
/// <see cref="GuildConfiguration.NotificationReceiver.Interested" />.
|
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="config">The configuration of the guild containing the scheduled event</param>
|
/// <param name="settings">The settings of the guild containing the scheduled event</param>
|
||||||
/// <param name="ct">The cancellation token for this operation.</param>
|
/// <param name="ct">The cancellation token for this operation.</param>
|
||||||
/// <returns>A result containing the string which may or may not have succeeded.</returns>
|
/// <returns>A result containing the string which may or may not have succeeded.</returns>
|
||||||
public async Task<Result<string>> GetEventNotificationMentions(
|
public async Task<Result<string>> GetEventNotificationMentions(
|
||||||
IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
|
IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
var receivers = config.EventStartedReceivers;
|
var role = GuildSettings.EventNotificationRole.Get(settings);
|
||||||
var role = config.EventNotificationRole.ToDiscordSnowflake();
|
|
||||||
var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync(
|
var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync(
|
||||||
scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct);
|
scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct);
|
||||||
if (!usersResult.IsDefined(out var users)) return Result<string>.FromError(usersResult);
|
if (!usersResult.IsDefined(out var users)) return Result<string>.FromError(usersResult);
|
||||||
|
|
||||||
if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0)
|
if (role.Value is not 0)
|
||||||
builder.Append($"{Mention.Role(role)} ");
|
builder.Append($"{Mention.Role(role)} ");
|
||||||
if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested))
|
|
||||||
builder = users.Where(
|
builder = users.Where(
|
||||||
user => {
|
user => {
|
||||||
if (!user.GuildMember.IsDefined(out var member)) return true;
|
if (!user.GuildMember.IsDefined(out var member)) return true;
|
||||||
return !member.Roles.Contains(role);
|
return !member.Roles.Contains(role);
|
||||||
})
|
})
|
||||||
.Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
|
.Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} "));
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue