diff --git a/PluralKit.API/Controllers/v1/MemberController.cs b/PluralKit.API/Controllers/v1/MemberController.cs index bea62798..539893c2 100644 --- a/PluralKit.API/Controllers/v1/MemberController.cs +++ b/PluralKit.API/Controllers/v1/MemberController.cs @@ -40,18 +40,21 @@ namespace PluralKit.API [Authorize] public async Task> PostMember([FromBody] JObject properties) { - var system = User.CurrentSystem(); if (!properties.ContainsKey("name")) return BadRequest("Member name must be specified."); + + var systemId = User.CurrentSystem(); await using var conn = await _db.Obtain(); + var systemData = await _repo.GetSystem(conn, systemId); // Enforce per-system member limit - var memberCount = await conn.QuerySingleAsync("select count(*) from members where system = @System", new {System = system}); - if (memberCount >= Limits.MaxMemberCount) - return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount})."); + var memberCount = await conn.QuerySingleAsync("select count(*) from members where system = @System", new {System = systemId}); + var memberLimit = systemData?.MemberLimitOverride ?? Limits.MaxMemberCount; + if (memberCount >= memberLimit) + return BadRequest($"Member limit reached ({memberCount} / {memberLimit})."); - var member = await _repo.CreateMember(conn, system, properties.Value("name")); + var member = await _repo.CreateMember(conn, systemId, properties.Value("name")); MemberPatch patch; try { 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/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 19b6a625..08554de7 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -38,8 +38,9 @@ namespace PluralKit.Bot // Check group cap var existingGroupCount = await conn.QuerySingleAsync("select count(*) from groups where system = @System", new { System = ctx.System.Id }); - if (existingGroupCount >= 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."); + var groupLimit = ctx.System.GroupLimitOverride ?? Limits.MaxGroupCount; + 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 var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName); 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 737b84f5..93bf1687 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; +using Dapper; + using PluralKit.Core; namespace PluralKit.Bot @@ -37,21 +39,26 @@ namespace PluralKit.Bot // Enforce per-system member limit var memberCount = await _repo.GetSystemMemberCount(conn, ctx.System.Id); - if (memberCount >= Limits.MaxMemberCount) - throw Errors.MemberLimitReachedError; + var memberLimit = ctx.System.MemberLimitOverride ?? Limits.MaxMemberCount; + if (memberCount >= memberLimit) + throw Errors.MemberLimitReachedError(memberLimit); // Create the member var member = await _repo.CreateMember(conn, ctx.System.Id, memberName); 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) @@ -83,4 +90,4 @@ namespace PluralKit.Bot await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); } } -} \ No newline at end of file +} 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/Errors.cs b/PluralKit.Bot/Errors.cs index a136ece0..79600f68 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -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 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 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 InvalidColorError(string color) => new PKError($"\"{color}\" is not a valid color. Color must be in 6-digit RGB hex format (eg. #ff0000)."); 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/Services/WebhookCacheService.cs b/PluralKit.Bot/Services/WebhookCacheService.cs index d2f10e09..99caf97e 100644 --- a/PluralKit.Bot/Services/WebhookCacheService.cs +++ b/PluralKit.Bot/Services/WebhookCacheService.cs @@ -68,7 +68,7 @@ namespace PluralKit.Bot // 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. 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; } diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 0cfb8203..855ae283 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -23,10 +23,10 @@ namespace PluralKit.Bot { else return true; } - 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.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index 96e7d7cb..4ce4b24c 100644 --- a/PluralKit.Bot/Utils/MiscUtils.cs +++ b/PluralKit.Bot/Utils/MiscUtils.cs @@ -52,6 +52,9 @@ namespace PluralKit.Bot // Ignore "Database is shutting down" error 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. return true; } diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 0bc88ad1..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 = 9; + private const int TargetSchemaVersion = 11; private readonly CoreConfig _config; private readonly ILogger _logger; @@ -37,7 +37,10 @@ namespace PluralKit.Core _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; } diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index bdbe51e9..227450ab 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -86,6 +86,15 @@ as $$ where accounts.uid = account_id $$ 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 $$ select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, 5) diff --git a/PluralKit.Core/Database/Migrations/10.sql b/PluralKit.Core/Database/Migrations/10.sql new file mode 100644 index 00000000..f6b966f4 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/10.sql @@ -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; \ No newline at end of file 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/Database/clean.sql b/PluralKit.Core/Database/clean.sql index 3338f4bf..6badec2c 100644 --- a/PluralKit.Core/Database/clean.sql +++ b/PluralKit.Core/Database/clean.sql @@ -9,6 +9,7 @@ drop view if exists group_list; drop function if exists message_context; drop function if exists proxy_members; +drop function if exists has_private_members; drop function if exists generate_hid; drop function if exists find_free_system_hid; drop function if exists find_free_member_hid; diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index 1dd5a8c9..9962e75f 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -23,6 +23,8 @@ namespace PluralKit.Core { public PrivacyLevel FrontPrivacy { get; } public PrivacyLevel FrontHistoryPrivacy { get; } public PrivacyLevel GroupListPrivacy { get; } + public int? MemberLimitOverride { get; } + public int? GroupLimitOverride { get; } [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); } 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 c1e44563..00f09075 100644 --- a/PluralKit.Core/Utils/Limits.cs +++ b/PluralKit.Core/Utils/Limits.cs @@ -5,8 +5,8 @@ namespace PluralKit.Core { public static readonly int MaxSystemNameLength = 100; public static readonly int MaxSystemTagLength = MaxProxyNameLength - 1; - public static readonly int MaxMemberCount = 1500; - public static readonly int MaxMembersWarnThreshold = MaxMemberCount - 50; + public static readonly int MaxMemberCount = 1000; + 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