1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-20 00:43:36 +03:00

Add xmldocs.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-06-11 14:12:17 +05:00
parent 1cd309e498
commit e883e143eb
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
15 changed files with 271 additions and 54 deletions

View file

@ -5,6 +5,9 @@
namespace Boyfriend; namespace Boyfriend;
/// <summary>
/// Contains all colors used in embeds.
/// </summary>
public static class ColorsList { public static class ColorsList {
public static readonly Color Default = Color.Gray; public static readonly Color Default = Color.Gray;
public static readonly Color Red = Color.Firebrick; public static readonly Color Red = Color.Firebrick;

View file

@ -19,6 +19,9 @@ using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
/// <summary>
/// Handles commands related to ban management: /ban and unban.
/// </summary>
public class BanCommandGroup : CommandGroup { public class BanCommandGroup : CommandGroup {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context; private readonly ICommandContext _context;
@ -41,12 +44,26 @@ public class BanCommandGroup : CommandGroup {
_utility = utility; _utility = utility;
} }
[Command("ban")] /// <summary>
/// A slash command that bans a Discord user with the specified reason.
/// </summary>
/// <param name="target">The user to ban.</param>
/// <param name="reason">
/// The reason for this ban. Must be encoded with <see cref="WebUtility.UrlEncode" /> when passed to
/// <see cref="IDiscordRestGuildAPI.CreateGuildBanAsync" />.
/// </param>
/// <returns>
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
/// was banned and vice-versa.
/// </returns>
/// <seealso cref="UnBanUserAsync" />
[Command("ban", "бан")]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireDiscordPermission(DiscordPermission.BanMembers)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("банит пидора")] [Description("банит пидора")]
public async Task<Result> BanUserAsync([Description("Юзер, кого банить")] IUser target, string reason) { public async Task<Result> BanUserAsync([Description("Юзер, кого банить")] IUser target, string reason) {
// Data checks
if (!_context.TryGetGuildID(out var guildId)) if (!_context.TryGetGuildID(out var guildId))
return Result.FromError(new ArgumentNullError(nameof(guildId))); return Result.FromError(new ArgumentNullError(nameof(guildId)));
if (!_context.TryGetUserID(out var userId)) if (!_context.TryGetUserID(out var userId))
@ -54,6 +71,7 @@ public class BanCommandGroup : CommandGroup {
if (!_context.TryGetChannelID(out var channelId)) if (!_context.TryGetChannelID(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(channelId))); return Result.FromError(new ArgumentNullError(nameof(channelId)));
// The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
@ -110,6 +128,7 @@ public class BanCommandGroup : CommandGroup {
return Result.FromError(logEmbed); return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt }; var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (cfg.PrivateFeedbackChannel != channelId.Value) if (cfg.PrivateFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync( _ = _channelApi.CreateMessageAsync(
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
@ -127,12 +146,26 @@ public class BanCommandGroup : CommandGroup {
return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken);
} }
/// <summary>
/// A slash command that unbans a Discord user with the specified reason.
/// </summary>
/// <param name="target">The user to unban.</param>
/// <param name="reason">
/// The reason for this unban. Must be encoded with <see cref="WebUtility.UrlEncode" /> when passed to
/// <see cref="IDiscordRestGuildAPI.RemoveGuildBanAsync" />.
/// </param>
/// <returns>
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
/// was unbanned and vice-versa.
/// </returns>
/// <seealso cref="BanUserAsync" />
[Command("unban")] [Command("unban")]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireDiscordPermission(DiscordPermission.BanMembers)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("разбанит пидора")] [Description("разбанит пидора")]
public async Task<Result> UnBanUserAsync([Description("Юзер, кого разбанить")] IUser target, string reason) { public async Task<Result> UnBanUserAsync([Description("Юзер, кого разбанить")] IUser target, string reason) {
// Data checks
if (!_context.TryGetGuildID(out var guildId)) if (!_context.TryGetGuildID(out var guildId))
return Result.FromError(new ArgumentNullError(nameof(guildId))); return Result.FromError(new ArgumentNullError(nameof(guildId)));
if (!_context.TryGetUserID(out var userId)) if (!_context.TryGetUserID(out var userId))
@ -140,6 +173,7 @@ public class BanCommandGroup : CommandGroup {
if (!_context.TryGetChannelID(out var channelId)) if (!_context.TryGetChannelID(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(channelId))); return Result.FromError(new ArgumentNullError(nameof(channelId)));
// The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
@ -158,8 +192,7 @@ public class BanCommandGroup : CommandGroup {
return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken);
} }
Result<Embed> responseEmbed; // Needed to get the tag and avatar
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user)) if (!userResult.IsDefined(out var user))
return Result.FromError(userResult); return Result.FromError(userResult);
@ -170,7 +203,7 @@ public class BanCommandGroup : CommandGroup {
if (!unbanResult.IsSuccess) if (!unbanResult.IsSuccess)
return Result.FromError(unbanResult.Error); return Result.FromError(unbanResult.Error);
responseEmbed = new EmbedBuilder().WithSmallTitle( var responseEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserUnbanned, target.GetTag()), target) string.Format(Messages.UserUnbanned, target.GetTag()), target)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
@ -187,6 +220,8 @@ public class BanCommandGroup : CommandGroup {
return Result.FromError(logEmbed); return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt }; var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (cfg.PrivateFeedbackChannel != channelId.Value) if (cfg.PrivateFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync( _ = _channelApi.CreateMessageAsync(
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,

View file

@ -7,6 +7,9 @@ using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
/// <summary>
/// Handles error logging for slash command groups.
/// </summary>
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent {
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger; private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
@ -14,6 +17,14 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent {
_logger = logger; _logger = logger;
} }
/// <summary>
/// Logs a warning using the injected <see cref="ILogger" /> if the <paramref name="commandResult" /> has not
/// succeeded.
/// </summary>
/// <param name="context">The context of the slash command. Unused.</param>
/// <param name="commandResult">The result whose success is checked.</param>
/// <param name="ct">The cancellation token for this operation. Unused.</param>
/// <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)

View file

@ -1,8 +1,19 @@
using System.Globalization; using System.Globalization;
using Remora.Discord.API.Abstractions.Objects;
namespace Boyfriend.Data; 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 { 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 { public enum NotificationReceiver {
Interested, Interested,
Role Role
@ -14,22 +25,62 @@ public class GuildConfiguration {
{ "mctaylors-ru", new CultureInfo("tt-RU") } { "mctaylors-ru", new CultureInfo("tt-RU") }
}; };
public string Language { get; set; } = "en"; public string Language { get; set; } = "en";
public string WelcomeMessage { get; set; } = "default";
public bool ReceiveStartupMessages { get; set; }
public bool RemoveRolesOnMute { get; set; }
public bool ReturnRolesOnRejoin { get; set; }
public bool AutoStartEvents { get; set; }
public ulong PublicFeedbackChannel { get; set; }
public ulong PrivateFeedbackChannel { get; set; }
public ulong EventNotificationChannel { get; set; }
public ulong StarterRole { get; set; }
public ulong MuteRole { get; set; }
public ulong EventNotificationRole { get; set; }
/// <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; } public List<NotificationReceiver> EventStartedReceivers { get; set; }
= new() { NotificationReceiver.Interested, NotificationReceiver.Role }; = 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; public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero;
public CultureInfo Culture => CultureInfoCache[Language]; public CultureInfo Culture => CultureInfoCache[Language];

View file

@ -2,12 +2,16 @@ using System.Globalization;
namespace Boyfriend.Data; namespace Boyfriend.Data;
/// <summary>
/// Stores information about a guild. This information is not accessible via the Discord API.
/// </summary>
/// <remarks>This information is stored on disk as a JSON file.</remarks>
public class GuildData { public class GuildData {
public readonly GuildConfiguration Configuration; public readonly GuildConfiguration Configuration;
public readonly string ConfigurationPath; public readonly string ConfigurationPath;
public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents; public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents;
public readonly string ScheduledEventsPath; public readonly string ScheduledEventsPath;
public GuildData( public GuildData(
GuildConfiguration configuration, string configurationPath, GuildConfiguration configuration, string configurationPath,

View file

@ -2,8 +2,12 @@ using Remora.Discord.API.Abstractions.Objects;
namespace Boyfriend.Data; namespace Boyfriend.Data;
/// <summary>
/// Stores information about scheduled events. This information is not provided by the Discord API.
/// </summary>
/// <remarks>This information is stored on disk as a JSON file.</remarks>
public class ScheduledEventData { public class ScheduledEventData {
public DateTimeOffset? ActualStartTime; public DateTimeOffset? ActualStartTime;
public GuildScheduledEventStatus Status; public GuildScheduledEventStatus Status;
public ScheduledEventData(GuildScheduledEventStatus status) { public ScheduledEventData(GuildScheduledEventStatus status) {

View file

@ -21,11 +21,15 @@ using Remora.Results;
namespace Boyfriend; 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> { public class GuildCreateResponder : IResponder<IGuildCreate> {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly ILogger<GuildCreateResponder> _logger; private readonly ILogger<GuildCreateResponder> _logger;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public GuildCreateResponder( public GuildCreateResponder(
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestUserAPI userApi,
@ -37,7 +41,7 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
} }
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // is IAvailableGuild if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild isn't IAvailableGuild
var guild = gatewayEvent.Guild.AsT0; var guild = gatewayEvent.Guild.AsT0;
_logger.LogInformation("Joined guild \"{Name}\"", guild.Name); _logger.LogInformation("Joined guild \"{Name}\"", guild.Name);
@ -68,11 +72,15 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
} }
} }
/// <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> { public class MessageDeletedResponder : IResponder<IMessageDelete> {
private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly IDiscordRestAuditLogAPI _auditLogApi;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public MessageDeletedResponder( public MessageDeletedResponder(
IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi,
@ -129,11 +137,15 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
} }
} }
/// <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> { public class MessageEditedResponder : IResponder<IMessageUpdate> {
private readonly CacheService _cacheService; private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public MessageEditedResponder( public MessageEditedResponder(
CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
@ -153,7 +165,7 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
if (!gatewayEvent.Content.IsDefined(out var newContent)) if (!gatewayEvent.Content.IsDefined(out var newContent))
return Result.FromSuccess(); return Result.FromSuccess();
if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
return Result.FromSuccess(); return Result.FromSuccess(); // The message wasn't actually edited
if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) if (!gatewayEvent.ChannelID.IsDefined(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID)));
@ -199,10 +211,14 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
} }
} }
/// <summary>
/// Handles sending a guild's <see cref="GuildConfiguration.WelcomeMessage" /> if one is set.
/// </summary>
/// <seealso cref="GuildConfiguration.WelcomeMessage" />
public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> { public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
public GuildMemberAddResponder( public GuildMemberAddResponder(
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) {
@ -243,10 +259,16 @@ public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
} }
} }
/// <summary>
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
/// set,
/// when a scheduled event is created
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
/// </summary>
public class GuildScheduledEventCreateResponder : IResponder<IGuildScheduledEventCreate> { public class GuildScheduledEventCreateResponder : IResponder<IGuildScheduledEventCreate> {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public GuildScheduledEventCreateResponder( public GuildScheduledEventCreateResponder(
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
@ -339,9 +361,15 @@ public class GuildScheduledEventCreateResponder : IResponder<IGuildScheduledEven
} }
} }
/// <summary>
/// Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
/// set,
/// when a scheduled event has started or completed
/// in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
/// </summary>
public class GuildScheduledEventUpdateResponder : IResponder<IGuildScheduledEventUpdate> { public class GuildScheduledEventUpdateResponder : IResponder<IGuildScheduledEventUpdate> {
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
public GuildScheduledEventUpdateResponder( public GuildScheduledEventUpdateResponder(
@ -353,10 +381,16 @@ public class GuildScheduledEventUpdateResponder : IResponder<IGuildScheduledEven
public async Task<Result> RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) { public async Task<Result> RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) {
var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct);
if (gatewayEvent.Status == guildData.ScheduledEvents[gatewayEvent.ID.Value].Status if (guildData.Configuration.EventNotificationChannel is 0)
|| guildData.Configuration.EventNotificationChannel is 0) return Result.FromSuccess(); return Result.FromSuccess();
if (!guildData.ScheduledEvents.TryGetValue(gatewayEvent.ID.Value, out var data)) {
guildData.ScheduledEvents.Add(gatewayEvent.ID.Value, new ScheduledEventData(gatewayEvent.Status));
} else {
if (gatewayEvent.Status == data.Status)
return Result.FromSuccess();
guildData.ScheduledEvents[gatewayEvent.ID.Value].Status = gatewayEvent.Status; guildData.ScheduledEvents[gatewayEvent.ID.Value].Status = gatewayEvent.Status;
}
var embed = new EmbedBuilder(); var embed = new EmbedBuilder();
StringBuilder? content = null; StringBuilder? content = null;
@ -442,11 +476,15 @@ public class GuildScheduledEventUpdateResponder : IResponder<IGuildScheduledEven
} }
} }
public class GuildScheduledEventResponder : IResponder<IGuildScheduledEventDelete> { /// <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 IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
public GuildScheduledEventResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) {
_channelApi = channelApi; _channelApi = channelApi;
_dataService = dataService; _dataService = dataService;
} }

View file

@ -10,6 +10,12 @@ using Remora.Rest.Core;
namespace Boyfriend; namespace Boyfriend;
public static class Extensions { public static class Extensions {
/// <summary>
/// Adds a footer with the <paramref name="user" />'s avatar and tag (username#0000).
/// </summary>
/// <param name="builder">The builder to add the footer to.</param>
/// <param name="user">The user whose tag and avatar to add.</param>
/// <returns>The builder with the added footer.</returns>
public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) { public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) {
var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess var avatarUrl = avatarUrlResult.IsSuccess
@ -19,6 +25,12 @@ public static class Extensions {
return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl)); return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl));
} }
/// <summary>
/// Adds a footer representing that an action was performed by a <paramref name="user" />.
/// </summary>
/// <param name="builder">The builder to add the footer to.</param>
/// <param name="user">The user that performed the action whose tag and avatar to use.</param>
/// <returns>The builder with the added footer.</returns>
public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) { public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) {
var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256); var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
var avatarUrl = avatarUrlResult.IsSuccess var avatarUrl = avatarUrlResult.IsSuccess
@ -29,6 +41,14 @@ public static class Extensions {
new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl)); new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl));
} }
/// <summary>
/// Adds a title using the author field, making it smaller than using the title field.
/// </summary>
/// <param name="builder">The builder to add the small title to.</param>
/// <param name="text">The text of the small title.</param>
/// <param name="avatarSource">The user whose avatar to use in the small title.</param>
/// <param name="url">The URL that will be opened if a user clicks on the small title.</param>
/// <returns>The builder with the added small title in the author field.</returns>
public static EmbedBuilder WithSmallTitle( public static EmbedBuilder WithSmallTitle(
this EmbedBuilder builder, string text, IUser? avatarSource = null, string? url = default) { this EmbedBuilder builder, string text, IUser? avatarSource = null, string? url = default) {
Uri? avatarUrl = null; Uri? avatarUrl = null;
@ -44,6 +64,12 @@ public static class Extensions {
return builder; return builder;
} }
/// <summary>
/// Adds a footer representing that the action was performed in the <paramref name="guild" />.
/// </summary>
/// <param name="builder">The builder to add the footer to.</param>
/// <param name="guild">The guild whose name and icon to use.</param>
/// <returns>The builder with the added footer.</returns>
public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) { public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) {
var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256); var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
var iconUrl = iconUrlResult.IsSuccess var iconUrl = iconUrlResult.IsSuccess
@ -53,6 +79,13 @@ public static class Extensions {
return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl)); return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl));
} }
/// <summary>
/// Adds a scheduled event's cover image.
/// </summary>
/// <param name="builder">The builder to add the image to.</param>
/// <param name="eventId">The ID of the scheduled event whose image to use.</param>
/// <param name="imageHashOptional">The Optional containing the image hash.</param>
/// <returns>The builder with the added cover image.</returns>
public static EmbedBuilder WithEventCover( public static EmbedBuilder WithEventCover(
this EmbedBuilder builder, Snowflake eventId, Optional<IImageHash?> imageHashOptional) { this EmbedBuilder builder, Snowflake eventId, Optional<IImageHash?> imageHashOptional) {
if (!imageHashOptional.IsDefined(out var imageHash)) return builder; if (!imageHashOptional.IsDefined(out var imageHash)) return builder;
@ -61,6 +94,12 @@ public static class Extensions {
return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder; return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder;
} }
/// <summary>
/// Sanitizes a string for use in <see cref="Markdown.BlockCode(string)" /> by inserting zero-width spaces in between
/// symbols used to format the string with block code.
/// </summary>
/// <param name="s">The string to sanitize.</param>
/// <returns>The sanitized string that can be safely used in <see cref="Markdown.BlockCode(string)" />.</returns>
public static string SanitizeForBlockCode(this string s) { public static string SanitizeForBlockCode(this string s) {
return s.Replace("```", "```"); return s.Replace("```", "```");
} }
@ -82,7 +121,6 @@ public static class Extensions {
return $"{user.Username}#{user.Discriminator:0000}"; return $"{user.Username}#{user.Discriminator:0000}";
} }
public static Snowflake ToDiscordSnowflake(this ulong id) { public static Snowflake ToDiscordSnowflake(this ulong id) {
return DiscordSnowflake.New(id); return DiscordSnowflake.New(id);
} }

