2019-07-14 21:49:14 +00:00
using Humanizer ;
2020-12-22 15:55:13 +00:00
2020-12-23 01:19:02 +00:00
using Myriad.Builders ;
2020-12-22 15:55:13 +00:00
using Myriad.Cache ;
2020-12-25 12:58:45 +00:00
using Myriad.Extensions ;
2020-12-22 15:55:13 +00:00
using Myriad.Rest ;
2021-01-31 16:56:33 +00:00
using Myriad.Rest.Exceptions ;
2020-12-22 15:55:13 +00:00
using Myriad.Types ;
2019-06-15 10:19:44 +00:00
using NodaTime ;
2019-04-21 13:33:22 +00:00
2020-02-12 14:16:19 +00:00
using PluralKit.Core ;
2021-11-27 02:10:56 +00:00
namespace PluralKit.Bot ;
public class EmbedService
2021-08-27 15:03:47 +00:00
{
2021-11-27 02:10:56 +00:00
private readonly IDiscordCache _cache ;
private readonly IDatabase _db ;
private readonly ModelRepository _repo ;
private readonly DiscordApiClient _rest ;
public EmbedService ( IDatabase db , ModelRepository repo , IDiscordCache cache , DiscordApiClient rest )
2019-10-26 17:45:30 +00:00
{
2021-11-27 02:10:56 +00:00
_db = db ;
_repo = repo ;
_cache = cache ;
_rest = rest ;
}
2019-04-21 13:33:22 +00:00
2021-11-27 02:10:56 +00:00
private Task < ( ulong Id , User ? User ) [ ] > GetUsers ( IEnumerable < ulong > ids )
{
async Task < ( ulong Id , User ? User ) > Inner ( ulong id )
2019-04-21 13:33:22 +00:00
{
2021-11-27 02:10:56 +00:00
var user = await _cache . GetOrFetchUser ( _rest , id ) ;
return ( id , user ) ;
2020-12-22 15:55:13 +00:00
}
2021-11-27 02:10:56 +00:00
return Task . WhenAll ( ids . Select ( Inner ) ) ;
}
2020-12-22 15:55:13 +00:00
2021-11-27 02:10:56 +00: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-24 04:23:16 +00:00
var countctx = LookupContext . ByNonOwner ;
if ( cctx . MatchFlag ( "a" , "all" ) )
{
if ( system . Id = = cctx . System . Id )
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetSystemMemberCount ( system . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
2021-08-27 15:03:47 +00:00
2021-11-27 02:10:56 +00:00
uint color ;
try
2020-06-29 12:54:11 +00:00
{
2021-11-27 02:10:56 +00:00
color = system . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
}
catch ( ArgumentException )
{
// There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea
color = DiscordUtils . Gray ;
}
2021-08-27 15:03:47 +00:00
2021-11-27 02:10:56 +00:00
var eb = new EmbedBuilder ( )
. Title ( system . Name )
. Thumbnail ( new Embed . EmbedThumbnail ( system . AvatarUrl . TryGetCleanCdnUrl ( ) ) )
. Footer ( new Embed . EmbedFooter (
2021-11-30 02:35:21 +00:00
$"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}" ) )
2022-06-03 05:09:02 +00:00
. Color ( color )
. Url ( $"https://dash.pluralkit.me/profile/s/{system.Hid}" ) ;
2020-09-12 22:10:37 +00:00
2021-11-27 02:10:56 +00:00
if ( system . DescriptionPrivacy . CanAccess ( ctx ) )
eb . Image ( new Embed . EmbedImage ( system . BannerImage ) ) ;
2019-04-21 13:33:22 +00:00
2021-11-27 02:10:56 +00: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 03:37:03 +00:00
{
var memberStr = string . Join ( ", " , switchMembers . Select ( m = > m . NameFor ( ctx ) ) ) ;
if ( memberStr . Length > 200 )
memberStr = $"[too many to show, see `pk;system {system.Hid} fronters`]" ;
eb . Field ( new Embed . Field ( "Fronter" . ToQuantity ( switchMembers . Count , ShowQuantityAs . None ) , memberStr ) ) ;
}
2021-11-27 02:10:56 +00:00
}
2021-03-28 10:02:41 +00:00
2021-11-27 02:10:56 +00:00
if ( system . Tag ! = null )
eb . Field ( new Embed . Field ( "Tag" , system . Tag . EscapeMarkdown ( ) , true ) ) ;
2020-12-23 01:19:02 +00:00
2021-11-27 02:10:56 +00:00
if ( cctx . Guild ! = null )
{
var guildSettings = await _repo . GetSystemGuild ( cctx . Guild . Id , system . Id ) ;
2021-08-02 17:46:12 +00:00
2021-11-27 02:10:56 +00: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 13:33:22 +00:00
2021-11-27 02:10:56 +00:00
if ( ! guildSettings . TagEnabled )
eb . Field ( new Embed . Field ( $"Tag (in server '{cctx.Guild.Name}')" ,
"*(tag is disabled in this server)*" ) ) ;
}
2021-03-28 17:22:45 +00:00
2022-03-23 18:20:16 +00:00
if ( system . PronounPrivacy . CanAccess ( ctx ) & & system . Pronouns ! = null )
eb . Field ( new Embed . Field ( "Pronouns" , system . Pronouns , true ) ) ;
2021-11-27 02:10:56 +00:00
if ( ! system . Color . EmptyOrNull ( ) ) eb . Field ( new Embed . Field ( "Color" , $"#{system.Color}" , true ) ) ;
2021-08-27 15:03:47 +00:00
2021-11-27 02:10:56 +00:00
eb . Field ( new Embed . Field ( "Linked accounts" , string . Join ( "\n" , users ) . Truncate ( 1000 ) , true ) ) ;
2021-08-02 21:22:06 +00:00
2021-11-27 02:10:56 +00:00
if ( system . MemberListPrivacy . CanAccess ( ctx ) )
{
if ( memberCount > 0 )
eb . Field ( new Embed . Field ( $"Members ({memberCount})" ,
$"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)" , true ) ) ;
else
eb . Field ( new Embed . Field ( $"Members ({memberCount})" , "Add one with `pk;member new`!" , true ) ) ;
}
2021-08-02 21:22:06 +00:00
2021-11-27 02:10:56 +00:00
if ( system . DescriptionFor ( ctx ) is { } desc )
eb . Field ( new Embed . Field ( "Description" , desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) ) ) ;
2021-03-28 17:22:45 +00:00
2021-11-27 02:10:56 +00:00
return eb . Build ( ) ;
}
2020-02-05 22:43:30 +00:00
2021-11-27 02:10:56 +00: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-01 17:11:35 +00:00
: member . WebhookAvatarFor ( LookupContext . ByNonOwner ) ;
2021-11-27 02:10:56 +00: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-14 01:18:24 +00:00
if ( oldContent = = "" )
oldContent = "*no message content*" ;
2021-11-27 02:10:56 +00:00
if ( oldContent ! = null )
embed . Field ( new Embed . Field ( "Old message" , oldContent ? . NormalizeLineEndSpacing ( ) . Truncate ( 1000 ) ) ) ;
return embed . Build ( ) ;
}
2019-07-21 14:29:48 +00:00
2021-11-30 02:35:21 +00:00
public async Task < Embed > CreateMemberEmbed ( PKSystem system , PKMember member , Guild guild , LookupContext ctx , DateTimeZone zone )
2021-11-27 02:10:56 +00:00
{
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
2019-08-09 10:55:15 +00:00
2021-11-27 02:10:56 +00:00
var name = member . NameFor ( ctx ) ;
if ( system . Name ! = null ) name = $"{name} ({system.Name})" ;
2019-04-22 15:10:18 +00:00
2021-11-27 02:10:56 +00:00
uint color ;
try
2021-08-27 15:03:47 +00:00
{
2021-11-27 02:10:56 +00:00
color = member . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
2021-05-07 16:35:09 +00:00
}
2021-11-27 02:10:56 +00:00
catch ( ArgumentException )
2019-05-11 22:44:02 +00:00
{
2021-11-27 02:10:56 +00:00
// Bad API use can cause an invalid color string
// this is now fixed in the API, but might still have some remnants in the database
// so we just default to a blank color, yolo
color = DiscordUtils . Gray ;
}
2020-06-17 19:31:39 +00:00
2021-11-27 02:10:56 +00:00
var guildSettings = guild ! = null ? await _repo . GetMemberGuild ( guild . Id , member . Id ) : null ;
var guildDisplayName = guildSettings ? . DisplayName ;
2023-03-01 17:11:35 +00:00
var webhook_avatar = guildSettings ? . AvatarUrl ? ? member . WebhookAvatarFor ( ctx ) ? ? member . AvatarFor ( ctx ) ;
2021-11-27 02:10:56 +00: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 ( )
2023-03-01 17:11:35 +00:00
. Author ( new Embed . EmbedAuthor ( name , IconUrl : webhook_avatar . TryGetCleanCdnUrl ( ) , Url : $"https://dash.pluralkit.me/profile/m/{member.Hid}" ) )
2021-11-27 02:10:56 +00:00
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
. Color ( color )
. Footer ( new Embed . EmbedFooter (
2021-11-30 02:35:21 +00:00
$"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $" | Created on { member . Created . FormatZoned ( zone ) } " : " ")}" ) ) ;
2021-11-27 02:10:56 +00:00
if ( member . DescriptionPrivacy . CanAccess ( ctx ) )
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 ) ) ;
if ( member . HasProxyTags )
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 ) )
: string . Join ( "\n" , groups . Select ( g = > $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**" ) ) ;
eb . Field ( new Embed . Field ( $"Groups ({groups.Count})" , content . Truncate ( 1000 ) ) ) ;
}
2020-06-17 19:31:39 +00:00
2021-11-27 02:10:56 +00: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-27 02:10:56 +00:00
return eb . Build ( ) ;
}
2020-07-18 11:26:36 +00:00
2021-11-27 02:10:56 +00:00
public async Task < Embed > CreateGroupEmbed ( Context ctx , PKSystem system , PKGroup target )
{
2021-12-06 05:32:54 +00:00
var pctx = ctx . LookupContextFor ( system . Id ) ;
2021-12-24 04:23:16 +00:00
var countctx = LookupContext . ByNonOwner ;
if ( ctx . MatchFlag ( "a" , "all" ) )
{
if ( system . Id = = ctx . System . Id )
countctx = LookupContext . ByOwner ;
else
throw Errors . LookupNotAllowed ;
}
var memberCount = await _repo . GetGroupMemberCount ( target . Id , countctx = = LookupContext . ByOwner ? null : PrivacyLevel . Public ) ;
2020-08-16 10:10:54 +00:00
2022-01-15 03:30:02 +00:00
var nameField = target . NamePrivacy . Get ( pctx , target . Name , target . DisplayName ? ? target . Name ) ;
2021-11-27 02:10:56 +00:00
if ( system . Name ! = null )
nameField = $"{nameField} ({system.Name})" ;
2019-05-11 22:44:02 +00:00
2021-11-27 02:10:56 +00:00
uint color ;
try
{
color = target . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
2020-11-22 16:57:54 +00:00
}
2021-11-27 02:10:56 +00:00
catch ( ArgumentException )
2020-11-22 16:57:54 +00:00
{
2021-11-27 02:10:56 +00:00
// There's no API for group colors yet, but defaulting to a blank color regardless
color = DiscordUtils . Gray ;
}
2021-03-28 10:02:41 +00:00
2021-11-27 02:10:56 +00:00
var eb = new EmbedBuilder ( )
2022-06-03 05:09:02 +00:00
. Author ( new Embed . EmbedAuthor ( nameField , IconUrl : target . IconFor ( pctx ) , Url : $"https://dash.pluralkit.me/profile/g/{target.Hid}" ) )
2022-01-15 03:30:02 +00:00
. Color ( color ) ;
2020-11-22 16:57:54 +00:00
2022-01-15 03:30:02 +00:00
eb . Footer ( new Embed . EmbedFooter ( $"System ID: {system.Hid} | Group ID: {target.Hid}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on { target . Created . FormatZoned ( ctx . Zone ) } " : " ")}" ) ) ;
if ( target . DescriptionPrivacy . CanAccess ( pctx ) )
2021-11-27 02:10:56 +00:00
eb . Image ( new Embed . EmbedImage ( target . BannerImage ) ) ;
2021-08-02 17:46:12 +00:00
2022-01-15 03:30:02 +00:00
if ( target . NamePrivacy . CanAccess ( pctx ) & & target . DisplayName ! = null )
2021-11-27 02:10:56 +00:00
eb . Field ( new Embed . Field ( "Display Name" , target . DisplayName , true ) ) ;
2021-08-27 15:03:47 +00:00
2021-11-27 02:10:56 +00:00
if ( ! target . Color . EmptyOrNull ( ) ) eb . Field ( new Embed . Field ( "Color" , $"#{target.Color}" , true ) ) ;
2020-11-22 16:57:54 +00:00
2021-11-27 02:10:56 +00: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)" ,
2022-02-05 14:26:14 +00:00
$"Add one with `pk;group {target.Reference(ctx)} add <member>`!" ) ) ;
2021-11-27 02:10:56 +00:00
else
2022-01-15 03:30:02 +00:00
{
var name = pctx = = LookupContext . ByOwner
2022-02-05 14:26:14 +00:00
? target . Reference ( ctx )
2022-01-15 03:30:02 +00:00
: target . Hid ;
eb . Field ( new Embed . Field ( $"Members ({memberCount})" , $"(see `pk;group {name} list`)" ) ) ;
}
2021-11-27 02:10:56 +00:00
}
2020-11-22 16:57:54 +00:00
2021-11-27 02:10:56 +00:00
if ( target . DescriptionFor ( pctx ) is { } desc )
eb . Field ( new Embed . Field ( "Description" , desc ) ) ;
2020-11-22 16:57:54 +00:00
2021-11-27 02:10:56 +00:00
if ( target . IconFor ( pctx ) is { } icon )
eb . Thumbnail ( new Embed . EmbedThumbnail ( icon . TryGetCleanCdnUrl ( ) ) ) ;
2020-11-22 16:57:54 +00:00
2021-11-27 02:10:56 +00:00
return eb . Build ( ) ;
}
2019-06-15 10:19:44 +00:00
2021-11-27 02:10:56 +00: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 03:37:03 +00: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-27 02:10:56 +00:00
return new EmbedBuilder ( )
. Color ( members . FirstOrDefault ( ) ? . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray )
2023-05-12 03:37:03 +00:00
. Field ( new Embed . Field ( $"Current {" fronter ".ToQuantity(members.Count, ShowQuantityAs.None)}" , memberStr ) )
2021-11-27 02:10:56 +00:00
. Field ( new Embed . Field ( "Since" ,
$"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)" ) )
. Build ( ) ;
}
2019-06-21 11:49:58 +00:00
2021-11-27 02:10:56 +00:00
public async Task < Embed > CreateMessageInfoEmbed ( FullMessage msg , bool showContent )
{
var channel = await _cache . GetOrFetchChannel ( _rest , 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 )
2019-06-21 11:49:58 +00:00
{
2021-11-27 02:10:56 +00:00
GuildMember member = null ;
try
2020-12-25 12:58:45 +00:00
{
2021-11-27 02:10:56 +00:00
member = await _rest . GetGuildMember ( channel . GuildId ! . Value , msg . Message . Sender ) ;
2020-12-25 12:58:45 +00:00
}
2021-11-27 02:10:56 +00:00
catch ( ForbiddenException )
2020-09-12 17:30:03 +00:00
{
2021-11-27 02:10:56 +00: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 18:43:27 +00:00
if ( showContent & & memberInfo ! = null & & memberInfo . Nick ! = null )
2021-11-27 02:10:56 +00: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 14:43:55 +00:00
. Author ( new Embed . EmbedAuthor ( msg . Member ? . NameFor ( ctx ) ? ? "(deleted member)" ,
IconUrl : msg . Member ? . AvatarFor ( ctx ) . TryGetCleanCdnUrl ( ) ) )
2021-11-27 02:10:56 +00:00
. Description ( content )
. Image ( showContent ? new Embed . EmbedImage ( serverMsg ? . Attachments ? . FirstOrDefault ( ) ? . Url ) : null )
. Field ( new Embed . Field ( "System" ,
2022-01-11 14:43:55 +00:00
msg . System = = null
? "*(deleted or unknown system)*"
: msg . System . Name ! = null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`"
, true ) )
. Field ( new Embed . Field ( "Member" ,
msg . Member = = null
? " * ( deleted member ) * "
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)"
, true ) )
2021-11-27 02:10:56 +00:00
. Field ( new Embed . Field ( "Sent by" , userStr , true ) )
. Timestamp ( DiscordUtils . SnowflakeToInstant ( msg . Message . Mid ) . ToDateTimeOffset ( ) . ToString ( "O" ) ) ;
var roles = memberInfo ? . Roles ? . ToList ( ) ;
if ( roles ! = null & & roles . Count > 0 & & showContent )
{
var rolesString = string . Join ( ", " , ( await Task . WhenAll ( roles
2021-11-18 18:11:02 +00:00
. Select ( async id = >
2021-11-03 02:47:14 +00:00
{
2021-11-18 18:11:02 +00:00
var role = await _cache . TryGetRole ( id ) ;
2021-11-03 02:36:14 +00:00
if ( role ! = null )
return role ;
2021-11-27 02:10:56 +00:00
return new Role { Name = "*(unknown role)*" , Position = 0 } ;
2021-11-18 18:11:02 +00:00
} ) ) )
2021-11-27 02:10:56 +00:00
. OrderByDescending ( role = > role . Position )
. Select ( role = > role . Name ) ) ;
eb . Field ( new Embed . Field ( $"Account roles ({roles.Count})" , rolesString . Truncate ( 1024 ) ) ) ;
2019-06-21 11:49:58 +00:00
}
2019-06-30 21:41:01 +00:00
2021-11-27 02:10:56 +00:00
return eb . Build ( ) ;
}
2021-04-22 00:18:41 +00:00
2021-11-27 02:10:56 +00: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 15:30:38 +00:00
2021-11-27 02:10:56 +00:00
uint embedColor ;
try
{
embedColor = color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
}
catch ( ArgumentException )
{
embedColor = DiscordUtils . Gray ;
}
2021-06-21 15:30:38 +00:00
2021-11-27 02:10:56 +00:00
var eb = new EmbedBuilder ( )
. Title ( embedTitle )
. Color ( embedColor ) ;
2021-06-21 15:30:38 +00:00
2021-11-27 02:10:56 +00:00
var footer =
$"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)" ;
2021-06-21 15:30:38 +00:00
2021-11-27 02:10:56 +00:00
Duration period ;
2021-06-21 15:30:38 +00:00
2021-11-27 02:10:56 +00: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 19:51:41 +00:00
2021-11-27 02:10:56 +00:00
eb . Footer ( new Embed . EmbedFooter ( footer ) ) ;
2019-08-09 10:55:15 +00:00
2021-11-27 02:10:56 +00:00
var maxEntriesToDisplay = 24 ; // max 25 fields allowed in embed - reserve 1 for "others"
2019-06-30 21:41:01 +00:00
2021-11-27 02:10:56 +00: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 21:41:01 +00:00
2021-11-27 02:10:56 +00: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 21:41:01 +00:00
}
2021-11-27 02:10:56 +00: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 13:33:22 +00:00
}
2021-08-27 15:03:47 +00:00
}