From 15191171f5dcec0e51bd13dc11113e2317192c42 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 4 Sep 2025 04:01:21 +0300 Subject: [PATCH] feat: implement member avatar commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 18 +-- PluralKit.Bot/Commands/MemberAvatar.cs | 135 ++++++++++++++--------- crates/command_definitions/src/member.rs | 84 ++++++++++++++ 3 files changed, 178 insertions(+), 59 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index a90ad7f5..663f506b 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -16,6 +16,15 @@ public partial class CommandTree Commands.MemberShow(var param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.MemberAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearAvatar(ctx, param.target)), + Commands.MemberAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeAvatar(ctx, param.target, param.avatar)), + Commands.MemberWebhookAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowWebhookAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberWebhookAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearWebhookAvatar(ctx, param.target)), + Commands.MemberWebhookAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeWebhookAvatar(ctx, param.target, param.avatar)), + Commands.MemberServerAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberServerAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearServerAvatar(ctx, param.target)), + Commands.MemberServerAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeServerAvatar(ctx, param.target, param.avatar)), Commands.MemberPronounsShow(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), Commands.MemberPronounsClear(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ClearPronouns(ctx, param.target, flags.yes)), Commands.MemberPronounsUpdate(var param, _) => ctx.Execute(MemberPronouns, m => m.ChangePronouns(ctx, param.target, param.pronouns)), @@ -435,11 +444,7 @@ public partial class CommandTree private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) { // Commands that have a member target (eg. pk;member delete) - 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", "pavatar", "ppfp")) - await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); - else if (ctx.Match("group", "groups", "g")) + if (ctx.Match("group", "groups", "g")) if (ctx.Match("add", "a")) await ctx.Execute(MemberGroupAdd, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add)); @@ -448,9 +453,6 @@ public partial class CommandTree m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); 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", "spfp")) - await ctx.Execute(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("id")) await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); else diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 26eb310e..8c5289f8 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -19,6 +19,9 @@ public class MemberAvatar private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs) { + ctx.CheckSystem().CheckOwnMember(target); + await ctx.ConfirmClear("this member's " + location.Name()); + await UpdateAvatar(location, ctx, target, null); if (location == MemberAvatarLocation.Server) { @@ -47,7 +50,7 @@ public class MemberAvatar } private async Task AvatarShow(MemberAvatarLocation location, Context ctx, PKMember target, - MemberGuildSettings? guildData) + MemberGuildSettings? guildData, ReplyFormat format) { // 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 @@ -86,7 +89,6 @@ public class MemberAvatar if (location == MemberAvatarLocation.Server) field += $" (for {ctx.Guild.Name})"; - var format = ctx.MatchFormat(); if (format == ReplyFormat.Raw) { await ctx.Reply($"`{currentValue?.TryGetCleanCdnUrl()}`"); @@ -110,58 +112,89 @@ public class MemberAvatar else throw new PKError("Format Not Recognized"); } - public async Task ServerAvatar(Context ctx, PKMember target) + private async Task AvatarChange(MemberAvatarLocation location, Context ctx, PKMember target, + MemberGuildSettings? guildData, ParsedImage avatar) { - ctx.CheckGuildContext(); - var guildData = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - await AvatarCommandTree(MemberAvatarLocation.Server, ctx, target, guildData); - } - - public async Task Avatar(Context ctx, PKMember target) - { - var guildData = ctx.Guild != null - ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) - : null; - - await AvatarCommandTree(MemberAvatarLocation.Member, ctx, target, guildData); - } - - 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()) - { - ctx.CheckSystem().CheckOwnMember(target); - await ctx.ConfirmClear("this member's " + location.Name()); - await AvatarClear(location, ctx, target, guildData); - return; - } - - // Then, parse an image from the command (from various sources...) - var avatarArg = await ctx.MatchImage(); - if (avatarArg == null) - { - // If we didn't get any, just show the current avatar - await AvatarShow(location, ctx, target, guildData); - return; - } - ctx.CheckSystem().CheckOwnMember(target); - avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(avatarArg.Value.Url); - await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url); - await PrintResponse(location, ctx, target, avatarArg.Value, guildData); + avatar = await _avatarHosting.TryRehostImage(avatar, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(avatar.Url); + await UpdateAvatar(location, ctx, target, avatar.CleanUrl ?? avatar.Url); + await PrintResponse(location, ctx, target, avatar, guildData); + } + + private Task GetServerAvatarGuildData(Context ctx, PKMember target) + { + ctx.CheckGuildContext(); + return ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + } + + private async Task GetAvatarGuildData(Context ctx, PKMember target) + { + return ctx.Guild != null + ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) + : null; + } + + private async Task GetWebhookAvatarGuildData(Context ctx, PKMember target) + { + return ctx.Guild != null + ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) + : null; + } + + public async Task ShowServerAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.Server, ctx, target, guildData, format); + } + + public async Task ClearServerAvatar(Context ctx, PKMember target) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.Server, ctx, target, guildData); + } + + public async Task ChangeServerAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.Server, ctx, target, guildData, avatar); + } + + public async Task ShowAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.Member, ctx, target, guildData, format); + } + + public async Task ClearAvatar(Context ctx, PKMember target) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.Member, ctx, target, guildData); + } + + public async Task ChangeAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.Member, ctx, target, guildData, avatar); + } + + public async Task ShowWebhookAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, format); + } + + public async Task ClearWebhookAvatar(Context ctx, PKMember target) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.MemberWebhook, ctx, target, guildData); + } + + public async Task ChangeWebhookAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, avatar); } private Task PrintResponse(MemberAvatarLocation location, Context ctx, PKMember target, ParsedImage avatar, diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 0a13eea1..7c070773 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -198,6 +198,89 @@ pub fn cmds() -> impl Iterator { .into_iter() }; + let member_avatar_cmd = { + let member_avatar = tokens!( + member_target, + ( + "avatar", + ["profile", "picture", "icon", "image", "pfp", "pic"] + ) + ); + [ + command!(member_avatar => "member_avatar_show").help("Shows a member's avatar"), + command!(member_avatar, ("avatar", Avatar) => "member_avatar_update") + .help("Changes a member's avatar"), + command!(member_avatar, ("clear", ["c"]) => "member_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's avatar"), + ] + .into_iter() + }; + + let member_webhook_avatar_cmd = { + let member_webhook_avatar = tokens!( + member_target, + ( + "proxyavatar", + [ + "proxypfp", + "webhookavatar", + "webhookpfp", + "pa", + "pavatar", + "ppfp" + ] + ) + ); + [ + command!(member_webhook_avatar => "member_webhook_avatar_show") + .help("Shows a member's proxy avatar"), + command!(member_webhook_avatar, ("avatar", Avatar) => "member_webhook_avatar_update") + .help("Changes a member's proxy avatar"), + command!(member_webhook_avatar, ("clear", ["c"]) => "member_webhook_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's proxy avatar"), + ] + .into_iter() + }; + + let member_server_avatar_cmd = { + let member_server_avatar = tokens!( + member_target, + ( + "serveravatar", + [ + "sa", + "servericon", + "serverimage", + "serverpfp", + "serverpic", + "savatar", + "spic", + "guildavatar", + "guildpic", + "guildicon", + "sicon", + "spfp" + ] + ) + ); + [ + command!(member_server_avatar => "member_server_avatar_show") + .help("Shows a member's server-specific avatar"), + command!(member_server_avatar, ("avatar", Avatar) => "member_server_avatar_update") + .help("Changes a member's server-specific avatar"), + command!(member_server_avatar, ("clear", ["c"]) => "member_server_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's server-specific avatar"), + ] + .into_iter() + }; + + let member_avatar_cmds = member_avatar_cmd + .chain(member_webhook_avatar_cmd) + .chain(member_server_avatar_cmd); + let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); @@ -217,6 +300,7 @@ pub fn cmds() -> impl Iterator { .chain(member_display_name_cmd) .chain(member_server_name_cmd) .chain(member_proxy_cmd) + .chain(member_avatar_cmds) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) .chain(member_delete_cmd)