Merge branch 'main' into confirm-clear

This commit is contained in:
Astrid 2020-10-23 11:14:36 +02:00 committed by GitHub
commit 17c3640fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 224 additions and 49 deletions

View File

@ -40,18 +40,21 @@ namespace PluralKit.API
[Authorize] [Authorize]
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties) public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
{ {
var system = User.CurrentSystem();
if (!properties.ContainsKey("name")) if (!properties.ContainsKey("name"))
return BadRequest("Member name must be specified."); return BadRequest("Member name must be specified.");
var systemId = User.CurrentSystem();
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
var systemData = await _repo.GetSystem(conn, systemId);
// Enforce per-system member limit // Enforce per-system member limit
var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System", new {System = system}); var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System", new {System = systemId});
if (memberCount >= Limits.MaxMemberCount) var memberLimit = systemData?.MemberLimitOverride ?? Limits.MaxMemberCount;
return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount})."); if (memberCount >= memberLimit)
return BadRequest($"Member limit reached ({memberCount} / {memberLimit}).");
var member = await _repo.CreateMember(conn, system, properties.Value<string>("name")); var member = await _repo.CreateMember(conn, systemId, properties.Value<string>("name"));
MemberPatch patch; MemberPatch patch;
try try
{ {

View File

@ -9,6 +9,8 @@ using App.Metrics;
using Autofac; using Autofac;
using Dapper;
using DSharpPlus; using DSharpPlus;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using DSharpPlus.EventArgs; using DSharpPlus.EventArgs;
@ -34,18 +36,21 @@ namespace PluralKit.Bot
private readonly PeriodicStatCollector _collector; private readonly PeriodicStatCollector _collector;
private readonly IMetrics _metrics; private readonly IMetrics _metrics;
private readonly ErrorMessageService _errorMessageService; private readonly ErrorMessageService _errorMessageService;
private readonly IDatabase _db;
private bool _hasReceivedReady = false; private bool _hasReceivedReady = false;
private Timer _periodicTask; // Never read, just kept here for GC reasons 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; _client = client;
_logger = logger.ForContext<Bot>();
_services = services; _services = services;
_collector = collector; _collector = collector;
_metrics = metrics; _metrics = metrics;
_errorMessageService = errorMessageService; _errorMessageService = errorMessageService;
_logger = logger.ForContext<Bot>(); _db = db;
} }
public void Init() public void Init()
@ -177,6 +182,9 @@ namespace PluralKit.Bot
await UpdateBotStatus(); 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 // Collect some stats, submit them to the metrics backend
await _collector.CollectStats(); await _collector.CollectStats();
await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync()); await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync());

View File

@ -64,7 +64,7 @@ namespace PluralKit.Bot
internal IDatabase Database => _db; internal IDatabase Database => _db;
internal ModelRepository Repository => _repo; 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)) if (!this.BotHasAllPermissions(Permissions.SendMessages))
// Will be "swallowed" during the error handler anyway, this message is never shown. // 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)) 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."); 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) 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 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 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 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 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 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 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 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 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 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 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"); 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")) if (ctx.Match("random", "r"))
return ctx.Execute<Member>(MemberRandom, m => m.MemberRandom(ctx)); 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>."); $"{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) private async Task HandleSystemCommand(Context ctx)
@ -382,7 +382,7 @@ namespace PluralKit.Bot
await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); await PrintCommandNotFoundError(ctx, GroupCommandsTargeted);
} }
else if (!ctx.HasNext()) else if (!ctx.HasNext())
await PrintCommandNotFoundError(ctx, GroupCommands); await PrintCommandExpectedError(ctx, GroupCommands);
else else
await ctx.Reply($"{Emojis.Error} {ctx.CreateGroupNotFoundError(ctx.PopArgument())}"); await ctx.Reply($"{Emojis.Error} {ctx.CreateGroupNotFoundError(ctx.PopArgument())}");
} }

View File