View file

@ -9,6 +9,9 @@ using Remora.Results;
namespace Boyfriend; namespace Boyfriend;
/// <summary>
/// Handles responding to various interactions.
/// </summary>
public class InteractionResponders : InteractionGroup { public class InteractionResponders : InteractionGroup {
private readonly FeedbackService _feedbackService; private readonly FeedbackService _feedbackService;
@ -16,6 +19,11 @@ public class InteractionResponders : InteractionGroup {
_feedbackService = feedbackService; _feedbackService = feedbackService;
} }
/// <summary>
/// A button that will output an ephemeral embed containing the information about a scheduled event.
/// </summary>
/// <param name="state">The ID of the guild and scheduled event, encoded as "guildId:eventId".</param>
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
[Button("scheduled-event-details")] [Button("scheduled-event-details")]
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)));

4
Messages.Designer.cs generated
View file

@ -705,9 +705,9 @@ namespace Boyfriend {
} }
} }
internal static string SettingsStarterRole { internal static string SettingsDefaultRole {
get { get {
return ResourceManager.GetString("SettingsStarterRole", resourceCulture); return ResourceManager.GetString("SettingsDefaultRole", resourceCulture);
} }
} }

View file

@ -435,9 +435,9 @@
<data name="UserNotFound" xml:space="preserve"> <data name="UserNotFound" xml:space="preserve">
<value>I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago</value> <value>I could not find this user in any guild I'm a member of! Check if the ID is correct and that the user was on this server no longer than 30 days ago</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve"> <data name="SettingsDefaultRole" xml:space="preserve">
<value>Starter role</value> <value>Default role</value>
</data> </data>
<data name="CommandDescriptionRemind" xml:space="preserve"> <data name="CommandDescriptionRemind" xml:space="preserve">
<value>Adds a reminder</value> <value>Adds a reminder</value>
</data> </data>

