2020-06-13 16:03:57 +02:00
#nullable enable
2020-12-24 14:52:44 +01:00
using Myriad.Builders ;
2021-11-26 21:10:56 -05:00
using Myriad.Types ;
2020-12-24 14:52:44 +01:00
2020-02-12 15:16:19 +01:00
using PluralKit.Core ;
2020-02-01 13:03:02 +01:00
2021-11-26 21:10:56 -05:00
namespace PluralKit.Bot ;
public class MemberAvatar
2020-02-01 13:03:02 +01:00
{
2021-11-26 21:10:56 -05:00
private readonly HttpClient _client ;
2024-02-11 03:53:46 +01:00
private readonly AvatarHostingService _avatarHosting ;
2021-11-26 21:10:56 -05:00
2024-02-11 03:53:46 +01:00
public MemberAvatar ( HttpClient client , AvatarHostingService avatarHosting )
2020-02-01 13:03:02 +01:00
{
2021-11-26 21:10:56 -05:00
_client = client ;
2024-02-11 03:53:46 +01:00
_avatarHosting = avatarHosting ;
2021-11-26 21:10:56 -05:00
}
2020-02-01 13:03:02 +01:00
2025-10-08 03:26:40 +00:00
private async Task AvatarClear ( MemberAvatarLocation location , Context ctx , PKMember target , MemberGuildSettings ? mgs , bool confirmYes )
2021-11-26 21:10:56 -05:00
{
2025-09-04 04:01:21 +03:00
ctx . CheckSystem ( ) . CheckOwnMember ( target ) ;
2025-10-08 03:26:40 +00:00
await ctx . ConfirmClear ( "this member's " + location . Name ( ) , confirmYes ) ;
2025-09-04 04:01:21 +03:00
2021-11-26 21:10:56 -05:00
await UpdateAvatar ( location , ctx , target , null ) ;
2023-03-02 06:11:35 +13:00
if ( location = = MemberAvatarLocation . Server )
2020-02-01 13:03:02 +01:00
{
2021-11-26 21:10:56 -05:00
if ( target . AvatarUrl ! = null )
await ctx . Reply (
$"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.Guild.Name}**)." ) ;
else
await ctx . Reply ( $"{Emojis.Success} Member server avatar cleared. This member now has no avatar." ) ;
2020-02-01 13:03:02 +01:00
}
2023-03-02 06:11:35 +13:00
else if ( location = = MemberAvatarLocation . MemberWebhook )
{
if ( mgs ? . AvatarUrl ! = null )
await ctx . Reply (
2024-12-31 08:09:18 -07:00
$"{Emojis.Success} Member proxy avatar cleared. Note that this member has a server-specific avatar set here, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} serveravatar clear` if you wish to clear that too." ) ;
2023-03-02 06:11:35 +13:00
else
await ctx . Reply ( $"{Emojis.Success} Member proxy avatar cleared. This member will now use the main avatar for proxied messages." ) ;
}
2021-11-26 21:10:56 -05:00
else
2020-02-01 13:03:02 +01:00
{
2021-11-26 21:10:56 -05:00
if ( mgs ? . AvatarUrl ! = null )
await ctx . Reply (
2024-12-31 08:09:18 -07:00
$"{Emojis.Success} Member avatar cleared. Note that this member has a server-specific avatar set here, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} serveravatar clear` if you wish to clear that too." ) ;
2020-06-13 16:03:57 +02:00
else
2021-11-26 21:10:56 -05:00
await ctx . Reply ( $"{Emojis.Success} Member avatar cleared." ) ;
2020-06-13 16:03:57 +02:00
}
2021-11-26 21:10:56 -05:00
}
2020-06-13 16:03:57 +02:00
2023-03-02 06:11:35 +13:00
private async Task AvatarShow ( MemberAvatarLocation location , Context ctx , PKMember target ,
2025-09-04 04:01:21 +03:00
MemberGuildSettings ? guildData , ReplyFormat format )
2021-11-26 21:10:56 -05:00
{
2021-12-07 01:32:29 -05:00
// 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
2023-03-02 06:11:35 +13:00
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 | |
2021-12-07 01:32:29 -05:00
target . AvatarPrivacy . CanAccess ( ctx . DirectLookupContextFor ( target . System ) ) ;
2023-03-02 06:11:35 +13:00
2021-11-26 21:10:56 -05:00
if ( string . IsNullOrEmpty ( currentValue ) | | ! canAccess )
2020-06-13 16:03:57 +02:00
{
2023-03-02 06:11:35 +13:00
if ( location = = MemberAvatarLocation . Member )
2020-02-01 13:03:02 +01:00
{
2021-11-26 21:10:56 -05:00
if ( target . System = = ctx . System ? . Id )
throw new PKSyntaxError (
"This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention." ) ;
throw new PKError ( "This member does not have an avatar set." ) ;
2020-02-01 13:03:02 +01:00
}
2021-08-27 11:03:47 -04:00
2023-03-02 06:11:35 +13:00
if ( location = = MemberAvatarLocation . MemberWebhook )
throw new PKError (
2024-12-31 08:09:18 -07:00
$"This member does not have a proxy avatar set. Type `{ctx.DefaultPrefix}member {target.Reference(ctx)} avatar` to see their global avatar." ) ;
2023-03-02 06:11:35 +13:00
if ( location = = MemberAvatarLocation . Server )
2021-11-26 21:10:56 -05:00
throw new PKError (
2024-12-31 08:09:18 -07:00
$"This member does not have a server avatar set. Type `{ctx.DefaultPrefix}member {target.Reference(ctx)} avatar` to see their global avatar." ) ;
2020-06-13 16:03:57 +02:00
}
2023-03-02 06:11:35 +13:00
var field = location . Name ( ) ;
if ( location = = MemberAvatarLocation . Server )
field + = $" (for {ctx.Guild.Name})" ;
2021-08-27 11:03:47 -04:00
2024-11-18 18:45:13 -07:00
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( $"`{currentValue?.TryGetCleanCdnUrl()}`" ) ;
}
else if ( format = = ReplyFormat . Plaintext )
{
var eb = new EmbedBuilder ( )
. Description ( $"Showing {field} link for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
await ctx . Reply ( $"<{currentValue?.TryGetCleanCdnUrl()}>" , embed : eb . Build ( ) ) ;
return ;
}
else if ( format = = ReplyFormat . Standard )
{
var eb = new EmbedBuilder ( )
. Title ( $"{target.NameFor(ctx)}'s {field}" )
. Image ( new Embed . EmbedImage ( currentValue ? . TryGetCleanCdnUrl ( ) ) ) ;
if ( target . System = = ctx . System ? . Id )
2024-12-31 08:09:18 -07:00
eb . Description ( $"To clear, use `{ctx.DefaultPrefix}member {target.Reference(ctx)} {location.Command()} clear`." ) ;
2024-11-18 18:45:13 -07:00
await ctx . Reply ( embed : eb . Build ( ) ) ;
}
else throw new PKError ( "Format Not Recognized" ) ;
2021-11-26 21:10:56 -05:00
}
2020-02-12 17:42:12 +01:00
2025-09-04 04:01:21 +03:00
private async Task AvatarChange ( MemberAvatarLocation location , Context ctx , PKMember target ,
MemberGuildSettings ? guildData , ParsedImage avatar )
{
ctx . CheckSystem ( ) . CheckOwnMember ( target ) ;
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 < MemberGuildSettings > GetServerAvatarGuildData ( Context ctx , PKMember target )
2021-11-26 21:10:56 -05:00
{
ctx . CheckGuildContext ( ) ;
2025-09-04 04:01:21 +03:00
return ctx . Repository . GetMemberGuild ( ctx . Guild . Id , target . Id ) ;
2021-11-26 21:10:56 -05:00
}
2020-02-12 17:42:12 +01:00
2025-09-04 04:01:21 +03:00
private async Task < MemberGuildSettings ? > GetAvatarGuildData ( Context ctx , PKMember target )
2021-11-26 21:10:56 -05:00
{
2025-09-04 04:01:21 +03:00
return ctx . Guild ! = null
2022-01-22 03:05:01 -05:00
? await ctx . Repository . GetMemberGuild ( ctx . Guild . Id , target . Id )
2021-11-26 21:10:56 -05:00
: null ;
2023-03-02 06:11:35 +13:00
}
2025-09-04 04:01:21 +03:00
private async Task < MemberGuildSettings ? > GetWebhookAvatarGuildData ( Context ctx , PKMember target )
2023-03-02 06:11:35 +13:00
{
2025-09-04 04:01:21 +03:00
return ctx . Guild ! = null
2023-03-02 06:11:35 +13:00
? await ctx . Repository . GetMemberGuild ( ctx . Guild . Id , target . Id )
: null ;
2025-09-04 04:01:21 +03:00
}
2023-03-02 06:11:35 +13:00
2025-09-04 04:01:21 +03:00
public async Task ShowServerAvatar ( Context ctx , PKMember target , ReplyFormat format )
{
var guildData = await GetServerAvatarGuildData ( ctx , target ) ;
await AvatarShow ( MemberAvatarLocation . Server , ctx , target , guildData , format ) ;
2021-11-26 21:10:56 -05:00
}
2020-07-07 23:41:51 +02:00
2025-10-08 03:26:40 +00:00
public async Task ClearServerAvatar ( Context ctx , PKMember target , bool confirmYes )
2021-11-26 21:10:56 -05:00
{
2025-09-04 04:01:21 +03:00
var guildData = await GetServerAvatarGuildData ( ctx , target ) ;
2025-10-08 03:26:40 +00:00
await AvatarClear ( MemberAvatarLocation . Server , ctx , target , guildData , confirmYes ) ;
2025-09-04 04:01:21 +03:00
}
2020-07-07 23:41:51 +02:00
2025-09-04 04:01:21 +03:00
public async Task ChangeServerAvatar ( Context ctx , PKMember target , ParsedImage avatar )
{
var guildData = await GetServerAvatarGuildData ( ctx , target ) ;
await AvatarChange ( MemberAvatarLocation . Server , ctx , target , guildData , avatar ) ;
}
2021-08-27 11:03:47 -04:00
2025-09-04 04:01:21 +03:00
public async Task ShowAvatar ( Context ctx , PKMember target , ReplyFormat format )
{
var guildData = await GetAvatarGuildData ( ctx , target ) ;
await AvatarShow ( MemberAvatarLocation . Member , ctx , target , guildData , format ) ;
}
2024-02-11 03:53:46 +01:00
2025-10-08 03:26:40 +00:00
public async Task ClearAvatar ( Context ctx , PKMember target , bool confirmYes )
2025-09-04 04:01:21 +03:00
{
var guildData = await GetAvatarGuildData ( ctx , target ) ;
2025-10-08 03:26:40 +00:00
await AvatarClear ( MemberAvatarLocation . Member , ctx , target , guildData , confirmYes ) ;
2025-09-04 04:01:21 +03:00
}
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 ) ;
}
2025-10-08 03:26:40 +00:00
public async Task ClearWebhookAvatar ( Context ctx , PKMember target , bool confirmYes )
2025-09-04 04:01:21 +03:00
{
var guildData = await GetWebhookAvatarGuildData ( ctx , target ) ;
2025-10-08 03:26:40 +00:00
await AvatarClear ( MemberAvatarLocation . MemberWebhook , ctx , target , guildData , confirmYes ) ;
2025-09-04 04:01:21 +03:00
}
public async Task ChangeWebhookAvatar ( Context ctx , PKMember target , ParsedImage avatar )
{
var guildData = await GetWebhookAvatarGuildData ( ctx , target ) ;
await AvatarChange ( MemberAvatarLocation . MemberWebhook , ctx , target , guildData , avatar ) ;
2021-11-26 21:10:56 -05:00
}
2020-07-07 23:41:51 +02:00
2023-03-02 06:11:35 +13:00
private Task PrintResponse ( MemberAvatarLocation location , Context ctx , PKMember target , ParsedImage avatar ,
2021-11-26 21:10:56 -05:00
MemberGuildSettings ? targetGuildData )
{
var serverFrag = location switch
2020-06-29 15:20:28 +02:00
{
2023-03-02 06:11:35 +13:00
MemberAvatarLocation . Server = >
2021-11-26 21:10:56 -05:00
$" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**)." ,
2023-03-02 06:11:35 +13:00
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." ,
2023-07-18 13:23:52 -06:00
MemberAvatarLocation . Member when ( targetGuildData ? . AvatarUrl ! = null & & target . WebhookAvatarUrl ! = 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." +
$"\n{Emojis.Note} Note that this member *also* has a proxy avatar set, and thus the global avatar will also have no effect on proxied messages in servers without server-specific avatars." ,
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 . Member when target . WebhookAvatarUrl ! = null = >
$"\n{Emojis.Note} Note that this member *also* has a proxy avatar set, and thus changing the global avatar will have no effect on proxied messages." ,
2021-11-26 21:10:56 -05:00
_ = > ""
} ;
var msg = avatar . Source switch
{
AvatarSource . User = >
2023-03-02 06:11:35 +13:00
$"{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." ,
2021-11-26 21:10:56 -05:00
AvatarSource . Url = >
2023-03-02 06:11:35 +13:00
$"{Emojis.Success} Member {location.Name()} changed to the image at the given URL.{serverFrag}" ,
2024-02-14 04:41:02 +13:00
AvatarSource . HostedCdn = >
$"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}" ,
2021-11-26 21:10:56 -05:00
AvatarSource . Attachment = >
2023-03-02 06:11:35 +13:00
$"{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." ,
2021-11-26 21:10:56 -05:00
_ = > throw new ArgumentOutOfRangeException ( )
} ;
// The attachment's already right there, no need to preview it.
2024-02-14 04:41:02 +13:00
var hasEmbed = avatar . Source ! = AvatarSource . Attachment & & avatar . Source ! = AvatarSource . HostedCdn ;
2021-11-26 21:10:56 -05:00
return hasEmbed
? ctx . Reply ( msg , new EmbedBuilder ( ) . Image ( new Embed . EmbedImage ( avatar . Url ) ) . Build ( ) )
: ctx . Reply ( msg ) ;
}
2020-02-12 17:42:12 +01:00
2023-03-02 06:11:35 +13:00
private Task UpdateAvatar ( MemberAvatarLocation location , Context ctx , PKMember target , string? url )
2021-11-26 21:10:56 -05:00
{
switch ( location )
2020-06-13 16:03:57 +02:00
{
2023-03-02 06:11:35 +13:00
case MemberAvatarLocation . Server :
2022-01-22 03:05:01 -05:00
return ctx . Repository . UpdateMemberGuild ( target . Id , ctx . Guild . Id , new MemberGuildPatch { AvatarUrl = url } ) ;
2023-03-02 06:11:35 +13:00
case MemberAvatarLocation . Member :
2022-01-22 03:05:01 -05:00
return ctx . Repository . UpdateMember ( target . Id , new MemberPatch { AvatarUrl = url } ) ;
2023-03-02 06:11:35 +13:00
case MemberAvatarLocation . MemberWebhook :
return ctx . Repository . UpdateMember ( target . Id , new MemberPatch { WebhookAvatarUrl = url } ) ;
2021-11-26 21:10:56 -05:00
default :
throw new ArgumentOutOfRangeException ( $"Unknown avatar location {location}" ) ;
2020-02-12 17:42:12 +01:00
}
2020-02-01 13:03:02 +01:00
}
2023-03-02 06:11:35 +13:00
}
internal enum MemberAvatarLocation
{
Member ,
MemberWebhook ,
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 ) )
} ;
}
2021-11-26 21:10:56 -05:00
2023-03-02 06:11:35 +13:00
public static string Command ( this MemberAvatarLocation location )
{
return location switch
{
MemberAvatarLocation . Server = > "serveravatar" ,
MemberAvatarLocation . MemberWebhook = > "proxyavatar" ,
MemberAvatarLocation . Member = > "avatar" ,
_ = > throw new ArgumentOutOfRangeException ( nameof ( location ) )
} ;
}
2020-02-01 13:03:02 +01:00
}