Merge branch 'main' into feature/public-reminder

This commit is contained in:
Astrid 2020-10-23 11:13:50 +02:00 committed by GitHub
commit 9976789467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 171 additions and 35 deletions

View File

@ -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<Bot>();
_services = services;
_collector = collector;
_metrics = metrics;
_errorMessageService = errorMessageService;
_logger = logger.ForContext<Bot>();
_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());

View File

@ -64,7 +64,7 @@ namespace PluralKit.Bot
internal IDatabase Database => _db;
internal ModelRepository Repository => _repo;
public Task<DiscordMessage> Reply(string text = null, DiscordEmbed embed = null, IEnumerable<IMention> mentions = null)
public async Task<DiscordMessage> Reply(string text = null, DiscordEmbed embed = null, IEnumerable<IMention> 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<T>(Command commandDef, Func<T, Task> handler)

View File

@ -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 <group> list", "Lists all members in a group");
public static Command GroupRename = new Command("group rename", "group <group> rename <new name>", "Renames a group");
public static Command GroupDisplayName = new Command("group displayname", "group <member> displayname [display name]", "Changes a group's display name");
public static Command GroupDisplayName = new Command("group displayname", "group <group> displayname [display name]", "Changes a group's display name");
public static Command GroupDesc = new Command("group description", "group <group> description [description]", "Changes a group's description");
public static Command GroupAdd = new Command("group add", "group <group> add <member> [member 2] [member 3...]", "Adds one or more members to a group");
public static Command GroupRemove = new Command("group remove", "group <group> remove <member> [member 2] [member 3...]", "Removes one or more members from a group");
public static Command GroupPrivacy = new Command("group privacy", "group <group> privacy <description|icon|visibility|all> <public|private>", "Changes a group's privacy settings");
public static Command GroupDelete = new Command("group delete", "group <group> delete", "Deletes a group");
public static Command GroupIcon = new Command("group icon", "group <group> icon [url|@mention]", "Changes a group's icon");
public static Command GroupDelete = new Command("group delete", "group <group> delete", "Deletes a group");
public static Command Switch = new Command("switch", "switch <member> [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 <date/time>", "Moves the latest switch in time");
@ -189,9 +189,9 @@ namespace PluralKit.Bot
if (ctx.Match("random", "r"))
return ctx.Execute<Member>(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 <https://pluralkit.me/commands>.");
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())}");
}

View File

@ -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!");

View File

@ -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<bool>("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)

View File

@ -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)

View File

@ -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.");
}

View File

@ -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

View File

@ -17,10 +17,10 @@ using PluralKit.Core;
namespace PluralKit.Bot {
public static class ContextUtils {
public static async Task<bool> PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable<IMention> mentions = null)
public static async Task<bool> PromptYesNo(this Context ctx, String msgString, DiscordUser user = null, Duration? timeout = null, IEnumerable<IMention> 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;

View File

@ -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;

View File

@ -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;

View File

@ -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<CommandMessage> GetCommandMessage(IPKConnection conn, ulong message_id) =>
conn.QuerySingleOrDefaultAsync<CommandMessage>("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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -56,6 +56,21 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
- `pk;member <name> delete` - Deletes a member.
- `pk;random` - Shows the member card of a randomly selected member in your system.
## Group commands
*Replace `<name>` with a group's name or 5-character ID. For most commands, adding `-clear` will clear/delete the field.*
- `pk;group <name>` - Shows information about a group.
- `pk;group new <name>` - Creates a new group.
- `pk;group list` - Lists all groups in your system.
- `pk;group <group> list` - Lists all members in a group.
- `pk;group <group> rename <new name>` - Renames a group.
- `pk;group <group> displayname [display name]` - Shows or changes a group's display name.
- `pk;group <group> description [description]` - Shows or changes a group's description.
- `pk;group <group> add <member> [member 2] [member 3...]` - Adds one or more members to a group.
- `pk;group <group> remove <member> [member 2] [member 3...]` - Removes one or more members from a group.
- `pk;group <group> privacy <description|icon|visibility|all> <public|private>` - Changes a group's privacy settings.
- `pk;group <group> icon [icon]` - Shows or changes a group's icon.
- `pk;group <group> delete` - Deletes a group.
## Switching commands
- `pk;switch [member...]` - Registers a switch with the given members.
- `pk;switch move <time>` - Moves the latest switch backwards in time.

View File

@ -24,4 +24,40 @@ PluralKit has a couple of useful command shorthands to reduce the typing:
|pk;member new|pk;m n|
|pk;switch|pk;sw|
|pk;message|pk;msg|
|pk;autoproxy|pk;ap|
|pk;autoproxy|pk;ap|
## Member list flags
There are a number of option flags that can be added to the `pk;system list` command.
### Sorting options
|Flag|Aliases|Description|
|---|---|---|
|-by-name|-bn|Sort by member name (default)|
|-by-display-name|-bdn|Sort by display name|
|-by-id|-bid|Sort by member ID|
|-by-message-count|-bmc|Sort by message count (members with the most messages will appear near the top)|
|-by-created|-bc|Sort by creation date (members least recently created will appear near the top)|
|-by-last-fronted|-by-last-front, -by-last-switch, -blf, -bls|Sort by most recently fronted|
|-by-last-message|-blm, -blp|Sort by last message time (members who most recently sent a proxied message will appear near the top)|
|-by-birthday|-by-birthdate, -bbd|Sort by birthday (members whose birthday is in January will appear near the top)|
|-reverse|-rev, -r|Reverse previously chosen sorting order|
|-random||Sort randomly|
### Filter options
|Flag|Aliases|Description|
|---|---|---|
|-all|-a|Show all members, including private members|
|-public-only|-public, -pub|Only show public members (default)|
|-private-only|-private, -priv|Only show private members|
::: warning
You cannot look up private members of another system.
:::
### Additional fields to include in the search results
|Flag|Aliases|Description|
|---|---|---|
|-with-last-switch|-with-last-fronted, -with-last-front, -wls, -wlf|Show each member's last switch date|
|-with-last-message|-with-last-proxy, -wlm, -wlp|Show each member's last message date|
|-with-message-count|-wmc|Show each member's message count|
|-with-created|-wc|Show each member's creation date|