View file

@ -435,9 +435,9 @@
<data name="UserNotFound" xml:space="preserve"> <data name="UserNotFound" xml:space="preserve">
<value>Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад</value> <value>Я не смог найти этого пользователя ни в одном из серверов, в которых я есть. Проверь правильность ID и нахождение пользователя на этом сервере максимум 30 дней назад</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve"> <data name="SettingsDefaultRole" xml:space="preserve">
<value>Начальная роль</value> <value>Общая роль</value>
</data> </data>
<data name="CommandDescriptionRemind" xml:space="preserve"> <data name="CommandDescriptionRemind" xml:space="preserve">
<value>Добавляет напоминание</value> <value>Добавляет напоминание</value>
</data> </data>

View file

@ -435,9 +435,9 @@
<data name="UserNotFound" xml:space="preserve"> <data name="UserNotFound" xml:space="preserve">
<value>у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад)</value> <value>у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад)</value>
</data> </data>
<data name="SettingsStarterRole" xml:space="preserve"> <data name="SettingsDefaultRole" xml:space="preserve">
<value>базовое звание</value> <value>дефолтное звание</value>
</data> </data>
<data name="CommandDescriptionRemind" xml:space="preserve"> <data name="CommandDescriptionRemind" xml:space="preserve">
<value>крафтит напоминалку</value> <value>крафтит напоминалку</value>
</data> </data>

