diff --git a/PluralKit.API/Controllers/v2/SystemControllerV2.cs b/PluralKit.API/Controllers/v2/SystemControllerV2.cs index 8990a98e..0445da88 100644 --- a/PluralKit.API/Controllers/v2/SystemControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SystemControllerV2.cs @@ -27,7 +27,7 @@ public class SystemControllerV2: PKControllerBase if (system == null) throw Errors.SystemNotFound; - return Ok(APIJsonExt.EmbedJson(system.Name ?? $"System with ID `{system.Hid}`", "System")); + return Ok(APIJsonExt.EmbedJson(system.NameFor(ContextFor(system)) ?? $"System with ID `{system.Hid}`", "System")); } [HttpPatch("{systemRef}")] diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 859c3c34..badd9d41 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -5,12 +5,14 @@ public partial class CommandTree public static Command SystemInfo = new Command("system", "system [system]", "Looks up information about a system"); public static Command SystemNew = new Command("system new", "system new [name]", "Creates a new system"); public static Command SystemRename = new Command("system name", "system [system] rename [name]", "Renames your system"); + public static Command SystemServerName = new Command("system servername", "system [system] servername [name]", "Changes your system displayname for this server"); public static Command SystemDesc = new Command("system description", "system [system] description [description]", "Changes your system's description"); public static Command SystemColor = new Command("system color", "system [system] color [color]", "Changes your system's color"); public static Command SystemTag = new Command("system tag", "system [system] tag [tag]", "Changes your system's tag"); public static Command SystemPronouns = new Command("system pronouns", "system [system] pronouns [pronouns]", "Changes your system's pronouns"); public static Command SystemServerTag = new Command("system servertag", "system [system] servertag [tag|enable|disable]", "Changes your system's tag in the current server"); public static Command SystemAvatar = new Command("system icon", "system [system] icon [url|@mention]", "Changes your system's icon"); + public static Command SystemServerAvatar = new Command("system serveravatar", "system [system] serveravatar [tag]", "Changes your system's icon in the current server"); public static Command SystemBannerImage = new Command("system banner", "system [system] banner [url]", "Set the system's banner image"); public static Command SystemDelete = new Command("system delete", "system [system] delete", "Deletes your system"); public static Command SystemProxy = new Command("system proxy", "system proxy [server id] [on|off]", "Enables or disables message proxying in a specific server"); @@ -20,7 +22,7 @@ public partial class CommandTree public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history"); public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown"); public static Command SystemId = new Command("system id", "system [system] id", "Prints your system's id."); - public static Command SystemPrivacy = new Command("system privacy", "system [system] privacy ", "Changes your system's privacy settings"); + public static Command SystemPrivacy = new Command("system privacy", "system [system] privacy ", "Changes your system's privacy settings"); public static Command ConfigTimezone = new Command("config timezone", "config timezone [timezone]", "Changes your system's time zone"); public static Command ConfigPing = new Command("config ping", "config ping [on|off]", "Changes your system's ping preferences"); public static Command ConfigAutoproxyAccount = new Command("config autoproxy account", "config autoproxy account [on|off]", "Toggles autoproxy globally for the current account"); @@ -107,7 +109,7 @@ public partial class CommandTree public static Command[] SystemCommands = { - SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemBannerImage, SystemColor, + SystemInfo, SystemNew, SystemRename, SystemServerName, SystemTag, SystemDesc, SystemAvatar, SystemServerAvatar, SystemBannerImage, SystemColor, SystemDelete, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy, SystemProxy }; diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index ba5b3474..b5d4cce9 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -220,6 +220,9 @@ public partial class CommandTree { if (ctx.Match("name", "rename", "changename", "rn")) await ctx.CheckSystem(target).Execute(SystemRename, m => m.Name(ctx, target)); + else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname", + "serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn")) + await ctx.Execute(SystemServerName, m => m.ServerName(ctx, target)); else if (ctx.Match("tag", "t")) await ctx.CheckSystem(target).Execute(SystemTag, m => m.Tag(ctx, target)); else if (ctx.Match("servertag", "st", "stag", "deer")) @@ -234,6 +237,9 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.CheckSystem(target).Execute(SystemAvatar, m => m.Avatar(ctx, target)); + else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", + "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) + await ctx.CheckSystem(target).Execute(SystemServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("list", "l", "members", "ls")) await ctx.CheckSystem(target).Execute(SystemList, m => m.MemberList(ctx, target)); else if (ctx.Match("find", "search", "query", "fd", "s")) @@ -319,7 +325,7 @@ public partial class CommandTree else await ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, target)); else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", - "guildavatar", "guildpic", "guildicon", "sicon")) + "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) await ctx.Execute(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname")) await ctx.Execute(MemberDisplayName, m => m.DisplayName(ctx, target)); diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index dd3b3e74..0df8c7e1 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -131,10 +131,23 @@ public class GroupMember opts.GroupFilter = target.Id; var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in "); - if (targetSystem.Name != null) - title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)"); + if (ctx.Guild != null) + { + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id); + if (guildSettings.DisplayName != null) + title.Append($"{guildSettings.DisplayName} (`{targetSystem.Hid}`)"); + else if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)"); + else + title.Append($"`{targetSystem.Hid}`"); + } else - title.Append($"`{targetSystem.Hid}`"); + { + if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)"); + else + title.Append($"`{targetSystem.Hid}`"); + } if (opts.Search != null) title.Append($" matching **{opts.Search.Truncate(100)}**"); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index e62bdb7d..e6d3ddcd 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -28,6 +28,7 @@ public class SystemEdit public async Task Name(Context ctx, PKSystem target) { + ctx.CheckSystemPrivacy(target.Id, target.NamePrivacy); var isOwnSystem = target.Id == ctx.System?.Id; var noNameSetMessage = $"{(isOwnSystem ? "Your" : "This")} system does not have a name set."; @@ -76,6 +77,60 @@ public class SystemEdit } } + public async Task ServerName(Context ctx, PKSystem target) + { + ctx.CheckGuildContext(); + + var isOwnSystem = target.Id == ctx.System?.Id; + + var noNameSetMessage = $"{(isOwnSystem ? "Your" : "This")} system does not have a name specific to this server."; + if (isOwnSystem) + noNameSetMessage += " Type `pk;system servername ` to set one."; + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + + if (ctx.MatchRaw()) + { + if (settings.DisplayName != null) + await ctx.Reply($"```\n{settings.DisplayName}\n```"); + else + await ctx.Reply(noNameSetMessage); + return; + } + + if (!ctx.HasNext(false)) + { + if (settings.DisplayName != null) + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." + + (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "") + + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters."); + else + await ctx.Reply(noNameSetMessage); + return; + } + + ctx.CheckSystem().CheckOwnSystem(target); + + if (ctx.MatchClear() && await ctx.ConfirmClear("your system's name for this server")) + { + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = null }); + + await ctx.Reply($"{Emojis.Success} System name for this server cleared."); + } + else + { + var newSystemGuildName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); + + if (newSystemGuildName.Length > Limits.MaxSystemNameLength) + throw Errors.StringTooLongError("System name for this server", newSystemGuildName.Length, Limits.MaxSystemNameLength); + + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = newSystemGuildName }); + + await ctx.Reply($"{Emojis.Success} System name for this server changed (using {newSystemGuildName.Length}/{Limits.MaxSystemNameLength} characters)."); + } + } + public async Task Description(Context ctx, PKSystem target) { ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); @@ -471,6 +526,80 @@ public class SystemEdit await ShowIcon(); } + public async Task ServerAvatar(Context ctx, PKSystem target) + { + + async Task ClearIcon() + { + ctx.CheckOwnSystem(target); + + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = null }); + await ctx.Reply($"{Emojis.Success} System server avatar cleared."); + } + + async Task SetIcon(ParsedImage img) + { + ctx.CheckOwnSystem(target); + + await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url); + + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.Url }); + + var msg = img.Source switch + { + AvatarSource.User => + $"{Emojis.Success} System icon for this server changed to {img.SourceUser?.Username}'s avatar! It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon for this server will need to be re-set.", + AvatarSource.Url => + $"{Emojis.Success} System icon for this server changed to the image at the given URL. It will now be used for anything that uses system avatar in this server.", + AvatarSource.Attachment => + $"{Emojis.Success} System icon for this server changed to attached image. It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon for this server will stop working.", + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); + } + + async Task ShowIcon() + { + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + + if ((settings.AvatarUrl?.Trim() ?? "").Length > 0) + { + var eb = new EmbedBuilder() + .Title("System server icon") + .Image(new Embed.EmbedImage(settings.AvatarUrl.TryGetCleanCdnUrl())); + if (target.Id == ctx.System?.Id) + eb.Description("To clear, use `pk;system servericon clear`."); + await ctx.Reply(embed: eb.Build()); + } + else + { + throw new PKSyntaxError( + "This system does not have a icon specific to this server. Set one by attaching an image to this command, or by passing an image URL or @mention."); + } + } + + ctx.CheckGuildContext(); + + if (target != null && target?.Id != ctx.System?.Id) + { + await ShowIcon(); + return; + } + + if (ctx.MatchClear() && await ctx.ConfirmClear("your system's icon for this server")) + await ClearIcon(); + else if (await ctx.MatchImage() is { } img) + await SetIcon(img); + else + await ShowIcon(); + } + public async Task BannerImage(Context ctx, PKSystem target) { ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); @@ -630,6 +759,8 @@ public class SystemEdit { var eb = new EmbedBuilder() .Title("Current privacy settings for your system") + .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) + .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) .Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation())) @@ -637,7 +768,7 @@ public class SystemEdit .Field(new Embed.Field("Current fronter(s)", target.FrontPrivacy.Explanation())) .Field(new Embed.Field("Front/switch history", target.FrontHistoryPrivacy.Explanation())) .Description( - "To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); + "To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `name`, `avatar`, `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); return ctx.Reply(embed: eb.Build()); } @@ -654,6 +785,8 @@ public class SystemEdit var subjectStr = subject switch { + SystemPrivacySubject.Name => "name", + SystemPrivacySubject.Avatar => "avatar", SystemPrivacySubject.Description => "description", SystemPrivacySubject.Pronouns => "pronouns", SystemPrivacySubject.Front => "front", diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index fa687556..6c4e093e 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -44,10 +44,17 @@ public class SystemFront .Scan(new FrontHistoryEntry(null, null), (lastEntry, newSwitch) => new FrontHistoryEntry(lastEntry.ThisSwitch?.Timestamp, newSwitch)); - var embedTitle = system.Name != null - ? $"Front history of {system.Name} (`{system.Hid}`)" + var embedTitle = system.NameFor(ctx) != null + ? $"Front history of {system.NameFor(ctx)} (`{system.Hid}`)" : $"Front history of `{system.Hid}`"; + if (ctx.Guild != null) + { + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, system.Id); + if (guildSettings.DisplayName != null) + embedTitle = $"Front history of {guildSettings.DisplayName} (`{system.Hid}`)"; + } + var showMemberId = ctx.MatchFlag("with-id", "wid"); await ctx.Paginate( @@ -127,10 +134,13 @@ public class SystemFront if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; var title = new StringBuilder("Frontpercent of "); + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, system.Id); if (group != null) title.Append($"{group.NameFor(ctx)} (`{group.Hid}`)"); - else if (system.Name != null) - title.Append($"{system.Name} (`{system.Hid}`)"); + else if (guildSettings.DisplayName != null) + title.Append($"{guildSettings.DisplayName} (`{system.Hid}`)"); + else if (system.NameFor(ctx) != null) + title.Append($"{system.NameFor(ctx)} (`{system.Hid}`)"); else title.Append($"`{system.Hid}`"); diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index ba68fe26..dd357c7f 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -21,18 +21,21 @@ public class SystemList await ctx.RenderMemberList( ctx.LookupContextFor(target.Id), target.Id, - GetEmbedTitle(target, opts), + await GetEmbedTitle(target, opts, ctx), target.Color, opts ); } - private string GetEmbedTitle(PKSystem target, ListOptions opts) + private async Task GetEmbedTitle(PKSystem target, ListOptions opts, Context ctx) { var title = new StringBuilder("Members of "); - if (target.Name != null) - title.Append($"{target.Name} (`{target.Hid}`)"); + var systemGuildSettings = ctx.Guild != null ? await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id) : null; + if (systemGuildSettings != null && systemGuildSettings.DisplayName != null) + title.Append($"{systemGuildSettings.DisplayName} (`{target.Hid}`)"); + else if (target.NameFor(ctx) != null) + title.Append($"{target.NameFor(ctx)} (`{target.Hid}`)"); else title.Append($"`{target.Hid}`"); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 362927ba..63410be6 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -68,13 +68,16 @@ public class EmbedService } var eb = new EmbedBuilder() - .Title(system.Name) - .Thumbnail(new Embed.EmbedThumbnail(system.AvatarUrl.TryGetCleanCdnUrl())) + .Title(system.NameFor(ctx)) .Footer(new Embed.EmbedFooter( $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}")) .Color(color) .Url($"https://dash.pluralkit.me/profile/s/{system.Hid}"); + var avatar = system.AvatarFor(ctx); + if (avatar != null) + eb.Thumbnail(new Embed.EmbedThumbnail(avatar)); + if (system.DescriptionPrivacy.CanAccess(ctx)) eb.Image(new Embed.EmbedImage(system.BannerImage)); @@ -106,6 +109,21 @@ public class EmbedService if (!guildSettings.TagEnabled) eb.Field(new Embed.Field($"Tag (in server '{cctx.Guild.Name}')", "*(tag is disabled in this server)*")); + + if (guildSettings.DisplayName != null) + eb.Title(guildSettings.DisplayName); + + var guildAvatar = guildSettings.AvatarUrl.TryGetCleanCdnUrl(); + if (guildAvatar != null) + { + eb.Thumbnail(new Embed.EmbedThumbnail(guildAvatar)); + var sysDesc = "*(this system has a server-specific avatar set"; + if (avatar != null) + sysDesc += $"; [click here]({system.AvatarUrl.TryGetCleanCdnUrl()}) to see their global avatar)*"; + else + sysDesc += ")*"; + eb.Description(sysDesc); + } } if (system.PronounPrivacy.CanAccess(ctx) && system.Pronouns != null) @@ -162,7 +180,13 @@ public class EmbedService // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); var name = member.NameFor(ctx); - if (system.Name != null) name = $"{name} ({system.Name})"; + var systemGuildSettings = guild != null ? await _repo.GetSystemGuild(guild.Id, system.Id) : null; + if (systemGuildSettings != null && systemGuildSettings.DisplayName != null) + name = $"{name} ({systemGuildSettings.DisplayName})"; + else if (system.NameFor(ctx) != null) + name = $"{name} ({system.NameFor(ctx)})"; + else + name = $"{name}"; uint color; try @@ -256,8 +280,13 @@ public class EmbedService var memberCount = await _repo.GetGroupMemberCount(target.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public); - var nameField = target.NamePrivacy.Get(pctx, target.Name, target.DisplayName ?? target.Name); - if (system.Name != null) + var nameField = target.NameFor(ctx); + var systemGuildSettings = ctx.Guild != null ? await _repo.GetSystemGuild(ctx.Guild.Id, system.Id) : null; + if (systemGuildSettings != null && systemGuildSettings.DisplayName != null) + nameField = $"{nameField} ({systemGuildSettings.DisplayName})"; + else if (system.NameFor(ctx) != null) + nameField = $"{nameField} ({system.NameFor(ctx)})"; + else nameField = $"{nameField} ({system.Name})"; uint color; @@ -395,7 +424,7 @@ public class EmbedService .Field(new Embed.Field("System", msg.System == null ? "*(deleted or unknown system)*" - : msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`" + : msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`" , true)) .Field(new Embed.Field("Member", msg.Member == null diff --git a/PluralKit.Bot/Utils/ModelUtils.cs b/PluralKit.Bot/Utils/ModelUtils.cs index 8944598e..4df17c32 100644 --- a/PluralKit.Bot/Utils/ModelUtils.cs +++ b/PluralKit.Bot/Utils/ModelUtils.cs @@ -12,6 +12,9 @@ public static class ModelUtils public static string NameFor(this PKGroup group, Context ctx) => group.NameFor(ctx.LookupContextFor(group.System)); + public static string NameFor(this PKSystem system, Context ctx) => + system.NameFor(ctx.LookupContextFor(system.Id)); + public static string AvatarFor(this PKMember member, Context ctx) => member.AvatarFor(ctx.LookupContextFor(member.System)).TryGetCleanCdnUrl(); diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 7c90f13c..8d424dce 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -26,6 +26,7 @@ public class MessageContext public string? SystemGuildTag { get; } public bool TagEnabled { get; } public string? SystemAvatar { get; } + public string? SystemGuildAvatar { get; } public bool AllowAutoproxy { get; } public int? LatchTimeout { get; } public bool CaseSensitiveProxyTags { get; } diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index 380110a7..bb79bb2f 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -42,5 +42,5 @@ public class ProxyMember return memberName; } - public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? WebhookAvatar ?? Avatar ?? ctx.SystemAvatar; + public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? WebhookAvatar ?? Avatar ?? ctx.SystemGuildAvatar ?? 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 6ce9e360..61ae9255 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -13,6 +13,7 @@ create function message_context(account_id bigint, guild_id bigint, channel_id b system_guild_tag text, tag_enabled bool, system_avatar text, + system_guild_avatar text, allow_autoproxy bool, latch_timeout integer, case_sensitive_proxy_tags bool, @@ -22,6 +23,7 @@ as $$ -- CTEs to query "static" (accessible only through args) data with system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, + system_guild.avatar_url as guild_avatar, allow_autoproxy as account_autoproxy, system_config.case_sensitive_proxy_tags, system_config.proxy_error_message_enabled from accounts left join systems on systems.id = accounts.system left join system_config on system_config.system = accounts.system @@ -44,6 +46,7 @@ as $$ system.guild_tag as system_guild_tag, coalesce(system.tag_enabled, true) as tag_enabled, system.avatar_url as system_avatar, + system.guild_avatar as system_guild_avatar, system.account_autoproxy as allow_autoproxy, system.latch_timeout as latch_timeout, system.case_sensitive_proxy_tags as case_sensitive_proxy_tags, diff --git a/PluralKit.Core/Database/Migrations/35.sql b/PluralKit.Core/Database/Migrations/35.sql new file mode 100644 index 00000000..bf5cb12a --- /dev/null +++ b/PluralKit.Core/Database/Migrations/35.sql @@ -0,0 +1,7 @@ +-- database version 35 +-- add guild avatar and guild name to system guild settings + +alter table system_guild add column avatar_url text; +alter table system_guild add column display_name text; + +update info set schema_version = 35; \ No newline at end of file diff --git a/PluralKit.Core/Database/Migrations/36.sql b/PluralKit.Core/Database/Migrations/36.sql new file mode 100644 index 00000000..a9e58277 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/36.sql @@ -0,0 +1,7 @@ +-- database version 36 +-- add system avatar privacy and system name privacy + +alter table systems add column name_privacy integer not null default 1; +alter table systems add column avatar_privacy integer not null default 1; + +update info set schema_version = 36; \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 4106cfb0..e6d18c3b 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 = 34; + private const int TargetSchemaVersion = 36; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index e42805cc..1b91bae7 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -46,6 +46,8 @@ public class PKSystem public string WebhookUrl { get; } public string WebhookToken { get; } public Instant Created { get; } + public PrivacyLevel NamePrivacy { get; } + public PrivacyLevel AvatarPrivacy { get; } public PrivacyLevel DescriptionPrivacy { get; } public PrivacyLevel MemberListPrivacy { get; } public PrivacyLevel FrontPrivacy { get; } @@ -59,18 +61,24 @@ public static class PKSystemExt public static string DescriptionFor(this PKSystem system, LookupContext ctx) => system.DescriptionPrivacy.Get(ctx, system.Description); + public static string NameFor(this PKSystem system, LookupContext ctx) => + system.NamePrivacy.Get(ctx, system.Name); + + public static string AvatarFor(this PKSystem system, LookupContext ctx) => + system.AvatarPrivacy.Get(ctx, system.AvatarUrl.TryGetCleanCdnUrl()); + public static JObject ToJson(this PKSystem system, LookupContext ctx) { var o = new JObject(); o.Add("id", system.Hid); o.Add("uuid", system.Uuid.ToString()); - o.Add("name", system.Name); + o.Add("name", system.NameFor(ctx)); o.Add("description", system.DescriptionFor(ctx)); o.Add("tag", system.Tag); o.Add("pronouns", system.PronounPrivacy.Get(ctx, system.Pronouns)); - o.Add("avatar_url", system.AvatarUrl.TryGetCleanCdnUrl()); + o.Add("avatar_url", system.AvatarFor(ctx)); o.Add("banner", system.DescriptionPrivacy.Get(ctx, system.BannerImage).TryGetCleanCdnUrl()); o.Add("color", system.Color); o.Add("created", system.Created.FormatExport()); @@ -83,6 +91,8 @@ public static class PKSystemExt var p = new JObject(); + p.Add("name_privacy", system.NamePrivacy.ToJsonString()); + p.Add("avatar_privacy", system.AvatarPrivacy.ToJsonString()); p.Add("description_privacy", system.DescriptionPrivacy.ToJsonString()); p.Add("pronoun_privacy", system.PronounPrivacy.ToJsonString()); p.Add("member_list_privacy", system.MemberListPrivacy.ToJsonString()); diff --git a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs index 628877e9..f19ff00f 100644 --- a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs @@ -11,17 +11,26 @@ public class SystemGuildPatch: PatchObject public Partial ProxyEnabled { get; set; } public Partial Tag { get; set; } public Partial TagEnabled { get; set; } + public Partial AvatarUrl { get; set; } + public Partial DisplayName { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("proxy_enabled", ProxyEnabled) .With("tag", Tag) .With("tag_enabled", TagEnabled) + .With("avatar_url", AvatarUrl) + .With("display_name", DisplayName) ); public new void AssertIsValid() { if (Tag.Value != null) AssertValid(Tag.Value, "tag", Limits.MaxSystemTagLength); + if (AvatarUrl.Value != null) + AssertValid(AvatarUrl.Value, "avatar_url", Limits.MaxUriLength, + s => MiscUtils.TryMatchUri(s, out var avatarUri)); + if (DisplayName.Value != null) + AssertValid(DisplayName.Value, "display_name", Limits.MaxMemberNameLength); } #nullable disable @@ -38,6 +47,12 @@ public class SystemGuildPatch: PatchObject if (o.ContainsKey("tag_enabled") && o["tag_enabled"].Type != JTokenType.Null) patch.TagEnabled = o.Value("tag_enabled"); + if (o.ContainsKey("avatar_url")) + patch.AvatarUrl = o.Value("avatar_url").NullIfEmpty(); + + if (o.ContainsKey("display_name")) + patch.DisplayName = o.Value("display_name").NullIfEmpty(); + return patch; } @@ -56,6 +71,12 @@ public class SystemGuildPatch: PatchObject if (TagEnabled.IsPresent) o.Add("tag_enabled", TagEnabled.Value); + if (AvatarUrl.IsPresent) + o.Add("avatar_url", AvatarUrl.Value); + + if (DisplayName.IsPresent) + o.Add("display_name", DisplayName.Value); + return o; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index 0fc0ef77..fd9c5b6d 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -18,6 +18,8 @@ public class SystemPatch: PatchObject public Partial Token { get; set; } public Partial WebhookUrl { get; set; } public Partial WebhookToken { get; set; } + public Partial NamePrivacy { get; set; } + public Partial AvatarPrivacy { get; set; } public Partial DescriptionPrivacy { get; set; } public Partial MemberListPrivacy { get; set; } public Partial GroupListPrivacy { get; set; } @@ -37,6 +39,8 @@ public class SystemPatch: PatchObject .With("token", Token) .With("webhook_url", WebhookUrl) .With("webhook_token", WebhookToken) + .With("name_privacy", NamePrivacy) + .With("avatar_privacy", AvatarPrivacy) .With("description_privacy", DescriptionPrivacy) .With("member_list_privacy", MemberListPrivacy) .With("group_list_privacy", GroupListPrivacy) @@ -93,6 +97,12 @@ public class SystemPatch: PatchObject { var privacy = o.Value("privacy"); + if (privacy.ContainsKey("name_privacy")) + patch.NamePrivacy = patch.ParsePrivacy(privacy, "name_privacy"); + + if (privacy.ContainsKey("avatar_privacy")) + patch.AvatarPrivacy = patch.ParsePrivacy(privacy, "avatar_privacy"); + if (privacy.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy"); @@ -137,7 +147,9 @@ public class SystemPatch: PatchObject o.Add("color", Color.Value); if ( - DescriptionPrivacy.IsPresent + NamePrivacy.IsPresent + || AvatarPrivacy.IsPresent + || DescriptionPrivacy.IsPresent || PronounPrivacy.IsPresent || MemberListPrivacy.IsPresent || GroupListPrivacy.IsPresent @@ -147,6 +159,12 @@ public class SystemPatch: PatchObject { var p = new JObject(); + if (NamePrivacy.IsPresent) + p.Add("name_privacy", NamePrivacy.Value.ToJsonString()); + + if (AvatarPrivacy.IsPresent) + p.Add("avatar_privacy", AvatarPrivacy.Value.ToJsonString()); + if (DescriptionPrivacy.IsPresent) p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString()); diff --git a/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs b/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs index 13f4af37..3bc8a2aa 100644 --- a/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs +++ b/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs @@ -2,6 +2,8 @@ namespace PluralKit.Core; public enum SystemPrivacySubject { + Name, + Avatar, Description, Pronouns, MemberList, @@ -17,6 +19,8 @@ public static class SystemPrivacyUtils // what do you mean switch expressions can't be statements >.> _ = subject switch { + SystemPrivacySubject.Name => system.NamePrivacy = level, + SystemPrivacySubject.Avatar => system.AvatarPrivacy = level, SystemPrivacySubject.Description => system.DescriptionPrivacy = level, SystemPrivacySubject.Pronouns => system.PronounPrivacy = level, SystemPrivacySubject.Front => system.FrontPrivacy = level, @@ -40,6 +44,15 @@ public static class SystemPrivacyUtils { switch (input.ToLowerInvariant()) { + case "name": + subject = SystemPrivacySubject.Name; + break; + case "avatar": + case "pfp": + case "pic": + case "icon": + subject = SystemPrivacySubject.Avatar; + break; case "description": case "desc": case "text": diff --git a/PluralKit.Core/Models/SystemGuildSettings.cs b/PluralKit.Core/Models/SystemGuildSettings.cs index e8b88e92..439712e0 100644 --- a/PluralKit.Core/Models/SystemGuildSettings.cs +++ b/PluralKit.Core/Models/SystemGuildSettings.cs @@ -9,6 +9,8 @@ public class SystemGuildSettings public bool ProxyEnabled { get; } = true; public string? Tag { get; } public bool TagEnabled { get; } + public string? AvatarUrl { get; } + public string? DisplayName { get; } } public static class SystemGuildExt @@ -20,6 +22,8 @@ public static class SystemGuildExt o.Add("proxying_enabled", settings.ProxyEnabled); o.Add("tag", settings.Tag); o.Add("tag_enabled", settings.TagEnabled); + o.Add("avatar_url", settings.AvatarUrl); + o.Add("display_name", settings.DisplayName); return o; } diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 5649dfa6..84156057 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -50,8 +50,10 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the - `pk;system [system]` - Shows information about a system. - `pk;system new [name]` - Creates a new system registered to your account. - `pk;system [system] rename [new name]` - Changes the name of your system. +- `pk;system [system] servername [servername]` - Changes the name of your system in the current server. - `pk;system [system] description [description]` - Changes the description of your system. - `pk;system [system] avatar [avatar url|@mention|upload]` - Changes the avatar of your system. +- `pk;system [system] serveravatar [avatar url|@mention|upload]` - Changes the avatar of your system in the current server. - `pk;system [system] banner [image url|upload]` - Changes your system's banner image. - `pk;system [system] color [color]` - Changes your system's color. - `pk;system [system] privacy` - Displays your system's current privacy settings. diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index a8ca876d..aa01718b 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -40,6 +40,18 @@ To view information about *a different* system, there are a number of ways to do pk;system @Craig#5432 pk;system 466378653216014359 pk;system abcde + +### System renaming +If you want to change the name of your system, you can use the `pk;system rename` command, like so: + + pk;system rename New System Name + +### System server names +If you'd like to set a name for your system, but only for a specific server, you can set the system's *server display name*. This shows up in replacement of the server name in the server you set it in. For example: + + pk;system servername Name For This Server + +To clear your system servername for a server, simply run `pk;system servername clear` in in the server in question. The servername cannot be run in DMs, it only applies to servers. ### System description If you'd like to add a small blurb to your system information card, you can add a *system description*. To do so, use the `pk;system description` command, as follows: @@ -51,13 +63,21 @@ There's a 1000 character length limit on your system description - which is quit If you'd like to remove your system description, just type `pk;system description` without any further parameters. ### System avatars -If you'd like your system to have an associated "system avatar", displayed on your system information card, you can add a system avatar. To do so, use the `pk;system avatar` command. You can either supply it with an direct URL to an image, or attach an image directly. For example. +If you'd like your system to have an associated "system avatar", displayed on your system information card, you can add a system avatar. Your system avatar will also show up as the avatar on members who do not have their own when proxying. To do so, use the `pk;system avatar` command. You can either supply it with an direct URL to an image, or attach an image directly. For example. pk;system avatar http://placebeard.it/512.jpg pk;system avatar [with attached image] To clear your avatar, simply type `pk;system avatar` with no attachment or link. +### System server avatars +If you'd like your system to have an avatar (as above), but only for a specific server, you can set the *system server avatar*. This will override the global system avatar, but only in the server you set it in. For example: + + pk;system serveravatar http://placebeard.it/512.jpg + pk;system serveravatar [with attached image] + +To clear your system serveravatar for a server, simply type `pk;system serveravatar clear` with no attachment or link in the server in question. The serveravatar command cannot be run in DMs, it only functions in servers. + ### System tags Your system tag is a little snippet of text that'll be added to the end of all proxied messages. For example, if you want to proxy a member named `James`, and your system tag is `| The Boys`, the final name displayed @@ -645,6 +665,8 @@ At the moment, there are a few aspects of system privacy that can be configured. - Group list - Current fronter - Front history +- System name +- System avatar Each of these can be set to **public** or **private**. When set to **public**, anyone who queries your system (by account or system ID, or through the API), will see this information. When set to **private**, the information will only be shown when *you yourself* query the information. The cards will still be displayed in the channel the commands are run in, so it's still your responsibility not to pull up information in servers where you don't want it displayed. @@ -659,6 +681,8 @@ To update your system privacy settings, use the following commands: * `groups` * `fronter` * `fronthistory` + * `name` + * `avatar` * `all` (to change all subjects at once) * `level` is either `public` or `private`