@ -38,8 +38,9 @@ namespace PluralKit.Bot
// Check group cap // Check group cap
var existingGroupCount = await conn.QuerySingleAsync<int>("select count(*) from groups where system = @System", new { System = ctx.System.Id }); var existingGroupCount = await conn.QuerySingleAsync<int>("select count(*) from groups where system = @System", new { System = ctx.System.Id });
if (existingGroupCount >= Limits.MaxGroupCount) var groupLimit = ctx.System.GroupLimitOverride ?? Limits.MaxGroupCount;
throw new PKError($"System has reached the maximum number of groups ({Limits.MaxGroupCount}). Please delete unused groups first in order to create new ones."); if (existingGroupCount >= groupLimit)
throw new PKError($"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones.");
// Warn if there's already a group by this name // Warn if there's already a group by this name
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName); var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName);

View File

@ -141,8 +141,9 @@ namespace PluralKit.Bot
try try
{ {
var dm = await ctx.Rest.CreateDmAsync(ctx.Author.Id); 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 the original message wasn't posted in DMs, send a public reminder
if (!(ctx.Channel is DiscordDmChannel)) if (!(ctx.Channel is DiscordDmChannel))
await ctx.Reply($"{Emojis.Success} Check your DMs!"); await ctx.Reply($"{Emojis.Success} Check your DMs!");

View File

@ -2,6 +2,8 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using Dapper;
using PluralKit.Core; using PluralKit.Core;
namespace PluralKit.Bot namespace PluralKit.Bot
@ -37,21 +39,26 @@ namespace PluralKit.Bot
// Enforce per-system member limit // Enforce per-system member limit
var memberCount = await _repo.GetSystemMemberCount(conn, ctx.System.Id); var memberCount = await _repo.GetSystemMemberCount(conn, ctx.System.Id);
if (memberCount >= Limits.MaxMemberCount) var memberLimit = ctx.System.MemberLimitOverride ?? Limits.MaxMemberCount;
throw Errors.MemberLimitReachedError; if (memberCount >= memberLimit)
throw Errors.MemberLimitReachedError(memberLimit);
// Create the member // Create the member
var member = await _repo.CreateMember(conn, ctx.System.Id, memberName); var member = await _repo.CreateMember(conn, ctx.System.Id, memberName);
memberCount++; memberCount++;
// Send confirmation and space hint // 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(" ")) 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}`)."); 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) if (memberCount >= memberLimit)
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."); 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) else if (memberCount >= Limits.MaxMembersWarnThreshold(memberLimit))
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."); 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) public async Task MemberRandom(Context ctx)
@ -83,4 +90,4 @@ namespace PluralKit.Bot
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system)));
} }
} }
} }

View File

@ -26,20 +26,24 @@ namespace PluralKit.Bot
{ {
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); 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()) 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(); var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel(); channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); 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)); await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}.");
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.");
} }
public async Task SetLogEnabled(Context ctx, bool enable) 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 msg = $"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.";
var mentions = new IMention[] { new UserMention(account) }; 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 _repo.AddAccount(conn, ctx.System.Id, account.Id);
await ctx.Reply($"{Emojis.Success} Account linked to system."); await ctx.Reply($"{Emojis.Success} Account linked to system.");
} }

View File

