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

@ -1,120 +0,0 @@
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.Commands.Attributes;
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.Rest.Core;
using Remora.Results;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
namespace Boyfriend.Commands;
/// <summary>
/// Handles the command to clear messages in a channel: /clear.
/// </summary>
public class ClearCommandGroup : CommandGroup {
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
private readonly GuildDataService _dataService;
private readonly FeedbackService _feedbackService;
private readonly IDiscordRestUserAPI _userApi;
public ClearCommandGroup(
IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService dataService,
FeedbackService feedbackService, IDiscordRestUserAPI userApi) {
_channelApi = channelApi;
_context = context;
_dataService = dataService;
_feedbackService = feedbackService;
_userApi = userApi;
}
/// <summary>
/// A slash command that clears messages in the channel it was executed.
/// </summary>
/// <param name="amount">The amount of messages to clear.</param>
/// <returns>
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages
/// were cleared and vice-versa.
/// </returns>
[Command("clear", "очистить")]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.ManageMessages)]
[Description("Remove multiple messages")]
public async Task<Result> ClearMessagesAsync(
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
int amount) {
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"));
var messagesResult = await _channelApi.GetChannelMessagesAsync(
channelId.Value, limit: amount + 1, ct: CancellationToken);
if (!messagesResult.IsDefined(out var messages))
return Result.FromError(messagesResult);
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
Messages.Culture = cfg.GetCulture();
var idList = new List<Snowflake>(messages.Count);
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine();
for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...')
var message = messages[i];
idList.Add(message.ID);
builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author)));
builder.Append(message.Content.InBlockCode());
}
var description = builder.ToString();
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId.Value, idList, user.GetTag().EncodeHeader(), CancellationToken);
if (!deleteResult.IsSuccess)
return Result.FromError(deleteResult.Error);
// The current user's avatar is used when sending messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult);
var title = string.Format(Messages.MessagesCleared, amount.ToString());
if (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value) {
var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser)
.WithDescription(description)
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
// Not awaiting to reduce response time
if (cfg.PrivateFeedbackChannel != channelId.Value)
_ = _channelApi.CreateMessageAsync(
cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: new[] { logBuilt },
ct: CancellationToken);
}
var embed = new EmbedBuilder().WithSmallTitle(title, currentUser)
.WithColour(ColorsList.Green).Build();
if (!embed.IsDefined(out var built)) return Result.FromError(embed);
return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken);
}
}