1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-05-05 05:26:28 +03:00

Tidy up project structure, fix bug with edit logging (#47)

The project structure has been changed because the previous one had
everything in 1 folder. From this PR onwards, the following is true:
- The source code is stored in `src/`
- `*.resx` and `Messages.Designer.cs` is stored in `locale/`
- Documentation is stored on the wiki and in `docs/`
- Miscellaneous files, such as dotfiles, are stored in the root folder
of the repository

This PR additionally fixes an issue that would cause logs of edited
messages to not be syntax highlighted. This happened because the
responder of edited messages was changed to use the universal
`InBlockCode` extension method which did not support syntax highlighting
until this PR

This PR additionally changes CODEOWNERS to be more reliable. Previously,
it would be possible for some PRs to be unable to be approved because
the only person who can approve them is the same person who opened the
PR.

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-07-09 22:36:44 +05:00 committed by GitHub
parent 2dd9f023ef
commit 3eb17b96c5
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 180 additions and 179 deletions

View file

@ -0,0 +1,255 @@
using System.ComponentModel;
using System.Text;
using Boyfriend.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Results;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
namespace Boyfriend.Commands;
/// <summary>
/// Handles commands related to mute management: /mute and /unmute.
/// </summary>
public class MuteCommandGroup : CommandGroup {
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
private readonly GuildDataService _dataService;
private readonly FeedbackService _feedbackService;
private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility;
public MuteCommandGroup(
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
UtilityService utility) {
_context = context;
_channelApi = channelApi;
_dataService = dataService;
_feedbackService = feedbackService;
_guildApi = guildApi;
_userApi = userApi;
_utility = utility;
}
/// <summary>
/// A slash command that mutes a Discord member with the specified reason.
/// </summary>
/// <param name="target">The member to mute.</param>
/// <param name="duration">The duration for this mute. The member will be automatically unmuted after this duration.</param>
/// <param name="reason">
/// The reason for this mute. Must be encoded with <see cref="Extensions.EncodeHeader" /> when passed to
/// <see cref="IDiscordRestGuildAPI.ModifyGuildMemberAsync" />.
/// </param>
/// <returns>
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
/// was muted and vice-versa.
/// </returns>
/// <seealso cref="UnmuteUserAsync" />
[Command("mute", "мут")]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Mute member")]
public async Task<Result> MuteUserAsync(
[Description("Member to mute")] IUser target,
[Description("Mute reason")] string reason,
[Description("Mute duration")] TimeSpan duration) {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId))
return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
// The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) {
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser)
.WithColour(ColorsList.Red).Build();
if (!embed.IsDefined(out var alreadyBuilt))
return Result.FromError(embed);
return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken);
}
var interactionResult
= await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Mute", CancellationToken);
if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
var cfg = data.Configuration;
Messages.Culture = data.Culture;
Result<Embed> responseEmbed;
if (interactionResult.Entity is not null) {
responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build();
} else {
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
var muteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: until, ct: CancellationToken);
if (!muteResult.IsSuccess)
return Result.FromError(muteResult.Error);
responseEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserMuted, target.GetTag()), target)
.WithColour(ColorsList.Green).Build();
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
.Append(
string.Format(
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until)));
var logEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserMuted, target.GetTag()), target)
.WithDescription(builder.ToString())
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (cfg.PublicFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync(
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
ct: CancellationToken);
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
&& cfg.PrivateFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync(
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
ct: CancellationToken);
}
}
if (!responseEmbed.IsDefined(out var built))
return Result.FromError(responseEmbed);
return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken);
}
/// <summary>
/// A slash command that unmutes a Discord member with the specified reason.
/// </summary>
/// <param name="target">The member to unmute.</param>
/// <param name="reason">
/// The reason for this unmute. Must be encoded with <see cref="Extensions.EncodeHeader" /> when passed to
/// <see cref="IDiscordRestGuildAPI.ModifyGuildMemberAsync" />.
/// </param>
/// <returns>
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
/// was unmuted and vice-versa.
/// </returns>
/// <seealso cref="MuteUserAsync" />
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
[Command("unmute", "размут")]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Unmute member")]
public async Task<Result> UnmuteUserAsync(
[Description("Member to unmute")] IUser target,
[Description("Unmute reason")] string reason) {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId))
return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
// The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult);
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
Messages.Culture = cfg.GetCulture();
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) {
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser)
.WithColour(ColorsList.Red).Build();
if (!embed.IsDefined(out var alreadyBuilt))
return Result.FromError(embed);
return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken);
}
var interactionResult
= await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Unmute", CancellationToken);
if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult);
// Needed to get the tag and avatar
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: null, ct: CancellationToken);
if (!unmuteResult.IsSuccess)
return Result.FromError(unmuteResult.Error);
var responseEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserUnmuted, target.GetTag()), target)
.WithColour(ColorsList.Green).Build();
if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value)
|| (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) {
var logEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserUnmuted, target.GetTag()), target)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Green)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (cfg.PublicFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync(
cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
ct: CancellationToken);
if (cfg.PrivateFeedbackChannel != cfg.PublicFeedbackChannel
&& cfg.PrivateFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync(
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray,
ct: CancellationToken);
}
if (!responseEmbed.IsDefined(out var built))
return Result.FromError(responseEmbed);
return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken);
}
}