@ -44,7 +44,7 @@ namespace PluralKit.Bot {
public static PKError DescriptionTooLongError(int length) => new PKError($"Description too long ({length}/{Limits.MaxDescriptionLength} characters)."); public static PKError DescriptionTooLongError(int length) => new PKError($"Description too long ({length}/{Limits.MaxDescriptionLength} characters).");
public static PKError MemberNameTooLongError(int length) => new PKError($"Member name too long ({length}/{Limits.MaxMemberNameLength} characters)."); public static PKError MemberNameTooLongError(int length) => new PKError($"Member name too long ({length}/{Limits.MaxMemberNameLength} characters).");
public static PKError MemberPronounsTooLongError(int length) => new PKError($"Member pronouns too long ({length}/{Limits.MaxMemberNameLength} characters)."); public static PKError MemberPronounsTooLongError(int length) => new PKError($"Member pronouns too long ({length}/{Limits.MaxMemberNameLength} characters).");
public static PKError MemberLimitReachedError => new PKError($"System has reached the maximum number of members ({Limits.MaxMemberCount}). Please delete unused members first in order to create new ones."); public static PKError MemberLimitReachedError(int limit) => new PKError($"System has reached the maximum number of members ({limit}). Please delete unused members first in order to create new ones.");
public static PKError NoMembersError => new PKError("Your system has no members! Please create at least one member before using this command."); public static PKError NoMembersError => new PKError("Your system has no members! Please create at least one member before using this command.");
public static PKError InvalidColorError(string color) => new PKError($"\"{color}\" is not a valid color. Color must be in 6-digit RGB hex format (eg. #ff0000)."); public static PKError InvalidColorError(string color) => new PKError($"\"{color}\" is not a valid color. Color must be in 6-digit RGB hex format (eg. #ff0000).");

View File

@ -48,12 +48,15 @@ namespace PluralKit.Bot
_db.Execute(c => _repo.GetMessage(c, evt.Message.Id)); _db.Execute(c => _repo.GetMessage(c, evt.Message.Id));
FullMessage msg; FullMessage msg;
CommandMessage cmdmsg;
switch (evt.Emoji.Name) switch (evt.Emoji.Name)
{ {
// Message deletion // Message deletion
case "\u274C": // Red X case "\u274C": // Red X
if ((msg = await GetMessage()) != null) if ((msg = await GetMessage()) != null)
await HandleDeleteReaction(evt, msg); await HandleDeleteReaction(evt, msg);
else if ((cmdmsg = await _db.Execute(conn => _repo.GetCommandMessage(conn, evt.Message.Id))) != null)
await HandleCommandDeleteReaction(evt, cmdmsg);
break; break;
case "\u2753": // Red question mark case "\u2753": // Red question mark
@ -92,6 +95,25 @@ namespace PluralKit.Bot
await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); 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) private async ValueTask HandleQueryReaction(MessageReactionAddEventArgs evt, FullMessage msg)
{ {
// Try to DM the user info about the message // Try to DM the user info about the message

View File

@ -68,7 +68,7 @@ namespace PluralKit.Bot
// It's possible to "move" a webhook to a different channel after creation // It's possible to "move" a webhook to a different channel after creation
// Here, we ensure it's actually still pointing towards the proper channel, and if not, wipe and refetch one. // Here, we ensure it's actually still pointing towards the proper channel, and if not, wipe and refetch one.
var webhook = await lazyWebhookValue.Value; var webhook = await lazyWebhookValue.Value;
if (webhook.ChannelId != channel.Id) return await InvalidateAndRefreshWebhook(channel, webhook); if (webhook.ChannelId != channel.Id && webhook.ChannelId != 0) return await InvalidateAndRefreshWebhook(channel, webhook);
return webhook; return webhook;
} }

View File

@ -23,10 +23,10 @@ namespace PluralKit.Bot {
else return true; else return true;
} }
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; 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); else message = await ctx.Reply(msgString, mentions: mentions);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
if (user == null) user = ctx.Author; if (user == null) user = ctx.Author;

View File

@ -52,6 +52,9 @@ namespace PluralKit.Bot
// Ignore "Database is shutting down" error // Ignore "Database is shutting down" error
if (e is PostgresException pe && pe.SqlState == "57P03") return false; if (e is PostgresException pe && pe.SqlState == "57P03") return false;
// Ignore thread pool exhaustion errors
if (e is NpgsqlException npe && npe.Message.Contains("The connection pool has been exhausted")) return false;
// This may expanded at some point. // This may expanded at some point.
return true; return true;
} }

View File

@ -19,7 +19,7 @@ namespace PluralKit.Core
internal class Database: IDatabase internal class Database: IDatabase
{ {
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 9; private const int TargetSchemaVersion = 11;
private readonly CoreConfig _config; private readonly CoreConfig _config;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -37,7 +37,10 @@ namespace PluralKit.Core
_connectionString = new NpgsqlConnectionStringBuilder(_config.Database) _connectionString = new NpgsqlConnectionStringBuilder(_config.Database)
{ {
Pooling = true, MaxPoolSize = 500, Enlist = false, NoResetOnClose = true Pooling = true, MaxPoolSize = 500, Enlist = false, NoResetOnClose = true,
// Lower timeout than default (15s -> 2s), should ideally fail-fast instead of hanging
Timeout = 2
}.ConnectionString; }.ConnectionString;
} }

View File

