Merge branch 'main' into feature/groups

This commit is contained in:
Ske 2020-08-21 18:31:22 +02:00
commit 8688f4d0bf
42 changed files with 359 additions and 219 deletions

View File

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

37
LEGACYMIGRATE.md Normal file
View File

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

View File

@ -10,7 +10,6 @@ namespace PluralKit.API
{
[ApiController]
[ApiVersion("1.0")]
[Route("a")]
[Route( "v{version:apiVersion}/a" )]
public class AccountController: ControllerBase
{

View File

@ -13,7 +13,6 @@ namespace PluralKit.API
{
[ApiController]
[ApiVersion("1.0")]
[Route("m")]
[Route( "v{version:apiVersion}/m" )]
public class MemberController: ControllerBase
{

View File

@ -25,7 +25,6 @@ namespace PluralKit.API
[ApiController]
[ApiVersion("1.0")]
[Route("msg")]
[Route( "v{version:apiVersion}/msg" )]
public class MessageController: ControllerBase
{

View File

@ -36,7 +36,6 @@ namespace PluralKit.API
[ApiController]
[ApiVersion("1.0")]
[Route("s")]
[Route( "v{version:apiVersion}/s" )]
public class SystemController : ControllerBase
{

View File

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

View File

@ -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>(T evt) where T: DiscordEventArgs

View File

@ -201,7 +201,7 @@ namespace PluralKit.Bot
await ctx.Execute<System>(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<System>(SystemNew, m => m.New(ctx));
else if (ctx.Match("name", "rename", "changename"))
await ctx.Execute<SystemEdit>(SystemRename, m => m.Name(ctx));

View File

@ -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;
@ -136,7 +137,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 +162,4 @@ namespace PluralKit.Bot
}
}
}
}
}

View File

@ -29,7 +29,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",
@ -40,6 +41,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}")
});
@ -77,6 +79,8 @@ namespace PluralKit.Bot
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
opts.Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
var randGen = new global::System.Random();
var culture = StringComparer.InvariantCultureIgnoreCase;
return (opts.SortProperty switch
{
@ -98,6 +102,8 @@ namespace PluralKit.Bot
SortProperty.LastSwitch => input
.OrderByDescending(m => m.LastSwitchTime.HasValue)
.ThenByDescending(m => m.LastSwitchTime, ReverseMaybe(Comparer<Instant?>.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)
@ -114,7 +120,8 @@ namespace PluralKit.Bot
CreationDate,
LastSwitch,
LastMessage,
Birthdate
Birthdate,
Random
}
public enum ListType

View File

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

View File

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

View File

@ -94,27 +94,23 @@ namespace PluralKit.Bot {
public async Task PermCheckGuild(Context ctx)
{
DiscordGuild guild;
DiscordMember senderGuildUser = null;
if (ctx.Guild != null && !ctx.HasNext())
{
guild = ctx.Guild;
senderGuildUser = (DiscordMember)ctx.Author;
}
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.");
// 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);
if (guild == null)
throw Errors.GuildNotFound(guildId);
guild = ctx.Client.GetGuild(guildId);
if (guild != null) senderGuildUser = await guild.GetMember(ctx.Author.Id);
if (guild == null || 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 []
{

View File

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

View File

@ -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: <https://pluralkit.me/start>");
}

View File

@ -99,7 +99,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.");
@ -115,5 +115,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.");
}
}

View File

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

View File

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

View File

@ -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;
}
@ -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);
@ -326,4 +326,4 @@ namespace PluralKit.Bot
return $"<{match.Value}>";
});
}
}
}

View File

@ -199,4 +199,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<IPKConnection, Task> func)
{
await using var conn = await db.Obtain();
await func(conn);
}
public static async Task<T> Execute<T>(this IDatabase db, Func<IPKConnection, Task<T>> func)
{
await using var conn = await db.Obtain();
return await func(conn);
}
}
}

View File

@ -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<IPKConnection, Task> func)
{
await using var conn = await db.Obtain();
await func(conn);
}
public static async Task<T> Execute<T>(this IDatabase db, Func<IPKConnection, Task<T>> func)
{
await using var conn = await db.Obtain();
return await func(conn);
}
}
}

View File

@ -1,37 +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);
public static string DescriptionFor(this PKGroup group, LookupContext ctx) =>
group.DescriptionPrivacy.Get(ctx, group.Description);
public static string IconFor(this PKGroup group, LookupContext ctx) =>
group.IconPrivacy.Get(ctx, group.Icon);
}
}

View File

@ -21,4 +21,13 @@ namespace PluralKit.Core
public Instant Created { get; private set; }
}
public static class PKGroupExt
{
public static string? DescriptionFor(this PKGroup group, LookupContext ctx) =>
group.DescriptionPrivacy.Get(ctx, group.Description);
public static string? IconFor(this PKGroup group, LookupContext ctx) =>
group.IconPrivacy.Get(ctx, group.Icon);
}
}

View File

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

View File

@ -26,4 +26,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);
}
}

View File

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

View File

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

View File

@ -54,8 +54,12 @@ $ 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
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)
# 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
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

View File

@ -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!',
@ -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"],
]
},

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -68,7 +68,7 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
- `pk;log channel <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 <on/off>` - Enables or disables [log cleanup](/guide#log-cleanup).
- `pk;logclean <on/off>` - 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 **\<angle brackets>** 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.

View File

@ -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 <member name>`.
## 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 :/

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <message-link>` 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:

View File

@ -0,0 +1,33 @@
# 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.
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
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).

View File

@ -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).
|pk;autoproxy|pk;ap|

View File

@ -529,49 +529,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.