diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index b973f54a..5e87f3da 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -9,6 +9,8 @@ using App.Metrics; using Autofac; +using Dapper; + using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.EventArgs; @@ -34,18 +36,21 @@ namespace PluralKit.Bot private readonly PeriodicStatCollector _collector; private readonly IMetrics _metrics; private readonly ErrorMessageService _errorMessageService; + private readonly IDatabase _db; private bool _hasReceivedReady = false; private Timer _periodicTask; // Never read, just kept here for GC reasons - public Bot(DiscordShardedClient client, ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics, ErrorMessageService errorMessageService) + public Bot(DiscordShardedClient client, ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics, + ErrorMessageService errorMessageService, IDatabase db) { _client = client; + _logger = logger.ForContext(); _services = services; _collector = collector; _metrics = metrics; _errorMessageService = errorMessageService; - _logger = logger.ForContext(); + _db = db; } public void Init() @@ -177,6 +182,9 @@ namespace PluralKit.Bot await UpdateBotStatus(); + // Clean up message cache in postgres + await _db.Execute(conn => conn.QueryAsync("select from cleanup_command_message()")); + // Collect some stats, submit them to the metrics backend await _collector.CollectStats(); await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync()); diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 1cff801b..1097cc18 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -64,7 +64,7 @@ namespace PluralKit.Bot internal IDatabase Database => _db; internal ModelRepository Repository => _repo; - public Task Reply(string text = null, DiscordEmbed embed = null, IEnumerable mentions = null) + public async Task Reply(string text = null, DiscordEmbed embed = null, IEnumerable mentions = null) { if (!this.BotHasAllPermissions(Permissions.SendMessages)) // Will be "swallowed" during the error handler anyway, this message is never shown. @@ -72,7 +72,12 @@ namespace PluralKit.Bot if (embed != null && !this.BotHasAllPermissions(Permissions.EmbedLinks)) throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled."); - return Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); + var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions); + if (embed != null) + // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) + // This may need to be changed at some point but works well enough for now + await _db.Execute(conn => _repo.SaveCommandMessage(conn, msg.Id, Author.Id)); + return msg; } public async Task Execute(Command commandDef, Func handler) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 0b0491a2..f0c825b9 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -50,13 +50,13 @@ namespace PluralKit.Bot public static Command GroupList = new Command("group list", "group list", "Lists all groups in this system"); public static Command GroupMemberList = new Command("group members", "group list", "Lists all members in a group"); public static Command GroupRename = new Command("group rename", "group rename ", "Renames a group"); - public static Command GroupDisplayName = new Command("group displayname", "group displayname [display name]", "Changes a group's display name"); + public static Command GroupDisplayName = new Command("group displayname", "group displayname [display name]", "Changes a group's display name"); public static Command GroupDesc = new Command("group description", "group description [description]", "Changes a group's description"); public static Command GroupAdd = new Command("group add", "group add [member 2] [member 3...]", "Adds one or more members to a group"); public static Command GroupRemove = new Command("group remove", "group remove [member 2] [member 3...]", "Removes one or more members from a group"); public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); - public static Command GroupDelete = new Command("group delete", "group delete", "Deletes a group"); public static Command GroupIcon = new Command("group icon", "group icon [url|@mention]", "Changes a group's icon"); + public static Command GroupDelete = new Command("group delete", "group delete", "Deletes a group"); public static Command Switch = new Command("switch", "switch [member 2] [member 3...]", "Registers a switch"); public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members"); public static Command SwitchMove = new Command("switch move", "switch move ", "Moves the latest switch in time"); @@ -189,9 +189,9 @@ namespace PluralKit.Bot if (ctx.Match("random", "r")) return ctx.Execute(MemberRandom, m => m.MemberRandom(ctx)); - ctx.Reply( + // remove compiler warning + return ctx.Reply( $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); - return Task.CompletedTask; } private async Task HandleSystemCommand(Context ctx) @@ -382,7 +382,7 @@ namespace PluralKit.Bot await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); } else if (!ctx.HasNext()) - await PrintCommandNotFoundError(ctx, GroupCommands); + await PrintCommandExpectedError(ctx, GroupCommands); else await ctx.Reply($"{Emojis.Error} {ctx.CreateGroupNotFoundError(ctx.PopArgument())}"); } diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index 0e08eb63..9003d1dc 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -141,8 +141,9 @@ namespace PluralKit.Bot try { var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); - await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); - + var msg = await dm.SendFileAsync("system.json", stream, $"{Emojis.Success} Here you go!"); + await dm.SendMessageAsync($"<{msg.Attachments[0].Url}>"); + // If the original message wasn't posted in DMs, send a public reminder if (!(ctx.Channel is DiscordDmChannel)) await ctx.Reply($"{Emojis.Success} Check your DMs!"); diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index e9720cdd..93bf1687 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -48,16 +48,17 @@ namespace PluralKit.Bot memberCount++; // Send confirmation and space hint - await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#members"); + await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member"); if (await _db.Execute(conn => conn.QuerySingleAsync("select has_private_members(@System)", new {System = ctx.System.Id}))) //if has private members await ctx.Reply($"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`."); + if (memberName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`)."); - if (memberCount >= Limits.MaxMemberCount) - await ctx.Reply($"{Emojis.Warn} You have reached the per-system member limit ({Limits.MaxMemberCount}). You will be unable to create additional members until existing members are deleted."); - else if (memberCount >= Limits.MaxMembersWarnThreshold) - await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {Limits.MaxMemberCount} members). Please review your member list for unused or duplicate members."); + if (memberCount >= memberLimit) + await ctx.Reply($"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). You will be unable to create additional members until existing members are deleted."); + else if (memberCount >= Limits.MaxMembersWarnThreshold(memberLimit)) + await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members."); } public async Task MemberRandom(Context ctx) diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 167cc089..2e1cdb88 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -26,20 +26,24 @@ namespace PluralKit.Bot { ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); - DiscordChannel channel = null; + if (ctx.MatchClear()) + { + await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null})); + await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); + return; + } + if (!ctx.HasNext()) - throw new PKSyntaxError("You must pass a #channel to set."); + throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it."); + + DiscordChannel channel = null; var channelString = ctx.PeekArgument(); channel = await ctx.MatchChannel(); if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); - var patch = new GuildPatch {LogChannel = channel?.Id}; + var patch = new GuildPatch {LogChannel = channel.Id}; await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch)); - - if (channel != null) - await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}."); - else - await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); + await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}."); } public async Task SetLogEnabled(Context ctx, bool enable) diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index af3cd053..70c829dd 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -35,7 +35,7 @@ namespace PluralKit.Bot var msg = $"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message."; var mentions = new IMention[] { new UserMention(account) }; - if (!await ctx.PromptYesNo(msg, user: account, mentions: mentions)) throw Errors.MemberLinkCancelled; + if (!await ctx.PromptYesNo(msg, user: account, mentions: mentions, matchFlag: false)) throw Errors.MemberLinkCancelled; await _repo.AddAccount(conn, ctx.System.Id, account.Id); await ctx.Reply($"{Emojis.Success} Account linked to system."); } diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 5abc5840..57b08dcb 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -48,12 +48,15 @@ namespace PluralKit.Bot _db.Execute(c => _repo.GetMessage(c, evt.Message.Id)); FullMessage msg; + CommandMessage cmdmsg; switch (evt.Emoji.Name) { // Message deletion case "\u274C": // Red X if ((msg = await GetMessage()) != null) await HandleDeleteReaction(evt, msg); + else if ((cmdmsg = await _db.Execute(conn => _repo.GetCommandMessage(conn, evt.Message.Id))) != null) + await HandleCommandDeleteReaction(evt, cmdmsg); break; case "\u2753": // Red question mark @@ -92,6 +95,25 @@ namespace PluralKit.Bot await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); } + private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEventArgs evt, CommandMessage msg) + { + if (!evt.Channel.BotHasAllPermissions(Permissions.ManageMessages)) return; + + // Can only delete your own message + if (msg.author_id != evt.User.Id) return; + + try + { + await evt.Message.DeleteAsync(); + } + catch (NotFoundException) + { + // Message was deleted by something/someone else before we got to it + } + + // No need to delete database row here, it'll get deleted by the once-per-minute scheduled task. + } + private async ValueTask HandleQueryReaction(MessageReactionAddEventArgs evt, FullMessage msg) { // Try to DM the user info about the message diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 5fab9056..3b6814ad 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -17,10 +17,10 @@ using PluralKit.Core; namespace PluralKit.Bot { public static class ContextUtils { - public static async Task PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable mentions = null) + public static async Task PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable mentions = null, bool matchFlag = true) { DiscordMessage message; - if (ctx.MatchFlag("y", "yes")) return true; + if (matchFlag && ctx.MatchFlag("y", "yes")) return true; else message = await ctx.Reply(msgString, mentions: mentions); var cts = new CancellationTokenSource(); if (user == null) user = ctx.Author; diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index aea4f6fe..a0436a23 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -19,7 +19,7 @@ namespace PluralKit.Core internal class Database: IDatabase { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 10; + private const int TargetSchemaVersion = 11; private readonly CoreConfig _config; private readonly ILogger _logger; diff --git a/PluralKit.Core/Database/Migrations/11.sql b/PluralKit.Core/Database/Migrations/11.sql new file mode 100644 index 00000000..bfed37e1 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/11.sql @@ -0,0 +1,17 @@ +-- SCHEMA VERSION 11: (insert date) -- +-- Create command message table -- + +create table command_message +( + message_id bigint primary key, + author_id bigint not null, + timestamp timestamp not null default now() +); + +create function cleanup_command_message() returns void as $$ +begin + delete from command_message where timestamp < now() - interval '2 hours'; +end; +$$ language plpgsql; + +update info set schema_version = 11; diff --git a/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs b/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs new file mode 100644 index 00000000..1e38c447 --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +using Dapper; + +namespace PluralKit.Core +{ + public partial class ModelRepository + { + public Task SaveCommandMessage(IPKConnection conn, ulong message_id, ulong author_id) => + conn.QueryAsync("insert into command_message (message_id, author_id) values (@Message, @Author)", + new {Message = message_id, Author = author_id }); + + public Task GetCommandMessage(IPKConnection conn, ulong message_id) => + conn.QuerySingleOrDefaultAsync("select message_id, author_id from command_message where message_id = @Message", + new {Message = message_id}); + } + + public class CommandMessage + { + public ulong author_id { get; set; } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Services/DataFileService.cs b/PluralKit.Core/Services/DataFileService.cs index a2422be0..c694a156 100644 --- a/PluralKit.Core/Services/DataFileService.cs +++ b/PluralKit.Core/Services/DataFileService.cs @@ -119,7 +119,9 @@ namespace PluralKit.Core system = result.System = await _repo.CreateSystem(conn, data.Name); await _repo.AddAccount(conn, system.Id, accountId); } - + + var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount; + // Apply system info var patch = new SystemPatch {Name = data.Name}; if (data.Description != null) patch.Description = data.Description; @@ -135,10 +137,10 @@ namespace PluralKit.Core // If creating the unmatched members would put us over the member limit, abort before creating any members var memberCountBefore = await _repo.GetSystemMemberCount(conn, system.Id); var membersToAdd = data.Members.Count(m => imp.IsNewMember(m.Id, m.Name)); - if (memberCountBefore + membersToAdd > Limits.MaxMemberCount) + if (memberCountBefore + membersToAdd > memberLimit) { result.Success = false; - result.Message = $"Import would exceed the maximum number of members ({Limits.MaxMemberCount})."; + result.Message = $"Import would exceed the maximum number of members ({memberLimit})."; return result; } @@ -204,7 +206,8 @@ namespace PluralKit.Core [JsonIgnore] public bool Valid => TimeZoneValid && Members != null && - Members.Count <= Limits.MaxMemberCount && + // no need to check this here, it is checked later as part of the import + // Members.Count <= Limits.MaxMemberCount && Members.All(m => m.Valid) && Switches != null && Switches.Count < 10000 && @@ -361,4 +364,4 @@ namespace PluralKit.Core [JsonIgnore] public bool Valid => true; } -} \ No newline at end of file +} diff --git a/PluralKit.Core/Utils/Limits.cs b/PluralKit.Core/Utils/Limits.cs index 1d2246ef..00f09075 100644 --- a/PluralKit.Core/Utils/Limits.cs +++ b/PluralKit.Core/Utils/Limits.cs @@ -6,7 +6,7 @@ namespace PluralKit.Core { public static readonly int MaxSystemNameLength = 100; public static readonly int MaxSystemTagLength = MaxProxyNameLength - 1; public static readonly int MaxMemberCount = 1000; - public static readonly int MaxMembersWarnThreshold = MaxMemberCount - 50; + public static int MaxMembersWarnThreshold (int memberLimit) => memberLimit - 50; public static readonly int MaxGroupCount = 250; public static readonly int MaxDescriptionLength = 1000; public static readonly int MaxMemberNameLength = 100; // Fair bit larger than MaxProxyNameLength for bookkeeping diff --git a/docs/content/command-list.md b/docs/content/command-list.md index d6cc2828..f07e2b0f 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -56,6 +56,21 @@ Words in **\** or **[square brackets]** mean fill-in-the-blank. - `pk;member delete` - Deletes a member. - `pk;random` - Shows the member card of a randomly selected member in your system. +## Group commands +*Replace `` with a group's name or 5-character ID. For most commands, adding `-clear` will clear/delete the field.* +- `pk;group ` - Shows information about a group. +- `pk;group new ` - Creates a new group. +- `pk;group list` - Lists all groups in your system. +- `pk;group list` - Lists all members in a group. +- `pk;group rename ` - Renames a group. +- `pk;group displayname [display name]` - Shows or changes a group's display name. +- `pk;group description [description]` - Shows or changes a group's description. +- `pk;group add [member 2] [member 3...]` - Adds one or more members to a group. +- `pk;group remove [member 2] [member 3...]` - Removes one or more members from a group. +- `pk;group privacy ` - Changes a group's privacy settings. +- `pk;group icon [icon]` - Shows or changes a group's icon. +- `pk;group delete` - Deletes a group. + ## Switching commands - `pk;switch [member...]` - Registers a switch with the given members. - `pk;switch move