diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 0d5eb3d7..ee95dc0d 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -304,9 +304,11 @@ public partial class CommandTree await ctx.Execute(MemberDelete, m => m.Delete(ctx, target)); else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); + else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa")) + await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); else if (ctx.Match("banner", "splash", "cover")) await ctx.Execute(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(MemberGroupAdd, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add)); @@ -406,9 +408,9 @@ public partial class CommandTree { if (ctx.Match("out")) await ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)); - else if (ctx.Match("move", "shift", "offset")) + else if (ctx.Match("move", "m", "shift", "offset")) await ctx.Execute(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(SwitchEditOut, m => m.SwitchEditOut(ctx)); else diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index ce73c522..50067fe0 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -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, }; diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index f189c8f7..99b3e680 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -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( await response.Content.ReadAsStringAsync(), _settings diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index cce0dd9e..bcf1b3c0 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -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)) + }; + } } \ No newline at end of file diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 5a3290da..dc665d67 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -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 diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index fb671565..0a8c56b5 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -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( diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index c0d41524..380110a7 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -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; } \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 0b9f3a76..a49fac8f 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -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 diff --git a/PluralKit.Core/Database/Migrations/33.sql b/PluralKit.Core/Database/Migrations/33.sql new file mode 100644 index 00000000..fc648ff6 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/33.sql @@ -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; \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 10183fdd..ff6efb4b 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -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) diff --git a/PluralKit.Core/Models/PKMember.cs b/PluralKit.Core/Models/PKMember.cs index bbae8440..ede4368c 100644 --- a/PluralKit.Core/Models/PKMember.cs +++ b/PluralKit.Core/Models/PKMember.cs @@ -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()); diff --git a/PluralKit.Core/Models/Patch/MemberPatch.cs b/PluralKit.Core/Models/Patch/MemberPatch.cs index da02557d..4101a7c6 100644 --- a/PluralKit.Core/Models/Patch/MemberPatch.cs +++ b/PluralKit.Core/Models/Patch/MemberPatch.cs @@ -12,6 +12,7 @@ public class MemberPatch: PatchObject public Partial Name { get; set; } public Partial Hid { get; set; } public Partial DisplayName { get; set; } + public Partial WebhookAvatarUrl { get; set; } public Partial AvatarUrl { get; set; } public Partial BannerImage { get; set; } public Partial 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("name"); if (o.ContainsKey("color")) patch.Color = o.Value("color").NullIfEmpty()?.ToLower(); if (o.ContainsKey("display_name")) patch.DisplayName = o.Value("display_name").NullIfEmpty(); + if (o.ContainsKey("webhook_avatar_url")) patch.WebhookAvatarUrl = o.Value("webhook_avatar_url").NullIfEmpty(); if (o.ContainsKey("avatar_url")) patch.AvatarUrl = o.Value("avatar_url").NullIfEmpty(); if (o.ContainsKey("banner")) patch.BannerImage = o.Value("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) diff --git a/dashboard/package.json b/dashboard/package.json index 44939acd..7dfe415e 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -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", diff --git a/dashboard/src/api/parse-markdown.ts b/dashboard/src/api/parse-markdown.ts new file mode 100644 index 00000000..daf9b833 --- /dev/null +++ b/dashboard/src/api/parse-markdown.ts @@ -0,0 +1,421 @@ +import { toHTML } from 'discord-markdown'; +import hljs from 'highlight.js/lib/core'; +import parseTimestamps from './parse-timestamps'; + +const languages: Record Promise> = { + "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 = { + "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; \ No newline at end of file diff --git a/dashboard/src/components/common/AwaitHtml.svelte b/dashboard/src/components/common/AwaitHtml.svelte new file mode 100644 index 00000000..f4d2eae3 --- /dev/null +++ b/dashboard/src/components/common/AwaitHtml.svelte @@ -0,0 +1,11 @@ + + +{#await htmlPromise} + (loading...) +{:then html} + {@html html ?? ""} +{:catch error} + (failed to parse: {error?.message ?? String(error)}) +{/await} \ No newline at end of file diff --git a/dashboard/src/components/common/CardsHeader.svelte b/dashboard/src/components/common/CardsHeader.svelte index 5d029633..53b0f854 100644 --- a/dashboard/src/components/common/CardsHeader.svelte +++ b/dashboard/src/components/common/CardsHeader.svelte @@ -1,23 +1,24 @@ @@ -123,6 +127,36 @@ {/if} + {#if token} + + + + + +
+ +
Recovery +
+
+ +

If you've lost access to your discord account, you can retrieve your token here.

+

Send a direct message to a staff member (a helper, moderator or developer in the support server), they can recover your system with this token.

+ + {#if showToken} + + + {token} + + + + + + {/if} +
+
+ +
+ {/if}
diff --git a/dashboard/vite.config.js b/dashboard/vite.config.js index 0ae16832..d93f2208 100644 --- a/dashboard/vite.config.js +++ b/dashboard/vite.config.js @@ -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}`; } - } + } } }) diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index f2d7ecf9..043a8352 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index ba2ad6f5..161fd133 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/content/api/errors.md b/docs/content/api/errors.md index b8419c0e..7fae4f6c 100644 --- a/docs/content/api/errors.md +++ b/docs/content/api/errors.md @@ -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.| diff --git a/docs/content/api/models.md b/docs/content/api/models.md index 35d159fe..c1d9c0e9 100644 --- a/docs/content/api/models.md +++ b/docs/content/api/models.md @@ -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|| diff --git a/docs/content/api/reference.md b/docs/content/api/reference.md index d233a9ac..20040331 100644 --- a/docs/content/api/reference.md +++ b/docs/content/api/reference.md @@ -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! diff --git a/docs/content/command-list.md b/docs/content/command-list.md index b96e3861..75b3c0ba 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -65,6 +65,7 @@ Some arguments indicate the use of specific Discord features. These include: - `pk;member servername ` - Changes the display name of a member, only in the current server. - `pk;member description [description]` - Changes the description of a member. - `pk;member avatar [avatar url|@mention|upload]` - Changes the avatar of a member. +- `pk;member proxyavatar [avatar url|@mention|upload]` - Changes the avatar used for proxied messages sent by a member. - `pk;member serveravatar [avatar url|@mention|upload]` - Changes the avatar of a member in a specific server. - `pk;member banner [image url|upload]` - Changes the banner image of a member. - `pk;member privacy` - Displays a members current privacy settings. diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index 41acceb6..bb4f47e9 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -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: diff --git a/services/web-proxy/main.go b/services/web-proxy/main.go index 7ffc091a..7738743a 100644 --- a/services/web-proxy/main.go +++ b/services/web-proxy/main.go @@ -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