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 ;
using Discord ;
2019-09-21 13:19:38 +00:00
using Discord.WebSocket ;
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
namespace PluralKit.Bot {
2019-10-26 17:45:30 +00:00
public class EmbedService
{
private IDataStore _data ;
2019-04-21 13:33:22 +00:00
private IDiscordClient _client ;
2019-10-26 17:45:30 +00:00
public EmbedService ( IDiscordClient client , IDataStore data )
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 ;
2019-04-21 13:33:22 +00:00
}
2020-01-11 15:49:20 +00:00
public async Task < Embed > CreateSystemEmbed ( 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
2019-07-14 22:05:19 +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-01-11 15:49:20 +00:00
var memberCount = await _data . GetSystemMemberCount ( system , false ) ;
2019-04-21 13:33:22 +00:00
var eb = new EmbedBuilder ( )
. WithColor ( Color . Blue )
. WithTitle ( system . Name ? ? null )
. WithThumbnailUrl ( system . AvatarUrl ? ? null )
2019-10-18 11:29:16 +00:00
. WithFooter ( $"System ID: {system.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}" ) ;
2019-10-26 17:45:30 +00:00
var latestSwitch = await _data . GetLatestSwitch ( system ) ;
2020-01-11 15:49:20 +00:00
if ( latestSwitch ! = null & & system . FrontPrivacy . CanAccess ( ctx ) )
2019-07-18 12:05:02 +00:00
{
2019-10-26 17:45:30 +00:00
var switchMembers = ( await _data . GetSwitchMembers ( latestSwitch ) ) . ToList ( ) ;
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
2019-07-18 12:01:28 +00:00
if ( system . Tag ! = null ) eb . AddField ( "Tag" , system . Tag ) ;
2019-12-22 12:53:38 +00:00
eb . AddField ( "Linked accounts" , string . Join ( ", " , users ) . Truncate ( 1000 ) , true ) ;
2020-01-11 15:49:20 +00:00
if ( system . MemberListPrivacy . CanAccess ( ctx ) )
eb . AddField ( $"Members ({memberCount})" , $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)" , 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 ) )
eb . AddField ( "Description" , system . Description . 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
2019-08-08 05:36:09 +00:00
public Embed CreateLoggedMessageEmbed ( PKSystem system , PKMember member , ulong messageId , ulong originalMsgId , IUser sender , string content , IGuildChannel channel ) {
2019-04-22 15:10:18 +00:00
// TODO: pronouns in ?-reacted response using this card
2019-07-15 15:53:01 +00:00
var timestamp = SnowflakeUtils . FromSnowflake ( messageId ) ;
2019-04-22 15:10:18 +00:00
return new EmbedBuilder ( )
2019-07-15 15:53:01 +00:00
. WithAuthor ( $"#{channel.Name}: {member.Name}" , member . AvatarUrl )
. WithDescription ( content )
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}" )
2019-07-15 15:53:01 +00:00
. WithTimestamp ( timestamp )
2019-04-22 15:10:18 +00:00
. Build ( ) ;
}
2019-05-11 22:44:02 +00:00
2020-01-11 15:49:20 +00:00
public async Task < Embed > CreateMemberEmbed ( PKSystem system , PKMember member , IGuild 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
2019-08-13 19:49:43 +00:00
Color color ;
try
{
color = member . Color ? . ToDiscordColor ( ) ? ? Color . Default ;
}
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
color = Color . Default ;
}
2019-05-11 22:44:02 +00:00
2019-10-26 17:45:30 +00:00
var messageCount = await _data . GetMemberMessageCount ( member ) ;
2019-05-11 22:44:02 +00:00
2019-12-26 19:39:47 +00:00
string guildDisplayName = null ;
if ( guild ! = null )
guildDisplayName = ( await _data . GetMemberGuildSettings ( member , guild . Id ) ) . DisplayName ;
2019-10-28 19:15:27 +00:00
var proxyTagsStr = string . Join ( '\n' , member . ProxyTags . Select ( t = > $"`{t.ProxyString}`" ) ) ;
2019-05-11 22:44:02 +00:00
var eb = new EmbedBuilder ( )
// TODO: add URL of website when that's up
. WithAuthor ( name , member . AvatarUrl )
2020-01-11 15:49:20 +00:00
. WithColor ( member . MemberPrivacy . CanAccess ( ctx ) ? color : Color . Default )
2019-10-18 11:29:16 +00:00
. WithFooter ( $"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}" ) ;
2019-05-11 22:44:02 +00:00
2020-01-11 15:49:20 +00:00
if ( member . MemberPrivacy = = PrivacyLevel . Private ) eb . WithDescription ( "*(this member is private)*" ) ;
2019-07-21 14:29:48 +00:00
if ( member . AvatarUrl ! = null ) eb . WithThumbnailUrl ( member . AvatarUrl ) ;
2019-08-09 08:39:03 +00:00
2019-12-26 19:39:47 +00:00
if ( member . DisplayName ! = null ) eb . AddField ( "Display Name" , member . DisplayName . Truncate ( 1024 ) , true ) ;
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 ) ;
if ( member . Pronouns ! = null & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Pronouns" , member . Pronouns . Truncate ( 1024 ) , true ) ;
if ( messageCount > 0 & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Message Count" , messageCount , true ) ;
2019-12-26 19:39:47 +00:00
if ( member . HasProxyTags ) eb . AddField ( "Proxy Tags" , string . Join ( '\n' , proxyTagsStr ) . Truncate ( 1024 ) , true ) ;
2020-01-11 15:49:20 +00:00
if ( member . Color ! = null & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Color" , $"#{member.Color}" , true ) ;
if ( member . Description ! = null & & member . MemberPrivacy . CanAccess ( ctx ) ) eb . AddField ( "Description" , member . Description , false ) ;
2019-05-11 22:44:02 +00:00
return eb . Build ( ) ;
}
2019-06-15 10:19:44 +00:00
2019-06-15 10:43:35 +00:00
public async Task < Embed > CreateFronterEmbed ( PKSwitch sw , DateTimeZone zone )
2019-06-15 10:19:44 +00:00
{
2019-10-26 17:45:30 +00:00
var members = ( await _data . GetSwitchMembers ( sw ) ) . ToList ( ) ;
2019-06-15 10:19:44 +00:00
var timeSinceSwitch = SystemClock . Instance . GetCurrentInstant ( ) - sw . Timestamp ;
return new EmbedBuilder ( )
. WithColor ( members . FirstOrDefault ( ) ? . Color ? . ToDiscordColor ( ) ? ? Color . Blue )
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)*" )
2019-07-14 21:18:51 +00:00
. AddField ( "Since" , $"{Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))} ({Formats.DurationFormat.Format(timeSinceSwitch)} ago)" )
2019-06-15 10:19:44 +00:00
. Build ( ) ;
}
2019-06-15 10:43:35 +00:00
public async Task < Embed > CreateFrontHistoryEmbed ( IEnumerable < PKSwitch > sws , DateTimeZone zone )
{
var outputStr = "" ;
PKSwitch lastSw = null ;
foreach ( var sw in sws )
{
// Fetch member list and format
2019-10-26 17:45:30 +00:00
var members = ( await _data . GetSwitchMembers ( sw ) ) . ToList ( ) ;
2019-06-15 10:43:35 +00:00
var membersStr = members . Any ( ) ? string . Join ( ", " , members . Select ( m = > m . Name ) ) : "no fronter" ;
var switchSince = SystemClock . Instance . GetCurrentInstant ( ) - sw . Timestamp ;
2019-08-09 10:55:15 +00:00
2019-06-15 10:43:35 +00:00
// If this isn't the latest switch, we also show duration
2019-11-02 23:46:38 +00:00
string stringToAdd ;
2019-06-15 10:43:35 +00:00
if ( lastSw ! = null )
{
// Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one
var switchDuration = lastSw . Timestamp - sw . Timestamp ;
2019-11-02 23:46:38 +00:00
stringToAdd = $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))}, {Formats.DurationFormat.Format(switchSince)} ago, for {Formats.DurationFormat.Format(switchDuration)})\n" ;
2019-06-15 10:43:35 +00:00
}
else
{
2019-11-02 23:46:38 +00:00
stringToAdd = $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))}, {Formats.DurationFormat.Format(switchSince)} ago)\n" ;
2019-06-15 10:43:35 +00:00
}
2019-11-02 23:46:38 +00:00
if ( outputStr . Length + stringToAdd . Length > EmbedBuilder . MaxDescriptionLength ) break ;
outputStr + = stringToAdd ;
2019-06-15 10:43:35 +00:00
lastSw = sw ;
}
2019-08-09 10:55:15 +00:00
2019-06-15 10:43:35 +00:00
return new EmbedBuilder ( )
. WithTitle ( "Past switches" )
. WithDescription ( outputStr )
. Build ( ) ;
}
2019-06-21 11:49:58 +00:00
2019-10-26 17:45:30 +00:00
public async Task < Embed > CreateMessageInfoEmbed ( FullMessage msg )
2019-06-21 11:49:58 +00:00
{
2019-09-21 13:19:38 +00:00
var channel = await _client . GetChannelAsync ( msg . Message . Channel ) as ITextChannel ;
var serverMsg = channel ! = null ? await channel . GetMessageAsync ( msg . Message . Mid ) : null ;
2019-06-21 11:49:58 +00:00
var memberStr = $"{msg.Member.Name} (`{msg.Member.Hid}`)" ;
2019-07-15 20:11:08 +00:00
2019-09-21 13:19:38 +00:00
var userStr = $"*(deleted user {msg.Message.Sender})*" ;
ICollection < IRole > roles = null ;
2019-08-09 10:55:15 +00:00
2019-09-21 13:19:38 +00:00
if ( channel ! = null )
{
// Look up the user with the REST client
// this ensures we'll still get the information even if the user's not cached,
// even if this means an extra API request (meh, it'll be fine)
var shard = ( ( DiscordShardedClient ) _client ) . GetShardFor ( channel . Guild ) ;
var guildUser = await shard . Rest . GetGuildUserAsync ( channel . Guild . Id , msg . Message . Sender ) ;
if ( guildUser ! = null )
{
2019-10-31 15:36:33 +00:00
if ( guildUser . RoleIds . Count > 0 )
roles = guildUser . RoleIds
. Select ( roleId = > channel . Guild . GetRole ( roleId ) )
. Where ( role = > role . Name ! = "@everyone" )
. OrderByDescending ( role = > role . Position )
. ToList ( ) ;
2019-09-21 13:19:38 +00:00
2019-09-21 16:34:20 +00:00
userStr = guildUser . Nickname ! = null ? $"**Username:** {guildUser?.NameAndMention()}\n**Nickname:** {guildUser.Nickname}" : guildUser ? . NameAndMention ( ) ;
2019-09-21 13:19:38 +00:00
}
}
var eb = new EmbedBuilder ( )
2019-06-21 11:49:58 +00:00
. WithAuthor ( msg . Member . Name , msg . Member . AvatarUrl )
2019-06-21 12:13:56 +00:00
. WithDescription ( serverMsg ? . Content ? ? "*(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 )
2019-06-21 11:49:58 +00:00
. AddField ( "Member" , memberStr , true )
2019-07-15 20:11:08 +00:00
. AddField ( "Sent by" , userStr , inline : true )
2019-09-21 13:19:38 +00:00
. WithTimestamp ( SnowflakeUtils . FromSnowflake ( msg . Message . Mid ) ) ;
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 ) ) ) ;
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
2019-10-26 17:45:30 +00:00
public Task < Embed > 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 ;
2019-06-30 21:41:01 +00:00
var eb = new EmbedBuilder ( )
. WithColor ( Color . Blue )
2019-10-26 17:45:30 +00:00
. WithFooter ( $"Since {Formats.ZonedDateTimeFormat.Format(breakdown.RangeStart.InZone(tz))} ({Formats.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 ;
2019-07-15 19:51:41 +00:00
eb . AddField ( pair . Key ? . Name ? ? "*(no fronter)*" , $"{frac*100:F0}% ({Formats.DurationFormat.Format(pair.Value)})" ) ;
2019-06-30 21:41:01 +00:00
}
if ( membersOrdered . Count > maxEntriesToDisplay )
{
eb . AddField ( "(others)" ,
Formats . DurationFormat . Format ( membersOrdered . Skip ( maxEntriesToDisplay )
. 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
}
}