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
using DSharpPlus ;
using DSharpPlus.Entities ;
2020-05-07 22:57:17 +00:00
using DSharpPlus.Exceptions ;
2019-09-21 13:19:38 +00:00
2019-07-14 21:49:14 +00:00
using Humanizer ;
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 ;
2019-04-21 13:33:22 +00:00
namespace PluralKit.Bot {
2019-10-26 17:45:30 +00:00
public class EmbedService
{
private IDataStore _data ;
2020-06-13 17:36:43 +00:00
private IDatabase _db ;
2020-02-01 14:00:36 +00:00
private DiscordShardedClient _client ;
2019-04-21 13:33:22 +00:00
2020-06-13 17:36:43 +00:00
public EmbedService ( DiscordShardedClient client , IDataStore data , IDatabase db )
2019-04-21 13:33:22 +00:00
{
2019-06-15 10:43:35 +00:00
_client = client ;
2019-10-26 17:45:30 +00:00
_data = data ;
2020-06-13 14:03:57 +00:00
_db = db ;
2019-04-21 13:33:22 +00:00
}
2020-04-17 21:10:01 +00:00
public async Task < DiscordEmbed > CreateSystemEmbed ( DiscordClient client , PKSystem system , LookupContext ctx ) {
2019-10-26 17:45:30 +00:00
var accounts = await _data . GetSystemAccounts ( system ) ;
2019-04-21 13:33:22 +00:00
// Fetch/render info for all accounts simultaneously
2020-04-17 21:10:01 +00:00
var users = await Task . WhenAll ( accounts . Select ( async uid = > ( await client . GetUserAsync ( uid ) ) ? . NameAndMention ( ) ? ? $"(deleted account {uid})" ) ) ;
2019-04-21 13:33:22 +00:00
2020-06-15 23:15:59 +00:00
var memberCount = await _data . GetSystemMemberCount ( system . Id , false ) ;
2020-04-17 21:10:01 +00:00
var eb = new DiscordEmbedBuilder ( )
2020-04-28 22:25:01 +00:00
. WithColor ( DiscordUtils . Gray )
2019-04-21 13:33:22 +00:00
. WithTitle ( system . Name ? ? null )
2020-05-05 17:09:18 +00:00
. WithThumbnailUrl ( system . AvatarUrl )
2020-02-12 14:16:19 +00:00
. WithFooter ( $"System ID: {system.Hid} | Created on {DateTimeFormats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}" ) ;
2019-10-18 11:29:16 +00:00
2020-06-15 23:15:59 +00:00
var latestSwitch = await _data . 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
{
2020-01-17 23:02:17 +00:00
var switchMembers = await _data . GetSwitchMembers ( latestSwitch ) . ToListAsync ( ) ;
2019-07-18 12:05:02 +00:00
if ( switchMembers . Count > 0 )
2019-08-09 10:55:15 +00:00
eb . AddField ( "Fronter" . ToQuantity ( switchMembers . Count ( ) , ShowQuantityAs . None ) ,
2019-07-18 12:05:02 +00:00
string . Join ( ", " , switchMembers . Select ( m = > m . Name ) ) ) ;
}
2019-04-21 13:33:22 +00:00
2020-05-15 11:32:28 +00:00
if ( system . Tag ! = null ) eb . AddField ( "Tag" , system . Tag . EscapeMarkdown ( ) ) ;
2019-12-22 12:53:38 +00:00
eb . AddField ( "Linked accounts" , string . Join ( ", " , 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 )
eb . AddField ( $"Members ({memberCount})" , $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)" , true ) ;
else
eb . AddField ( $"Members ({memberCount})" , "Add one with `pk;member new`!" , true ) ;
}
2019-07-21 14:29:48 +00:00
2020-01-11 15:49:20 +00:00
if ( system . Description ! = null & & system . DescriptionPrivacy . CanAccess ( ctx ) )
2020-02-20 21:57:37 +00:00
eb . AddField ( "Description" , system . Description . NormalizeLineEndSpacing ( ) . Truncate ( 1024 ) , false ) ;
2019-08-09 10:55:15 +00:00
2019-04-21 13:33:22 +00:00
return eb . Build ( ) ;
}
2019-04-22 15:10:18 +00:00
2020-04-17 21:10:01 +00:00
public DiscordEmbed CreateLoggedMessageEmbed ( PKSystem system , PKMember member , ulong messageId , ulong originalMsgId , DiscordUser sender , string content , DiscordChannel channel ) {
2019-04-22 15:10:18 +00:00
// TODO: pronouns in ?-reacted response using this card
2020-04-17 21:10:01 +00:00
var timestamp = DiscordUtils . SnowflakeToInstant ( messageId ) ;
return new DiscordEmbedBuilder ( )
2020-05-05 17:09:18 +00:00
. WithAuthor ( $"#{channel.Name}: {member.Name}" , iconUrl : DiscordUtils . WorkaroundForUrlBug ( member . AvatarUrl ) )
. WithThumbnailUrl ( member . AvatarUrl )
2020-02-20 21:57:37 +00:00
. WithDescription ( content ? . NormalizeLineEndSpacing ( ) )
2019-08-08 05:36:09 +00:00
. WithFooter ( $"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}" )
2020-04-17 21:10:01 +00:00
. WithTimestamp ( timestamp . ToDateTimeOffset ( ) )
2019-04-22 15:10:18 +00:00
. Build ( ) ;
}
2019-05-11 22:44:02 +00:00
2020-04-17 21:10:01 +00:00
public async Task < DiscordEmbed > CreateMemberEmbed ( PKSystem system , PKMember member , DiscordGuild guild , LookupContext ctx )
2019-05-11 22:44:02 +00:00
{
var name = member . Name ;
if ( system . Name ! = null ) name = $"{member.Name} ({system.Name})" ;
2019-08-09 10:55:15 +00:00
2020-04-17 21:10:01 +00:00
DiscordColor 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
// TODO: fix that in the API
// for now 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-06-12 18:29:50 +00:00
2020-06-13 14:03:57 +00:00
var guildSettings = guild ! = null ? await _db . Execute ( c = > c . QueryOrInsertMemberGuildConfig ( guild . Id , member . Id ) ) : null ;
2020-02-12 16:42:12 +00:00
var guildDisplayName = guildSettings ? . DisplayName ;
var avatar = guildSettings ? . AvatarUrl ? ? member . AvatarUrl ;
2019-12-26 19:39:47 +00:00
2019-10-28 19:15:27 +00:00
var proxyTagsStr = string . Join ( '\n' , member . ProxyTags . Select ( t = > $"`{t.ProxyString}`" ) ) ;
2020-04-17 21:10:01 +00:00
var eb = new DiscordEmbedBuilder ( )
2019-05-11 22:44:02 +00:00
// TODO: add URL of website when that's up
2020-05-05 17:09:18 +00:00
. WithAuthor ( name , iconUrl : DiscordUtils . WorkaroundForUrlBug ( avatar ) )
2020-04-28 22:25:01 +00:00
. WithColor ( member . MemberPrivacy . CanAccess ( ctx ) ? color : DiscordUtils . Gray )
2020-02-12 14:16:19 +00:00
. WithFooter ( $"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {DateTimeFormats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}" ) ;
2019-05-11 22:44:02 +00:00
2020-02-12 16:42:12 +00:00
var description = "" ;
if ( member . MemberPrivacy = = PrivacyLevel . Private ) description + = "*(this member is private)*\n" ;
if ( guildSettings ? . AvatarUrl ! = null )
if ( member . AvatarUrl ! = null )
description + = $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n" ;
else
description + = "*(this member has a server-specific avatar set)*\n" ;
if ( description ! = "" ) eb . WithDescription ( description ) ;
2020-01-11 15:49:20 +00:00
2020-02-12 16:42:12 +00:00
if ( avatar ! = null ) eb . WithThumbnailUrl ( avatar ) ;
2019-08-09 08:39:03 +00:00
2020-02-12 22:18:31 +00:00
if ( ! member . DisplayName . EmptyOrNull ( ) ) eb . AddField ( "Display Name" , member . DisplayName . Truncate ( 1024 ) , true ) ;
2019-12-26 19:39:47 +00:00
if ( guild ! = null & & guildDisplayName ! = null ) eb . AddField ( $"Server Nickname (for {guild.Name})" , guildDisplayName . Truncate ( 1024 ) , true ) ;
2020-01-11 15:49:20 +00:00
if ( member . Birthday ! = null & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Birthdate" , member . BirthdayString , true ) ;
2020-02-12 22:18:31 +00:00
if ( ! member . Pronouns . EmptyOrNull ( ) & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Pronouns" , member . Pronouns . Truncate ( 1024 ) , true ) ;
2020-06-12 18:29:50 +00:00
if ( member . MessageCount > 0 & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Message Count" , member . MessageCount . ToString ( ) , true ) ;
2019-12-26 19:39:47 +00:00
if ( member . HasProxyTags ) eb . AddField ( "Proxy Tags" , string . Join ( '\n' , proxyTagsStr ) . Truncate ( 1024 ) , true ) ;
2020-02-12 22:18:31 +00:00
if ( ! member . Color . EmptyOrNull ( ) & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Color" , $"#{member.Color}" , true ) ;
2020-02-20 21:57:37 +00:00
if ( ! member . Description . EmptyOrNull ( ) & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Description" , member . Description . NormalizeLineEndSpacing ( ) , false ) ;
2019-05-11 22:44:02 +00:00
return eb . Build ( ) ;
}
2019-06-15 10:19:44 +00:00
2020-04-17 21:10:01 +00:00
public async Task < DiscordEmbed > CreateFronterEmbed ( PKSwitch sw , DateTimeZone zone )
2019-06-15 10:19:44 +00:00
{
2020-01-17 23:02:17 +00:00
var members = await _data . GetSwitchMembers ( sw ) . ToListAsync ( ) ;
2019-06-15 10:19:44 +00:00
var timeSinceSwitch = SystemClock . Instance . GetCurrentInstant ( ) - sw . Timestamp ;
2020-04-17 21:10:01 +00:00
return new DiscordEmbedBuilder ( )
2020-04-28 22:25:01 +00:00
. WithColor ( members . FirstOrDefault ( ) ? . Color ? . ToDiscordColor ( ) ? ? DiscordUtils . Gray )
2019-07-14 21:49:14 +00:00
. AddField ( $"Current {" fronter ".ToQuantity(members.Count, ShowQuantityAs.None)}" , members . Count > 0 ? string . Join ( ", " , members . Select ( m = > m . Name ) ) : "*(no fronter)*" )
2020-02-12 14:16:19 +00:00
. AddField ( "Since" , $"{DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))} ({DateTimeFormats.DurationFormat.Format(timeSinceSwitch)} ago)" )
2019-06-15 10:19:44 +00:00
. Build ( ) ;
}
2019-06-21 11:49:58 +00:00
2020-04-17 21:10:01 +00:00
public async Task < DiscordEmbed > CreateMessageInfoEmbed ( DiscordClient client , FullMessage msg )
2019-06-21 11:49:58 +00:00
{
2020-04-17 21:10:01 +00:00
var channel = await client . GetChannelAsync ( msg . Message . Channel ) ;
2019-09-21 13:19:38 +00:00
var serverMsg = channel ! = null ? await channel . GetMessageAsync ( msg . Message . Mid ) : null ;
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)
DiscordMember memberInfo = null ;
DiscordUser userInfo = null ;
if ( channel ! = null ) try { memberInfo = await channel . Guild . GetMemberAsync ( msg . Message . Sender ) ; } catch ( NotFoundException ) { }
if ( memberInfo ! = null ) userInfo = memberInfo ; // Don't do an extra request if we already have this info from the member lookup
else try { userInfo = await client . GetUserAsync ( msg . Message . Sender ) ; } catch ( NotFoundException ) { }
// Calculate string displayed under "Sent by"
string userStr ;
if ( memberInfo ! = null & & memberInfo . Nickname ! = null )
userStr = $"**Username:** {memberInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nickname}" ;
else if ( userInfo ! = null ) userStr = userInfo . NameAndMention ( ) ;
else userStr = $"*(deleted user {msg.Message.Sender})*" ;
// Put it all together
2020-04-17 21:10:01 +00:00
var eb = new DiscordEmbedBuilder ( )
2020-05-05 17:09:18 +00:00
. WithAuthor ( msg . Member . Name , iconUrl : DiscordUtils . WorkaroundForUrlBug ( msg . Member . AvatarUrl ) )
2020-02-20 21:57:37 +00:00
. WithDescription ( serverMsg ? . Content ? . NormalizeLineEndSpacing ( ) ? ? "*(message contents deleted or inaccessible)*" )
2019-10-31 20:14:01 +00:00
. WithImageUrl ( serverMsg ? . Attachments ? . FirstOrDefault ( ) ? . Url )
2019-09-21 13:19:38 +00:00
. AddField ( "System" ,
msg . System . Name ! = null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`" , true )
2020-05-07 22:57:17 +00:00
. AddField ( "Member" , $"{msg.Member.Name} (`{msg.Member.Hid}`)" , true )
2019-07-15 20:11:08 +00:00
. AddField ( "Sent by" , userStr , inline : true )
2020-04-17 21:10:01 +00:00
. WithTimestamp ( DiscordUtils . SnowflakeToInstant ( msg . Message . Mid ) . ToDateTimeOffset ( ) ) ;
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 )
eb . AddField ( $"Account roles ({roles.Count})" , string . Join ( ", " , roles . Select ( role = > role . Name ) ) ) ;
2020-05-07 22:57:17 +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
2020-04-17 21:10:01 +00:00
public Task < DiscordEmbed > CreateFrontPercentEmbed ( FrontBreakdown breakdown , DateTimeZone tz )
2019-06-30 21:41:01 +00:00
{
2019-10-26 17:45:30 +00:00
var actualPeriod = breakdown . RangeEnd - breakdown . RangeStart ;
2020-04-17 21:10:01 +00:00
var eb = new DiscordEmbedBuilder ( )
2020-04-28 22:25:01 +00:00
. WithColor ( DiscordUtils . Gray )
2020-02-12 14:16:19 +00:00
. WithFooter ( $"Since {DateTimeFormats.ZonedDateTimeFormat.Format(breakdown.RangeStart.InZone(tz))} ({DateTimeFormats.DurationFormat.Format(actualPeriod)} ago)" ) ;
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 ( ) ;
if ( breakdown . NoFronterDuration ! = Duration . Zero )
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 )
{
2019-07-16 19:18:46 +00:00
var frac = pair . Value / actualPeriod ;
2020-02-12 14:16:19 +00:00
eb . AddField ( pair . Key ? . Name ? ? "*(no fronter)*" , $"{frac*100:F0}% ({DateTimeFormats.DurationFormat.Format(pair.Value)})" ) ;
2019-06-30 21:41:01 +00:00
}
if ( membersOrdered . Count > maxEntriesToDisplay )
{
eb . AddField ( "(others)" ,
2020-02-12 14:16:19 +00:00
DateTimeFormats . DurationFormat . Format ( membersOrdered . Skip ( maxEntriesToDisplay )
2019-06-30 21:41:01 +00:00
. Aggregate ( Duration . Zero , ( prod , next ) = > prod + next . Value ) ) , true ) ;
}
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
}
}