diff --git a/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs b/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs new file mode 100644 index 0000000..dc05f96 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.ComponentModel; +using Catalogger.Backend.Cache; +using Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database.Models; +using Catalogger.Backend.Database.Repositories; +using Catalogger.Backend.Extensions; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Pagination.Extensions; +using Remora.Rest.Core; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +[Group("watchlist")] +[Description("Commands for managing the server's watchlist.")] +[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] +public class WatchlistCommands( + WatchlistRepository watchlistRepository, + GuildCache guildCache, + IMemberCache memberCache, + UserCache userCache, + ContextInjectionService contextInjectionService, + FeedbackService feedbackService +) : CommandGroup +{ + [Command("add")] + [Description("Add a user to the watchlist.")] + public async Task AddAsync(IUser user, string reason) + { + var (userId, guildId) = contextInjectionService.GetUserAndGuild(); + + var entry = await watchlistRepository.CreateEntryAsync(guildId, user.ID, userId, reason); + return await feedbackService.ReplyAsync( + $"Added {user.PrettyFormat()} to this server's watchlist, with the following reason:\n>>> {entry.Reason}" + ); + } + + [Command("remove")] + [Description("Remove a user from the watchlist.")] + public async Task RemoveAsync(IUser user) + { + var (userId, guildId) = contextInjectionService.GetUserAndGuild(); + if (!await watchlistRepository.RemoveEntryAsync(guildId, user.ID)) + { + return await feedbackService.ReplyAsync( + $"{user.PrettyFormat()} is not on the watchlist, so you can't remove them from it." + ); + } + + return await feedbackService.ReplyAsync( + $"Removed {user.PrettyFormat()} from the watchlist!" + ); + } + + [Command("show")] + [Description("Show the current watchlist.")] + public async Task ShowAsync() + { + var (userId, guildId) = contextInjectionService.GetUserAndGuild(); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild was not cached"); + + var watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId); + if (watchlist.Count == 0) + return await feedbackService.ReplyAsync( + "There are no entries on the watchlist right now." + ); + + var fields = new List(); + foreach (var entry in watchlist) + fields.Add(await GenerateWatchlistEntryFieldAsync(guildId, entry)); + + return await feedbackService.SendContextualPaginatedMessageAsync( + userId, + DiscordUtils.PaginateFields( + fields, + title: $"Watchlist for {guild.Name} ({fields.Count})", + fieldsPerPage: 5 + ) + ); + } + + private async Task GenerateWatchlistEntryFieldAsync( + Snowflake guildId, + Watchlist entry + ) + { + var user = await TryGetUserAsync(guildId, DiscordSnowflake.New(entry.UserId)); + var fieldName = user != null ? user.Tag() : $"unknown user {entry.UserId}"; + + var moderator = await TryGetUserAsync(guildId, DiscordSnowflake.New(entry.ModeratorId)); + var modName = + moderator != null + ? moderator.PrettyFormat() + : $"*(unknown user {entry.ModeratorId})* <@{entry.ModeratorId}>"; + + return new EmbedField( + Name: fieldName, + Value: $""" + **Moderator:** {modName} + **Added:** + **Reason:** + >>> {entry.Reason} + """ + ); + } + + private async Task TryGetUserAsync(Snowflake guildId, Snowflake userId) => + (await memberCache.TryGetAsync(guildId, userId))?.User.Value + ?? await userCache.GetUserAsync(userId); +} diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs index 5ea725a..855affd 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs @@ -156,7 +156,7 @@ public class GuildMemberAddResponder( ); } - var watchlist = await watchlistRepository.GetWatchlistEntryAsync(member.GuildID, user.ID); + var watchlist = await watchlistRepository.GetEntryAsync(member.GuildID, user.ID); if (watchlist != null) { var moderator = await userCache.GetUserAsync( diff --git a/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs b/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs index 8177f78..ce6bacd 100644 --- a/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs +++ b/Catalogger.Backend/Database/Repositories/WatchlistRepository.cs @@ -33,12 +33,43 @@ public class WatchlistRepository(ILogger logger, DatabaseConnection conn) ) ).ToList(); - public async Task GetWatchlistEntryAsync(Snowflake guildId, Snowflake userId) => + public async Task GetEntryAsync(Snowflake guildId, Snowflake userId) => await conn.QueryFirstOrDefaultAsync( "select * from watchlists where guild_id = @GuildId and user_id = @UserId", new { GuildId = guildId.Value, UserId = userId.Value } ); + public async Task CreateEntryAsync( + Snowflake guildId, + Snowflake userId, + Snowflake moderatorId, + string reason + ) => + await conn.QueryFirstAsync( + """ + insert into watchlists (guild_id, user_id, added_at, moderator_id, reason) + values (@GuildId, @UserId, now(), @ModeratorId, @Reason) + on conflict (guild_id, user_id) do update + set moderator_id = @ModeratorId, added_at = now(), reason = @Reason + returning * + """, + new + { + GuildId = guildId.Value, + UserId = userId.Value, + ModeratorId = moderatorId.Value, + Reason = reason, + } + ); + + public async Task RemoveEntryAsync(Snowflake guildId, Snowflake userId) => + ( + await conn.ExecuteAsync( + "delete from watchlists where guild_id = @GuildId and user_id = @UserId", + new { GuildId = guildId.Value, UserId = userId.Value } + ) + ) != 0; + public void Dispose() { conn.Dispose(); diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index 27710e0..8516224 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -93,6 +93,7 @@ builder .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() + .WithCommandGroup() // End command tree .Finish() .AddPagination()