@ -86,6 +86,15 @@ as $$
where accounts.uid = account_id where accounts.uid = account_id
$$ language sql stable rows 10; $$ language sql stable rows 10;
create function has_private_members(system_hid int) returns bool as $$
declare m int;
begin
m := count(id) from members where system = system_hid and member_visibility = 2;
if m > 0 then return true;
else return false;
end if;
end
$$ language plpgsql;
create function generate_hid() returns char(5) as $$ create function generate_hid() returns char(5) as $$
select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, 5) select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, 5)

View File

@ -0,0 +1,11 @@
-- SCHEMA VERSION 10: 2020-10-09 --
-- Member/group limit override per-system
alter table systems add column member_limit_override smallint default null;
alter table systems add column group_limit_override smallint default null;
-- Lowering global limit to 1000 in this commit, so increase it for systems already above that
update systems s set member_limit_override = 1500
where (select count(*) from members m where m.system = s.id) > 1000;
update info set schema_version = 10;

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

@ -9,6 +9,7 @@ drop view if exists group_list;
drop function if exists message_context; drop function if exists message_context;
drop function if exists proxy_members; drop function if exists proxy_members;
drop function if exists has_private_members;
drop function if exists generate_hid; drop function if exists generate_hid;
drop function if exists find_free_system_hid; drop function if exists find_free_system_hid;
drop function if exists find_free_member_hid; drop function if exists find_free_member_hid;

View File

@ -23,6 +23,8 @@ namespace PluralKit.Core {
public PrivacyLevel FrontPrivacy { get; } public PrivacyLevel FrontPrivacy { get; }
public PrivacyLevel FrontHistoryPrivacy { get; } public PrivacyLevel FrontHistoryPrivacy { get; }
public PrivacyLevel GroupListPrivacy { get; } public PrivacyLevel GroupListPrivacy { get; }
public int? MemberLimitOverride { get; }
public int? GroupLimitOverride { get; }
[JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
} }

View File

@ -119,7 +119,9 @@ namespace PluralKit.Core
system = result.System = await _repo.CreateSystem(conn, data.Name); system = result.System = await _repo.CreateSystem(conn, data.Name);
await _repo.AddAccount(conn, system.Id, accountId); await _repo.AddAccount(conn, system.Id, accountId);
} }
var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount;
// Apply system info // Apply system info
var patch = new SystemPatch {Name = data.Name}; var patch = new SystemPatch {Name = data.Name};
if (data.Description != null) patch.Description = data.Description; 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 // 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 memberCountBefore = await _repo.GetSystemMemberCount(conn, system.Id);
var membersToAdd = data.Members.Count(m => imp.IsNewMember(m.Id, m.Name)); 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.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; return result;
} }
@ -204,7 +206,8 @@ namespace PluralKit.Core
[JsonIgnore] public bool Valid => [JsonIgnore] public bool Valid =>
TimeZoneValid && TimeZoneValid &&
Members != null && 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) && Members.All(m => m.Valid) &&
Switches != null && Switches != null &&
Switches.Count < 10000 && Switches.Count < 10000 &&
@ -361,4 +364,4 @@ namespace PluralKit.Core
[JsonIgnore] public bool Valid => true; [JsonIgnore] public bool Valid => true;
} }
} }

View File

@ -5,8 +5,8 @@ namespace PluralKit.Core {
public static readonly int MaxSystemNameLength = 100; public static readonly int MaxSystemNameLength = 100;
public static readonly int MaxSystemTagLength = MaxProxyNameLength - 1; public static readonly int MaxSystemTagLength = MaxProxyNameLength - 1;
public static readonly int MaxMemberCount = 1500; 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 MaxGroupCount = 250;
public static readonly int MaxDescriptionLength = 1000; public static readonly int MaxDescriptionLength = 1000;
public static readonly int MaxMemberNameLength = 100; // Fair bit larger than MaxProxyNameLength for bookkeeping 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;member <name> delete` - Deletes a member.
- `pk;random` - Shows the member card of a randomly selected member in your system. - `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 ## Switching commands
- `pk;switch [member...]` - Registers a switch with the given members. - `pk;switch [member...]` - Registers a switch with the given members.
- `pk;switch move <time>` - Moves the latest switch backwards in time. - `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;member new|pk;m n|
|pk;switch|pk;sw| |pk;switch|pk;sw|
|pk;message|pk;msg| |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|