2019-08-13 19:49:43 +00:00
using System ;
2019-06-15 10:19:44 +00:00
using System.Collections.Generic ;
2019-04-21 13:33:22 +00:00
using System.Linq ;
using System.Threading.Tasks ;
2020-04-17 21:10:01 +00:00
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-08-27 15:03:47 +00:00
namespace PluralKit.Bot
{
2019-10-26 17:45:30 +00:00
public class EmbedService
{
2020-08-29 11:46:27 +00:00
private readonly IDatabase _db ;
private readonly ModelRepository _repo ;
2020-12-22 15:55:13 +00:00
private readonly IDiscordCache _cache ;
private readonly DiscordApiClient _rest ;
2019-04-21 13:33:22 +00:00
2020-12-25 12:58:45 +00:00
public EmbedService ( IDatabase db , ModelRepository repo , IDiscordCache cache , DiscordApiClient rest )
2019-04-21 13:33:22 +00:00
{
2020-06-13 14:03:57 +00:00
_db = db ;
2020-08-29 11:46:27 +00:00
_repo = repo ;
2020-12-22 15:55:13 +00:00
_cache = cache ;
_rest = rest ;
}
private Task < ( ulong Id , User ? User ) [ ] > GetUsers ( IEnumerable < ulong > ids )
{
async Task < ( ulong Id , User ? User ) > Inner ( ulong id )
{
2020-12-25 12:58:45 +00:00
var user = await _cache . GetOrFetchUser ( _rest , id ) ;
2020-12-22 15:55:13 +00:00
return ( id , user ) ;
}
return Task . WhenAll ( ids . Select ( Inner ) ) ;
2019-04-21 13:33:22 +00:00
}
2021-08-27 15:03:47 +00:00
2020-12-22 15:55:13 +00:00
public async Task < Embed > CreateSystemEmbed ( Context cctx , PKSystem system , LookupContext ctx )
2020-06-29 12:54:11 +00:00
{
2021-08-27 15:03:47 +00:00
2019-04-21 13:33:22 +00:00
// Fetch/render info for all accounts simultaneously
2021-09-30 01:51:38 +00:00
var accounts = await _repo . GetSystemAccounts ( system . Id ) ;
2020-12-22 15:55:13 +00:00
var users = ( await GetUsers ( accounts ) ) . Select ( x = > x . User ? . NameAndMention ( ) ? ? $"(deleted account {x.Id})" ) ;
2020-09-12 22:10:37 +00:00
2021-09-30 01:51:38 +00:00
var memberCount = cctx . MatchPrivateFlag ( ctx ) ? await _repo . GetSystemMemberCount ( system . Id , PrivacyLevel . Public ) : await _repo . GetSystemMemberCount ( system . Id ) ;
2019-04-21 13:33:22 +00:00
2021-03-28 10:02:41 +00:00
uint color ;
try
{
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 ;
}
2020-12-23 01:19:02 +00:00
var eb = new EmbedBuilder ( )
. Title ( system . Name )
2021-08-01 16:51:54 +00:00
. Thumbnail ( new ( system . AvatarUrl . TryGetCleanCdnUrl ( ) ) )
2020-12-23 01:19:02 +00:00
. Footer ( new ( $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}" ) )
2021-03-28 10:02:41 +00:00
. Color ( color ) ;
2020-12-23 01:19:02 +00:00
2021-08-02 17:46:12 +00:00
if ( system . DescriptionPrivacy . CanAccess ( ctx ) )
eb . Image ( new ( system . BannerImage ) ) ;
2021-09-30 01:51:38 +00:00
var latestSwitch = await _repo . GetLatestSwitch ( system . Id ) ;
2020-01-11 15:49:20 +00:00
if ( latestSwitch ! = null & & system . FrontPrivacy . CanAccess ( ctx ) )
2019-07-18 12:05:02 +00:00
{
2021-09-30 01:51:38 +00:00
var switchMembers = await _db . Execute ( conn = > _repo . GetSwitchMembers ( conn , latestSwitch . Id ) ) . ToListAsync ( ) ;
2020-12-22 15:55:13 +00:00
if ( switchMembers . Count > 0 )
2020-12-23 01:19:02 +00:00
eb . Field ( new ( "Fronter" . ToQuantity ( switchMembers . Count , ShowQuantityAs . None ) , string . Join ( ", " , switchMembers . Select ( m = > m . NameFor ( ctx ) ) ) ) ) ;
2019-07-18 12:05:02 +00:00
}
2019-04-21 13:33:22 +00:00
2021-08-27 15:03:47 +00:00
if ( system . Tag ! = null )
2021-03-28 17:22:45 +00:00
eb . Field ( new ( "Tag" , system . Tag . EscapeMarkdown ( ) , true ) ) ;
2021-08-08 13:31:03 +00:00
if ( cctx . Guild ! = null )
{
2021-09-30 01:51:38 +00:00
var guildSettings = await _repo . GetSystemGuild ( cctx . Guild . Id , system . Id ) ;
2021-08-27 15:03:47 +00:00
2021-08-25 16:30:42 +00:00
if ( guildSettings . Tag ! = null & & guildSettings . TagEnabled )
eb . Field ( new ( $"Tag (in server '{cctx.Guild.Name}')" , guildSettings . Tag
2021-08-08 13:31:03 +00:00
. EscapeMarkdown ( ) , true ) ) ;
2021-08-02 21:22:06 +00:00
2021-08-25 16:30:42 +00:00
if ( ! guildSettings . TagEnabled )
2021-08-08 13:31:03 +00:00
eb . Field ( new ( $"Tag (in server '{cctx.Guild.Name}')" , "*(tag is disabled in this server)*" ) ) ;
}
2021-08-02 21:22:06 +00:00
2021-03-28 17:22:45 +00:00
if ( ! system . Color . EmptyOrNull ( ) ) eb . Field ( new ( "Color" , $"#{system.Color}" , true ) ) ;
2020-12-23 01:19:02 +00:00
eb . Field ( new ( "Linked accounts" , string . Join ( "\n" , users ) . Truncate ( 1000 ) , true ) ) ;
2020-02-05 22:43:30 +00:00
2020-01-11 15:49:20 +00:00
if ( system . MemberListPrivacy . CanAccess ( ctx ) )
2020-02-05 22:43:30 +00:00
{
if ( memberCount > 0 )
2020-12-23 01:19:02 +00:00
eb . Field ( new ( $"Members ({memberCount})" , $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)" , true ) ) ;
2020-02-05 22:43:30 +00:00
else
2020-12-23 01:19:02 +00:00
eb . Field ( new ( $"Members ({memberCount})" , "Add one with `pk;member new`!" , true ) ) ;
2020-02-05 22:43:30 +00:00
}
2019-07-21 14:29:48 +00:00
2020-06-21 13:51:08 +00:00
if ( system . DescriptionFor ( ctx ) is { } desc )
2020-12-23 01:19:02 +00:00
eb . Field ( new ( "Description" , desc . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) , false ) ) ;
2019-08-09 10:55:15 +00:00
2020-12-23 01:19:02 +00:00
return eb . Build ( ) ;
2019-04-21 13:33:22 +00:00
}
2019-04-22 15:10:18 +00:00
2021-08-27 15:03:47 +00:00
public Embed CreateLoggedMessageEmbed ( Message triggerMessage , Message proxiedMessage , string systemHid , PKMember member , string channelName , string oldContent = null )
{
2019-04-22 15:10:18 +00:00
// TODO: pronouns in ?-reacted response using this card
2021-08-04 04:41:51 +00:00
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 ( ) : member . AvatarFor ( LookupContext . ByNonOwner ) ;
var embed = new EmbedBuilder ( )
. Author ( new ( $"#{channelName}: {name}" , IconUrl : avatar ) )
. Thumbnail ( new ( avatar ) )
. Description ( proxiedMessage . Content ? . NormalizeLineEndSpacing ( ) )
. Footer ( new ( $"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" ) ) ;
if ( oldContent ! = null )
embed . Field ( new ( "Old message" , oldContent ? . NormalizeLineEndSpacing ( ) . Truncate ( 1000 ) ) ) ;
2021-08-27 15:03:47 +00:00
2021-08-04 04:41:51 +00:00
return embed . Build ( ) ;
2021-05-07 16:35:09 +00:00
}
2020-12-25 12:58:45 +00:00
public async Task < Embed > CreateMemberEmbed ( PKSystem system , PKMember member , Guild guild , LookupContext ctx )
2019-05-11 22:44:02 +00:00
{
2020-06-17 19:31:39 +00:00
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
2020-06-18 15:08:36 +00:00
var name = member . NameFor ( ctx ) ;
if ( system . Name ! = null ) name = $"{name} ({system.Name})" ;
2019-08-09 10:55:15 +00:00
2021-01-15 10:29:43 +00:00
uint color ;
2019-08-13 19:49:43 +00:00
try
{
2020-04-28 22:25:01 +00:00
color = member . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
2019-08-13 19:49:43 +00:00
}
catch ( ArgumentException )
{
// Bad API use can cause an invalid color string
2021-09-30 01:51:38 +00:00
// 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
2020-04-28 22:25:01 +00:00
color = DiscordUtils . Gray ;
2019-08-13 19:49:43 +00:00
}
2020-07-18 11:26:36 +00:00
2021-09-30 01:51:38 +00:00
var guildSettings = guild ! = null ? await _repo . GetMemberGuild ( guild . Id , member . Id ) : null ;
2020-02-12 16:42:12 +00:00
var guildDisplayName = guildSettings ? . DisplayName ;
2020-06-20 14:00:50 +00:00
var avatar = guildSettings ? . AvatarUrl ? ? member . AvatarFor ( ctx ) ;
2019-12-26 19:39:47 +00:00
2021-09-30 01:51:38 +00:00
var groups = await _repo . GetMemberGroups ( member . Id )
2020-08-29 11:46:27 +00:00
. Where ( g = > g . Visibility . CanAccess ( ctx ) )
2020-08-30 15:03:28 +00:00
. OrderBy ( g = > g . Name , StringComparer . InvariantCultureIgnoreCase )
2020-08-29 11:46:27 +00:00
. ToListAsync ( ) ;
2020-12-23 01:19:02 +00:00
var eb = new EmbedBuilder ( )
2019-05-11 22:44:02 +00:00
// TODO: add URL of website when that's up
2021-08-04 05:39:41 +00:00
. Author ( new ( name , IconUrl : avatar . TryGetCleanCdnUrl ( ) ) )
2020-06-17 19:51:40 +00:00
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
2021-01-15 10:29:43 +00:00
. Color ( color )
2020-12-23 01:19:02 +00:00
. Footer ( new (
$"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $" | Created on { member . Created . FormatZoned ( system ) } " : " ")}" ) ) ;
2019-05-11 22:44:02 +00:00
2021-08-02 17:46:12 +00:00
if ( member . DescriptionPrivacy . CanAccess ( ctx ) )
eb . Image ( new ( member . BannerImage ) ) ;
2020-02-12 16:42:12 +00:00
var description = "" ;
2020-06-17 19:31:39 +00:00
if ( member . MemberVisibility = = PrivacyLevel . Private ) description + = "*(this member is hidden)*\n" ;
2020-02-12 16:42:12 +00:00
if ( guildSettings ? . AvatarUrl ! = null )
2021-08-27 15:03:47 +00:00
if ( member . AvatarFor ( ctx ) ! = null )
2021-08-01 16:51:54 +00:00
description + = $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n" ;
2020-02-12 16:42:12 +00:00
else
description + = "*(this member has a server-specific avatar set)*\n" ;
2020-12-23 01:19:02 +00:00
if ( description ! = "" ) eb . Description ( description ) ;
2021-08-27 15:03:47 +00:00
2021-08-01 16:51:54 +00:00
if ( avatar ! = null ) eb . Thumbnail ( new ( avatar . TryGetCleanCdnUrl ( ) ) ) ;
2020-12-23 01:19:02 +00:00
if ( ! member . DisplayName . EmptyOrNull ( ) & & member . NamePrivacy . CanAccess ( ctx ) ) eb . Field ( new ( "Display Name" , member . DisplayName . Truncate ( 1024 ) , true ) ) ;
if ( guild ! = null & & guildDisplayName ! = null ) eb . Field ( new ( $"Server Nickname (for {guild.Name})" , guildDisplayName . Truncate ( 1024 ) , true ) ) ;
if ( member . BirthdayFor ( ctx ) ! = null ) eb . Field ( new ( "Birthdate" , member . BirthdayString , true ) ) ;
2021-08-27 15:03:47 +00:00
if ( member . PronounsFor ( ctx ) is { } pronouns & & ! string . IsNullOrWhiteSpace ( pronouns ) ) eb . Field ( new ( "Pronouns" , pronouns . Truncate ( 1024 ) , true ) ) ;
if ( member . MessageCountFor ( ctx ) is { } count & & count > 0 ) eb . Field ( new ( "Message Count" , member . MessageCount . ToString ( ) , true ) ) ;
2020-12-23 01:19:02 +00:00
if ( member . HasProxyTags ) eb . Field ( new ( "Proxy Tags" , member . ProxyTagsString ( "\n" ) . Truncate ( 1024 ) , true ) ) ;
2020-06-17 19:31:39 +00:00
// --- 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));
2020-06-17 19:51:40 +00:00
// if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
2020-12-23 01:19:02 +00:00
if ( ! member . Color . EmptyOrNull ( ) ) eb . Field ( new ( "Color" , $"#{member.Color}" , true ) ) ;
2020-08-16 10:10:54 +00:00
if ( groups . Count > 0 )
{
// More than 5 groups show in "compact" format without ID
var content = groups . Count > 5
2020-08-20 19:43:17 +00:00
? string . Join ( ", " , groups . Select ( g = > g . DisplayName ? ? g . Name ) )
: string . Join ( "\n" , groups . Select ( g = > $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**" ) ) ;
2020-12-23 01:19:02 +00:00
eb . Field ( new ( $"Groups ({groups.Count})" , content . Truncate ( 1000 ) ) ) ;
2020-08-16 10:10:54 +00:00
}
2021-08-27 15:03:47 +00:00
if ( member . DescriptionFor ( ctx ) is { } desc )
2020-12-23 01:19:02 +00:00
eb . Field ( new ( "Description" , member . Description . NormalizeLineEndSpacing ( ) , false ) ) ;
2019-05-11 22:44:02 +00:00
2020-11-22 16:57:54 +00:00
return eb . Build ( ) ;
}
2020-12-23 01:19:02 +00:00
public async Task < Embed > CreateGroupEmbed ( Context ctx , PKSystem system , PKGroup target )
2020-11-22 16:57:54 +00:00
{
var pctx = ctx . LookupContextFor ( system ) ;
2021-09-30 01:51:38 +00:00
var memberCount = ctx . MatchPrivateFlag ( pctx ) ? await _repo . GetGroupMemberCount ( target . Id , PrivacyLevel . Public ) : await _repo . GetGroupMemberCount ( target . Id ) ;
2020-11-22 16:57:54 +00:00
var nameField = target . Name ;
if ( system . Name ! = null )
nameField = $"{nameField} ({system.Name})" ;
2021-03-28 10:02:41 +00:00
uint color ;
try
{
color = target . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
}
catch ( ArgumentException )
{
// There's no API for group colors yet, but defaulting to a blank color regardless
color = DiscordUtils . Gray ;
}
2020-12-23 01:19:02 +00:00
var eb = new EmbedBuilder ( )
2021-08-04 05:39:41 +00:00
. Author ( new ( nameField , IconUrl : target . IconFor ( pctx ) ) )
2021-03-28 10:02:41 +00:00
. Color ( color )
2020-12-23 01:19:02 +00:00
. Footer ( new ( $"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}" ) ) ;
2020-11-22 16:57:54 +00:00
2021-08-02 17:46:12 +00:00
if ( target . DescriptionPrivacy . CanAccess ( ctx . LookupContextFor ( target . System ) ) )
eb . Image ( new ( target . BannerImage ) ) ;
2020-11-22 16:57:54 +00:00
if ( target . DisplayName ! = null )
2021-03-28 17:22:45 +00:00
eb . Field ( new ( "Display Name" , target . DisplayName , true ) ) ;
2021-08-27 15:03:47 +00:00
2021-03-28 17:22:45 +00:00
if ( ! target . Color . EmptyOrNull ( ) ) eb . Field ( new ( "Color" , $"#{target.Color}" , true ) ) ;
2020-11-22 16:57:54 +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
2021-03-28 17:22:45 +00:00
eb . Field ( new ( "Members (0)" , $"Add one with `pk;group {target.Reference()} add <member>`!" , false ) ) ;
2020-11-22 16:57:54 +00:00
else
2021-03-28 17:22:45 +00:00
eb . Field ( new ( $"Members ({memberCount})" , $"(see `pk;group {target.Reference()} list`)" , false ) ) ;
2020-11-22 16:57:54 +00:00
}
2020-12-23 01:19:02 +00:00
if ( target . DescriptionFor ( pctx ) is { } desc )
eb . Field ( new ( "Description" , desc ) ) ;
2020-11-22 16:57:54 +00:00
2021-08-27 15:03:47 +00:00
if ( target . IconFor ( pctx ) is { } icon )
2021-08-01 16:51:54 +00:00
eb . Thumbnail ( new ( icon . TryGetCleanCdnUrl ( ) ) ) ;
2020-11-22 16:57:54 +00:00
2019-05-11 22:44:02 +00:00
return eb . Build ( ) ;
}
2019-06-15 10:19:44 +00:00
2020-12-23 01:19:02 +00:00
public async Task < Embed > CreateFronterEmbed ( PKSwitch sw , DateTimeZone zone , LookupContext ctx )
2019-06-15 10:19:44 +00:00
{
2020-08-29 11:46:27 +00:00
var members = await _db . Execute ( c = > _repo . GetSwitchMembers ( c , sw . Id ) . ToListAsync ( ) . AsTask ( ) ) ;
2019-06-15 10:19:44 +00:00
var timeSinceSwitch = SystemClock . Instance . GetCurrentInstant ( ) - sw . Timestamp ;
2020-12-23 01:19:02 +00:00
return new EmbedBuilder ( )
2021-01-15 10:29:43 +00:00
. Color ( members . FirstOrDefault ( ) ? . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray )
2020-12-23 01:19:02 +00:00
. Field ( new ( $"Current {" fronter ".ToQuantity(members.Count, ShowQuantityAs.None)}" , members . Count > 0 ? string . Join ( ", " , members . Select ( m = > m . NameFor ( ctx ) ) ) : "*(no fronter)*" ) )
. Field ( new ( "Since" , $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)" ) )
2019-06-15 10:19:44 +00:00
. Build ( ) ;
}
2019-06-21 11:49:58 +00:00
2020-12-25 12:58:45 +00:00
public async Task < Embed > CreateMessageInfoEmbed ( FullMessage msg )
2019-06-21 11:49:58 +00:00
{
2020-12-25 12:58:45 +00:00
var channel = await _cache . GetOrFetchChannel ( _rest , msg . Message . Channel ) ;
2020-06-18 15:08:36 +00:00
var ctx = LookupContext . ByNonOwner ;
2021-01-31 16:56:33 +00:00
2021-10-29 20:42:10 +00:00
var serverMsg = await _rest . GetMessageOrNull ( msg . Message . Channel , msg . Message . Mid ) ;
2019-06-21 11:49:58 +00:00
2020-05-07 22:57:17 +00:00
// 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)
2020-12-25 12:58:45 +00:00
// 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 )
{
2021-01-31 16:56:33 +00:00
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 )
2020-12-25 12:58:45 +00:00
// Don't do an extra request if we already have this info from the member lookup
2021-01-31 16:56:33 +00:00
userInfo = member . User ;
memberInfo = member ;
2020-12-25 12:58:45 +00:00
}
2021-07-19 05:02:27 +00:00
if ( userInfo = = null )
userInfo = await _cache . GetOrFetchUser ( _rest , msg . Message . Sender ) ;
2020-05-07 22:57:17 +00:00
// Calculate string displayed under "Sent by"
string userStr ;
2020-12-25 12:58:45 +00:00
if ( memberInfo ! = null & & memberInfo . Nick ! = null )
userStr = $"**Username:** {userInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nick}" ;
2020-05-07 22:57:17 +00:00
else if ( userInfo ! = null ) userStr = userInfo . NameAndMention ( ) ;
else userStr = $"*(deleted user {msg.Message.Sender})*" ;
// Put it all together
2020-12-23 01:19:02 +00:00
var eb = new EmbedBuilder ( )
2021-08-04 05:39:41 +00:00
. Author ( new ( msg . Member . NameFor ( ctx ) , IconUrl : msg . Member . AvatarFor ( ctx ) . TryGetCleanCdnUrl ( ) ) )
2020-12-23 01:19:02 +00:00
. Description ( serverMsg ? . Content ? . NormalizeLineEndSpacing ( ) ? ? "*(message contents deleted or inaccessible)*" )
. Image ( new ( serverMsg ? . Attachments ? . FirstOrDefault ( ) ? . Url ) )
. Field ( new ( "System" ,
msg . System . Name ! = null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`" , true ) )
. Field ( new ( "Member" , $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)" , true ) )
. Field ( new ( "Sent by" , userStr , true ) )
. Timestamp ( DiscordUtils . SnowflakeToInstant ( msg . Message . Mid ) . ToDateTimeOffset ( ) . ToString ( "O" ) ) ;
2019-09-21 13:19:38 +00:00
2020-05-07 22:57:17 +00:00
var roles = memberInfo ? . Roles ? . ToList ( ) ;
2019-10-28 15:50:41 +00:00
if ( roles ! = null & & roles . Count > 0 )
2020-09-12 17:30:03 +00:00
{
2020-12-25 12:58:45 +00:00
// TODO: what if role isn't in cache? figure out a fallback
2021-07-19 05:02:27 +00:00
var rolesString = string . Join ( ", " , roles
. Select ( id = > _cache . GetRole ( id ) )
. OrderByDescending ( role = > role . Position )
. Select ( role = > role . Name ) ) ;
2020-12-23 01:19:02 +00:00
eb . Field ( new ( $"Account roles ({roles.Count})" , rolesString . Truncate ( 1024 ) ) ) ;
2020-09-12 17:30:03 +00:00
}
2021-08-27 15:03:47 +00:00
2019-09-21 13:19:38 +00:00
return eb . Build ( ) ;
2019-06-21 11:49:58 +00:00
}
2019-06-30 21:41:01 +00:00
2021-06-21 15:30:38 +00:00
public Task < Embed > CreateFrontPercentEmbed ( FrontBreakdown breakdown , PKSystem system , PKGroup group , DateTimeZone tz , LookupContext ctx , string embedTitle , bool ignoreNoFronters , bool showFlat )
2019-06-30 21:41:01 +00:00
{
2021-05-07 08:40:57 +00:00
string color = system . Color ;
2021-08-27 15:03:47 +00:00
if ( group ! = null )
2021-05-07 08:40:57 +00:00
{
color = group . Color ;
}
uint embedColor ;
try
{
embedColor = color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray ;
}
catch ( ArgumentException )
{
embedColor = DiscordUtils . Gray ;
}
2021-04-22 00:18:41 +00:00
2020-12-23 01:19:02 +00:00
var eb = new EmbedBuilder ( )
2021-02-09 22:36:43 +00:00
. Title ( embedTitle )
2021-06-21 15:30:38 +00:00
. Color ( embedColor ) ;
string footer = $"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)" ;
Duration period ;
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 ;
eb . Footer ( new ( footer ) ) ;
2019-06-30 21:41:01 +00:00
var maxEntriesToDisplay = 24 ; // max 25 fields allowed in embed - reserve 1 for "others"
2019-07-15 19:51:41 +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 ;)
2019-10-26 17:45:30 +00:00
var pairs = breakdown . MemberSwitchDurations . ToList ( ) ;
2021-04-22 00:18:41 +00:00
if ( breakdown . NoFronterDuration ! = Duration . Zero & & ! ignoreNoFronters )
2019-10-26 17:45:30 +00:00
pairs . Add ( new KeyValuePair < PKMember , Duration > ( null , breakdown . NoFronterDuration ) ) ;
2019-08-09 10:55:15 +00:00
2019-07-15 19:51:41 +00:00
var membersOrdered = pairs . OrderByDescending ( pair = > pair . Value ) . Take ( maxEntriesToDisplay ) . ToList ( ) ;
2019-06-30 21:41:01 +00:00
foreach ( var pair in membersOrdered )
{
2021-06-21 14:52:06 +00:00
var frac = pair . Value / period ;
2021-08-27 15:03:47 +00:00
eb . Field ( new ( pair . Key ? . NameFor ( ctx ) ? ? "*(no fronter)*" , $"{frac * 100:F0}% ({pair.Value.FormatDuration()})" ) ) ;
2019-06-30 21:41:01 +00:00
}
if ( membersOrdered . Count > maxEntriesToDisplay )
{
2020-12-23 01:19:02 +00:00
eb . Field ( new ( "(others)" ,
2020-06-21 14:05:04 +00:00
membersOrdered . Skip ( maxEntriesToDisplay )
. Aggregate ( Duration . Zero , ( prod , next ) = > prod + next . Value )
2020-12-23 01:19:02 +00:00
. FormatDuration ( ) , true ) ) ;
2019-06-30 21:41:01 +00:00
}
2019-07-21 14:43:28 +00:00
return Task . FromResult ( eb . Build ( ) ) ;
2019-06-30 21:41:01 +00:00
}
2019-04-21 13:33:22 +00:00
}
2021-08-27 15:03:47 +00:00
}