Merge branch 'main' into rust-api

This commit is contained in:
spiral 2023-03-18 23:20:40 -04:00 committed by GitHub
commit f382d30693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 727 additions and 155 deletions

View File

@ -304,9 +304,11 @@ public partial class CommandTree
await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target));
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target));
else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa"))
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.WebhookAvatar(ctx, target));
else if (ctx.Match("banner", "splash", "cover"))
await ctx.Execute<MemberEdit>(MemberBannerImage, m => m.BannerImage(ctx, target));
else if (ctx.Match("group", "groups"))
else if (ctx.Match("group", "groups", "g"))
if (ctx.Match("add", "a"))
await ctx.Execute<GroupMember>(MemberGroupAdd,
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add));
@ -406,9 +408,9 @@ public partial class CommandTree
{
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx));
else if (ctx.Match("move", "shift", "offset"))
else if (ctx.Match("move", "m", "shift", "offset"))
await ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx));
else if (ctx.Match("edit", "replace"))
else if (ctx.Match("edit", "e", "replace"))
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchEditOut, m => m.SwitchEditOut(ctx));
else

View File

@ -11,7 +11,7 @@ public class Help
{
Title = "PluralKit",
Description = "PluralKit is a bot designed for plural communities on Discord, and is open for anyone to use. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.",
Footer = new("By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"),
Footer = new("By @Ske#6201 | Myriad design by @Layl#8888, art by https://twitter.com/sillyvizion | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"),
Color = DiscordUtils.Blue,
};

View File