View file

@ -5,6 +5,9 @@ using Remora.Rest.Core;
namespace Boyfriend.Services.Data; namespace Boyfriend.Services.Data;
/// <summary>
/// Handles saving, loading, initializing and providing <see cref="GuildData" />.
/// </summary>
public class GuildDataService : IHostedService { public class GuildDataService : IHostedService {
private readonly Dictionary<Snowflake, GuildData> _datas = new(); private readonly Dictionary<Snowflake, GuildData> _datas = new();

View file

@ -5,6 +5,10 @@ using Remora.Results;
namespace Boyfriend.Services; namespace Boyfriend.Services;
/// <summary>
/// Provides utility methods that cannot be transformed to extension methods because they require usage
/// of some Discord APIs.
/// </summary>
public class UtilityService : IHostedService { public class UtilityService : IHostedService {
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
@ -22,6 +26,24 @@ public class UtilityService : IHostedService {
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Checks whether or not a member can interact with another member
/// </summary>
/// <param name="guildId">The ID of the guild in which an operation is being performed.</param>
/// <param name="interacterId">The executor of the operation.</param>
/// <param name="targetId">The target of the operation.</param>
/// <param name="action">The operation.</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>
/// <list type="bullet">
/// <item>A result which has succeeded with a null string if the member can interact with the target.</item>
/// <item>
/// A result which has succeeded with a non-null string containing the error message if the member cannot
/// interact with the target.
/// </item>
/// <item>A result which has failed if an error occurred during the execution of this method.</item>
/// </list>
/// </returns>
public async Task<Result<string?>> CheckInteractionsAsync( public async Task<Result<string?>> CheckInteractionsAsync(
Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) { Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) {
if (interacterId == targetId) if (interacterId == targetId)