Merge branch 'main' into feature/public-reminder
This commit is contained in:
commit
9976789467
@ -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());
|
||||
|
@ -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)
|
||||
|
@ -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())}");
|
||||
}
|
||||
|
@ -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!");
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
17
PluralKit.Core/Database/Migrations/11.sql
Normal file
17
PluralKit.Core/Database/Migrations/11.sql
Normal 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;
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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|
|
Loading…
Reference in New Issue
Block a user