@ -47,6 +47,9 @@ public class ImportExport
var response = await _client.GetAsync(url);
if (!response.IsSuccessStatusCode)
throw Errors.InvalidImportFile;
// hacky fix for discord api returning nonsense charsets sometimes
response.Content.Headers.Remove("content-type");
response.Content.Headers.Add("content-type", "application/json; charset=UTF-8");
data = JsonConvert.DeserializeObject<JObject>(
await response.Content.ReadAsStringAsync(),
_settings

View File

@ -15,10 +15,10 @@ public class MemberAvatar
_client = client;
}
private async Task AvatarClear(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
{
await UpdateAvatar(location, ctx, target, null);
if (location == AvatarLocation.Server)
if (location == MemberAvatarLocation.Server)
{
if (target.AvatarUrl != null)
await ctx.Reply(
@ -26,6 +26,14 @@ public class MemberAvatar
else
await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar.");
}
else if (location == MemberAvatarLocation.MemberWebhook)
{
if (mgs?.AvatarUrl != null)
await ctx.Reply(
$"{Emojis.Success} Member proxy avatar cleared. Note that this member has a server-specific avatar set here, type `pk;member {target.Reference(ctx)} serveravatar clear` if you wish to clear that too.");
else
await ctx.Reply($"{Emojis.Success} Member proxy avatar cleared. This member will now use the main avatar for proxied messages.");
}
else
{
if (mgs?.AvatarUrl != null)
@ -36,18 +44,26 @@ public class MemberAvatar
}
}
private async Task AvatarShow(AvatarLocation location, Context ctx, PKMember target,
private async Task AvatarShow(MemberAvatarLocation location, Context ctx, PKMember target,
MemberGuildSettings? guildData)
{
// todo: this privacy code is really confusing
// for now, we skip privacy flag/config parsing for this, but it would be good to fix that at some point
var currentValue = location == AvatarLocation.Member ? target.AvatarUrl : guildData?.AvatarUrl;
var canAccess = location != AvatarLocation.Member ||
var currentValue = location switch
{
MemberAvatarLocation.Server => guildData?.AvatarUrl,
MemberAvatarLocation.MemberWebhook => target.WebhookAvatarUrl,
MemberAvatarLocation.Member => target.AvatarUrl,
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
var canAccess = location == MemberAvatarLocation.Server ||
target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.System));
if (string.IsNullOrEmpty(currentValue) || !canAccess)
{
if (location == AvatarLocation.Member)
if (location == MemberAvatarLocation.Member)
{
if (target.System == ctx.System?.Id)
throw new PKSyntaxError(
@ -55,19 +71,24 @@ public class MemberAvatar
throw new PKError("This member does not have an avatar set.");
}
if (location == AvatarLocation.Server)
if (location == MemberAvatarLocation.MemberWebhook)
throw new PKError(
$"This member does not have a proxy avatar set. Type `pk;member {target.Reference(ctx)} avatar` to see their global avatar.");
if (location == MemberAvatarLocation.Server)
throw new PKError(
$"This member does not have a server avatar set. Type `pk;member {target.Reference(ctx)} avatar` to see their global avatar.");
}
var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var field = location.Name();
if (location == MemberAvatarLocation.Server)
field += $" (for {ctx.Guild.Name})";
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s {field}")
.Image(new Embed.EmbedImage(currentValue?.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
eb.Description($"To clear, use `pk;member {target.Reference(ctx)} {cmd} clear`.");
eb.Description($"To clear, use `pk;member {target.Reference(ctx)} {location.Command()} clear`.");
await ctx.Reply(embed: eb.Build());
}
@ -75,7 +96,7 @@ public class MemberAvatar
{
ctx.CheckGuildContext();
var guildData = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData);
await AvatarCommandTree(MemberAvatarLocation.Server, ctx, target, guildData);
}
public async Task Avatar(Context ctx, PKMember target)
@ -84,16 +105,23 @@ public class MemberAvatar
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData);
await AvatarCommandTree(MemberAvatarLocation.Member, ctx, target, guildData);
}
private async Task AvatarCommandTree(AvatarLocation location, Context ctx, PKMember target,
public async Task WebhookAvatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
await AvatarCommandTree(MemberAvatarLocation.MemberWebhook, ctx, target, guildData);
}
private async Task AvatarCommandTree(MemberAvatarLocation location, Context ctx, PKMember target,
MemberGuildSettings? guildData)
{
// First, see if we need to *clear*
if (ctx.MatchClear() && await ctx.ConfirmClear(location == AvatarLocation.Server
? "this member's server avatar"
: "this member's avatar"))
if (ctx.MatchClear() && await ctx.ConfirmClear("this member's " + location.Name()))
{
ctx.CheckSystem().CheckOwnMember(target);
await AvatarClear(location, ctx, target, guildData);
@ -115,33 +143,30 @@ public class MemberAvatar
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);
}
private Task PrintResponse(AvatarLocation location, Context ctx, PKMember target, ParsedImage avatar,
private Task PrintResponse(MemberAvatarLocation location, Context ctx, PKMember target, ParsedImage avatar,
MemberGuildSettings? targetGuildData)
{
var typeFrag = location switch
{
AvatarLocation.Server => "server avatar",
AvatarLocation.Member => "avatar",
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
var serverFrag = location switch
{
AvatarLocation.Server =>
MemberAvatarLocation.Server =>
$" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).",
AvatarLocation.Member when targetGuildData?.AvatarUrl != null =>
MemberAvatarLocation.Member when targetGuildData?.AvatarUrl != null =>
$"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.Guild.Name}**), and thus changing the global avatar will have no effect here.",
MemberAvatarLocation.MemberWebhook when targetGuildData?.AvatarUrl != null =>
$" This avatar will now be used for this member's proxied messages, instead of their main avatar.\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.Guild.Name}**), and thus changing the global avatar will have no effect here.",
MemberAvatarLocation.MemberWebhook =>
$" This avatar will now be used for this member's proxied messages, instead of their main avatar.",
_ => ""
};
var msg = avatar.Source switch
{
AvatarSource.User =>
$"{Emojis.Success} Member {typeFrag} changed to {avatar.SourceUser?.Username}'s avatar!{serverFrag}\n{Emojis.Warn} If {avatar.SourceUser?.Username} changes their avatar, the member's avatar will need to be re-set.",
$"{Emojis.Success} Member {location.Name()} changed to {avatar.SourceUser?.Username}'s avatar!{serverFrag}\n{Emojis.Warn} If {avatar.SourceUser?.Username} changes their avatar, the member's avatar will need to be re-set.",
AvatarSource.Url =>
$"{Emojis.Success} Member {typeFrag} changed to the image at the given URL.{serverFrag}",
$"{Emojis.Success} Member {location.Name()} changed to the image at the given URL.{serverFrag}",
AvatarSource.Attachment =>
$"{Emojis.Success} Member {typeFrag} changed to attached image.{serverFrag}\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working.",
$"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
@ -152,18 +177,49 @@ public class MemberAvatar
: ctx.Reply(msg);
}
private Task UpdateAvatar(AvatarLocation location, Context ctx, PKMember target, string? url)
private Task UpdateAvatar(MemberAvatarLocation location, Context ctx, PKMember target, string? url)
{
switch (location)
{
case AvatarLocation.Server:
case MemberAvatarLocation.Server:
return ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, new MemberGuildPatch { AvatarUrl = url });
case AvatarLocation.Member:
case MemberAvatarLocation.Member:
return ctx.Repository.UpdateMember(target.Id, new MemberPatch { AvatarUrl = url });
case MemberAvatarLocation.MemberWebhook:
return ctx.Repository.UpdateMember(target.Id, new MemberPatch { WebhookAvatarUrl = url });
default:
throw new ArgumentOutOfRangeException($"Unknown avatar location {location}");
}
}
}
internal enum MemberAvatarLocation
{
Member,
MemberWebhook,
Server,
}
private enum AvatarLocation { Member, Server }
internal static class MemberAvatarLocationExt
{
public static string Name(this MemberAvatarLocation location)
{
return location switch
{
MemberAvatarLocation.Server => "server avatar",
MemberAvatarLocation.MemberWebhook => "proxy avatar",
MemberAvatarLocation.Member => "avatar",
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
}
public static string Command(this MemberAvatarLocation location)
{
return location switch
{
MemberAvatarLocation.Server => "serveravatar",
MemberAvatarLocation.MemberWebhook => "proxyavatar",
MemberAvatarLocation.Member => "avatar",
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
}
}

View File

@ -228,7 +228,7 @@ public class ProxyService
var guildMember = await _rest.GetGuildMember(msg.Guild!.Value, trigger.Author.Id);
// Grab user permissions
var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, trigger.Author.Id, guildMember);
var senderPermissions = PermissionExtensions.PermissionsFor(guild, messageChannel, trigger.Author.Id, guildMember);
var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone);
// Make sure user has permissions to send messages

View File

@ -135,7 +135,7 @@ public class EmbedService
// sometimes Discord will just... not return the avatar hash with webhook messages
var avatar = proxiedMessage.Author.Avatar != null
? proxiedMessage.Author.AvatarUrl()
: member.AvatarFor(LookupContext.ByNonOwner);
: member.WebhookAvatarFor(LookupContext.ByNonOwner);
var embed = new EmbedBuilder()
.Author(new Embed.EmbedAuthor($"#{channelName}: {name}", IconUrl: avatar))
.Thumbnail(new Embed.EmbedThumbnail(avatar))
@ -175,6 +175,7 @@ public class EmbedService
var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null;
var guildDisplayName = guildSettings?.DisplayName;
var webhook_avatar = guildSettings?.AvatarUrl ?? member.WebhookAvatarFor(ctx) ?? member.AvatarFor(ctx);
var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx);
var groups = await _repo.GetMemberGroups(member.Id)
@ -183,7 +184,7 @@ public class EmbedService
.ToListAsync();
var eb = new EmbedBuilder()
.Author(new Embed.EmbedAuthor(name, IconUrl: avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}"))
.Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}"))
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
.Color(color)
.Footer(new Embed.EmbedFooter(

View File

@ -23,9 +23,9 @@ public class ProxyMember
public string Name { get; } = "";
public string? ServerAvatar { get; }
public string? WebhookAvatar { get; }
public string? Avatar { get; }
public bool AllowAutoproxy { get; }
public string? Color { get; }
@ -42,5 +42,5 @@ public class ProxyMember
return memberName;
}
public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? Avatar ?? ctx.SystemAvatar;
public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? WebhookAvatar ?? Avatar ?? ctx.SystemAvatar;
}

View File

@ -1,4 +1,4 @@
create function message_context(account_id bigint, guild_id bigint, channel_id bigint)
create function message_context(account_id bigint, guild_id bigint, channel_id bigint)
returns table (
system_id int,
log_channel bigint,
@ -67,6 +67,7 @@ create function proxy_members(account_id bigint, guild_id bigint)
name text,
server_avatar text,
webhook_avatar text,
avatar text,
color char(6),
@ -76,22 +77,23 @@ create function proxy_members(account_id bigint, guild_id bigint)
as $$
select
-- Basic data
members.id as id,
members.proxy_tags as proxy_tags,
members.keep_proxy as keep_proxy,
members.id as id,
members.proxy_tags as proxy_tags,
members.keep_proxy as keep_proxy,
-- Name info
member_guild.display_name as server_name,
members.display_name as display_name,
members.name as name,
member_guild.display_name as server_name,
members.display_name as display_name,
members.name as name,
-- Avatar info
member_guild.avatar_url as server_avatar,
members.avatar_url as avatar,
member_guild.avatar_url as server_avatar,
members.webhook_avatar_url as webhook_avatar,
members.avatar_url as avatar,
members.color as color,
members.color as color,
members.allow_autoproxy as allow_autoproxy
members.allow_autoproxy as allow_autoproxy
from accounts
inner join systems on systems.id = accounts.system
inner join members on members.system = systems.id

View File

@ -0,0 +1,6 @@
-- database version 33
-- add webhook_avatar_url to system members
alter table members add column webhook_avatar_url text;
update info set schema_version = 33;

View File

@ -9,7 +9,7 @@ namespace PluralKit.Core;
internal class DatabaseMigrator
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 32;
private const int TargetSchemaVersion = 33;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)

View File

@ -39,6 +39,7 @@ public class PKMember
public Guid Uuid { get; private set; }
public SystemId System { get; private set; }
public string Color { get; private set; }
public string WebhookAvatarUrl { get; private set; }
public string AvatarUrl { get; private set; }
public string BannerImage { get; private set; }
public string Name { get; private set; }
@ -90,6 +91,9 @@ public static class PKMemberExt
public static string AvatarFor(this PKMember member, LookupContext ctx) =>
member.AvatarPrivacy.Get(ctx, member.AvatarUrl.TryGetCleanCdnUrl());
public static string WebhookAvatarFor(this PKMember member, LookupContext ctx) =>
member.AvatarPrivacy.Get(ctx, (member.WebhookAvatarUrl ?? member.AvatarUrl).TryGetCleanCdnUrl());
public static string DescriptionFor(this PKMember member, LookupContext ctx) =>
member.DescriptionPrivacy.Get(ctx, member.Description);
@ -128,6 +132,7 @@ public static class PKMemberExt
o.Add("birthday", member.BirthdayFor(ctx)?.FormatExport());
o.Add("pronouns", member.PronounsFor(ctx));
o.Add("avatar_url", member.AvatarFor(ctx).TryGetCleanCdnUrl());
o.Add("webhook_avatar_url", member.WebhookAvatarFor(ctx).TryGetCleanCdnUrl());
o.Add("banner", member.DescriptionPrivacy.Get(ctx, member.BannerImage).TryGetCleanCdnUrl());
o.Add("description", member.DescriptionFor(ctx));
o.Add("created", member.CreatedFor(ctx)?.FormatExport());

View File

@ -12,6 +12,7 @@ public class MemberPatch: PatchObject
public Partial<string> Name { get; set; }
public Partial<string> Hid { get; set; }
public Partial<string?> DisplayName { get; set; }
public Partial<string?> WebhookAvatarUrl { get; set; }
public Partial<string?> AvatarUrl { get; set; }
public Partial<string?> BannerImage { get; set; }
public Partial<string?> Color { get; set; }
@ -34,6 +35,7 @@ public class MemberPatch: PatchObject
.With("name", Name)
.With("hid", Hid)
.With("display_name", DisplayName)
.With("webhook_avatar_url", WebhookAvatarUrl)
.With("avatar_url", AvatarUrl)
.With("banner_image", BannerImage)
.With("color", Color)
@ -62,6 +64,9 @@ public class MemberPatch: PatchObject
if (AvatarUrl.Value != null)
AssertValid(AvatarUrl.Value, "avatar_url", Limits.MaxUriLength,
s => MiscUtils.TryMatchUri(s, out var avatarUri));
if (WebhookAvatarUrl.Value != null)
AssertValid(WebhookAvatarUrl.Value, "webhook_avatar_url", Limits.MaxUriLength,
s => MiscUtils.TryMatchUri(s, out var webhookAvatarUri));
if (BannerImage.Value != null)
AssertValid(BannerImage.Value, "banner", Limits.MaxUriLength,
s => MiscUtils.TryMatchUri(s, out var bannerUri));
@ -93,6 +98,7 @@ public class MemberPatch: PatchObject
if (o.ContainsKey("name")) patch.Name = o.Value<string>("name");
if (o.ContainsKey("color")) patch.Color = o.Value<string>("color").NullIfEmpty()?.ToLower();
if (o.ContainsKey("display_name")) patch.DisplayName = o.Value<string>("display_name").NullIfEmpty();
if (o.ContainsKey("webhook_avatar_url")) patch.WebhookAvatarUrl = o.Value<string>("webhook_avatar_url").NullIfEmpty();
if (o.ContainsKey("avatar_url")) patch.AvatarUrl = o.Value<string>("avatar_url").NullIfEmpty();
if (o.ContainsKey("banner")) patch.BannerImage = o.Value<string>("banner").NullIfEmpty();
@ -177,6 +183,8 @@ public class MemberPatch: PatchObject
o.Add("display_name", DisplayName.Value);
if (AvatarUrl.IsPresent)
o.Add("avatar_url", AvatarUrl.Value);
if (WebhookAvatarUrl.IsPresent)
o.Add("webhook_avatar_url", WebhookAvatarUrl.Value);
if (BannerImage.IsPresent)
o.Add("banner", BannerImage.Value);
if (Color.IsPresent)

View File

@ -26,8 +26,9 @@
"bootstrap": "^5.1.3",
"bootstrap-dark-5": "^1.1.3",
"core-js-pure": "^3.23.4",
"discord-markdown": "^2.5.1",
"discord-markdown": "https://github.com/repository/discord-markdown#b9608feef6856c9baa68f96c932a25c1d2bc55c2",
"gh-pages": "^3.2.3",
"highlight.js": "^11.7.0",
"import": "^0.0.6",
"moment": "^2.29.1",
"sass": "^1.52.2",

View File

@ -0,0 +1,421 @@
import { toHTML } from 'discord-markdown';
import hljs from 'highlight.js/lib/core';
import parseTimestamps from './parse-timestamps';
const languages: Record<string, () => Promise<typeof import("highlight.js/lib/languages/*")>> = {
"1c": () => import("highlight.js/lib/languages/1c"),
"abnf": () => import("highlight.js/lib/languages/abnf"),
"accesslog": () => import("highlight.js/lib/languages/accesslog"),
"actionscript": () => import("highlight.js/lib/languages/actionscript"),
"ada": () => import("highlight.js/lib/languages/ada"),
"angelscript": () => import("highlight.js/lib/languages/angelscript"),
"apache": () => import("highlight.js/lib/languages/apache"),
"applescript": () => import("highlight.js/lib/languages/applescript"),
"arcade": () => import("highlight.js/lib/languages/arcade"),
"arduino": () => import("highlight.js/lib/languages/arduino"),
"armasm": () => import("highlight.js/lib/languages/armasm"),
"xml": () => import("highlight.js/lib/languages/xml"),
"asciidoc": () => import("highlight.js/lib/languages/asciidoc"),
"aspectj": () => import("highlight.js/lib/languages/aspectj"),
"autohotkey": () => import("highlight.js/lib/languages/autohotkey"),
"autoit": () => import("highlight.js/lib/languages/autoit"),
"avrasm": () => import("highlight.js/lib/languages/avrasm"),
"awk": () => import("highlight.js/lib/languages/awk"),
"axapta": () => import("highlight.js/lib/languages/axapta"),
"bash": () => import("highlight.js/lib/languages/bash"),
"basic": () => import("highlight.js/lib/languages/basic"),
"bnf": () => import("highlight.js/lib/languages/bnf"),
"brainfuck": () => import("highlight.js/lib/languages/brainfuck"),
"c": () => import("highlight.js/lib/languages/c"),
"cal": () => import("highlight.js/lib/languages/cal"),
"capnproto": () => import("highlight.js/lib/languages/capnproto"),
"ceylon": () => import("highlight.js/lib/languages/ceylon"),
"clean": () => import("highlight.js/lib/languages/clean"),
"clojure": () => import("highlight.js/lib/languages/clojure"),
"clojure-repl": () => import("highlight.js/lib/languages/clojure-repl"),
"cmake": () => import("highlight.js/lib/languages/cmake"),
"coffeescript": () => import("highlight.js/lib/languages/coffeescript"),
"coq": () => import("highlight.js/lib/languages/coq"),
"cos": () => import("highlight.js/lib/languages/cos"),
"cpp": () => import("highlight.js/lib/languages/cpp"),
"crmsh": () => import("highlight.js/lib/languages/crmsh"),
"crystal": () => import("highlight.js/lib/languages/crystal"),
"csharp": () => import("highlight.js/lib/languages/csharp"),
"csp": () => import("highlight.js/lib/languages/csp"),
"css": () => import("highlight.js/lib/languages/css"),
"d": () => import("highlight.js/lib/languages/d"),
"markdown": () => import("highlight.js/lib/languages/markdown"),
"dart": () => import("highlight.js/lib/languages/dart"),
"delphi": () => import("highlight.js/lib/languages/delphi"),
"diff": () => import("highlight.js/lib/languages/diff"),
"django": () => import("highlight.js/lib/languages/django"),
"dns": () => import("highlight.js/lib/languages/dns"),
"dockerfile": () => import("highlight.js/lib/languages/dockerfile"),
"dos": () => import("highlight.js/lib/languages/dos"),
"dsconfig": () => import("highlight.js/lib/languages/dsconfig"),
"dts": () => import("highlight.js/lib/languages/dts"),
"dust": () => import("highlight.js/lib/languages/dust"),
"ebnf": () => import("highlight.js/lib/languages/ebnf"),
"elixir": () => import("highlight.js/lib/languages/elixir"),
"elm": () => import("highlight.js/lib/languages/elm"),
"ruby": () => import("highlight.js/lib/languages/ruby"),
"erb": () => import("highlight.js/lib/languages/erb"),
"erlang-repl": () => import("highlight.js/lib/languages/erlang-repl"),
"erlang": () => import("highlight.js/lib/languages/erlang"),
"excel": () => import("highlight.js/lib/languages/excel"),
"fix": () => import("highlight.js/lib/languages/fix"),
"flix": () => import("highlight.js/lib/languages/flix"),
"fortran": () => import("highlight.js/lib/languages/fortran"),
"fsharp": () => import("highlight.js/lib/languages/fsharp"),
"gams": () => import("highlight.js/lib/languages/gams"),
"gauss": () => import("highlight.js/lib/languages/gauss"),
"gcode": () => import("highlight.js/lib/languages/gcode"),
"gherkin": () => import("highlight.js/lib/languages/gherkin"),
"glsl": () => import("highlight.js/lib/languages/glsl"),
"gml": () => import("highlight.js/lib/languages/gml"),
"go": () => import("highlight.js/lib/languages/go"),
"golo": () => import("highlight.js/lib/languages/golo"),
"gradle": () => import("highlight.js/lib/languages/gradle"),
"graphql": () => import("highlight.js/lib/languages/graphql"),
"groovy": () => import("highlight.js/lib/languages/groovy"),
"haml": () => import("highlight.js/lib/languages/haml"),
"handlebars": () => import("highlight.js/lib/languages/handlebars"),
"haskell": () => import("highlight.js/lib/languages/haskell"),
"haxe": () => import("highlight.js/lib/languages/haxe"),
"hsp": () => import("highlight.js/lib/languages/hsp"),
"http": () => import("highlight.js/lib/languages/http"),
"hy": () => import("highlight.js/lib/languages/hy"),
"inform7": () => import("highlight.js/lib/languages/inform7"),
"ini": () => import("highlight.js/lib/languages/ini"),
"irpf90": () => import("highlight.js/lib/languages/irpf90"),
"isbl": () => import("highlight.js/lib/languages/isbl"),
"java": () => import("highlight.js/lib/languages/java"),
"javascript": () => import("highlight.js/lib/languages/javascript"),
"jboss-cli": () => import("highlight.js/lib/languages/jboss-cli"),
"json": () => import("highlight.js/lib/languages/json"),
"julia": () => import("highlight.js/lib/languages/julia"),
"julia-repl": () => import("highlight.js/lib/languages/julia-repl"),
"kotlin": () => import("highlight.js/lib/languages/kotlin"),
"lasso": () => import("highlight.js/lib/languages/lasso"),
"latex": () => import("highlight.js/lib/languages/latex"),
"ldif": () => import("highlight.js/lib/languages/ldif"),
"leaf": () => import("highlight.js/lib/languages/leaf"),
"less": () => import("highlight.js/lib/languages/less"),
"lisp": () => import("highlight.js/lib/languages/lisp"),
"livecodeserver": () => import("highlight.js/lib/languages/livecodeserver"),
"livescript": () => import("highlight.js/lib/languages/livescript"),
"llvm": () => import("highlight.js/lib/languages/llvm"),
"lsl": () => import("highlight.js/lib/languages/lsl"),
"lua": () => import("highlight.js/lib/languages/lua"),
"makefile": () => import("highlight.js/lib/languages/makefile"),
"mathematica": () => import("highlight.js/lib/languages/mathematica"),
"matlab": () => import("highlight.js/lib/languages/matlab"),
"maxima": () => import("highlight.js/lib/languages/maxima"),
"mel": () => import("highlight.js/lib/languages/mel"),
"mercury": () => import("highlight.js/lib/languages/mercury"),
"mipsasm": () => import("highlight.js/lib/languages/mipsasm"),
"mizar": () => import("highlight.js/lib/languages/mizar"),
"perl": () => import("highlight.js/lib/languages/perl"),
"mojolicious": () => import("highlight.js/lib/languages/mojolicious"),
"monkey": () => import("highlight.js/lib/languages/monkey"),
"moonscript": () => import("highlight.js/lib/languages/moonscript"),
"n1ql": () => import("highlight.js/lib/languages/n1ql"),
"nestedtext": () => import("highlight.js/lib/languages/nestedtext"),
"nginx": () => import("highlight.js/lib/languages/nginx"),
"nim": () => import("highlight.js/lib/languages/nim"),
"nix": () => import("highlight.js/lib/languages/nix"),
"node-repl": () => import("highlight.js/lib/languages/node-repl"),
"nsis": () => import("highlight.js/lib/languages/nsis"),
"objectivec": () => import("highlight.js/lib/languages/objectivec"),
"ocaml": () => import("highlight.js/lib/languages/ocaml"),
"openscad": () => import("highlight.js/lib/languages/openscad"),
"oxygene": () => import("highlight.js/lib/languages/oxygene"),
"parser3": () => import("highlight.js/lib/languages/parser3"),
"pf": () => import("highlight.js/lib/languages/pf"),
"pgsql": () => import("highlight.js/lib/languages/pgsql"),
"php": () => import("highlight.js/lib/languages/php"),
"php-template": () => import("highlight.js/lib/languages/php-template"),
"plaintext": () => import("highlight.js/lib/languages/plaintext"),
"pony": () => import("highlight.js/lib/languages/pony"),
"powershell": () => import("highlight.js/lib/languages/powershell"),
"processing": () => import("highlight.js/lib/languages/processing"),
"profile": () => import("highlight.js/lib/languages/profile"),
"prolog": () => import("highlight.js/lib/languages/prolog"),
"properties": () => import("highlight.js/lib/languages/properties"),
"protobuf": () => import("highlight.js/lib/languages/protobuf"),
"puppet": () => import("highlight.js/lib/languages/puppet"),
"purebasic": () => import("highlight.js/lib/languages/purebasic"),
"python": () => import("highlight.js/lib/languages/python"),
"python-repl": () => import("highlight.js/lib/languages/python-repl"),
"q": () => import("highlight.js/lib/languages/q"),
"qml": () => import("highlight.js/lib/languages/qml"),
"r": () => import("highlight.js/lib/languages/r"),
"reasonml": () => import("highlight.js/lib/languages/reasonml"),
"rib": () => import("highlight.js/lib/languages/rib"),
"roboconf": () => import("highlight.js/lib/languages/roboconf"),
"routeros": () => import("highlight.js/lib/languages/routeros"),
"rsl": () => import("highlight.js/lib/languages/rsl"),
"ruleslanguage": () => import("highlight.js/lib/languages/ruleslanguage"),
"rust": () => import("highlight.js/lib/languages/rust"),
"sas": () => import("highlight.js/lib/languages/sas"),
"scala": () => import("highlight.js/lib/languages/scala"),
"scheme": () => import("highlight.js/lib/languages/scheme"),
"scilab": () => import("highlight.js/lib/languages/scilab"),
"scss": () => import("highlight.js/lib/languages/scss"),
"shell": () => import("highlight.js/lib/languages/shell"),
"smali": () => import("highlight.js/lib/languages/smali"),
"smalltalk": () => import("highlight.js/lib/languages/smalltalk"),
"sml": () => import("highlight.js/lib/languages/sml"),
"sqf": () => import("highlight.js/lib/languages/sqf"),
"sql": () => import("highlight.js/lib/languages/sql"),
"stan": () => import("highlight.js/lib/languages/stan"),
"stata": () => import("highlight.js/lib/languages/stata"),
"step21": () => import("highlight.js/lib/languages/step21"),
"stylus": () => import("highlight.js/lib/languages/stylus"),
"subunit": () => import("highlight.js/lib/languages/subunit"),
"swift": () => import("highlight.js/lib/languages/swift"),
"taggerscript": () => import("highlight.js/lib/languages/taggerscript"),
"yaml": () => import("highlight.js/lib/languages/yaml"),
"tap": () => import("highlight.js/lib/languages/tap"),
"tcl": () => import("highlight.js/lib/languages/tcl"),
"thrift": () => import("highlight.js/lib/languages/thrift"),
"tp": () => import("highlight.js/lib/languages/tp"),
"twig": () => import("highlight.js/lib/languages/twig"),
"typescript": () => import("highlight.js/lib/languages/typescript"),
"vala": () => import("highlight.js/lib/languages/vala"),
"vbnet": () => import("highlight.js/lib/languages/vbnet"),
"vbscript": () => import("highlight.js/lib/languages/vbscript"),
"vbscript-html": () => import("highlight.js/lib/languages/vbscript-html"),
"verilog": () => import("highlight.js/lib/languages/verilog"),
"vhdl": () => import("highlight.js/lib/languages/vhdl"),
"vim": () => import("highlight.js/lib/languages/vim"),
"wasm": () => import("highlight.js/lib/languages/wasm"),
"wren": () => import("highlight.js/lib/languages/wren"),
"x86asm": () => import("highlight.js/lib/languages/x86asm"),
"xl": () => import("highlight.js/lib/languages/xl"),
"xquery": () => import("highlight.js/lib/languages/xquery"),
"zephir": () => import("highlight.js/lib/languages/zephir"),
}
// hljs.listLanguages().map(l => ([l, hljs.getLanguage(l).aliases])).filter(([, b]) => b).map(([n, a]) => a.map(al => ([al, n]))).flat().map(([a, n]) => `"${a}": languages["${n}"]`).join(",\n")
const aliases: Record<string, typeof languages[keyof typeof languages]> = {
"as": languages["actionscript"],
"asc": languages["angelscript"],
"apacheconf": languages["apache"],
"osascript": languages["applescript"],
"ino": languages["arduino"],
"arm": languages["armasm"],
"html": languages["xml"],
"xhtml": languages["xml"],
"rss": languages["xml"],
"atom": languages["xml"],
"xjb": languages["xml"],
"xsd": languages["xml"],
"xsl": languages["xml"],
"plist": languages["xml"],
"wsf": languages["xml"],
"svg": languages["xml"],
"adoc": languages["asciidoc"],
"ahk": languages["autohotkey"],
"x++": languages["axapta"],
"sh": languages["bash"],
"bf": languages["brainfuck"],
"h": languages["c"],
"capnp": languages["capnproto"],
"icl": languages["clean"],
"dcl": languages["clean"],
"clj": languages["clojure"],
"edn": languages["clojure"],
"cmake.in": languages["cmake"],
"coffee": languages["coffeescript"],
"cson": languages["coffeescript"],
"iced": languages["coffeescript"],
"cls": languages["cos"],
"cc": languages["cpp"],
"c++": languages["cpp"],
"h++": languages["cpp"],
"hpp": languages["cpp"],
"hh": languages["cpp"],
"hxx": languages["cpp"],
"cxx": languages["cpp"],
"crm": languages["crmsh"],
"pcmk": languages["crmsh"],
"cr": languages["crystal"],
"cs": languages["csharp"],
"c#": languages["csharp"],
"md": languages["markdown"],
"mkdown": languages["markdown"],
"mkd": languages["markdown"],
"dpr": languages["delphi"],
"dfm": languages["delphi"],
"pas": languages["delphi"],
"pascal": languages["delphi"],
"patch": languages["diff"],
"jinja": languages["django"],
"bind": languages["dns"],
"zone": languages["dns"],
"docker": languages["dockerfile"],
"bat": languages["dos"],
"cmd": languages["dos"],
"dst": languages["dust"],
"ex": languages["elixir"],
"exs": languages["elixir"],
"rb": languages["ruby"],
"gemspec": languages["ruby"],
"podspec": languages["ruby"],
"thor": languages["ruby"],
"irb": languages["ruby"],
"erl": languages["erlang"],
"xlsx": languages["excel"],
"xls": languages["excel"],
"f90": languages["fortran"],
"f95": languages["fortran"],
"fs": languages["fsharp"],
"f#": languages["fsharp"],
"gms": languages["gams"],
"gss": languages["gauss"],
"nc": languages["gcode"],
"feature": languages["gherkin"],
"golang": languages["go"],
"gql": languages["graphql"],
"hbs": languages["handlebars"],
"html.hbs": languages["handlebars"],
"html.handlebars": languages["handlebars"],
"htmlbars": languages["handlebars"],
"hs": languages["haskell"],
"hx": languages["haxe"],
"https": languages["http"],
"hylang": languages["hy"],
"i7": languages["inform7"],
"toml": languages["ini"],
"jsp": languages["java"],
"js": languages["javascript"],
"jsx": languages["javascript"],
"mjs": languages["javascript"],
"cjs": languages["javascript"],
"wildfly-cli": languages["jboss-cli"],
"jldoctest": languages["julia-repl"],
"kt": languages["kotlin"],
"kts": languages["kotlin"],
"ls": languages["lasso"],
"lassoscript": languages["lasso"],
"tex": languages["latex"],
"mk": languages["makefile"],
"mak": languages["makefile"],
"make": languages["makefile"],
"mma": languages["mathematica"],
"wl": languages["mathematica"],
"m": languages["mercury"],
"moo": languages["mercury"],
"mips": languages["mipsasm"],
"pl": languages["perl"],
"pm": languages["perl"],
"moon": languages["moonscript"],
"nt": languages["nestedtext"],
"nginxconf": languages["nginx"],
"nixos": languages["nix"],
"mm": languages["objectivec"],
"objc": languages["objectivec"],
"obj-c": languages["objectivec"],
"obj-c++": languages["objectivec"],
"objective-c++": languages["objectivec"],
"ml": languages["ocaml"],
"scad": languages["openscad"],
"pf.conf": languages["pf"],
"postgres": languages["pgsql"],
"postgresql": languages["pgsql"],
"text": languages["plaintext"],
"txt": languages["plaintext"],
"pwsh": languages["powershell"],
"ps": languages["powershell"],
"ps1": languages["powershell"],
"pde": languages["processing"],
"pp": languages["puppet"],
"pb": languages["purebasic"],
"pbi": languages["purebasic"],
"py": languages["python"],
"gyp": languages["python"],
"ipython": languages["python"],
"pycon": languages["python-repl"],
"k": languages["q"],
"kdb": languages["q"],
"qt": languages["qml"],
"re": languages["reasonml"],
"graph": languages["roboconf"],
"instances": languages["roboconf"],
"mikrotik": languages["routeros"],
"rs": languages["rust"],
"scm": languages["scheme"],
"sci": languages["scilab"],
"console": languages["shell"],
"shellsession": languages["shell"],
"st": languages["smalltalk"],
"stanfuncs": languages["stan"],
"do": languages["stata"],
"ado": languages["stata"],
"p21": languages["step21"],
"step": languages["step21"],
"stp": languages["step21"],
"styl": languages["stylus"],
"yml": languages["yaml"],
"tk": languages["tcl"],
"craftcms": languages["twig"],
"ts": languages["typescript"],
"tsx": languages["typescript"],
"vb": languages["vbnet"],
"vbs": languages["vbscript"],
"v": languages["verilog"],
"sv": languages["verilog"],
"svh": languages["verilog"],
"tao": languages["xl"],
"xpath": languages["xquery"],
"xq": languages["xquery"],
"zep": languages["zephir"]
}
interface ParseMarkdownOptions {
parseTimestamps?: boolean;
embed?: boolean;
}
const parseMarkdown = async (raw: string, opts?: ParseMarkdownOptions) => {
if (opts?.parseTimestamps) {
raw = parseTimestamps(raw);
}
const markdownUnparsed = toHTML(raw, { embed: opts?.embed });
const markdownUnparsedDom = new DOMParser().parseFromString(markdownUnparsed, "text/html");
const codeBlocks = markdownUnparsedDom.querySelectorAll("pre code[data-code]");
const promies = Array.from(codeBlocks).map(async (codeBlock) => {
let code: string = window.atob(codeBlock.getAttribute("data-code"));
codeBlock.classList.add("hljs");
const specifiedLanguage = codeBlock.getAttribute("data-code-language");
const languageImportFn = languages[specifiedLanguage] ?? aliases[specifiedLanguage];
if (languageImportFn) {
if (!hljs.getLanguage(specifiedLanguage)) {
const languageImport = await languageImportFn();
hljs.registerLanguage(specifiedLanguage, languageImport.default);
}
codeBlock.classList.add(specifiedLanguage);
codeBlock.innerHTML = hljs.highlight(code, {language: specifiedLanguage}).value;
} else {
codeBlock.textContent = code;
}
codeBlock.removeAttribute("data-code");
codeBlock.removeAttribute("data-code-language");
});
await Promise.all(promies);
return markdownUnparsedDom.body.innerHTML;
}
export default parseMarkdown;

View File

@ -0,0 +1,11 @@
<script lang="ts">
export let htmlPromise: Promise<string> = Promise.resolve("");
</script>
{#await htmlPromise}
(loading...)
{:then html}
{@html html ?? ""}
{:catch error}
(failed to parse: {error?.message ?? String(error)})
{/await}

View File

@ -1,23 +1,24 @@
<script lang="ts">
import { tick } from 'svelte';
import { Modal, CardTitle} from 'sveltestrap';
import AwaitHtml from './AwaitHtml.svelte';
import default_avatar from '../../assets/default_avatar.png';
import resizeMedia from '../../api/resize-media';
import { toHTML } from 'discord-markdown';
import parseMarkdown from '../../api/parse-markdown';
import twemoji from 'twemoji';
export let item: any;
export let searchBy: string = null;
export let sortBy: string = null;
let htmlName: string;
let nameElement: any;
let htmlNamePromise: Promise<string>;
let nameElement: any;
let settings = JSON.parse(localStorage.getItem("pk-settings"));
$: if (item.name) {
if ((searchBy === "display_name" || sortBy === "display_name") && item.display_name) htmlName = toHTML(item.display_name);
else htmlName = toHTML(item.name);
} else htmlName = "";
if ((searchBy === "display_name" || sortBy === "display_name") && item.display_name) htmlNamePromise = parseMarkdown(item.display_name);
else htmlNamePromise = parseMarkdown(item.name);
}
$: if (settings && settings.appearance.twemoji) {
if (nameElement) twemoji.parse(nameElement, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/' });
@ -47,7 +48,7 @@
<div class="icon d-inline-block">
<slot name="icon" />
</div>
<span bind:this={nameElement} style="vertical-align: middle;">{@html htmlName} ({item.id})</span>
<span bind:this={nameElement} style="vertical-align: middle;"><AwaitHtml htmlPromise={htmlNamePromise} /> ({item.id})</span>
</div>
<div style="margin-left: auto;">
{#if item && (item.avatar_url || item.icon)}

View File

@ -2,8 +2,7 @@
import { tick } from 'svelte';
import { Row, Col, Modal, Image, Button, CardBody, ModalHeader, ModalBody, ModalFooter, Spinner } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import parseTimestamps from '../../api/parse-timestamps';
import parseMarkdown from '../../api/parse-markdown';
import resizeMedia from '../../api/resize-media';
import Edit from './Edit.svelte';
import twemoji from 'twemoji';
@ -12,6 +11,7 @@
import { Link, useLocation } from 'svelte-navigator';
import type { Member, Group } from '../../api/types';
import AwaitHtml from '../common/AwaitHtml.svelte';
export let group: Group;
let editMode: boolean = false;
@ -21,14 +21,13 @@
export let isMainDash = true;
export let isPage = false;
let htmlDescription: string;
$: if (group.description) {
htmlDescription = toHTML(parseTimestamps(group.description), {embed: true});
} else {
htmlDescription = "(no description)";
let htmlDescriptionPromise: Promise<string>;
$: if (group.description) {
htmlDescriptionPromise = parseMarkdown(group.description, { parseTimestamps: true, embed: true });
}
let htmlDisplayName: string;
$: if (group.display_name) htmlDisplayName = toHTML(group.display_name)
let htmlDisplayNamePromise: Promise<string>;
$: if (group.display_name) htmlDisplayNamePromise = parseMarkdown(group.display_name, { parseTimestamps: true, embed: true });
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let descriptionElement: any;
@ -83,7 +82,7 @@
{/if}
{#if group.display_name}
<Col xs={12} lg={4} class="mb-2">
<b>Display Name:</b> <span bind:this={displayNameElement}>{@html htmlDisplayName}</span>
<b>Display Name:</b> <span bind:this={displayNameElement}><AwaitHtml htmlPromise={htmlDisplayNamePromise} /></span>
</Col>
{/if}
{#if group.created && !isPublic}
@ -122,7 +121,7 @@
</Row>
<div class="mt-2 mb-3 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription && htmlDescription}
<AwaitHtml htmlPromise={htmlDescriptionPromise} />
</div>
{#if (group.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img on:click={toggleBannerModal} src={resizeMedia(group.banner, [1200, 480])} alt="group banner" class="w-100 mb-3 rounded" style="max-height: 13em; object-fit: cover; cursor: pointer"/>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Card, CardHeader, CardTitle, Modal, Button, ListGroup, ListGroupItem, Label, Input, Alert, Tooltip, Row, Col } from 'sveltestrap';
import { toHTML } from 'discord-markdown';
import parseMarkdown from '../../api/parse-markdown';
import twemoji from 'twemoji';
import { Link } from 'svelte-navigator';
import { autoresize } from 'svelte-textarea-autoresize';
@ -16,6 +16,7 @@
import api from '../../api';
import default_avatar from '../../assets/default_avatar.png';
import resizeMedia from '../../api/resize-media';
import AwaitHtml from '../common/AwaitHtml.svelte';
export let group: Group;
export let searchBy: string;
@ -32,18 +33,18 @@
let success = false;
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let htmlName: string;
$: htmlDesc = group.description && toHTML(group.description, { embed: true}) || "(no description)";
$: htmlDisplayName = group.display_name && toHTML(group.display_name);
let htmlNamePromise: Promise<string>;
$: htmlDescPromise = group.description ? parseMarkdown(group.description, { embed: true }) : Promise.resolve("(no description)");
$: htmlDisplayNamePromise = group.display_name ? parseMarkdown(group.display_name, { embed: true }) : undefined;
let nameElement: any;
let descElement: any;
let dnElement: any;
$: if (group.name) {
if ((searchBy === "display name" || sortBy === "display name") && group.display_name) htmlName = toHTML(group.display_name);
else htmlName = toHTML(group.name);
if ((searchBy === "display name" || sortBy === "display name") && group.display_name) htmlNamePromise = parseMarkdown(group.display_name);
else htmlNamePromise = parseMarkdown(group.name);
}
if (settings && settings.appearance.twemoji) {
if (nameElement) twemoji.parse(nameElement, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/' });
@ -111,7 +112,7 @@
<div class="icon d-inline-block">
<slot name="icon" />
</div>
<span bind:this={nameElement} style="vertical-align: middle; margin-bottom: 0;">{@html htmlName} ({group.id})</span>
<span bind:this={nameElement} style="vertical-align: middle; margin-bottom: 0;">{@html htmlNamePromise} ({group.id})</span>
</CardTitle>
</CardHeader>
<div class="card-body d-block hide-scrollbar" style="flex: 1; overflow: auto;">
@ -123,11 +124,11 @@
</div>
</Modal>
{#if group.display_name}
<div class="text-center" bind:this={dnElement}><b>{@html htmlDisplayName}</b></div>
<div class="text-center" bind:this={dnElement}><b><AwaitHtml htmlPromise={htmlDisplayNamePromise} /></b></div>
{/if}
<hr style="min-height: 1px;"/>
<div bind:this={descElement}>
{@html htmlDesc}
<AwaitHtml htmlPromise={htmlDescPromise} />
</div>
<hr style="min-height: 1px;"/>
<Row>
@ -150,7 +151,7 @@
<hr style="min-height: 1px"/>
<ListGroup>
{#each memberList as member, index (member.id)}
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]}><span><b>{@html toHTML(member.name)}</b> (<code>{member.id}</code>)</span></ListGroupItem>
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]}><span><b><AwaitHtml htmlPromise={parseMarkdown(member.name)} /></b> (<code>{member.id}</code>)</span></ListGroupItem>
{/each}
</ListGroup>
{:else}

View File

@ -144,7 +144,7 @@
{:else}<Button style="flex: 0" color="primary" disabled aria-label="submit edits"><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled aria-label="cancel edits">Back</Button><Button style="flex: 0; float: right;" color="danger" disabled aria-label="delete group">Delete</Button>{/if}
<Modal size="lg" isOpen={deleteOpen} toggle={toggleDeleteModal}>
<ModalHeader toggle={toggleDeleteModal}>
Delete member
Delete group
</ModalHeader>
<ModalBody>
{#if deleteErr}<Alert color="danger">{deleteErr}</Alert>{/if}

View File

@ -1,9 +1,8 @@
<script lang="ts">
import { tick } from 'svelte';
import { Row, Col, Modal, Image, Button, CardBody, ModalHeader, ModalBody } from 'sveltestrap';
import { Row, Col, Modal, Button, CardBody, ModalHeader, ModalBody } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import parseTimestamps from '../../api/parse-timestamps';
import parseMarkdown from '../../api/parse-markdown';
import resizeMedia from '../../api/resize-media';
import twemoji from 'twemoji';
@ -14,6 +13,7 @@
import type { Member, Group } from '../../api/types';
import { Link, useLocation } from 'svelte-navigator';
import AwaitHtml from '../common/AwaitHtml.svelte';
export let groups: Group[] = [];
export let member: Member;
@ -24,16 +24,14 @@
let editMode: boolean = false;
let groupMode: boolean = false;
let htmlDescription: string;
$: if (member.description) {
htmlDescription = toHTML(parseTimestamps(member.description), {embed: true});
} else {
htmlDescription = "(no description)";
let htmlDescriptionPromise: Promise<string> = Promise.resolve("(no description)");
$: if (member.description) {
htmlDescriptionPromise = parseMarkdown(member.description, { parseTimestamps: true, embed: true });
}
let htmlPronouns: string;
$: if (member.pronouns) {
htmlPronouns = toHTML(parseTimestamps(member.pronouns), {embed: true});
let htmlPronounsPromise: Promise<string>;
$: if (member.pronouns) {
htmlPronounsPromise = parseMarkdown(member.pronouns, { parseTimestamps: true, embed: true });
}
let settings = JSON.parse(localStorage.getItem("pk-settings"));
@ -58,12 +56,12 @@
let created = moment(member.created).format("MMM D, YYYY");
let birthday: string;
$: {member.birthday;
$: {member.birthday;
if (member.birthday) birthday = moment(member.birthday, "YYYY-MM-DD").format("MMM D, YYYY");
}
$: trimmedBirthday = birthday && birthday.endsWith(', 0004') ? trimmedBirthday = birthday.replace(', 0004', '') : birthday;
async function focus(el) {
await tick();
el.focus();
@ -115,7 +113,7 @@
{/if}
{#if member.pronouns}
<Col xs={12} lg={4} class="mb-2">
<b>Pronouns:</b> <span bind:this={pronounElement}>{@html htmlPronouns}</span>
<b>Pronouns:</b> <span bind:this={pronounElement}><AwaitHtml htmlPromise={htmlPronounsPromise} /></span>
</Col>
{/if}
{#if member.birthday}
@ -172,7 +170,7 @@
</Row>
<div class="my-2 mb-3 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription && htmlDescription}
<AwaitHtml htmlPromise={htmlDescriptionPromise} />
</div>
{#if (member.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img on:click={toggleBannerModal} src={resizeMedia(member.banner, [1200, 480])} alt="member banner" class="w-100 mb-3 rounded" style="max-height: 13em; object-fit: cover; cursor: pointer"/>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Card, CardHeader, CardTitle, Modal, Button, ListGroup, ListGroupItem, Input, Alert, Label, Spinner, Row, Col, Tooltip } from 'sveltestrap';
import { toHTML } from 'discord-markdown';
import AwaitHtml from '../common/AwaitHtml.svelte';
import parseMarkdown from '../../api/parse-markdown';
import twemoji from 'twemoji';
import { Link } from 'svelte-navigator';
import { autoresize } from 'svelte-textarea-autoresize';
@ -34,10 +35,10 @@
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let htmlName: string;
$: htmlDesc = member.description && toHTML(member.description, { embed: true}) || "(no description)";
$: htmlDisplayName = member.display_name && toHTML(member.display_name);
$: htmlPronouns = member.pronouns && toHTML(member.pronouns, {embed: true});
let htmlNamePromise: Promise<string>;
$: htmlDescPromise = member.description ? parseMarkdown(member.description, { embed: true }) : Promise.resolve("(no description)");
$: htmlDisplayNamePromise = member.display_name ? parseMarkdown(member.display_name) : undefined;
$: htmlPronounsPromise = member.pronouns ? parseMarkdown(member.pronouns, {embed: true}) : undefined;
let nameElement: any;
let descElement: any;
@ -45,8 +46,8 @@
let prnsElement: any;
$: if (member.name) {
if ((searchBy === "display name" || sortBy === "display name") && member.display_name) htmlName = toHTML(member.display_name);
else htmlName = toHTML(member.name);
if ((searchBy === "display name" || sortBy === "display name") && member.display_name) htmlNamePromise = parseMarkdown(member.display_name);
else htmlNamePromise = parseMarkdown(member.name);
}
if (settings && settings.appearance.twemoji) {
if (nameElement) twemoji.parse(nameElement, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/' });
@ -117,7 +118,7 @@
<div class="icon d-inline-block">
<slot name="icon" />
</div>
<span bind:this={nameElement} style="vertical-align: middle; margin-bottom: 0;">{@html htmlName} ({member.id})</span>
<span bind:this={nameElement} style="vertical-align: middle; margin-bottom: 0;"><AwaitHtml htmlPromise={htmlNamePromise} /> ({member.id})</span>
</CardTitle>
</CardHeader>
<div class="card-body d-block hide-scrollbar" style="flex: 1; overflow: auto;">
@ -129,14 +130,14 @@
</div>
</Modal>
{#if member.display_name}
<div class="text-center" bind:this={dnElement}><b>{@html htmlDisplayName}</b></div>
<div class="text-center" bind:this={dnElement}><b><AwaitHtml htmlPromise={htmlDisplayNamePromise} /></b></div>
{/if}
{#if member.pronouns}
<div class="text-center" bind:this={prnsElement}>{@html htmlPronouns}</div>
<div class="text-center" bind:this={prnsElement}><AwaitHtml htmlPromise={htmlPronounsPromise} /></div>
{/if}
<hr style="min-height: 1px;"/>
<div bind:this={descElement}>
{@html htmlDesc}
<AwaitHtml htmlPromise={htmlDescPromise} />
</div>
<hr style="min-height: 1px;"/>
<Row>
@ -159,7 +160,7 @@
<hr style="min-height: 1px"/>
<ListGroup>
{#each groupList as group, index (group.id)}
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]}><span><b>{@html toHTML(group.name)}</b> (<code>{group.id}</code>)</span></ListGroupItem>
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]}><span><b><AwaitHtml htmlPromise={parseMarkdown(group.name)} /></b> (<code>{group.id}</code>)</span></ListGroupItem>
{/each}
</ListGroup>
{:else}

View File

@ -3,7 +3,8 @@
import ListPagination from "../common/ListPagination.svelte";
import twemoji from "twemoji";
import Svelecte, { addFormatter } from 'svelecte';
import { toHTML } from 'discord-markdown';
import AwaitHtml from '../common/AwaitHtml.svelte';
import parseMarkdown from '../../api/parse-markdown';
import FaFolderOpen from 'svelte-icons/fa/FaFolderOpen.svelte'
import FaFolderPlus from 'svelte-icons/fa/FaFolderPlus.svelte'
@ -115,7 +116,7 @@
{#if finalGroupsList && finalGroupsList.length > 0}
<ListGroup>
{#each finalGroupsList as group, index (group.id)}
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]} class="d-flex justify-content-between flex-grow-1"><span><b>{group.name}</b> (<code>{group.id}</code>)</span> <span>{@html group.display_name ? `${toHTML(group.display_name)}` : ""}</span></span></ListGroupItem>
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]} class="d-flex justify-content-between flex-grow-1"><span><b>{group.name}</b> (<code>{group.id}</code>)</span> <span><AwaitHtml htmlPromise={parseMarkdown(group.display_name)} /></span></span></ListGroupItem>
{/each}
</ListGroup>
{:else}

View File

@ -1,33 +1,33 @@
<script lang="ts">
import { Row, Col, Modal, Image, Button } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import parseTimestamps from '../../api/parse-timestamps';
import parseMarkdown from '../../api/parse-markdown';
import resizeMedia from '../../api/resize-media';
import twemoji from 'twemoji';
import type { System } from '../../api/types';
import AwaitHtml from '../common/AwaitHtml.svelte';
export let user: System;
export let editMode: boolean;
export let isPublic: boolean;
let htmlDescription: string;
let htmlName: string;
let htmlPronouns: string;
let htmlDescriptionPromise: Promise<string>;
let htmlNamePromise: Promise<string>;
let htmlPronounsPromise: Promise<string>;
if (user.description) {
htmlDescription = toHTML(parseTimestamps(user.description), {embed: true});
if (user.description) {
htmlDescriptionPromise = parseMarkdown(user.description, { embed: true, parseTimestamps: true });
} else {
htmlDescription = "(no description)";
htmlDescriptionPromise = Promise.resolve("(no description)");
}
if (user.name) {
htmlName = toHTML(user.name);
htmlNamePromise = parseMarkdown(user.name);
}
if (user.pronouns) {
htmlPronouns = toHTML(user.pronouns);
htmlPronounsPromise = parseMarkdown(user.pronouns);
}
let created = moment(user.created).format("MMM D, YYYY");
@ -58,7 +58,7 @@
{/if}
{#if user.name}
<Col xs={12} lg={4} class="mb-2">
<span bind:this={nameElement}><b>Name:</b> {@html htmlName}</span>
<span bind:this={nameElement}><b>Name:</b> <AwaitHtml htmlPromise={htmlNamePromise} /></span>
</Col>
{/if}
{#if user.tag}
@ -68,7 +68,7 @@
{/if}
{#if user.pronouns}
<Col xs={12} lg={4} class="mb-2">
<span bind:this={pronounElement}><b>Pronouns:</b> {@html htmlPronouns}</span>
<span bind:this={pronounElement}><b>Pronouns:</b> <AwaitHtml htmlPromise={htmlPronounsPromise} /></span>
</Col>
{/if}
{#if user.created && !isPublic}
@ -99,7 +99,7 @@
</Row>
<div class="my-2 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription}
<AwaitHtml htmlPromise={htmlDescriptionPromise} />
</div>
{#if (user.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img on:click={toggleBannerModal} src={resizeMedia(user.banner, [1200, 480])} alt="system banner" class="w-100 mb-3 rounded" style="max-height: 13em; object-fit: cover; cursor: pointer;"/>

View File

@ -2,7 +2,7 @@ import * as Sentry from "@sentry/browser";
import { Integrations } from "@sentry/tracing";
// polyfill for replaceAll
import * as replaceAll from 'core-js-pure/es/string/virtual/replace-all.js';
import replaceAll from 'core-js-pure/es/string/virtual/replace-all.js';
if (!String.prototype.replaceAll)
String.prototype.replaceAll = replaceAll;

View File

@ -6,10 +6,11 @@
import { loggedIn, currentUser } from '../stores';
import { Link } from 'svelte-navigator';
import twemoji from 'twemoji';
import { toHTML } from 'discord-markdown';
import parseMarkdown from '../api/parse-markdown';
import type { System } from '../api/types';
import api from '../api';
import AwaitHtml from '../components/common/AwaitHtml.svelte';
let loading = false;
let err: string;
@ -70,9 +71,9 @@
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let welcomeElement: any;
let htmlName: string;
let htmlNamePromise: Promise<string>;
$: if (user && user.name) {
htmlName = toHTML(user.name);
htmlNamePromise = parseMarkdown(user.name);
}
$: if (settings && settings.appearance.twemoji) {
if (welcomeElement) twemoji.parse(welcomeElement, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/' });
@ -103,7 +104,7 @@
verifying login...
{:else if isLoggedIn}
{#if user && user.name}
<p bind:this={welcomeElement}>Welcome, <b>{@html htmlName}</b>!</p>
<p bind:this={welcomeElement}>Welcome, <b><AwaitHtml htmlPromise={htmlNamePromise} /></b>!</p>
{:else}
<p>Welcome!</p>
{/if}

View File

@ -2,12 +2,15 @@
import { Card, CardHeader, CardBody, Container, Row, Col, CardTitle, Tooltip, Button } from 'sveltestrap';
import Toggle from 'svelte-toggle';
import { autoresize } from 'svelte-textarea-autoresize';
import FaAmbulance from 'svelte-icons/fa/FaAmbulance.svelte'
import FaCogs from 'svelte-icons/fa/FaCogs.svelte'
import type { Config } from '../../api/types';
import api from '../../api';
let savedSettings = JSON.parse(localStorage.getItem("pk-settings"));
let apiConfig: Config = JSON.parse(localStorage.getItem("pk-config"));
let token = localStorage.getItem("pk-token");
let showToken = false;
let settings = {
appearance: {
@ -40,6 +43,7 @@
else document.getElementById("app").classList.remove("dyslexic");
}
const revealToken = () => showToken = !showToken;
</script>
<Container>
@ -123,6 +127,36 @@
</Col>
</Row>
{/if}
{#if token}
<Row>
<Col class="mx-auto" xs={12} lg={11} xl={10}>
<Card class="mb-4">
<CardHeader>
<CardTitle style="margin-top: 8px; outline: none;">
<div class="icon d-inline-block">
<FaAmbulance />
</div>Recovery
</CardTitle>
</CardHeader>
<CardBody>
<p>If you've lost access to your discord account, you can retrieve your token here.</p>
<p>Send a direct message to a staff member (a helper, moderator or developer <a href="https://discord.gg/PczBt78">in the support server</a>), they can recover your system with this token.</p>
<Button color="danger" on:click={() => revealToken()}>Reveal token</Button>
{#if showToken}
<Row>
<Col xs={12} md={9}>
<span class="mt-2 form-control">{token}</span>
</Col>
<Col xs={12} md={3}>
<Button color="primary" class="w-100 mt-2" on:click={() => navigator.clipboard.writeText(token)}>Copy</Button>
</Col>
</Row>
{/if}
</CardBody>
</Card>
</Col>
</Row>
{/if}
</Container>
<svelte:head>

View File

@ -15,12 +15,15 @@ export default defineConfig({
if (filename.length < 2) return 'index';
else filename = filename[1];
// this is really big and makes the map size go over the sentry file cache limit
if (filename.includes("highlight.js")) return 'vendor-0';
if (filename.startsWith("/highlight.js/es/languages/")) {
const lang = filename.split("/").pop().split(".").shift();
return `vendor_hljs-${lang}`;
}
return 'vendor-1';
// return `vendor-${filename.charCodeAt(1) % 2}`;
}
}
}
}
})

View File

@ -225,6 +225,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@ -341,12 +346,11 @@ detect-indent@^6.0.0:
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
discord-markdown@^2.5.1:
"discord-markdown@https://github.com/repository/discord-markdown#b9608feef6856c9baa68f96c932a25c1d2bc55c2":
version "2.5.1"
resolved "https://registry.yarnpkg.com/discord-markdown/-/discord-markdown-2.5.1.tgz#d18773c6e3cff8df90f305654ecbbc5e38c507eb"
integrity sha512-SGNlL1Y8NYjY2MA5Vj1SI5+Ue5GUW2HkkDAq5jPQ6fI5j/rwOB814lFNhfs2AJMT72Jij8usTEqWZfdU8C3uag==
resolved "https://github.com/repository/discord-markdown#b9608feef6856c9baa68f96c932a25c1d2bc55c2"
dependencies:
highlight.js "^11.2.0"
base-64 "^1.0.0"
simple-markdown "^0.7.3"
email-addresses@^3.0.1:
@ -653,10 +657,10 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
highlight.js@^11.2.0:
version "11.3.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291"
integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw==
highlight.js@^11.7.0:
version "11.7.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
immutable@^4.0.0:
version "4.1.0"

View File

@ -24,6 +24,7 @@ services:
command: ["bin/PluralKit.API.dll"]
environment:
- "PluralKit:Database=Host=db;Username=postgres;Password=postgres;Database=postgres;Maximum Pool Size=1000"
- "PluralKit:RedisAddr=redis"
ports:
- "127.0.0.1:2838:5000"
restart: unless-stopped

View File

@ -31,8 +31,9 @@ When something goes wrong, the API will send back a 4xx HTTP status code, along
|code|HTTP response code|meaning|
|---|---|---|
|0|500|Internal server error, try again later|
|0|400|Bad Request (usually invalid JSON)|
|0|400|Invalid JSON, or invalid request format (check `error` key in the response body)|
|0|401|Missing or invalid Authorization header|
|0|403|Your access to the API is blocked - please contact us in the support server|
|20001|404|System not found.|
|20002|404|Member not found.|
|20003|404|Member '{memberRef}' not found.|

View File

@ -46,6 +46,7 @@ Every PluralKit entity has two IDs: a short (5-character) ID and a longer UUID.
|birthday|?string|`YYYY-MM-DD` format, 0004 hides the year|
|pronouns|?string|100-character-limit|
|avatar_url|?string|256-character limit, must be a publicly-accessible URL|
|webhook_avatar_url|?string|256-character limit, must be a publicly-accessible URL|
|banner|?string|256-character limit, must be a publicly-accessible URL|
|description|?string|1000-character limit|
|created|?datetime||

View File

@ -21,7 +21,14 @@ For models that have them, the keys `id`, `uuid` and `created` are **not** user-
Endpoints taking JSON bodies (eg. most `PATCH` and `PUT` endpoints) require the `Content-Type: application/json` header set.
## User agent
The API requires the `User-Agent` header to be set to a non-empty string. Not doing so will return a `400 Bad Request` with a JSON body.
If you are developing an application exposed to the public, we would appreciate if your `User-Agent` uniquely identifies your application, and (if possible) provides some contact information for the developers - so that we are able to contact you if we notice your application doing something it shouldn't.
## Authentication
Authentication is done with a simple "system token". You can get your system token by running `pk;token` using the
Discord bot, either in a channel with the bot or in DMs. Then, pass this token in the `Authorization` HTTP header
on requests that require it. Failure to do so on endpoints that require authentication will return a `401 Unauthorized`.
@ -59,7 +66,7 @@ The following API libraries have been created by members of our community. Pleas
- **Python:** *PluralKit.py* ([PyPI](https://pypi.org/project/pluralkit/) | [Docs](https://pluralkit.readthedocs.io/en/latest/source/quickstart.html) | [Source code](https://github.com/almonds0166/pluralkit.py))
- **JavaScript:** *pkapi.js* ([npmjs](https://npmjs.com/package/pkapi.js) | [Docs](https://github.com/greysdawn/pk.js/wiki) | [Source code](https://github.com/greysdawn/pk.js))
- **Golang:** *pkgo* (install: `go get github.com/starshine-sys/pkgo` | [Docs (godoc)](https://godocs.io/github.com/starshine-sys/pkgo) | [Docs (pkg.go.dev)](https://pkg.go.dev/github.com/starshine-sys/pkgo) | [Source code](https://github.com/starshine-sys/pkgo))
- **Golang:** *pkgo* (install: `go get github.com/starshine-sys/pkgo/v2` | [Docs (godoc)](https://godocs.io/github.com/starshine-sys/pkgo/v2) | [Docs (pkg.go.dev)](https://pkg.go.dev/github.com/starshine-sys/pkgo/v2) | [Source code](https://github.com/starshine-sys/pkgo))
- **Kotlin:** *Plural.kt* ([Maven Repository](https://maven.proxyfox.dev/dev/proxyfox/pluralkt) | [Source code](https://github.com/The-ProxyFox-Group/Plural.kt))
Do let us know in the support server if you made a new library and would like to see it listed here!

View File

@ -65,6 +65,7 @@ Some arguments indicate the use of specific Discord features. These include:
- `pk;member <member> servername <new server name>` - Changes the display name of a member, only in the current server.
- `pk;member <member> description [description]` - Changes the description of a member.
- `pk;member <member> avatar [avatar url|@mention|upload]` - Changes the avatar of a member.
- `pk;member <member> proxyavatar [avatar url|@mention|upload]` - Changes the avatar used for proxied messages sent by a member.
- `pk;member <member> serveravatar [avatar url|@mention|upload]` - Changes the avatar of a member in a specific server.
- `pk;member <name> banner [image url|upload]` - Changes the banner image of a member.
- `pk;member <member> privacy` - Displays a members current privacy settings.

View File

@ -229,6 +229,13 @@ To preview the current avatar (if one is set), use the command with no arguments
To clear your avatar, use the subcommand `avatar clear` (eg. `pk;member John avatar clear`).
### Member proxy avatar
If you want your member to have a different avatar for proxies messages than the one displayed on the member card, you can set a proxy avatar. To do so, use the `pk;member proxyavatar` command, in the same way as the normal avatar command above:
pk;member John avatar
pk;member John proxyavatar http://placebeard.it/512.jpg
pk;member "Craig Johnson" proxyavatar (with an attached image)
### Member server avatar
You can also set an avatar for a specific server. This will "override" the normal avatar, and will be used when proxying messages and looking up member cards in that server. To do so, use the `pk;member serveravatar` command, in the same way as the normal avatar command above:

View File

@ -25,20 +25,15 @@ func proxyTo(host string) *httputil.ReverseProxy {
// todo: this shouldn't be in this repo
var remotes = map[string]*httputil.ReverseProxy{
"api.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:202]:5000"),
"dash.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:202]:8080"),
"sentry.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:202]:9000"),
"api.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:902]:5000"),
"dash.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:902]:8080"),
"sentry.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:902]:9000"),
"grafana.pluralkit.me": proxyTo("[fdaa:0:ae33:a7b:8dd7:0:a:802]:3000"),
}
type ProxyHandler struct{}
func (p ProxyHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.Header.Get("User-Agent") == "" {
// please set a valid user-agent
rw.WriteHeader(403)
return
}
remote, ok := remotes[r.Host]
if !ok {
// unknown domains redirect to landing page