From fbfba5222734a3f96777b0790fc96e5b9772db8e Mon Sep 17 00:00:00 2001 From: Biquet Date: Tue, 28 Jul 2020 19:29:27 +0200 Subject: [PATCH 01/32] Add upgrade from legacy doc in README (#204) Co-authored-by: Astrid --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cc80f64..0ad45e91 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,34 @@ $ docker-compose up -d (tip: use `scripts/run-test-db.sh` to run a temporary PostgreSQL database on your local system. Requires Docker.) +# Upgrading database from legacy version +There are a few adjustments to do for the new versions of PluralKit to work with a legacy PluralKit database : + +* Dump the database (you never know what may happen !) +* Upgrade the database by firing those lines in psql (or by putting them in an sql file and giving it to postgres) : +```sql +do $$ begin + create type proxy_tag as ( + prefix text, + suffix text + ); +exception when duplicate_object then null; +end $$; + +alter table members add column IF NOT EXISTS display_name text; +alter table members add column IF NOT EXISTS proxy_tags proxy_tag[] not null default array[]::proxy_tag[]; +alter table members add column IF NOT EXISTS keep_proxy bool not null default false; +update members set proxy_tags = array[(members.prefix, members.suffix)]::proxy_tag[] where members.prefix is not null or members.suffix is not null; +alter table members drop column IF EXISTS prefix cascade; +alter table members drop column IF EXISTS suffix cascade; +alter table messages add column IF NOT EXISTS original_mid bigint; +alter table servers add column IF NOT EXISTS log_blacklist bigint[] not null default array[]::bigint[]; +alter table servers add column IF NOT EXISTS blacklist bigint[] not null default array[]::bigint[]; +``` +* Start PluralKit and let it finish the automatic database upgrades + # Documentation See [the docs/ directory](./docs/README.md) # License -This project is under the Apache License, Version 2.0. It is available at the following link: https://www.apache.org/licenses/LICENSE-2.0 \ No newline at end of file +This project is under the Apache License, Version 2.0. It is available at the following link: https://www.apache.org/licenses/LICENSE-2.0 From c20c4dab42a244517d49b51728f3cb9742ce274d Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 25 Jul 2020 06:22:55 -0400 Subject: [PATCH 02/32] use Shard.GetGuild instead of Rest.GetGuild --- PluralKit.Bot/Commands/Misc.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 9ca25343..3ca66bd1 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -105,8 +105,7 @@ namespace PluralKit.Bot { if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse `{guildIdStr}` as an ID."); - // TODO: will this call break for sharding if you try to request a guild on a different bot instance? - guild = await ctx.Rest.GetGuild(guildId); + guild = ctx.Client.GetGuild(guildId); if (guild == null) throw Errors.GuildNotFound(guildId); } From 687dd2323417e86e253e9f70a33e7458682499c0 Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 25 Jul 2020 06:25:45 -0400 Subject: [PATCH 03/32] Fix missing ID error --- PluralKit.Bot/Commands/Misc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 3ca66bd1..4f464fb0 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -101,7 +101,7 @@ namespace PluralKit.Bot { } else { - var guildIdStr = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a server ID or run this command as ."); + var guildIdStr = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a server ID or run this command in a server."); if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse `{guildIdStr}` as an ID."); From bf07294f5f612184f0c807da24f1441c0683aa16 Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 25 Jul 2020 07:05:16 -0400 Subject: [PATCH 04/32] Change error to be more ambiguous --- PluralKit.Bot/Errors.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index 2b6c47ab..c2ab07ce 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -98,7 +98,7 @@ namespace PluralKit.Bot { public static PKError DurationParseError(string durationStr) => new PKError($"Could not parse '{durationStr}' as a valid duration. Try a format such as `30d`, `1d3h` or `20m30s`."); public static PKError FrontPercentTimeInFuture => new PKError("Cannot get the front percent between now and a time in the future."); - public static PKError GuildNotFound(ulong guildId) => new PKError($"Guild with ID {guildId} not found."); + public static PKError GuildNotFound(ulong guildId) => new PKError($"Guild with ID {guildId} not found. Note that you must be a member of the guild you are querying."); public static PKError DisplayNameTooLong(string displayName, int maxLength) => new PKError( $"Display name too long ({displayName.Length} > {maxLength} characters). Use a shorter display name, or shorten your system tag."); From 1138c1a2a92bb79afb231c3dff7a93d3a73cecca Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 25 Jul 2020 07:17:34 -0400 Subject: [PATCH 05/32] Clean up getting guild/member --- PluralKit.Bot/Commands/Misc.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index 4f464fb0..f95bb1d7 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -94,10 +94,12 @@ namespace PluralKit.Bot { public async Task PermCheckGuild(Context ctx) { DiscordGuild guild; + DiscordMember senderGuildUser; if (ctx.Guild != null && !ctx.HasNext()) { guild = ctx.Guild; + senderGuildUser = (DiscordMember)ctx.Author; } else { @@ -106,14 +108,10 @@ namespace PluralKit.Bot { throw new PKSyntaxError($"Could not parse `{guildIdStr}` as an ID."); guild = ctx.Client.GetGuild(guildId); - if (guild == null) - throw Errors.GuildNotFound(guildId); + if (guild == null) throw Errors.GuildNotFound(guildId); + senderGuildUser = await guild.GetMember(ctx.Author.Id); + if (senderGuildUser == null) throw Errors.GuildNotFound(guildId); } - - // Ensure people can't query guilds they're not in + get their own permissions (for view access checking) - var senderGuildUser = await guild.GetMember(ctx.Author.Id); - if (senderGuildUser == null) - throw new PKError("You must be a member of the guild you are querying."); var requiredPermissions = new [] { From 47fcfeca0fb0ffcdb14f30860e84fe805963289d Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 25 Jul 2020 07:25:10 -0400 Subject: [PATCH 06/32] Consolidate conditionals --- PluralKit.Bot/Commands/Misc.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index f95bb1d7..8b9c4bd8 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -94,7 +94,7 @@ namespace PluralKit.Bot { public async Task PermCheckGuild(Context ctx) { DiscordGuild guild; - DiscordMember senderGuildUser; + DiscordMember senderGuildUser = null; if (ctx.Guild != null && !ctx.HasNext()) { @@ -108,9 +108,8 @@ namespace PluralKit.Bot { throw new PKSyntaxError($"Could not parse `{guildIdStr}` as an ID."); guild = ctx.Client.GetGuild(guildId); - if (guild == null) throw Errors.GuildNotFound(guildId); - senderGuildUser = await guild.GetMember(ctx.Author.Id); - if (senderGuildUser == null) throw Errors.GuildNotFound(guildId); + if (guild != null) senderGuildUser = await guild.GetMember(ctx.Author.Id); + if (guild == null || senderGuildUser == null) throw Errors.GuildNotFound(guildId); } var requiredPermissions = new [] From 62cdb8a9b8b71ad2db24484a62f0252e290571f7 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 19:35:34 +0200 Subject: [PATCH 07/32] Check for avatar whitespace instead of null. This may be relevant for #206, although unsure if this is actually a fix. --- PluralKit.Bot/Services/WebhookExecutorService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index 5f7f62ea..3f48ba1d 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -67,7 +67,8 @@ namespace PluralKit.Bot dwb.WithUsername(FixClyde(name).Truncate(80)); dwb.WithContent(content); dwb.AddMentions(content.ParseAllMentions(allowEveryone, channel.Guild)); - if (avatarUrl != null) dwb.WithAvatarUrl(avatarUrl); + if (!string.IsNullOrWhiteSpace(avatarUrl)) + dwb.WithAvatarUrl(avatarUrl); var attachmentChunks = ChunkAttachmentsOrThrow(attachments, 8 * 1024 * 1024); if (attachmentChunks.Count > 0) From 8e32b074665b3b80e89b6cd62ea67c284f2bb567 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 19:41:17 +0200 Subject: [PATCH 08/32] Move legacy migration docs to a separate file and reword --- LEGACYMIGRATE.md | 37 +++++++++++++++++++++++++++++++++++++ README.md | 26 ++------------------------ 2 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 LEGACYMIGRATE.md diff --git a/LEGACYMIGRATE.md b/LEGACYMIGRATE.md new file mode 100644 index 00000000..a08c3b82 --- /dev/null +++ b/LEGACYMIGRATE.md @@ -0,0 +1,37 @@ +# Legacy bot migration +Until the introduction of the database migration system around December 2019, migrations were done manually. + +To bridge the gap between the `legacy` branch's database schema and something the modern migration system can work with, run the following SQL commands on the database: + +```sql +-- Create the proxy_tag type +do $$ begin + create type proxy_tag as ( + prefix text, + suffix text + ); +exception when duplicate_object then null; +end $$; + +-- Add new columns to `members` +alter table members add column IF NOT EXISTS display_name text; +alter table members add column IF NOT EXISTS proxy_tags proxy_tag[] not null default array[]::proxy_tag[]; +alter table members add column IF NOT EXISTS keep_proxy bool not null default false; + +-- Transfer member proxy tags from the `prefix` and `suffix` columns to the `proxy_tags` array +update members set proxy_tags = array[(members.prefix, members.suffix)]::proxy_tag[] + where members.prefix is not null or members.suffix is not null; + +-- Add other columns +alter table messages add column IF NOT EXISTS original_mid bigint; +alter table servers add column IF NOT EXISTS log_blacklist bigint[] not null default array[]::bigint[]; +alter table servers add column IF NOT EXISTS blacklist bigint[] not null default array[]::bigint[]; + +-- Drop old proxy tag columns +alter table members drop column IF EXISTS prefix cascade; +alter table members drop column IF EXISTS suffix cascade; +``` + +You should probably take a database backup before doing any of this. + +The .NET version of the bot should pick up on any further migrations from this point :) \ No newline at end of file diff --git a/README.md b/README.md index 0ad45e91..1c750a17 100644 --- a/README.md +++ b/README.md @@ -55,30 +55,8 @@ $ docker-compose up -d (tip: use `scripts/run-test-db.sh` to run a temporary PostgreSQL database on your local system. Requires Docker.) # Upgrading database from legacy version -There are a few adjustments to do for the new versions of PluralKit to work with a legacy PluralKit database : - -* Dump the database (you never know what may happen !) -* Upgrade the database by firing those lines in psql (or by putting them in an sql file and giving it to postgres) : -```sql -do $$ begin - create type proxy_tag as ( - prefix text, - suffix text - ); -exception when duplicate_object then null; -end $$; - -alter table members add column IF NOT EXISTS display_name text; -alter table members add column IF NOT EXISTS proxy_tags proxy_tag[] not null default array[]::proxy_tag[]; -alter table members add column IF NOT EXISTS keep_proxy bool not null default false; -update members set proxy_tags = array[(members.prefix, members.suffix)]::proxy_tag[] where members.prefix is not null or members.suffix is not null; -alter table members drop column IF EXISTS prefix cascade; -alter table members drop column IF EXISTS suffix cascade; -alter table messages add column IF NOT EXISTS original_mid bigint; -alter table servers add column IF NOT EXISTS log_blacklist bigint[] not null default array[]::bigint[]; -alter table servers add column IF NOT EXISTS blacklist bigint[] not null default array[]::bigint[]; -``` -* Start PluralKit and let it finish the automatic database upgrades +If you have an instance of the Python version of the bot (from the `legacy` branch), you may need to take extra database migration steps. +For more information, see [LEGACYMIGRATE.md](./LEGACYMIGRATE.md). # Documentation See [the docs/ directory](./docs/README.md) From 8056144899b6fc31ec2b4b4611e9dab59887d6ef Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 19:43:52 +0200 Subject: [PATCH 09/32] master branch was deleted, update docs accordingly --- BRANCHRENAME.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BRANCHRENAME.md b/BRANCHRENAME.md index d61ee33f..3584ea8d 100644 --- a/BRANCHRENAME.md +++ b/BRANCHRENAME.md @@ -14,5 +14,5 @@ you'll need to update the branch references, like so: (steps from https://dev.to/rhymu8354/git-renaming-the-master-branch-137b) -I'm going to re-branch `master` from `main`, leaving it at this notice's commit, and then delete in a week's time -so people have a chance to migrate. Hopefully this doesn't cause too much breakage. (if it does, do yell at me in the issues) \ No newline at end of file +The `master` branch was fully deleted on 2020-07-28. +If you get an error on pull on an old clone, that's why. The commands above should still work, though. \ No newline at end of file From fb236726aa752d98580e038f02f99d5139eee800 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 19:52:57 +0200 Subject: [PATCH 10/32] Consolidate conditional --- PluralKit.Bot/Commands/MemberEdit.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index b7dc3e94..18db4a58 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -36,8 +36,8 @@ namespace PluralKit.Bot // Warn if there's already a member by this name var existingMember = await _data.GetMemberByName(ctx.System, newName); - if (existingMember != null) - if (existingMember.Id != target.Id) { + if (existingMember != null && existingMember.Id != target.Id) + { var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?"; if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled."); } From 45775f2e8e2b4ca3f50c63f4439933c029c4ba69 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 19:59:28 +0200 Subject: [PATCH 11/32] Remove unversioned API endpoints --- PluralKit.API/Controllers/v1/AccountController.cs | 1 - PluralKit.API/Controllers/v1/MemberController.cs | 1 - PluralKit.API/Controllers/v1/MessageController.cs | 1 - PluralKit.API/Controllers/v1/SystemController.cs | 1 - PluralKit.API/Startup.cs | 7 +------ 5 files changed, 1 insertion(+), 10 deletions(-) diff --git a/PluralKit.API/Controllers/v1/AccountController.cs b/PluralKit.API/Controllers/v1/AccountController.cs index 0ffec724..b563bfca 100644 --- a/PluralKit.API/Controllers/v1/AccountController.cs +++ b/PluralKit.API/Controllers/v1/AccountController.cs @@ -10,7 +10,6 @@ namespace PluralKit.API { [ApiController] [ApiVersion("1.0")] - [Route("a")] [Route( "v{version:apiVersion}/a" )] public class AccountController: ControllerBase { diff --git a/PluralKit.API/Controllers/v1/MemberController.cs b/PluralKit.API/Controllers/v1/MemberController.cs index 769a682d..30bc8bef 100644 --- a/PluralKit.API/Controllers/v1/MemberController.cs +++ b/PluralKit.API/Controllers/v1/MemberController.cs @@ -13,7 +13,6 @@ namespace PluralKit.API { [ApiController] [ApiVersion("1.0")] - [Route("m")] [Route( "v{version:apiVersion}/m" )] public class MemberController: ControllerBase { diff --git a/PluralKit.API/Controllers/v1/MessageController.cs b/PluralKit.API/Controllers/v1/MessageController.cs index f3738742..f4f12f67 100644 --- a/PluralKit.API/Controllers/v1/MessageController.cs +++ b/PluralKit.API/Controllers/v1/MessageController.cs @@ -25,7 +25,6 @@ namespace PluralKit.API [ApiController] [ApiVersion("1.0")] - [Route("msg")] [Route( "v{version:apiVersion}/msg" )] public class MessageController: ControllerBase { diff --git a/PluralKit.API/Controllers/v1/SystemController.cs b/PluralKit.API/Controllers/v1/SystemController.cs index b092989b..ef44acf1 100644 --- a/PluralKit.API/Controllers/v1/SystemController.cs +++ b/PluralKit.API/Controllers/v1/SystemController.cs @@ -36,7 +36,6 @@ namespace PluralKit.API [ApiController] [ApiVersion("1.0")] - [Route("s")] [Route( "v{version:apiVersion}/s" )] public class SystemController : ControllerBase { diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs index 80f3fb3e..0c041fce 100644 --- a/PluralKit.API/Startup.cs +++ b/PluralKit.API/Startup.cs @@ -54,16 +54,11 @@ namespace PluralKit.API .SetCompatibilityVersion(CompatibilityVersion.Latest) .AddNewtonsoftJson(); // sorry MS, this just does *more* - services.AddApiVersioning(c => - { - c.AssumeDefaultVersionWhenUnspecified = true; - c.DefaultApiVersion = ApiVersion.Parse("1.0"); - }); + services.AddApiVersioning(); services.AddVersionedApiExplorer(c => { c.GroupNameFormat = "'v'VV"; - c.DefaultApiVersion = ApiVersion.Parse("1.0"); c.ApiVersionParameterSource = new UrlSegmentApiVersionReader(); c.SubstituteApiVersionInUrl = true; }); From 0fd56d83e169275a196140b203708c0e53debec6 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 20:01:30 +0200 Subject: [PATCH 12/32] Document removal of unversioned API endpoints --- docs/content/api-documentation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/api-documentation.md b/docs/content/api-documentation.md index 5308e55e..54179ec6 100644 --- a/docs/content/api-documentation.md +++ b/docs/content/api-documentation.md @@ -519,6 +519,8 @@ The returned system and member's privacy settings will be respected, and as such ``` ## Version history +* 2020-07-28 + * The unversioned API endpoints have been removed. * 2020-06-17 (v1.1) * The API now has values for granular member privacy. The new fields are as follows: `visibility`, `name_privacy`, `description_privacy`, `avatar_privacy`, `birthday_privacy`, `pronoun_privacy`, `metadata_privacy`. All are strings and accept the values of `public`, `private` and `null`. * The `privacy` field has now been deprecated and should not be used. It's still returned (mirroring the `visibility` field), and writing to it will write to *all privacy options*. From 9fc042eddf2dc8ea26f95d535da09fe040e510ef Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 20:11:34 +0200 Subject: [PATCH 13/32] Remove commented-out hero front matter --- docs/content/index.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/content/index.md b/docs/content/index.md index 8d44c73f..93d1e5de 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -1,17 +1,4 @@ --- -# home: true -# heroImage: https://v1.vuepress.vuejs.org/hero.png -# tagline: Documentation for PluralKit -# actionText: Quick Start → -# actionLink: /guide/ -# features: -# - title: Feature 1 Title -# details: Feature 1 Description -# - title: Feature 2 Title -# details: Feature 2 Description -# - title: Feature 3 Title -# details: Feature 3 Description -# footer: Made by with ❤️ title: Home --- From 93e4275b9758ca4611af09b1e7336255e780f0fe Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 20:11:45 +0200 Subject: [PATCH 14/32] Fix the edit link on the docs --- docs/content/.vuepress/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/.vuepress/config.js b/docs/content/.vuepress/config.js index 4cd06925..b7000005 100644 --- a/docs/content/.vuepress/config.js +++ b/docs/content/.vuepress/config.js @@ -18,7 +18,7 @@ module.exports = { themeConfig: { repo: 'xSke/PluralKit', - docsDir: 'docs', + docsDir: 'docs/content/', docsBranch: 'main', editLinks: true, editLinkText: 'Help us improve this page!', From 80fe6729cbaef1b9a6b250a01a0d2047fb416eba Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 23:13:32 +0200 Subject: [PATCH 15/32] Expand on documentation aimed at server staff --- docs/content/.vuepress/config.js | 11 +++++++ docs/content/assets/log_example.png | Bin 0 -> 49484 bytes docs/content/command-list.md | 8 +++--- docs/content/staff/compatibility.md | 43 ++++++++++++++++++++++++++++ docs/content/staff/disabling.md | 9 ++++++ docs/content/staff/logging.md | 18 ++++++++++++ docs/content/staff/moderation.md | 34 ++++++++++++++++++++++ docs/content/staff/permissions.md | 31 ++++++++++++++++++++ docs/content/tips-and-tricks.md | 14 +-------- docs/content/user-guide.md | 43 ---------------------------- 10 files changed, 151 insertions(+), 60 deletions(-) create mode 100644 docs/content/assets/log_example.png create mode 100644 docs/content/staff/compatibility.md create mode 100644 docs/content/staff/disabling.md create mode 100644 docs/content/staff/logging.md create mode 100644 docs/content/staff/moderation.md create mode 100644 docs/content/staff/permissions.md diff --git a/docs/content/.vuepress/config.js b/docs/content/.vuepress/config.js index b7000005..abd2de20 100644 --- a/docs/content/.vuepress/config.js +++ b/docs/content/.vuepress/config.js @@ -46,6 +46,17 @@ module.exports = { "/tips-and-tricks" ] }, + { + title: "For server staff", + collapsable: false, + children: [ + "/staff/permissions", + "/staff/moderation", + "/staff/disabling", + "/staff/logging", + "/staff/compatibility", + ] + }, ["https://discord.gg/PczBt78", "Join the support server"], ] }, diff --git a/docs/content/assets/log_example.png b/docs/content/assets/log_example.png new file mode 100644 index 0000000000000000000000000000000000000000..205833f5d98cf4108c556346d33d1c923f939bde GIT binary patch literal 49484 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 z zbelHX_w~^FAq|G#>{pHq-2 zGXFmphM<@JZ=KKkG$>5|@73x&Kj*yIR3p9mBJfXF8TfdH^yK09S`;aW3_0lY$eGd0 zv%wwz|9PXL2tR*X>BB_YuF^CdG7n_4mg21_AK*zK9Z>DAL~t p@oFNIaDN|c^w!r`Z literal 0 HcmV?d00001 diff --git a/docs/content/command-list.md b/docs/content/command-list.md index efb9b18a..d6cc2828 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -68,7 +68,7 @@ Words in **\** or **[square brackets]** mean fill-in-the-blank. - `pk;log channel ` - Sets the given channel to log all proxied messages. - `pk;log disable <#channel> [#channel...]` - Disables logging messages posted in the given channel(s) (useful for staff channels and such). - `pk;log enable <#channel> [#channel...]` - Re-enables logging messages posted in the given channel(s). -- `pk;logclean ` - Enables or disables [log cleanup](/guide#log-cleanup). +- `pk;logclean ` - Enables or disables [log cleanup](./staff/compatibility.md#log-cleanup). - `pk;blacklist add <#channel> [#channel...]` - Adds the given channel(s) to the proxy blacklist (proxying will be disabled here) - `pk;blacklist remove <#channel> [#channel...]` - Removes the given channel(s) from the proxy blacklist. @@ -77,16 +77,16 @@ Words in **\** or **[square brackets]** mean fill-in-the-blank. - `pk;invite` - Sends the bot invite link for PluralKit. - `pk;import` - Imports a data file from PluralKit or Tupperbox. - `pk;export` - Exports a data file containing your system information. -- `pk;permcheck [server id]` - [Checks the given server's permission setup](/tips#permission-checker-command) to check if it's compatible with PluralKit. +- `pk;permcheck [server id]` - [Checks the given server's permission setup](./staff/permissions.md#permission-checker-command) to check if it's compatible with PluralKit. ## API -*(for using the [PluralKit API](/api), useful for developers)* +*(for using the [PluralKit API](./api-documentation.md), useful for developers)* - `pk;token` - DMs you a token for using the PluralKit API. - `pk;token refresh` - Refreshes your API token and invalidates the old one. ## Help - `pk;help` - Displays a basic help message describing how to use the bot. -- `pk;help proxy` - Directs you to [this page](/guide#proxying). +- `pk;help proxy` - Directs you to [this page](./user-guide.md#proxying). - `pk;system help` - Lists system-related commands. - `pk;member help` - Lists member-related commands. - `pk;switch help` - Lists switch-related commands. diff --git a/docs/content/staff/compatibility.md b/docs/content/staff/compatibility.md new file mode 100644 index 00000000..758dfa06 --- /dev/null +++ b/docs/content/staff/compatibility.md @@ -0,0 +1,43 @@ +# Compatibility with other bots +Many servers use *logger bots* for keeping track of edited and deleted messages, nickname changes, and other server events. +Because PluralKit deletes messages as part of proxying, this can often clutter up these logs. + +## Bots with PluralKit support +Some logger bots have offical PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages: + +- [**Gabby Gums**](https://github.com/amadea-system/GabbyGums) + +If your server uses an in-house bot for logging, you can use [the API](../api-documentation.md) to implement support yourself. + +## Log cleanup +Another solution is for PluralKit to automatically delete log messages from other bots when they get posted. +PluralKit supports this through the **log cleanup** feature. To enable it, use the following command: + + pk;logclean on + +This requires you to have the *Manage Server* permission on the server. + +### Supported bots +At the moment, log cleanup works with the following bots: +- [Auttaja](https://auttaja.io/) (precise) +- [blargbot](https://blargbot.xyz/) (precise) +- [Carl-bot](https://carl.gg/) (fuzzy) +- [Circle](https://circlebot.xyz/) (fuzzy) +- [Dyno](https://dyno.gg/) (precise) +- [GearBot](https://gearbot.rocks/) (fuzzy) +- [GenericBot](https://github.com/galenguyer/GenericBot) (precise) +- [Logger#6088](https://logger.bot/) (precise) +- [Logger#6278](https://loggerbot.chat/) (precise) +- [Mantaro](https://mantaro.site/) (precise) +- [Pancake](https://pancake.gg/) (fuzzy) +- [SafetyAtLast](https://www.safetyatlast.net/) (fuzzy) +- [UnbelievaBoat](https://unbelievaboat.com/) (precise) +- Vanessa (fuzzy) + +::: warning +In most cases, PluralKit will match log messages by the ID of the deleted message itself. However, some bots (marked with *(fuzzy)* above) don't include this in their logs. In this case, PluralKit will attempt to match based on other parameters, but there may be false positives. + +**For best results, use a bot marked *(precise)* in the above list.** +::: + +If you want support for another logging bot, [let me know on the support server](https://discord.gg/PczBt78). \ No newline at end of file diff --git a/docs/content/staff/disabling.md b/docs/content/staff/disabling.md new file mode 100644 index 00000000..2e5b62f4 --- /dev/null +++ b/docs/content/staff/disabling.md @@ -0,0 +1,9 @@ +# Disabling proxying in a channel +It's possible to block a channel from being used for proxying. To do so, use the `pk;blacklist` command. For example: + + pk;blacklist add #admin-channel #mod-channel #welcome + pk;blacklist add all + pk;blacklist remove #general-two + pk;blacklist remove all + +This requires you to have the *Manage Server* permission on the server. \ No newline at end of file diff --git a/docs/content/staff/logging.md b/docs/content/staff/logging.md new file mode 100644 index 00000000..673f5766 --- /dev/null +++ b/docs/content/staff/logging.md @@ -0,0 +1,18 @@ +# Proxy logging +If you want to log every proxied message to a separate channel for moderation purposes, you can use the `pk;log` command with the channel name.For example: + + pk;log #proxy-log + +This requires you to have the *Manage Server* permission on the server. To disable logging, use the `pk;log` command with no channel name. + +Log messages have the following format: + +![Example log message from PluralKit](../assets/log_example.png) + +## Blacklisting channels from logging +Depending on your server setup, you may want to exclude some messages from logging. For example, if you have public proxy logs, you may want to exclude staff-only channels. + +To manage logging in a channel, use the following commands: + + pk;log disable #some-secret-channel + pk;log enable #some-secret-channel \ No newline at end of file diff --git a/docs/content/staff/moderation.md b/docs/content/staff/moderation.md new file mode 100644 index 00000000..8966ee47 --- /dev/null +++ b/docs/content/staff/moderation.md @@ -0,0 +1,34 @@ +# Moderation tools +Since PluralKit proxies work by deleting and reposting messages through webhooks, some of Discord's standard moderation tools won't function. + +Specifically, you can't kick or ban individual members of a system; all moderation actions have to be taken on the concrete Discord account. + +## Identifying users +You can use PluralKit's lookup tools to connect a message to the sender account. This allows you to use standard moderation tools on that account (kicking, banning, using other moderation tools, etc). + +### Querying messages +To look up which account's behind a given message (as well as other information), you can either: + +- React to the message with the :question: emoji, which will DM you a message card +- Use the `pk;msg ` command with the message's link, which will reply with a message card *(this also works in PluralKit's DMs)* + +An example of a message card is seen below: + +![Example of a message query card](../assets/ExampleQuery.png) + +### Looking up systems and accounts +Looking up a system by its 5-character ID (`exmpl` in the above screenshot) will show you a list of its linked account IDs. For example: + + pk;system exmpl + +You can also do the reverse operation by passing a Discord account ID (or a @mention), like so: + + pk;system 466378653216014359 + +Both commands output a system card, which includes a linked account list. These commands also work in PluralKit's DMs. + +### System tags +A common rule on servers with PluralKit is to enforce system tags. System tags are a little snippet of text, a symbol, an emoji, etc, that's added to the webhook name of every message proxied by a system. A system tag will allow you to identify members that share a system at a glance. Note that this isn't enforced by the bot; this is simply a suggestion for a helpful server policy :slightly_smiling_face: + +## Blocking users +It's not possible to block specific PluralKit users. Discord webhooks don't count as 'real accounts', so there's no way to block them. PluralKit also can't control who gets to see a message, so there's also no way to implement user blocking on the bot's end. Sorry. :slightly_frowning_face: \ No newline at end of file diff --git a/docs/content/staff/permissions.md b/docs/content/staff/permissions.md new file mode 100644 index 00000000..0a6a14c0 --- /dev/null +++ b/docs/content/staff/permissions.md @@ -0,0 +1,31 @@ +# Roles and permissions + +PluralKit requires some channel permissions in order to function properly: + +- Message proxying requires the **Manage Messages** and **Manage Webhooks** permissions in a channel. +- Most commands require the **Embed Links**, **Attach Files** and **Add Reactions** permissions to function properly. + - Commands with reaction menus also require **Manage Messages** to remove reactions after clicking. +- [Proxy logging](./logging.md) requires the **Send Messages** permission in the log channel. +- [Log cleanup](./compatibility.md#log-cleanup) requires the **Manage Messages** permission in the log channels. + +Denying the **Send Messages** permission will *not* stop the bot from proxying, although it will prevent it from sending command responses. Denying the **Read Messages** permission will, as any other bot, prevent the bot from interacting in that channel at all. + +## Webhook permissions +Webhooks exist outside of the normal Discord permissions system, and (with a few exceptions) it's not possible to modify their permissions. + +However, PluralKit will make an attempt to apply the sender account's permissions to proxied messages. For example, role mentions, `@everyone`, and `@here` +will only function if the sender account has that permission. The same applies to link embeds. + +## Troubleshooting + +### Permission checker command +To quickly check if PluralKit is missing channel permissions, you can use the `pk;permcheck` command in the server +in question. It'll return a list of channels on the server with missing permissions. This may include channels +you don't want PluralKit to have access to for one reason or another (eg. admin channels). + +If you want to check permissions in DMs, you'll need to add a server ID, and run the command with that. +For example: + + pk;permcheck 466707357099884544 + +You can find this ID [by enabling Developer Mode and right-clicking (or long-pressing) on the server icon](https://discordia.me/developer-mode). \ No newline at end of file diff --git a/docs/content/tips-and-tricks.md b/docs/content/tips-and-tricks.md index 74b5e00b..03dcfd15 100644 --- a/docs/content/tips-and-tricks.md +++ b/docs/content/tips-and-tricks.md @@ -24,16 +24,4 @@ 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| - -## Permission checker command -If you're having issues with PluralKit not proxying, it may be an issue with your server's channel permission setup. -PluralKit needs the *Read Messages*, *Manage Messages* and *Manage Webhooks* permission to function. - -To quickly check if PluralKit is missing channel permissions, you can use the `pk;permcheck` command in the server -in question. It'll return a list of channels on the server with missing permissions. This may include channels -you don't want PluralKit to have access to for one reason or another (eg. admin channels). - -If you want to check permissions in DMs, you'll need to add a server ID, and run the command with that. -For example: `pk;permcheck 466707357099884544`. You can find this ID -[by enabling Developer Mode and right-clicking (or long-pressing) on the server icon](https://discordia.me/developer-mode). \ No newline at end of file +|pk;autoproxy|pk;ap| \ No newline at end of file diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index e1db6995..65644b18 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -473,49 +473,6 @@ For example: pk;member Robert privacy birthday public pk;member Skyler privacy all private -## Moderation commands - -### Log channel -If you want to log every proxied message to a separate channel for moderation purposes, you can use the `pk;log` command with the channel name. -This requires you to have the *Manage Server* permission on the server. For example: - - pk;log #proxy-log - -To disable logging, use the `pk;log` command with no channel name. - -### Channel blacklisting -It's possible to blacklist a channel from being used for proxying. To do so, use the `pk;blacklist` command, for examplle: - - pk;blacklist add #admin-channel #mod-channel #welcome - pk;blacklist add all - pk;blacklist remove #general-two - pk;blacklist remove all - -This requires you to have the *Manage Server* permission on the server. - -### Log cleanup -Many servers use *logger bots* for keeping track of edited and deleted messages, nickname changes, and other server events. Because -PluralKit deletes messages as part of proxying, this can often clutter up these logs. To remedy this, PluralKit can delete those -log messages from the logger bots. To enable this, use the following command: - - pk;logclean on - -This requires you to have the *Manage Server* permission on the server. At the moment, log cleanup works with the following bots: -- Auttaja -- blargbot -- Carl-bot -- Circle -- Dyno -- GenericBot -- Logger (#6088 and #6278) -- Mantaro -- Pancake -- UnbelievaBoat - -If you want support for another logging bot, [let me know on the support server](https://discord.gg/PczBt78). - -Another alternative is to use the **Gabby Gums** logging bot - an invite link for which can be found [on Gabby Gums' support server](https://discord.gg/Xwhk89T). - ## Importing and exporting data If you're a user of another proxy bot (eg. Tupperbox), or you want to import a saved system backup, you can use the importing and exporting commands. From 2d755edc892c8268cb707e0a0c9c393cacc13a49 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 28 Jul 2020 23:16:41 +0200 Subject: [PATCH 16/32] Expand on FAQ with content from the support server --- docs/content/faq.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/content/faq.md b/docs/content/faq.md index 216c42d9..53400a88 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -14,3 +14,9 @@ Although this bot is designed with plural systems and their use cases in mind, t ## How can I support the bot's development? I (the bot author, [Ske](https://twitter.com/floofstrid)) have a Patreon. The income from there goes towards server hosting, domains, infrastructure, my Monster Energy addiction, et cetera. There are no benefits. There might never be any. But nevertheless, it can be found here: [https://www.patreon.com/floofstrid](https://www.patreon.com/floofstrid) + +## The name color doesn't work/can we color our proxy names? +No. This is a limitation in Discord itself, and cannot be changed. The color command instead colors your member card that come up when you type `pk;member `. + +## Is it possible to block proxied messages (like blocking a user)? +No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/ \ No newline at end of file From 3c5aec1df89b7776009dc548a9cf9a790d2ddf8e Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 28 Jul 2020 17:56:40 -0400 Subject: [PATCH 17/32] Show bot status as "Idle" when restarting --- PluralKit.Bot/Bot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 68a1af4e..b6b71e99 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -90,7 +90,7 @@ namespace PluralKit.Bot // We're not actually properly disconnecting from the gateway (lol) so it'll linger for a few minutes // Should be plenty of time for the bot to connect again next startup and set the real status if (_hasReceivedReady) - await _client.UpdateStatusAsync(new DiscordActivity("Restarting... (please wait)")); + await _client.UpdateStatusAsync(new DiscordActivity("Restarting... (please wait)"), UserStatus.Idle); } private Task HandleEvent(T evt) where T: DiscordEventArgs From 2f8f819e221487d7065d7aa1ce4e8726d94d0cff Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 28 Jul 2020 17:57:26 -0400 Subject: [PATCH 18/32] Add "n" as an alias to "new" for creating a system --- PluralKit.Bot/Commands/CommandTree.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 08fd4a6e..fba7cc0a 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -175,7 +175,7 @@ namespace PluralKit.Bot await ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System)); // First, we match own-system-only commands (ie. no target system parameter) - else if (ctx.Match("new", "create", "make", "add", "register", "init")) + else if (ctx.Match("new", "create", "make", "add", "register", "init", "n")) await ctx.Execute(SystemNew, m => m.New(ctx)); else if (ctx.Match("name", "rename", "changename")) await ctx.Execute(SystemRename, m => m.Name(ctx)); From 2dee8ab1ae1653dbc69dc49b94a4690991e2749d Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 28 Jul 2020 18:05:00 -0400 Subject: [PATCH 19/32] Document external emoji permissions --- docs/content/staff/permissions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/staff/permissions.md b/docs/content/staff/permissions.md index 0a6a14c0..3f6b9595 100644 --- a/docs/content/staff/permissions.md +++ b/docs/content/staff/permissions.md @@ -16,6 +16,8 @@ Webhooks exist outside of the normal Discord permissions system, and (with a few However, PluralKit will make an attempt to apply the sender account's permissions to proxied messages. For example, role mentions, `@everyone`, and `@here` will only function if the sender account has that permission. The same applies to link embeds. +For external emojis to work in proxied messages, the @everyone role must have the "Use External Emojis" permission. If it still doesn't work, check if the permission was denied in channel-specific permission settings. + ## Troubleshooting ### Permission checker command From 4006d353f2bd0ea2e40d0b83aed10a1d96912b11 Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 28 Jul 2020 18:13:39 -0400 Subject: [PATCH 20/32] fix formatting --- docs/content/staff/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/staff/permissions.md b/docs/content/staff/permissions.md index 3f6b9595..8d86de13 100644 --- a/docs/content/staff/permissions.md +++ b/docs/content/staff/permissions.md @@ -16,7 +16,7 @@ Webhooks exist outside of the normal Discord permissions system, and (with a few However, PluralKit will make an attempt to apply the sender account's permissions to proxied messages. For example, role mentions, `@everyone`, and `@here` will only function if the sender account has that permission. The same applies to link embeds. -For external emojis to work in proxied messages, the @everyone role must have the "Use External Emojis" permission. If it still doesn't work, check if the permission was denied in channel-specific permission settings. +For external emojis to work in proxied messages, the `@everyone` role must have the "Use External Emojis" permission. If it still doesn't work, check if the permission was denied in channel-specific permission settings. ## Troubleshooting From d9c644ec0e983d6d163cf301b79842fb050846c8 Mon Sep 17 00:00:00 2001 From: acw0 Date: Sat, 1 Aug 2020 14:31:46 -0400 Subject: [PATCH 21/32] Change "channel not found" error messages to be more ambiguous; also, put them in Errors instead of inline --- PluralKit.Bot/Commands/ServerConfig.cs | 18 +++++++++++------- PluralKit.Bot/Errors.cs | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index b21ea868..d6fa8fcb 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -25,9 +25,11 @@ namespace PluralKit.Bot ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server"); DiscordChannel channel = null; - if (ctx.HasNext()) - channel = await ctx.MatchChannel() ?? throw new PKSyntaxError("You must pass a #channel to set."); - if (channel != null && channel.GuildId != ctx.Guild.Id) throw new PKError("That channel is not in this server!"); + if (!ctx.HasNext()) + throw new PKSyntaxError("You must pass a #channel to set."); + 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}; await _db.Execute(conn => conn.UpsertGuild(ctx.Guild.Id, patch)); @@ -48,8 +50,9 @@ namespace PluralKit.Bot else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { - var channel = await ctx.MatchChannel() ?? throw new PKSyntaxError($"Channel \"{ctx.PopArgument()}\" not found."); - if (channel.GuildId != ctx.Guild.Id) throw new PKError($"Channel {ctx.Guild.Id} is not in this server."); + var channelString = ctx.PeekArgument(); + var channel = await ctx.MatchChannel(); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } @@ -127,8 +130,9 @@ namespace PluralKit.Bot else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); else while (ctx.HasNext()) { - var channel = await ctx.MatchChannel() ?? throw new PKSyntaxError($"Channel \"{ctx.PopArgument()}\" not found."); - if (channel.GuildId != ctx.Guild.Id) throw new PKError($"Channel {ctx.Guild.Id} is not in this server."); + var channelString = ctx.PeekArgument(); + var channel = await ctx.MatchChannel(); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); affectedChannels.Add(channel); } diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index c2ab07ce..bef64691 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -114,5 +114,6 @@ namespace PluralKit.Bot { public static PKError AttachmentTooLarge => new PKError("PluralKit cannot proxy attachments over 8 megabytes (as webhooks aren't considered as having Discord Nitro) :("); public static PKError LookupNotAllowed => new PKError("You do not have permission to access this information."); + public static PKError ChannelNotFound(string channelString) => new PKError($"Channel \"{channelString}\" not found or is not in this server."); } } \ No newline at end of file From 687eaaa928dbb20d6c5146d13f501d70d889d106 Mon Sep 17 00:00:00 2001 From: ent3r <32072697+ent3r@users.noreply.github.com> Date: Mon, 3 Aug 2020 17:18:51 +0200 Subject: [PATCH 22/32] Fix formatting Removed bold on a colon --- PluralKit.Bot/Commands/Lists/ContextListExt.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index e687eeb8..f1efadb0 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -136,7 +136,7 @@ namespace PluralKit.Bot profile.Append($"\n**Birthdate**: {m.BirthdayString}"); if (m.ProxyTags.Count > 0) - profile.Append($"\n**Proxy tags:** {m.ProxyTagsString()}"); + profile.Append($"\n**Proxy tags**: {m.ProxyTagsString()}"); if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is {} count && count > 0) profile.Append($"\n**Message count:** {count}"); @@ -161,4 +161,4 @@ namespace PluralKit.Bot } } } -} \ No newline at end of file +} From f6d2f4b6209b5d5ddeb56f3438cad4c6bf869a75 Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 4 Aug 2020 18:43:17 -0400 Subject: [PATCH 23/32] Add -all flag in random command --- PluralKit.Bot/Commands/Member.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index f9285f2b..92fb5dcb 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using PluralKit.Core; @@ -61,7 +62,11 @@ namespace PluralKit.Bot //Maybe move this somewhere else in the file structure since it doesn't need to get created at every command // TODO: don't buffer these, find something else to do ig - var members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberVisibility == PrivacyLevel.Public).ToListAsync(); + + List members; + if (ctx.MatchFlag("all", "a")) members = await _data.GetSystemMembers(ctx.System).ToListAsync(); + else members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberVisibility == PrivacyLevel.Public).ToListAsync(); + if (members == null || !members.Any()) throw Errors.NoMembersError; var randInt = randGen.Next(members.Count); From df7fdce14498ef73494cae20582a6a14be2a5699 Mon Sep 17 00:00:00 2001 From: acw0 Date: Tue, 4 Aug 2020 19:28:29 -0400 Subject: [PATCH 24/32] Add sorting member list randomly --- PluralKit.Bot/Commands/Lists/ContextListExt.cs | 3 ++- PluralKit.Bot/Commands/Lists/MemberListOptions.cs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index f1efadb0..8bee0927 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -40,7 +40,8 @@ namespace PluralKit.Bot if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls")) p.SortProperty = SortProperty.LastSwitch; if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage; if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate; - + if (ctx.MatchFlag("random")) p.SortProperty = SortProperty.Random; + // Sort reverse? if (ctx.MatchFlag("r", "rev", "reverse")) p.Reverse = true; diff --git a/PluralKit.Bot/Commands/Lists/MemberListOptions.cs b/PluralKit.Bot/Commands/Lists/MemberListOptions.cs index 9e00b226..2f6a7aad 100644 --- a/PluralKit.Bot/Commands/Lists/MemberListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/MemberListOptions.cs @@ -75,6 +75,8 @@ namespace PluralKit.Bot IComparer ReverseMaybe(IComparer c) => opts.Reverse ? Comparer.Create((a, b) => c.Compare(b, a)) : c; + var randGen = new global::System.Random(); + var culture = StringComparer.InvariantCultureIgnoreCase; return (opts.SortProperty switch { @@ -96,6 +98,8 @@ namespace PluralKit.Bot SortProperty.LastSwitch => input .OrderByDescending(m => m.LastSwitchTime.HasValue) .ThenByDescending(m => m.LastSwitchTime, ReverseMaybe(Comparer.Default)), + SortProperty.Random => input + .OrderBy(m => randGen.Next()), _ => throw new ArgumentOutOfRangeException($"Unknown sort property {opts.SortProperty}") }) // Lastly, add a by-name fallback order for collisions (generally hits w/ lots of null values) @@ -112,7 +116,8 @@ namespace PluralKit.Bot CreationDate, LastSwitch, LastMessage, - Birthdate + Birthdate, + Random } public enum ListType From 7ab5e66d7bf7eb301644986d4b2468bce7621447 Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 5 Aug 2020 20:20:11 +0200 Subject: [PATCH 25/32] Show new color in the member color change embed --- PluralKit.Bot/Commands/MemberEdit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 18db4a58..a1ffda09 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -187,7 +187,7 @@ namespace PluralKit.Bot await ctx.Reply(embed: new DiscordEmbedBuilder() .WithTitle($"{Emojis.Success} Member color changed.") .WithColor(color.ToDiscordColor().Value) - .WithThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") + .WithThumbnail($"https://fakeimg.pl/256x256/{color}/?text=%20") .Build()); } } From a0fc9d3826acba83d9e64407c13859cccb3aa8bf Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 5 Aug 2020 20:24:51 +0200 Subject: [PATCH 26/32] Move some extension methods into their respective class files --- PluralKit.Core/Database/Database.cs | 15 ++++++++ PluralKit.Core/Database/DatabaseExt.cs | 20 ----------- PluralKit.Core/Models/ModelExtensions.cs | 31 ---------------- PluralKit.Core/Models/PKMember.cs | 24 +++++++++++++ PluralKit.Core/Models/PKSystem.cs | 6 ++++ PluralKit.Core/Models/Privacy/PrivacyExt.cs | 36 ------------------- PluralKit.Core/Models/Privacy/PrivacyLevel.cs | 36 ++++++++++++++++++- 7 files changed, 80 insertions(+), 88 deletions(-) delete mode 100644 PluralKit.Core/Database/DatabaseExt.cs delete mode 100644 PluralKit.Core/Models/ModelExtensions.cs delete mode 100644 PluralKit.Core/Models/Privacy/PrivacyExt.cs diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 09e6ff20..0071c629 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -197,4 +197,19 @@ namespace PluralKit.Core public override T[] Parse(object value) => Array.ConvertAll((TInner[]) value, v => _factory(v)); } } + + public static class DatabaseExt + { + public static async Task Execute(this IDatabase db, Func func) + { + await using var conn = await db.Obtain(); + await func(conn); + } + + public static async Task Execute(this IDatabase db, Func> func) + { + await using var conn = await db.Obtain(); + return await func(conn); + } + } } \ No newline at end of file diff --git a/PluralKit.Core/Database/DatabaseExt.cs b/PluralKit.Core/Database/DatabaseExt.cs deleted file mode 100644 index afae07f7..00000000 --- a/PluralKit.Core/Database/DatabaseExt.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace PluralKit.Core -{ - public static class DatabaseExt - { - public static async Task Execute(this IDatabase db, Func func) - { - await using var conn = await db.Obtain(); - await func(conn); - } - - public static async Task Execute(this IDatabase db, Func> func) - { - await using var conn = await db.Obtain(); - return await func(conn); - } - } -} \ No newline at end of file diff --git a/PluralKit.Core/Models/ModelExtensions.cs b/PluralKit.Core/Models/ModelExtensions.cs deleted file mode 100644 index 6092fa24..00000000 --- a/PluralKit.Core/Models/ModelExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NodaTime; - -namespace PluralKit.Core -{ - public static class ModelExtensions - { - public static string DescriptionFor(this PKSystem system, LookupContext ctx) => - system.DescriptionPrivacy.Get(ctx, system.Description); - - public static string NameFor(this PKMember member, LookupContext ctx) => - member.NamePrivacy.Get(ctx, member.Name, member.DisplayName ?? member.Name); - - public static string AvatarFor(this PKMember member, LookupContext ctx) => - member.AvatarPrivacy.Get(ctx, member.AvatarUrl); - - public static string DescriptionFor(this PKMember member, LookupContext ctx) => - member.DescriptionPrivacy.Get(ctx, member.Description); - - public static LocalDate? BirthdayFor(this PKMember member, LookupContext ctx) => - member.BirthdayPrivacy.Get(ctx, member.Birthday); - - public static string PronounsFor(this PKMember member, LookupContext ctx) => - member.PronounPrivacy.Get(ctx, member.Pronouns); - - public static Instant? CreatedFor(this PKMember member, LookupContext ctx) => - member.MetadataPrivacy.Get(ctx, (Instant?) member.Created); - - public static int MessageCountFor(this PKMember member, LookupContext ctx) => - member.MetadataPrivacy.Get(ctx, member.MessageCount); - } -} \ No newline at end of file diff --git a/PluralKit.Core/Models/PKMember.cs b/PluralKit.Core/Models/PKMember.cs index 57efa792..40a6ad8a 100644 --- a/PluralKit.Core/Models/PKMember.cs +++ b/PluralKit.Core/Models/PKMember.cs @@ -50,4 +50,28 @@ namespace PluralKit.Core { [JsonIgnore] public bool HasProxyTags => ProxyTags.Count > 0; } + + public static class PKMemberExt + { + public static string NameFor(this PKMember member, LookupContext ctx) => + member.NamePrivacy.Get(ctx, member.Name, member.DisplayName ?? member.Name); + + public static string AvatarFor(this PKMember member, LookupContext ctx) => + member.AvatarPrivacy.Get(ctx, member.AvatarUrl); + + public static string DescriptionFor(this PKMember member, LookupContext ctx) => + member.DescriptionPrivacy.Get(ctx, member.Description); + + public static LocalDate? BirthdayFor(this PKMember member, LookupContext ctx) => + member.BirthdayPrivacy.Get(ctx, member.Birthday); + + public static string PronounsFor(this PKMember member, LookupContext ctx) => + member.PronounPrivacy.Get(ctx, member.Pronouns); + + public static Instant? CreatedFor(this PKMember member, LookupContext ctx) => + member.MetadataPrivacy.Get(ctx, (Instant?) member.Created); + + public static int MessageCountFor(this PKMember member, LookupContext ctx) => + member.MetadataPrivacy.Get(ctx, member.MessageCount); + } } \ No newline at end of file diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index aff336af..fdc6ceea 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -25,4 +25,10 @@ namespace PluralKit.Core { [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); } + + public static class PKSystemExt + { + public static string DescriptionFor(this PKSystem system, LookupContext ctx) => + system.DescriptionPrivacy.Get(ctx, system.Description); + } } diff --git a/PluralKit.Core/Models/Privacy/PrivacyExt.cs b/PluralKit.Core/Models/Privacy/PrivacyExt.cs deleted file mode 100644 index 548f963e..00000000 --- a/PluralKit.Core/Models/Privacy/PrivacyExt.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace PluralKit.Core -{ - public static class PrivacyExt - { - public static bool CanAccess(this PrivacyLevel level, LookupContext ctx) => - level == PrivacyLevel.Public || ctx == LookupContext.ByOwner; - - public static string LevelName(this PrivacyLevel level) => - level == PrivacyLevel.Public ? "public" : "private"; - - public static T Get(this PrivacyLevel level, LookupContext ctx, T input, T fallback = default) => - level.CanAccess(ctx) ? input : fallback; - - public static string Explanation(this PrivacyLevel level) => - level switch - { - PrivacyLevel.Private => "**Private** (visible only when queried by you)", - PrivacyLevel.Public => "**Public** (visible to everyone)", - _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) - }; - - public static bool TryGet(this PrivacyLevel level, LookupContext ctx, T input, out T output, T absentValue = default) - { - output = default; - if (!level.CanAccess(ctx)) - return false; - if (Equals(input, absentValue)) - return false; - - output = input; - return true; - } - } -} \ No newline at end of file diff --git a/PluralKit.Core/Models/Privacy/PrivacyLevel.cs b/PluralKit.Core/Models/Privacy/PrivacyLevel.cs index 8dad4658..03a6ea99 100644 --- a/PluralKit.Core/Models/Privacy/PrivacyLevel.cs +++ b/PluralKit.Core/Models/Privacy/PrivacyLevel.cs @@ -1,8 +1,42 @@ -namespace PluralKit.Core +using System; + +namespace PluralKit.Core { public enum PrivacyLevel { Public = 1, Private = 2 } + + public static class PrivacyLevelExt + { + public static bool CanAccess(this PrivacyLevel level, LookupContext ctx) => + level == PrivacyLevel.Public || ctx == LookupContext.ByOwner; + + public static string LevelName(this PrivacyLevel level) => + level == PrivacyLevel.Public ? "public" : "private"; + + public static T Get(this PrivacyLevel level, LookupContext ctx, T input, T fallback = default) => + level.CanAccess(ctx) ? input : fallback; + + public static string Explanation(this PrivacyLevel level) => + level switch + { + PrivacyLevel.Private => "**Private** (visible only when queried by you)", + PrivacyLevel.Public => "**Public** (visible to everyone)", + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) + }; + + public static bool TryGet(this PrivacyLevel level, LookupContext ctx, T input, out T output, T absentValue = default) + { + output = default; + if (!level.CanAccess(ctx)) + return false; + if (Equals(input, absentValue)) + return false; + + output = input; + return true; + } + } } \ No newline at end of file From 5bc31cbf3e34de787901da04f0d19023fb8da8c0 Mon Sep 17 00:00:00 2001 From: acw0 Date: Wed, 5 Aug 2020 19:56:40 -0400 Subject: [PATCH 27/32] Fix error message --- PluralKit.Bot/Commands/Lists/MemberListOptions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/Lists/MemberListOptions.cs b/PluralKit.Bot/Commands/Lists/MemberListOptions.cs index 2f6a7aad..6e760aed 100644 --- a/PluralKit.Bot/Commands/Lists/MemberListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/MemberListOptions.cs @@ -28,7 +28,8 @@ namespace PluralKit.Bot public string CreateFilterString() { var str = new StringBuilder(); - str.Append("Sorting by "); + str.Append("Sorting "); + if (SortProperty != SortProperty.Random) str.Append("by "); str.Append(SortProperty switch { SortProperty.Name => "member name", @@ -39,6 +40,7 @@ namespace PluralKit.Bot SortProperty.LastSwitch => "last switch", SortProperty.MessageCount => "message count", SortProperty.Birthdate => "birthday", + SortProperty.Random => "randomly", _ => new ArgumentOutOfRangeException($"Couldn't find readable string for sort property {SortProperty}") }); From 0de284cd3658d233f6c6e6d7609a9d4726342874 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 11 Aug 2020 22:05:27 +0200 Subject: [PATCH 28/32] Bounds check system name in new system command --- PluralKit.Bot/Commands/System.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index 2fde9b66..4923adac 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -25,7 +25,11 @@ namespace PluralKit.Bot { ctx.CheckNoSystem(); - var system = await _data.CreateSystem(ctx.RemainderOrNull()); + var systemName = ctx.RemainderOrNull(); + if (systemName != null && systemName.Length > Limits.MaxSystemNameLength) + throw Errors.SystemNameTooLongError(systemName.Length); + + var system = await _data.CreateSystem(systemName); await _data.AddAccount(system, ctx.Author.Id); await ctx.Reply($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: "); } From 26418871addb60d31b58031d43272ab8d84fc919 Mon Sep 17 00:00:00 2001 From: kittens Date: Wed, 12 Aug 2020 10:51:59 -0400 Subject: [PATCH 29/32] Fix doubleBacktick replacing --- PluralKit.Bot/Utils/DiscordUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index cfc911ca..c6d10b75 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -212,7 +212,7 @@ namespace PluralKit.Bot public static string EscapeBacktickPair(this string input){ Regex doubleBacktick = new Regex(@"``", RegexOptions.Multiline); //Run twice to catch any pairs that are created from the first pass, pairs shouldn't be created in the second as they are created from odd numbers of backticks, even numbers are all caught on the first pass - if(input != null) return doubleBacktick.Replace(doubleBacktick.Replace(input, @"`‌ `"),@"`‌`"); + if(input != null) return doubleBacktick.Replace(doubleBacktick.Replace(input, @"`‌`"),@"`‌`"); else return input; } @@ -326,4 +326,4 @@ namespace PluralKit.Bot return $"<{match.Value}>"; }); } -} \ No newline at end of file +} From 2d9111727d186f48ac2052624892bd5e062535c4 Mon Sep 17 00:00:00 2001 From: kittens Date: Wed, 12 Aug 2020 11:05:11 -0400 Subject: [PATCH 30/32] Use unicode escape sequence for less confusion --- PluralKit.Bot/Utils/DiscordUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index c6d10b75..4203518b 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -212,7 +212,7 @@ namespace PluralKit.Bot public static string EscapeBacktickPair(this string input){ Regex doubleBacktick = new Regex(@"``", RegexOptions.Multiline); //Run twice to catch any pairs that are created from the first pass, pairs shouldn't be created in the second as they are created from odd numbers of backticks, even numbers are all caught on the first pass - if(input != null) return doubleBacktick.Replace(doubleBacktick.Replace(input, @"`‌`"),@"`‌`"); + if(input != null) return doubleBacktick.Replace(doubleBacktick.Replace(input, "`‌\ufeff`"),"`‌\ufeff`"); else return input; } From bed43379cff6814a83ddc997728edd99e87f8cce Mon Sep 17 00:00:00 2001 From: Ske Date: Wed, 12 Aug 2020 21:32:39 +0200 Subject: [PATCH 31/32] Add additional (debug) logging to (proxy) logger --- PluralKit.Bot/Services/LogChannelService.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index c75ebb48..e840e89b 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -37,8 +37,14 @@ namespace PluralKit.Bot { if (logChannel == null || logChannel.Type != ChannelType.Text) return; // Check bot permissions - if (!trigger.Channel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) return; - + if (!trigger.Channel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) + { + _logger.Information( + "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", + ctx.LogChannel.Value, trigger.Channel.GuildId, trigger.Channel.BotPermissions()); + return; + } + // Send embed! await using var conn = await _db.Obtain(); var embed = _embed.CreateLoggedMessageEmbed(await conn.QuerySystem(ctx.SystemId.Value), @@ -55,7 +61,7 @@ namespace PluralKit.Bot { if (obj == null) { // Channel doesn't exist or we don't have permission to access it, let's remove it from the database too - _logger.Warning("Attempted to fetch missing log channel {LogChannel}, removing from database", channel); + _logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channel, guild); await using var conn = await _db.Obtain(); await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild", new {Guild = guild}); From dd3b87cb23a48eb518d6155c1f7a4bc211c7a861 Mon Sep 17 00:00:00 2001 From: acw0 Date: Thu, 13 Aug 2020 04:27:44 -0400 Subject: [PATCH 32/32] use guild ID instead of channel ID in GetChannel --- PluralKit.Bot/Utils/DiscordUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index 4203518b..28067d4b 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -271,8 +271,8 @@ namespace PluralKit.Bot { // we need to know the channel's guild ID to get the cached guild object, so we grab it from the API if (guildId == null) { - var guild = await WrapDiscordCall(client.ShardClients.Values.FirstOrDefault().GetChannelAsync(id)); - if (guild != null) guildId = guild.Id; + var channel = await WrapDiscordCall(client.ShardClients.Values.FirstOrDefault().GetChannelAsync(id)); + if (channel != null) guildId = channel.GuildId; else return null; // we probably don't have the guild in cache if the API doesn't give it to us } return client.GetGuild(guildId.Value).GetChannel(id);