2019-07-14 23:49:14 +02:00
using Humanizer ;
2020-12-22 16:55:13 +01:00
2020-12-23 02:19:02 +01:00
using Myriad.Builders ;
2020-12-22 16:55:13 +01:00
using Myriad.Cache ;
2020-12-25 13:58:45 +01:00
using Myriad.Extensions ;
2020-12-22 16:55:13 +01:00
using Myriad.Rest ;
2021-01-31 17:56:33 +01:00
using Myriad.Rest.Exceptions ;
2020-12-22 16:55:13 +01:00
using Myriad.Types ;
2019-06-15 12:19:44 +02:00
using NodaTime ;
2019-04-21 15:33:22 +02:00
2020-02-12 15:16:19 +01:00
using PluralKit.Core ;
2021-11-26 21:10:56 -05:00
namespace PluralKit.Bot ;
public class EmbedService
2021-08-27 11:03:47 -04:00
{
2025-09-07 10:16:50 +12:00
public const string LEGACY_EMBED_WARNING = "\u26A0\uFE0F The \"legacy\" embeds for system/member/group cards are deprecated, and will be removed in future." ;
2021-11-26 21:10:56 -05:00
private readonly IDiscordCache _cache ;
private readonly IDatabase _db ;
private readonly ModelRepository _repo ;
private readonly DiscordApiClient _rest ;
2025-12-21 01:19:02 -05:00
private readonly BotConfig _config ;
2025-09-07 10:16:50 +12:00
private readonly CoreConfig _coreConfig ;
2021-11-26 21:10:56 -05:00
2025-12-21 01:19:02 -05:00
public EmbedService ( IDatabase db , ModelRepository repo , IDiscordCache cache , DiscordApiClient rest , BotConfig config , CoreConfig coreConfig )
2019-10-26 19:45:30 +02:00
{
2021-11-26 21:10:56 -05:00
_db = db ;
_repo = repo ;
_cache = cache ;
_rest = rest ;
2025-12-21 01:19:02 -05:00
_config = config ;
2025-09-07 10:16:50 +12:00
_coreConfig = coreConfig ;
2021-11-26 21:10:56 -05:00
}
2019-04-21 15:33:22 +02:00
2021-11-26 21:10:56 -05:00
private Task < ( ulong Id , User ? User ) [ ] > GetUsers ( IEnumerable < ulong > ids )
{
async Task < ( ulong Id , User ? User ) > Inner ( ulong id )
2019-04-21 15:33:22 +02:00
{
2021-11-26 21:10:56 -05:00
var user = await _cache . GetOrFetchUser ( _rest , id ) ;
return ( id , user ) ;
2020-12-22 16:55:13 +01:00
}
2021-11-26 21:10:56 -05:00
return Task . WhenAll ( ids . Select ( Inner ) ) ;
}
2020-12-22 16:55:13 +01:00
2025-09-07 10:16:50 +12:00
public async Task < MessageComponent [ ] > CreateSystemMessageComponents ( Context cctx , PKSystem system , LookupContext ctx )
{
// Fetch/render info for all accounts simultaneously
var accounts = await _repo . GetSystemAccounts ( system . Id ) ;
var users = ( await GetUsers ( accounts ) ) . Select ( x = > x . User ? . NameAndMention ( ) ? ? $"(deleted account {x.Id})" ) ;
var linkedAccounts = new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = "**Linked accounts:**\n" + string . Join ( "\n" , users ) . Truncate ( 1000 ) ,
} ;
var countctx = LookupContext . ByNonOwner ;
if ( cctx . MatchFlag ( "a" , "all" ) )
{
2025-09-18 23:32:00 +00:00
if ( system . Id = = cctx . System ? . Id )
2025-09-07 10:16:50 +12:00
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetSystemMemberCount ( system . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
var guildSettings = cctx . Guild ! = null ? await _repo . GetSystemGuild ( cctx . Guild . Id , system . Id ) : null ;
var avatar = system . AvatarFor ( ctx ) ;
var headerText = "" ;
if ( system . PronounPrivacy . CanAccess ( ctx ) & & system . Pronouns ! = null )
headerText + = $"\n**Pronouns:** {system.Pronouns}" ;
if ( system . Tag ! = null )
headerText + = $"\n**Tag:** {system.Tag.EscapeMarkdown()}" ;
2025-09-08 10:43:38 +12:00
if ( cctx . Config ! = null & & cctx . Config . CardShowColorHex & & ! system . Color . EmptyOrNull ( ) )
2025-09-07 10:16:50 +12:00
headerText + = $"\n**Color:** #{system.Color}" ;
if ( cctx . Guild ! = null )
{
if ( guildSettings . Tag ! = null & & guildSettings . TagEnabled )
headerText + = $"\n**Tag (in server '{cctx.Guild.Name}'):** {guildSettings.Tag.EscapeMarkdown()}" ;
if ( ! guildSettings . TagEnabled )
headerText + = $"\n**Tag (in server '{cctx.Guild.Name}'):** *(tag is disabled in this server)*" ;
}
if ( system . MemberListPrivacy . CanAccess ( ctx ) )
{
headerText + = $"\n**Members:** {memberCount}" ;
2025-09-18 23:32:00 +00:00
if ( system . Id = = cctx . System ? . Id )
2025-09-07 10:16:50 +12:00
if ( memberCount > 0 )
headerText + = $" (see `{cctx.DefaultPrefix}system list`)" ;
else
headerText + = $" (add one with `{cctx.DefaultPrefix}member new`!)" ;
else if ( memberCount > 0 )
headerText + = $" (see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list`)" ;
}
List < MessageComponent > switchComponent = [ ] ;
var latestSwitch = await _repo . GetLatestSwitch ( system . Id ) ;
if ( latestSwitch ! = null & & system . FrontPrivacy . CanAccess ( ctx ) )
{
var switchMembers =
await _db . Execute ( conn = > _repo . GetSwitchMembers ( conn , latestSwitch . Id ) ) . ToListAsync ( ) ;
if ( switchMembers . Count > 0 )
{
var memberStr = string . Join ( ", " , switchMembers . Select ( m = > m . NameFor ( ctx ) ) ) ;
if ( memberStr . Length > 200 )
memberStr = $"(too many to show, see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} fronters`)" ;
switchComponent . Add ( new ( )
{
Type = ComponentType . Text ,
Content = $"**{" Current fronter ".ToQuantity(switchMembers.Count, ShowQuantityAs.None)}:** {memberStr}" ,
} ) ;
}
}
List < MessageComponent > descComponents = [ ] ;
2025-09-08 13:30:31 +12:00
if ( system . DescriptionFor ( ctx ) is { } desc & & ! string . IsNullOrWhiteSpace ( desc ) )
2025-09-07 10:16:50 +12:00
{
descComponents . Add ( new ( )
{
Type = ComponentType . Separator ,
} ) ;
descComponents . Add ( new ( )
{
Type = ComponentType . Text ,
Content = desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) ,
} ) ;
}
if ( system . BannerPrivacy . CanAccess ( ctx ) & & ! string . IsNullOrWhiteSpace ( system . BannerImage ) )
descComponents . Add ( new ( )
{
Type = ComponentType . MediaGallery ,
Items = [ new ( ) { Media = new ( ) { Url = system . BannerImage } } ] ,
} ) ;
var systemName = ( cctx . Guild ! = null & & guildSettings ? . DisplayName ! = null ) ? guildSettings ? . DisplayName ! : system . NameFor ( ctx ) ;
var premiumText = "" ; // TODO(iris): "\n\U0001F31F *PluralKit Premium supporter!*";
List < MessageComponent > header = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"### {systemName ?? $" ` { system . DisplayHid ( cctx . Config ) } ` "}{premiumText}" ,
} ,
] ;
if ( ! string . IsNullOrWhiteSpace ( headerText ) )
header . Add ( new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = headerText ,
} ) ;
if ( cctx . Guild ! = null )
{
var guildAvatar = guildSettings . AvatarUrl . TryGetCleanCdnUrl ( ) ;
if ( ! string . IsNullOrWhiteSpace ( guildAvatar ) )
avatar = guildAvatar ;
}
if ( ! string . IsNullOrWhiteSpace ( avatar ) )
header = [
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [ . . header ] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Thumbnail ,
Media = new ( ) { Url = avatar } ,
} ,
} ,
] ;
return [
new MessageComponent ( )
{
Type = ComponentType . Container ,
AccentColor = system . Color ? . ToDiscordColor ( ) ,
Components = [ . . header , . . switchComponent , linkedAccounts , . . descComponents ] ,
} ,
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
2025-12-21 01:19:02 -05:00
Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`{cctx.PremiumEmoji}\n-# Created: {system.Created.FormatZoned(cctx.Zone)}" ,
2025-09-07 10:16:50 +12:00
} ,
] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Button ,
Style = ButtonStyle . Link ,
Label = "View on dashboard" ,
Url = $"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}" ,
} ,
} ,
] ;
}
2021-11-26 21:10:56 -05:00
public async Task < Embed > CreateSystemEmbed ( Context cctx , PKSystem system , LookupContext ctx )
{
// Fetch/render info for all accounts simultaneously
var accounts = await _repo . GetSystemAccounts ( system . Id ) ;
var users = ( await GetUsers ( accounts ) ) . Select ( x = > x . User ? . NameAndMention ( ) ? ? $"(deleted account {x.Id})" ) ;
2021-12-23 23:23:16 -05:00
var countctx = LookupContext . ByNonOwner ;
if ( cctx . MatchFlag ( "a" , "all" ) )
{
2025-09-18 23:32:00 +00:00
if ( system . Id = = cctx . System ? . Id )
2021-12-23 23:23:16 -05:00
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetSystemMemberCount ( system . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
var eb = new EmbedBuilder ( )
2023-07-19 12:48:04 +12:00
. Title ( system . NameFor ( ctx ) )
2021-11-26 21:10:56 -05:00
. Footer ( new Embed . EmbedFooter (
2024-04-28 15:46:06 +12:00
$"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}" ) )
2024-08-29 06:32:03 -06:00
. Color ( system . Color ? . ToDiscordColor ( ) )
2025-09-07 10:16:50 +12:00
. Url ( $"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}" ) ;
2020-09-12 18:10:37 -04:00
2023-07-19 12:48:04 +12:00
var avatar = system . AvatarFor ( ctx ) ;
if ( avatar ! = null )
eb . Thumbnail ( new Embed . EmbedThumbnail ( avatar ) ) ;
2024-11-09 11:44:48 -07:00
if ( system . BannerPrivacy . CanAccess ( ctx ) )
2021-11-26 21:10:56 -05:00
eb . Image ( new Embed . EmbedImage ( system . BannerImage ) ) ;
2019-04-21 15:33:22 +02:00
2021-11-26 21:10:56 -05:00
var latestSwitch = await _repo . GetLatestSwitch ( system . Id ) ;
if ( latestSwitch ! = null & & system . FrontPrivacy . CanAccess ( ctx ) )
{
var switchMembers =
await _db . Execute ( conn = > _repo . GetSwitchMembers ( conn , latestSwitch . Id ) ) . ToListAsync ( ) ;
if ( switchMembers . Count > 0 )
2023-05-12 15:37:03 +12:00
{
var memberStr = string . Join ( ", " , switchMembers . Select ( m = > m . NameFor ( ctx ) ) ) ;
if ( memberStr . Length > 200 )
2024-12-31 08:09:18 -07:00
memberStr = $"[too many to show, see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} fronters`]" ;
2023-05-12 15:37:03 +12:00
eb . Field ( new Embed . Field ( "Fronter" . ToQuantity ( switchMembers . Count , ShowQuantityAs . None ) , memberStr ) ) ;
}
2021-11-26 21:10:56 -05:00
}
2021-03-28 12:02:41 +02:00
2021-11-26 21:10:56 -05:00
if ( system . Tag ! = null )
eb . Field ( new Embed . Field ( "Tag" , system . Tag . EscapeMarkdown ( ) , true ) ) ;
2020-12-23 02:19:02 +01:00
2021-11-26 21:10:56 -05:00
if ( cctx . Guild ! = null )
{
var guildSettings = await _repo . GetSystemGuild ( cctx . Guild . Id , system . Id ) ;
2021-08-02 13:46:12 -04:00
2021-11-26 21:10:56 -05:00
if ( guildSettings . Tag ! = null & & guildSettings . TagEnabled )
eb . Field ( new Embed . Field ( $"Tag (in server '{cctx.Guild.Name}')" , guildSettings . Tag
. EscapeMarkdown ( ) , true ) ) ;
2019-04-21 15:33:22 +02:00
2021-11-26 21:10:56 -05:00
if ( ! guildSettings . TagEnabled )
eb . Field ( new Embed . Field ( $"Tag (in server '{cctx.Guild.Name}')" ,
"*(tag is disabled in this server)*" ) ) ;
2023-07-19 12:48:04 +12:00
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 ) ;
}
2021-11-26 21:10:56 -05:00
}
2021-03-28 19:22:45 +02:00
2022-03-23 19:20:16 +01:00
if ( system . PronounPrivacy . CanAccess ( ctx ) & & system . Pronouns ! = null )
eb . Field ( new Embed . Field ( "Pronouns" , system . Pronouns , true ) ) ;
2021-11-26 21:10:56 -05:00
if ( ! system . Color . EmptyOrNull ( ) ) eb . Field ( new Embed . Field ( "Color" , $"#{system.Color}" , true ) ) ;
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
eb . Field ( new Embed . Field ( "Linked accounts" , string . Join ( "\n" , users ) . Truncate ( 1000 ) , true ) ) ;
2021-08-02 17:22:06 -04:00
2021-11-26 21:10:56 -05:00
if ( system . MemberListPrivacy . CanAccess ( ctx ) )
{
if ( memberCount > 0 )
eb . Field ( new Embed . Field ( $"Members ({memberCount})" ,
2024-12-31 08:09:18 -07:00
$"(see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list` or `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list full`)" , true ) ) ;
2021-11-26 21:10:56 -05:00
else
2024-12-31 08:09:18 -07:00
eb . Field ( new Embed . Field ( $"Members ({memberCount})" , $"Add one with `{cctx.DefaultPrefix}member new`!" , true ) ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-02 17:22:06 -04:00
2021-11-26 21:10:56 -05:00
if ( system . DescriptionFor ( ctx ) is { } desc )
eb . Field ( new Embed . Field ( "Description" , desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) ) ) ;
2021-03-28 19:22:45 +02:00
2021-11-26 21:10:56 -05:00
return eb . Build ( ) ;
}
2020-02-05 23:43:30 +01:00
2021-11-26 21:10:56 -05:00
public Embed CreateLoggedMessageEmbed ( Message triggerMessage , Message proxiedMessage , string systemHid ,
PKMember member , string channelName , string oldContent = null )
{
// TODO: pronouns in ?-reacted response using this card
var timestamp = DiscordUtils . SnowflakeToInstant ( proxiedMessage . Id ) ;
var name = proxiedMessage . Author . Username ;
// sometimes Discord will just... not return the avatar hash with webhook messages
var avatar = proxiedMessage . Author . Avatar ! = null
? proxiedMessage . Author . AvatarUrl ( )
2023-03-02 06:11:35 +13:00
: member . WebhookAvatarFor ( LookupContext . ByNonOwner ) ;
2021-11-26 21:10:56 -05:00
var embed = new EmbedBuilder ( )
. Author ( new Embed . EmbedAuthor ( $"#{channelName}: {name}" , IconUrl : avatar ) )
. Thumbnail ( new Embed . EmbedThumbnail ( avatar ) )
. Description ( proxiedMessage . Content ? . NormalizeLineEndSpacing ( ) )
. Footer ( new Embed . EmbedFooter (
$"System ID: {systemHid} | Member ID: {member.Hid} | Sender: {triggerMessage.Author.Username}#{triggerMessage.Author.Discriminator} ({triggerMessage.Author.Id}) | Message ID: {proxiedMessage.Id} | Original Message ID: {triggerMessage.Id}" ) )
. Timestamp ( timestamp . ToDateTimeOffset ( ) . ToString ( "O" ) ) ;
2022-06-13 21:18:24 -04:00
if ( oldContent = = "" )
oldContent = "*no message content*" ;
2021-11-26 21:10:56 -05:00
if ( oldContent ! = null )
embed . Field ( new Embed . Field ( "Old message" , oldContent ? . NormalizeLineEndSpacing ( ) . Truncate ( 1000 ) ) ) ;
return embed . Build ( ) ;
}
2019-07-21 16:29:48 +02:00
2025-09-07 10:16:50 +12:00
public async Task < MessageComponent [ ] > CreateMemberMessageComponents ( PKSystem system , PKMember member , Guild guild , SystemConfig ? ccfg , LookupContext ctx , DateTimeZone zone )
{
var name = member . NameFor ( ctx ) ;
var systemGuildSettings = guild ! = null ? await _repo . GetSystemGuild ( guild . Id , system . Id ) : null ;
var systemName = ( guild ! = null & & systemGuildSettings ? . DisplayName ! = null ) ? systemGuildSettings ? . DisplayName ! : system . NameFor ( ctx ) ;
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 )
. Where ( g = > g . Visibility . CanAccess ( ctx ) )
. OrderBy ( g = > g . Name , StringComparer . InvariantCultureIgnoreCase )
. ToListAsync ( ) ;
var headerText = "" ;
if ( member . MemberVisibility = = PrivacyLevel . Private )
headerText + = "*(this member is hidden)*\n" ;
if ( guildSettings ? . AvatarUrl ! = null )
if ( member . AvatarFor ( ctx ) ! = null )
headerText + =
$"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n" ;
else
headerText + = "*(this member has a server-specific avatar set)*\n" ;
if ( ! member . DisplayName . EmptyOrNull ( ) & & member . NamePrivacy . CanAccess ( ctx ) )
headerText + = $"\n**Display name:** {member.DisplayName.Truncate(1024)}" ;
if ( guild ! = null & & guildDisplayName ! = null )
headerText + = $"\n**Server nickname (for '{guild.Name}'):** {guildDisplayName.Truncate(1024)}" ;
2025-09-08 10:43:38 +12:00
if ( ccfg ! = null & & ccfg . CardShowColorHex & & ! member . Color . EmptyOrNull ( ) )
2025-09-07 10:16:50 +12:00
headerText + = $"\n**Color:** #{member.Color}" ;
if ( member . PronounsFor ( ctx ) is { } pronouns & & ! string . IsNullOrWhiteSpace ( pronouns ) )
headerText + = $"\n**Pronouns:** {pronouns}" ;
if ( member . BirthdayFor ( ctx ) ! = null )
headerText + = $"\n**Birthday:** {member.BirthdayString}" ;
if ( member . MessageCountFor ( ctx ) is { } count & & count > 0 )
headerText + = $"\n**Message count:** {member.MessageCount}" ;
List < MessageComponent > extraData = [ ] ;
if ( member . HasProxyTags & & member . ProxyPrivacy . CanAccess ( ctx ) )
{
extraData . Add ( new MessageComponent
{
Type = ComponentType . Separator ,
} ) ;
extraData . Add ( new MessageComponent
{
Type = ComponentType . Text ,
Content = $"**Proxy tags:**\n{member.ProxyTagsString(" \ n ").Truncate(1024)}" ,
} ) ;
}
if ( groups . Count > 0 )
{
// More than 5 groups show in "compact" format without ID
var content = groups . Count > 5
? string . Join ( ", " , groups . Select ( g = > g . DisplayName ? ? g . Name ) )
: string . Join ( "\n" , groups . Select ( g = > $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**" ) ) ;
extraData . Add ( new MessageComponent
{
Type = ComponentType . Separator ,
} ) ;
extraData . Add ( new MessageComponent
{
Type = ComponentType . Text ,
Content = $"**Groups ({groups.Count}):**\n{content.Truncate(1000)}" ,
} ) ;
}
List < MessageComponent > descComponents = [ ] ;
2025-09-08 13:30:31 +12:00
if ( member . DescriptionFor ( ctx ) is { } desc & & ! string . IsNullOrWhiteSpace ( desc ) )
2025-09-07 10:16:50 +12:00
{
descComponents . Add ( new ( )
{
Type = ComponentType . Separator ,
} ) ;
descComponents . Add ( new ( )
{
Type = ComponentType . Text ,
Content = desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) ,
} ) ;
}
if ( member . BannerPrivacy . CanAccess ( ctx ) & & ! string . IsNullOrWhiteSpace ( member . BannerImage ) )
descComponents . Add ( new ( )
{
Type = ComponentType . MediaGallery ,
Items = [ new ( ) { Media = new ( ) { Url = member . BannerImage } } ] ,
} ) ;
List < MessageComponent > header = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"### {name}{(systemName != null ? $" ( { systemName } ) " : " ")}" ,
} ,
] ;
if ( ! string . IsNullOrWhiteSpace ( headerText ) )
header . Add ( new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = headerText ,
} ) ;
if ( ! string . IsNullOrWhiteSpace ( avatar ) )
header = [
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [ . . header ] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Thumbnail ,
Media = new ( ) { Url = avatar } ,
} ,
} ,
] ;
return [
new MessageComponent ( )
{
Type = ComponentType . Container ,
AccentColor = member . Color ? . ToDiscordColor ( ) ,
Components = [ . . header , . . extraData , . . descComponents ] ,
} ,
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"-# System ID: `{system.DisplayHid(ccfg)}` \u2219 Member ID: `{member.DisplayHid(ccfg)}`{(member.MetadataPrivacy.CanAccess(ctx) ? $" \ n - # Created : { member . Created . FormatZoned ( zone ) } " : " ")}" ,
} ,
] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Button ,
Style = ButtonStyle . Link ,
Label = "View on dashboard" ,
Url = $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}" ,
} ,
} ,
] ;
}
2024-04-28 15:46:06 +12:00
public async Task < Embed > CreateMemberEmbed ( PKSystem system , PKMember member , Guild guild , SystemConfig ? ccfg , LookupContext ctx , DateTimeZone zone )
2021-11-26 21:10:56 -05:00
{
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
2019-08-09 10:55:15 +00:00
2021-11-26 21:10:56 -05:00
var name = member . NameFor ( ctx ) ;
2023-07-19 12:48:04 +12:00
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}" ;
2019-04-22 17:10:18 +02:00
2021-11-26 21:10:56 -05:00
var guildSettings = guild ! = null ? await _repo . GetMemberGuild ( guild . Id , member . Id ) : null ;
var guildDisplayName = guildSettings ? . DisplayName ;
2023-03-02 06:11:35 +13:00
var webhook_avatar = guildSettings ? . AvatarUrl ? ? member . WebhookAvatarFor ( ctx ) ? ? member . AvatarFor ( ctx ) ;
2021-11-26 21:10:56 -05:00
var avatar = guildSettings ? . AvatarUrl ? ? member . AvatarFor ( ctx ) ;
var groups = await _repo . GetMemberGroups ( member . Id )
. Where ( g = > g . Visibility . CanAccess ( ctx ) )
. OrderBy ( g = > g . Name , StringComparer . InvariantCultureIgnoreCase )
. ToListAsync ( ) ;
var eb = new EmbedBuilder ( )
2025-09-07 10:16:50 +12:00
. Author ( new Embed . EmbedAuthor ( name , IconUrl : webhook_avatar . TryGetCleanCdnUrl ( ) , Url : $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}" ) )
2024-08-29 06:32:03 -06:00
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null)
. Color ( member . Color ? . ToDiscordColor ( ) )
2021-11-26 21:10:56 -05:00
. Footer ( new Embed . EmbedFooter (
2024-04-28 15:46:06 +12:00
$"System ID: {system.DisplayHid(ccfg)} | Member ID: {member.DisplayHid(ccfg)} {(member.MetadataPrivacy.CanAccess(ctx) ? $" | Created on { member . Created . FormatZoned ( zone ) } " : " ")}" ) ) ;
2021-11-26 21:10:56 -05:00
2024-11-09 11:44:48 -07:00
if ( member . BannerPrivacy . CanAccess ( ctx ) )
2021-11-26 21:10:56 -05:00
eb . Image ( new Embed . EmbedImage ( member . BannerImage ) ) ;
var description = "" ;
if ( member . MemberVisibility = = PrivacyLevel . Private ) description + = "*(this member is hidden)*\n" ;
if ( guildSettings ? . AvatarUrl ! = null )
if ( member . AvatarFor ( ctx ) ! = null )
description + =
$"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n" ;
else
description + = "*(this member has a server-specific avatar set)*\n" ;
if ( description ! = "" ) eb . Description ( description ) ;
if ( avatar ! = null ) eb . Thumbnail ( new Embed . EmbedThumbnail ( avatar . TryGetCleanCdnUrl ( ) ) ) ;
if ( ! member . DisplayName . EmptyOrNull ( ) & & member . NamePrivacy . CanAccess ( ctx ) )
eb . Field ( new Embed . Field ( "Display Name" , member . DisplayName . Truncate ( 1024 ) , true ) ) ;
if ( guild ! = null & & guildDisplayName ! = null )
eb . Field ( new Embed . Field ( $"Server Nickname (for {guild.Name})" , guildDisplayName . Truncate ( 1024 ) , true ) ) ;
if ( member . BirthdayFor ( ctx ) ! = null ) eb . Field ( new Embed . Field ( "Birthdate" , member . BirthdayString , true ) ) ;
if ( member . PronounsFor ( ctx ) is { } pronouns & & ! string . IsNullOrWhiteSpace ( pronouns ) )
eb . Field ( new Embed . Field ( "Pronouns" , pronouns . Truncate ( 1024 ) , true ) ) ;
if ( member . MessageCountFor ( ctx ) is { } count & & count > 0 )
eb . Field ( new Embed . Field ( "Message Count" , member . MessageCount . ToString ( ) , true ) ) ;
2023-08-10 17:54:53 +12:00
if ( member . HasProxyTags & & member . ProxyPrivacy . CanAccess ( ctx ) )
2021-11-26 21:10:56 -05:00
eb . Field ( new Embed . Field ( "Proxy Tags" , member . ProxyTagsString ( "\n" ) . Truncate ( 1024 ) , true ) ) ;
// --- For when this gets added to the member object itself or however they get added
// if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value)));
// if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value));
// if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
if ( ! member . Color . EmptyOrNull ( ) ) eb . Field ( new Embed . Field ( "Color" , $"#{member.Color}" , true ) ) ;
if ( groups . Count > 0 )
{
// More than 5 groups show in "compact" format without ID
var content = groups . Count > 5
? string . Join ( ", " , groups . Select ( g = > g . DisplayName ? ? g . Name ) )
2024-06-04 18:56:28 +09:00
: string . Join ( "\n" , groups . Select ( g = > $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**" ) ) ;
2021-11-26 21:10:56 -05:00
eb . Field ( new Embed . Field ( $"Groups ({groups.Count})" , content . Truncate ( 1000 ) ) ) ;
}
2020-06-18 05:31:39 +10:00
2021-11-26 21:10:56 -05:00
if ( member . DescriptionFor ( ctx ) is { } desc )
eb . Field ( new Embed . Field ( "Description" , member . Description . NormalizeLineEndSpacing ( ) ) ) ;
2019-08-09 10:55:15 +00:00
2021-11-26 21:10:56 -05:00
return eb . Build ( ) ;
}
2020-07-18 13:26:36 +02:00
2025-09-07 10:16:50 +12:00
public async Task < MessageComponent [ ] > CreateGroupMessageComponents ( Context ctx , PKSystem system , PKGroup target )
{
var pctx = ctx . LookupContextFor ( system . Id ) ;
var name = target . NameFor ( ctx ) ;
var systemGuildSettings = ctx . Guild ! = null ? await _repo . GetSystemGuild ( ctx . Guild . Id , system . Id ) : null ;
var systemName = ( ctx . Guild ! = null & & systemGuildSettings ? . DisplayName ! = null ) ? systemGuildSettings ? . DisplayName ! : system . NameFor ( ctx ) ;
var countctx = LookupContext . ByNonOwner ;
if ( ctx . MatchFlag ( "a" , "all" ) )
{
2025-09-18 23:32:00 +00:00
if ( system . Id = = ctx . System ? . Id )
2025-09-07 10:16:50 +12:00
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetGroupMemberCount ( target . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
var headerText = "" ;
if ( target . NamePrivacy . CanAccess ( pctx ) & & target . DisplayName ! = null )
headerText + = $"\n**Display name:** {target.DisplayName}" ;
2025-09-08 10:43:38 +12:00
if ( ctx . Config ! = null & & ctx . Config . CardShowColorHex & & ! target . Color . EmptyOrNull ( ) )
2025-09-07 10:16:50 +12:00
headerText + = $"\n**Color:** #{target.Color}" ;
if ( target . ListPrivacy . CanAccess ( pctx ) )
{
headerText + = $"\n**Members:** {memberCount}" ;
2025-09-18 23:32:00 +00:00
if ( system . Id = = ctx . System ? . Id & & memberCount = = 0 )
2025-09-07 10:16:50 +12:00
headerText + = $" (add one with `{ctx.DefaultPrefix}group {target.Reference(ctx)} add <member>`!)" ;
else if ( memberCount > 0 )
headerText + = $" (see `{ctx.DefaultPrefix}group {target.Reference(ctx)} list`)" ;
}
List < MessageComponent > descComponents = [ ] ;
2025-09-08 13:30:31 +12:00
if ( target . DescriptionFor ( pctx ) is { } desc & & ! string . IsNullOrWhiteSpace ( desc ) )
2025-09-07 10:16:50 +12:00
{
descComponents . Add ( new ( )
{
Type = ComponentType . Separator ,
} ) ;
descComponents . Add ( new ( )
{
Type = ComponentType . Text ,
Content = desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) ,
} ) ;
}
if ( target . BannerPrivacy . CanAccess ( pctx ) & & ! string . IsNullOrWhiteSpace ( target . BannerImage ) )
descComponents . Add ( new ( )
{
Type = ComponentType . MediaGallery ,
Items = [ new ( ) { Media = new ( ) { Url = target . BannerImage } } ] ,
} ) ;
List < MessageComponent > header = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"### {name}{(systemName != null ? $" ( { systemName } ) " : " ")}" ,
} ,
] ;
if ( ! string . IsNullOrWhiteSpace ( headerText ) )
header . Add ( new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = headerText ,
} ) ;
if ( target . IconFor ( pctx ) is { } icon )
header = [
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [ . . header ] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Thumbnail ,
Media = new ( ) { Url = icon . TryGetCleanCdnUrl ( ) } ,
} ,
} ,
] ;
return [
new MessageComponent ( )
{
Type = ComponentType . Container ,
AccentColor = target . Color ? . ToDiscordColor ( ) ,
Components = [ . . header , . . descComponents ] ,
} ,
new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"-# System ID: `{system.DisplayHid(ctx.Config)}` \u2219 Group ID: `{target.DisplayHid(ctx.Config)}`{(target.MetadataPrivacy.CanAccess(pctx) ? $" \ n - # Created : { target . Created . FormatZoned ( ctx . Zone ) } " : " ")}" ,
} ,
] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Button ,
Style = ButtonStyle . Link ,
Label = "View on dashboard" ,
Url = $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}" ,
} ,
} ,
] ;
}
2021-11-26 21:10:56 -05:00
public async Task < Embed > CreateGroupEmbed ( Context ctx , PKSystem system , PKGroup target )
{
2021-12-06 00:32:54 -05:00
var pctx = ctx . LookupContextFor ( system . Id ) ;
2021-12-23 23:23:16 -05:00
var countctx = LookupContext . ByNonOwner ;
if ( ctx . MatchFlag ( "a" , "all" ) )
{
2025-09-18 23:32:00 +00:00
if ( system . Id = = ctx . System ? . Id )
2021-12-23 23:23:16 -05:00
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetGroupMemberCount ( target . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
2020-08-16 12:10:54 +02:00
2023-07-19 12:48:04 +12:00
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
2024-10-03 00:52:43 -06:00
nameField = $"{nameField}" ;
2019-05-12 00:44:02 +02:00
2021-11-26 21:10:56 -05:00
var eb = new EmbedBuilder ( )
2025-09-07 10:16:50 +12:00
. Author ( new Embed . EmbedAuthor ( nameField , IconUrl : target . IconFor ( pctx ) , Url : $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}" ) )
2024-08-29 06:32:03 -06:00
. Color ( target . Color ? . ToDiscordColor ( ) ) ;
2020-11-22 11:57:54 -05:00
2024-04-28 15:46:06 +12:00
eb . Footer ( new Embed . EmbedFooter ( $"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on { target . Created . FormatZoned ( ctx . Zone ) } " : " ")}" ) ) ;
2022-01-14 22:30:02 -05:00
2024-11-09 11:44:48 -07:00
if ( target . BannerPrivacy . CanAccess ( pctx ) )
2021-11-26 21:10:56 -05:00
eb . Image ( new Embed . EmbedImage ( target . BannerImage ) ) ;
2021-08-02 13:46:12 -04:00
2022-01-14 22:30:02 -05:00
if ( target . NamePrivacy . CanAccess ( pctx ) & & target . DisplayName ! = null )
2021-11-26 21:10:56 -05:00
eb . Field ( new Embed . Field ( "Display Name" , target . DisplayName , true ) ) ;
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
if ( ! target . Color . EmptyOrNull ( ) ) eb . Field ( new Embed . Field ( "Color" , $"#{target.Color}" , true ) ) ;
2020-11-22 11:57:54 -05:00
2021-11-26 21:10:56 -05:00
if ( target . ListPrivacy . CanAccess ( pctx ) )
{
if ( memberCount = = 0 & & pctx = = LookupContext . ByOwner )
// Only suggest the add command if this is actually the owner lol
eb . Field ( new Embed . Field ( "Members (0)" ,
2024-12-31 08:09:18 -07:00
$"Add one with `{ctx.DefaultPrefix}group {target.Reference(ctx)} add <member>`!" ) ) ;
2021-11-26 21:10:56 -05:00
else
2022-01-14 22:30:02 -05:00
{
var name = pctx = = LookupContext . ByOwner
2022-02-05 09:26:14 -05:00
? target . Reference ( ctx )
2024-05-11 21:32:16 +09:00
: target . DisplayHid ( ctx . Config ) ;
2024-12-31 08:09:18 -07:00
eb . Field ( new Embed . Field ( $"Members ({memberCount})" , $"(see `{ctx.DefaultPrefix}group {name} list`)" ) ) ;
2022-01-14 22:30:02 -05:00
}
2021-11-26 21:10:56 -05:00
}
2020-11-22 11:57:54 -05:00
2021-11-26 21:10:56 -05:00
if ( target . DescriptionFor ( pctx ) is { } desc )
eb . Field ( new Embed . Field ( "Description" , desc ) ) ;
2020-11-22 11:57:54 -05:00
2021-11-26 21:10:56 -05:00
if ( target . IconFor ( pctx ) is { } icon )
eb . Thumbnail ( new Embed . EmbedThumbnail ( icon . TryGetCleanCdnUrl ( ) ) ) ;
2020-11-22 11:57:54 -05:00
2021-11-26 21:10:56 -05:00
return eb . Build ( ) ;
}
2019-06-15 12:19:44 +02:00
2021-11-26 21:10:56 -05:00
public async Task < Embed > CreateFronterEmbed ( PKSwitch sw , DateTimeZone zone , LookupContext ctx )
{
var members = await _db . Execute ( c = > _repo . GetSwitchMembers ( c , sw . Id ) . ToListAsync ( ) . AsTask ( ) ) ;
var timeSinceSwitch = SystemClock . Instance . GetCurrentInstant ( ) - sw . Timestamp ;
2023-05-12 15:37:03 +12:00
var memberStr = "*(no fronter)*" ;
if ( members . Count > 0 )
{
memberStr = "" ;
foreach ( var item in members . Select ( ( value , i ) = > new { i , value } ) )
{
memberStr + = item . i = = 0 ? "" : ", " ;
// field limit is 1024, capping after 900 gives us plenty of room
// for the remaining count message
if ( memberStr . Length < 900 )
memberStr + = item . value . NameFor ( ctx ) ;
else
{
memberStr + = $"*({members.Count - item.i} not shown)*" ;
break ;
}
}
}
2021-11-26 21:10:56 -05:00
return new EmbedBuilder ( )
2024-08-29 06:32:03 -06:00
. Color ( members . FirstOrDefault ( ) ? . Color ? . ToDiscordColor ( ) )
2023-05-12 15:37:03 +12:00
. Field ( new Embed . Field ( $"Current {" fronter ".ToQuantity(members.Count, ShowQuantityAs.None)}" , memberStr ) )
2021-11-26 21:10:56 -05:00
. Field ( new Embed . Field ( "Since" ,
$"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)" ) )
. Build ( ) ;
}
2019-06-21 13:49:58 +02:00
2025-10-24 10:23:38 -04:00
public async Task < MessageComponent [ ] > CreateMessageInfoMessageComponents ( FullMessage msg , bool showContent , SystemConfig ? ccfg = null )
{
var channel = await _cache . GetOrFetchChannel ( _rest , msg . Message . Guild ? ? 0 , msg . Message . Channel ) ;
var ctx = LookupContext . ByNonOwner ;
var serverMsg = await _rest . GetMessageOrNull ( msg . Message . Channel , msg . Message . Mid ) ;
// Need this whole dance to handle cases where:
// - the user is deleted (userInfo == null)
// - the bot's no longer in the server we're querying (channel == null)
// - the member is no longer in the server we're querying (memberInfo == null)
// TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s?
GuildMemberPartial memberInfo = null ;
User userInfo = null ;
if ( channel ! = null )
{
GuildMember member = null ;
try
{
member = await _rest . GetGuildMember ( channel . GuildId ! . Value , msg . Message . Sender ) ;
}
catch ( ForbiddenException )
{
// no permission, couldn't fetch, oh well
}
if ( member ! = null )
// Don't do an extra request if we already have this info from the member lookup
userInfo = member . User ;
memberInfo = member ;
}
if ( userInfo = = null )
userInfo = await _cache . GetOrFetchUser ( _rest , msg . Message . Sender ) ;
// Calculate string displayed under "Sent by"
string userStr ;
if ( showContent & & memberInfo ! = null & & memberInfo . Nick ! = null )
userStr = $"**\n Username:** {userInfo.NameAndMention()}\n** Nickname:** {memberInfo.Nick}" ;
else if ( userInfo ! = null ) userStr = userInfo . NameAndMention ( ) ;
else userStr = $"*(deleted user {msg.Message.Sender})*" ;
var content = serverMsg ? . Content ? . NormalizeLineEndSpacing ( ) ;
if ( content = = null | | ! showContent )
content = "*(message contents deleted or inaccessible)*" ;
var systemStr = msg . System = = null
? "*(deleted or unknown system)*"
: msg . System . NameFor ( ctx ) ! = null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`" ;
var memberStr = msg . Member = = null
? "*(deleted member)*"
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)" ;
var roles = memberInfo ? . Roles ? . ToList ( ) ;
var rolesContent = "" ;
if ( roles ! = null & & roles . Count > 0 & & showContent )
{
var guild = await _cache . GetGuild ( channel . GuildId ! . Value ) ;
var rolesString = string . Join ( ", " , ( roles
. Select ( id = >
{
var role = Array . Find ( guild . Roles , r = > r . Id = = id ) ;
if ( role ! = null )
return role ;
return new Role { Name = "*(unknown role)*" , Position = 0 } ;
} ) )
. OrderByDescending ( role = > role . Position )
. Select ( role = > role . Name ) ) ;
rolesContent = $"**Account Roles ({roles.Count})**\n{rolesString}" ;
}
MessageComponent authorData = new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"**System:** {systemStr}\n**Member:** {memberStr}\n**Sent by:** {userStr}\n\n{rolesContent}"
} ;
var avatarURL = msg . Member ? . AvatarFor ( ctx ) . TryGetCleanCdnUrl ( ) ;
2025-10-24 19:55:53 -04:00
MessageComponent header = ( avatarURL = = "" | | avatarURL = = null ) ? authorData : new MessageComponent ( )
2025-10-24 10:23:38 -04:00
{
Type = ComponentType . Section ,
Components = [ authorData ] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Thumbnail ,
Media = new ComponentMedia ( )
{
Url = avatarURL
}
}
} ;
List < MessageComponent > body = [
new MessageComponent ( )
{
Type = ComponentType . Separator ,
Spacing = 2
}
] ;
if ( content ! = "" )
{
body . Add ( new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = content
} ) ;
}
if ( showContent )
{
if ( serverMsg ! = null )
{
var media = new List < ComponentMediaItem > ( ) ;
foreach ( Message . Attachment attachment in serverMsg ? . Attachments )
{
var url = attachment . Url ;
if ( url ! = null & & url ! = "" )
media . Add ( new ComponentMediaItem ( )
{
Media = new ComponentMedia ( )
{
Url = url
}
} ) ;
}
if ( media . Count > 0 )
body . Add ( new MessageComponent ( )
{
Type = ComponentType . MediaGallery ,
Items = media . ToArray ( )
} ) ;
}
}
MessageComponent footer = new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"-# Original Message ID: {msg.Message.OriginalMid} · <t:{DiscordUtils.SnowflakeToTimestamp(msg.Message.Mid)}:f>"
} ;
return [
new MessageComponent ( )
{
Type = ComponentType . Container ,
Components = [
header ,
. . body
]
} ,
footer
] ;
}
2024-04-28 15:46:06 +12:00
public async Task < Embed > CreateMessageInfoEmbed ( FullMessage msg , bool showContent , SystemConfig ? ccfg = null )
2021-11-26 21:10:56 -05:00
{
2024-09-14 12:19:47 +09:00
var channel = await _cache . GetOrFetchChannel ( _rest , msg . Message . Guild ? ? 0 , msg . Message . Channel ) ;
2021-11-26 21:10:56 -05:00
var ctx = LookupContext . ByNonOwner ;
var serverMsg = await _rest . GetMessageOrNull ( msg . Message . Channel , msg . Message . Mid ) ;
// Need this whole dance to handle cases where:
// - the user is deleted (userInfo == null)
// - the bot's no longer in the server we're querying (channel == null)
// - the member is no longer in the server we're querying (memberInfo == null)
// TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s?
GuildMemberPartial memberInfo = null ;
User userInfo = null ;
if ( channel ! = null )
2019-06-21 13:49:58 +02:00
{
2021-11-26 21:10:56 -05:00
GuildMember member = null ;
try
2020-12-25 13:58:45 +01:00
{
2021-11-26 21:10:56 -05:00
member = await _rest . GetGuildMember ( channel . GuildId ! . Value , msg . Message . Sender ) ;
2020-12-25 13:58:45 +01:00
}
2021-11-26 21:10:56 -05:00
catch ( ForbiddenException )
2020-09-12 19:30:03 +02:00
{
2021-11-26 21:10:56 -05:00
// no permission, couldn't fetch, oh well
}
if ( member ! = null )
// Don't do an extra request if we already have this info from the member lookup
userInfo = member . User ;
memberInfo = member ;
}
if ( userInfo = = null )
userInfo = await _cache . GetOrFetchUser ( _rest , msg . Message . Sender ) ;
// Calculate string displayed under "Sent by"
string userStr ;
2022-04-24 14:43:27 -04:00
if ( showContent & & memberInfo ! = null & & memberInfo . Nick ! = null )
2021-11-26 21:10:56 -05:00
userStr = $"**Username:** {userInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nick}" ;
else if ( userInfo ! = null ) userStr = userInfo . NameAndMention ( ) ;
else userStr = $"*(deleted user {msg.Message.Sender})*" ;
var content = serverMsg ? . Content ? . NormalizeLineEndSpacing ( ) ;
if ( content = = null | | ! showContent )
content = "*(message contents deleted or inaccessible)*" ;
// Put it all together
var eb = new EmbedBuilder ( )
2022-01-11 09:43:55 -05:00
. Author ( new Embed . EmbedAuthor ( msg . Member ? . NameFor ( ctx ) ? ? "(deleted member)" ,
IconUrl : msg . Member ? . AvatarFor ( ctx ) . TryGetCleanCdnUrl ( ) ) )
2021-11-26 21:10:56 -05:00
. Description ( content )
. Image ( showContent ? new Embed . EmbedImage ( serverMsg ? . Attachments ? . FirstOrDefault ( ) ? . Url ) : null )
. Field ( new Embed . Field ( "System" ,
2022-01-11 09:43:55 -05:00
msg . System = = null
? "*(deleted or unknown system)*"
2024-04-28 15:46:06 +12:00
: msg . System . NameFor ( ctx ) ! = null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`"
2022-01-11 09:43:55 -05:00
, true ) )
. Field ( new Embed . Field ( "Member" ,
msg . Member = = null
? "*(deleted member)*"
2024-04-28 15:46:06 +12:00
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)"
2022-01-11 09:43:55 -05:00
, true ) )
2021-11-26 21:10:56 -05:00
. Field ( new Embed . Field ( "Sent by" , userStr , true ) )
2024-01-26 17:56:37 -07:00
. Timestamp ( DiscordUtils . SnowflakeToInstant ( msg . Message . Mid ) . ToDateTimeOffset ( ) . ToString ( "O" ) )
. Footer ( new Embed . EmbedFooter ( $"Original Message ID: {msg.Message.OriginalMid}" ) ) ;
2021-11-26 21:10:56 -05:00
var roles = memberInfo ? . Roles ? . ToList ( ) ;
if ( roles ! = null & & roles . Count > 0 & & showContent )
{
2024-09-14 12:19:47 +09:00
var guild = await _cache . GetGuild ( channel . GuildId ! . Value ) ;
var rolesString = string . Join ( ", " , ( roles
. Select ( id = >
2021-11-02 22:47:14 -04:00
{
2024-09-14 12:19:47 +09:00
var role = Array . Find ( guild . Roles , r = > r . Id = = id ) ;
2021-11-02 22:36:14 -04:00
if ( role ! = null )
return role ;
2021-11-26 21:10:56 -05:00
return new Role { Name = "*(unknown role)*" , Position = 0 } ;
2024-09-14 12:19:47 +09:00
} ) )
2021-11-26 21:10:56 -05:00
. OrderByDescending ( role = > role . Position )
. Select ( role = > role . Name ) ) ;
eb . Field ( new Embed . Field ( $"Account roles ({roles.Count})" , rolesString . Truncate ( 1024 ) ) ) ;
2019-06-21 13:49:58 +02:00
}
2019-06-30 23:41:01 +02:00
2021-11-26 21:10:56 -05:00
return eb . Build ( ) ;
}
2021-04-22 01:18:41 +01:00
2025-10-24 10:23:38 -04:00
public async Task < MessageComponent [ ] > CreateAuthorMessageComponents ( User ? user , FullMessage msg )
{
MessageComponent authorInfo ;
var author = user ! = null
? $"{user.Username}#{user.Discriminator}"
: $"Deleted user ${msg.Message.Sender}" ;
var avatarUrl = user ? . AvatarUrl ( ) ;
var authorString = $"{author}\n**ID: **`{msg.Message.Sender.ToString()}`" ;
if ( user ! = null & & avatarUrl ! = "" )
{
authorInfo = new MessageComponent ( )
{
Type = ComponentType . Section ,
Components = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = authorString
}
] ,
Accessory = new MessageComponent ( )
{
Type = ComponentType . Thumbnail ,
Media = new ComponentMedia ( )
{
Url = avatarUrl
}
}
} ;
}
else
{
authorInfo = new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = authorString
} ;
}
MessageComponent container = new MessageComponent ( )
{
Type = ComponentType . Container ,
Components = [
authorInfo ,
]
} ;
return (
[
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = user ! = null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {msg.Message.Sender})*"
} ,
container
]
) ;
}
public async Task < MessageComponent [ ] > CreateCommandMessageInfoMessageComponents ( Core . CommandMessage msg , bool showContent )
{
var content = "*(command message deleted or inaccessible)*" ;
if ( showContent )
{
var discordMessage = await _rest . GetMessageOrNull ( msg . Channel , msg . OriginalMid ) ;
if ( discordMessage ! = null )
content = discordMessage . Content ;
}
List < MessageComponent > body = [
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"### Command response message\n**Original message:** https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}\n**Sent By:** <@{msg.Sender}>"
} ,
new MessageComponent ( )
{
Type = ComponentType . Separator ,
} ,
new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = content
} ,
] ;
MessageComponent footer = new MessageComponent ( )
{
Type = ComponentType . Text ,
Content = $"-# Original Message ID: {msg.OriginalMid} · <t:{DiscordUtils.SnowflakeToTimestamp(msg.OriginalMid)}:f>"
} ;
return [
new MessageComponent ( ) {
Type = ComponentType . Container ,
Components = [
. . body
]
} ,
footer
] ;
}
2025-06-08 19:52:37 +00:00
public async Task < Embed > CreateCommandMessageInfoEmbed ( Core . CommandMessage msg , bool showContent )
{
var content = "*(command message deleted or inaccessible)*" ;
if ( showContent )
{
var discordMessage = await _rest . GetMessageOrNull ( msg . Channel , msg . OriginalMid ) ;
if ( discordMessage ! = null )
content = discordMessage . Content ;
}
return new EmbedBuilder ( )
. Title ( "Command response message" )
. Description ( content )
. Field ( new ( "Original message" , $"https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}" , true ) )
. Field ( new ( "Sent by" , $"<@{msg.Sender}>" , true ) )
. Build ( ) ;
}
2021-11-26 21:10:56 -05:00
public Task < Embed > CreateFrontPercentEmbed ( FrontBreakdown breakdown , PKSystem system , PKGroup group ,
DateTimeZone tz , LookupContext ctx , string embedTitle ,
bool ignoreNoFronters , bool showFlat )
{
var color = system . Color ;
if ( group ! = null ) color = group . Color ;
2021-06-21 11:30:38 -04:00
2021-11-26 21:10:56 -05:00
var eb = new EmbedBuilder ( )
. Title ( embedTitle )
2024-08-29 06:32:03 -06:00
. Color ( color ? . ToDiscordColor ( ) ) ;
2021-06-21 11:30:38 -04:00
2021-11-26 21:10:56 -05:00
var footer =
$"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)" ;
2021-06-21 11:30:38 -04:00
2021-11-26 21:10:56 -05:00
Duration period ;
2021-06-21 11:30:38 -04:00
2021-11-26 21:10:56 -05:00
if ( showFlat )
{
period = Duration . FromTicks ( breakdown . MemberSwitchDurations . Values . ToList ( ) . Sum ( i = > i . TotalTicks ) ) ;
footer + = ". Showing flat list (percentages add up to 100%)" ;
if ( ! ignoreNoFronters ) period + = breakdown . NoFronterDuration ;
else footer + = ", ignoring switch-outs" ;
}
else if ( ignoreNoFronters )
{
period = breakdown . RangeEnd - breakdown . RangeStart - breakdown . NoFronterDuration ;
footer + = ". Ignoring switch-outs" ;
}
else
{
period = breakdown . RangeEnd - breakdown . RangeStart ;
}
2019-07-15 21:51:41 +02:00
2021-11-26 21:10:56 -05:00
eb . Footer ( new Embed . EmbedFooter ( footer ) ) ;
2019-08-09 10:55:15 +00:00
2021-11-26 21:10:56 -05:00
var maxEntriesToDisplay = 24 ; // max 25 fields allowed in embed - reserve 1 for "others"
2019-06-30 23:41:01 +02:00
2021-11-26 21:10:56 -05:00
// We convert to a list of pairs so we can add the no-fronter value
// Dictionary doesn't allow for null keys so we instead have a pair with a null key ;)
var pairs = breakdown . MemberSwitchDurations . ToList ( ) ;
if ( breakdown . NoFronterDuration ! = Duration . Zero & & ! ignoreNoFronters )
pairs . Add ( new KeyValuePair < PKMember , Duration > ( null , breakdown . NoFronterDuration ) ) ;
2019-06-30 23:41:01 +02:00
2021-11-26 21:10:56 -05:00
var membersOrdered = pairs . OrderByDescending ( pair = > pair . Value ) . Take ( maxEntriesToDisplay ) . ToList ( ) ;
foreach ( var pair in membersOrdered )
{
var frac = pair . Value / period ;
eb . Field ( new Embed . Field ( pair . Key ? . NameFor ( ctx ) ? ? "*(no fronter)*" ,
$"{frac * 100:F0}% ({pair.Value.FormatDuration()})" ) ) ;
2019-06-30 23:41:01 +02:00
}
2021-11-26 21:10:56 -05:00
if ( membersOrdered . Count > maxEntriesToDisplay )
eb . Field ( new Embed . Field ( "(others)" ,
membersOrdered . Skip ( maxEntriesToDisplay )
. Aggregate ( Duration . Zero , ( prod , next ) = > prod + next . Value )
. FormatDuration ( ) , true ) ) ;
return Task . FromResult ( eb . Build ( ) ) ;
2019-04-21 15:33:22 +02:00
}
2021-08-27 11:03:47 -04:00
}