2020-10-04 08:53:07 +00:00
using System ;
2020-07-18 11:53:02 +00:00
using System.Collections.Generic ;
2020-07-06 17:50:39 +00:00
using System.Linq ;
2021-08-23 20:53:58 +00:00
using System.Net.Http ;
2020-07-06 17:50:39 +00:00
using System.Text ;
2021-03-28 10:02:41 +00:00
using System.Text.RegularExpressions ;
2020-07-06 17:50:39 +00:00
using System.Threading.Tasks ;
2020-06-29 21:51:12 +00:00
2020-07-18 11:30:54 +00:00
using Dapper ;
2020-08-16 10:10:54 +00:00
using Humanizer ;
2021-02-09 22:36:43 +00:00
using NodaTime ;
2020-12-24 13:52:44 +00:00
using Myriad.Builders ;
2020-06-29 21:51:12 +00:00
using PluralKit.Core ;
namespace PluralKit.Bot
{
public class Groups
{
private readonly IDatabase _db ;
2020-08-29 11:46:27 +00:00
private readonly ModelRepository _repo ;
2020-11-22 16:57:54 +00:00
private readonly EmbedService _embeds ;
2021-08-23 20:53:58 +00:00
private readonly HttpClient _client ;
2020-06-29 21:51:12 +00:00
2021-08-23 20:53:58 +00:00
public Groups ( IDatabase db , ModelRepository repo , EmbedService embeds , HttpClient client )
2020-06-29 21:51:12 +00:00
{
_db = db ;
2020-08-29 11:46:27 +00:00
_repo = repo ;
2020-11-22 16:57:54 +00:00
_embeds = embeds ;
2021-08-23 20:53:58 +00:00
_client = client ;
2020-06-29 21:51:12 +00:00
}
public async Task CreateGroup ( Context ctx )
{
ctx . CheckSystem ( ) ;
2021-08-27 15:03:47 +00:00
2020-08-16 10:10:54 +00:00
// Check group name length
2020-06-29 21:51:12 +00:00
var groupName = ctx . RemainderOrNull ( ) ? ? throw new PKSyntaxError ( "You must pass a group name." ) ;
if ( groupName . Length > Limits . MaxGroupNameLength )
2020-08-08 12:56:34 +00:00
throw new PKError ( $"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters)." ) ;
2021-08-27 15:03:47 +00:00
2020-06-29 21:51:12 +00:00
await using var conn = await _db . Obtain ( ) ;
2021-08-27 15:03:47 +00:00
2020-08-16 10:10:54 +00:00
// Check group cap
2021-09-22 01:42:41 +00:00
var existingGroupCount = await _repo . GetSystemGroupCount ( conn , ctx . System . Id ) ;
2020-10-09 10:18:29 +00:00
var groupLimit = ctx . System . GroupLimitOverride ? ? Limits . MaxGroupCount ;
if ( existingGroupCount > = groupLimit )
throw new PKError ( $"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones." ) ;
2020-08-16 10:10:54 +00:00
// Warn if there's already a group by this name
2020-08-29 11:46:27 +00:00
var existingGroup = await _repo . GetGroupByName ( conn , ctx . System . Id , groupName ) ;
2021-08-27 15:03:47 +00:00
if ( existingGroup ! = null )
{
2020-08-16 10:10:54 +00:00
var msg = $"{Emojis.Warn} You already have a group in your system with the name \" { existingGroup . Name } \ " (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?" ;
2021-07-02 10:40:40 +00:00
if ( ! await ctx . PromptYesNo ( msg , "Create" ) )
2020-08-16 10:10:54 +00:00
throw new PKError ( "Group creation cancelled." ) ;
}
2021-08-27 15:03:47 +00:00
2020-08-29 11:46:27 +00:00
var newGroup = await _repo . CreateGroup ( conn , ctx . System . Id , groupName ) ;
2021-08-27 15:03:47 +00:00
2020-12-25 11:56:46 +00:00
var eb = new EmbedBuilder ( )
. Description ( $"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:" )
. Field ( new ( "View the group card" , $"> pk;group **{newGroup.Reference()}**" ) )
. Field ( new ( "Add members to the group" , $"> pk;group **{newGroup.Reference()}** add **MemberName**\n> pk;group **{newGroup.Reference()}** add **Member1** **Member2** **Member3** (and so on...)" ) )
. Field ( new ( "Set the description" , $"> pk;group **{newGroup.Reference()}** description **This is my new group, and here is the description!**" ) )
. Field ( new ( "Set the group icon" , $"> pk;group **{newGroup.Reference()}** icon\n*(with an image attached)*" ) ) ;
2020-07-18 14:49:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Group created!" , eb . Build ( ) ) ;
2021-09-13 06:46:40 +00:00
if ( existingGroupCount > = Limits . WarnThreshold ( groupLimit ) )
await ctx . Reply ( $"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} members). Please review your group list for unused or duplicate groups." ) ;
2020-06-29 21:51:12 +00:00
}
2020-07-06 17:50:39 +00:00
public async Task RenameGroup ( Context ctx , PKGroup target )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-27 15:03:47 +00:00
2020-08-16 10:10:54 +00:00
// Check group name length
2020-07-06 17:50:39 +00:00
var newName = ctx . RemainderOrNull ( ) ? ? throw new PKSyntaxError ( "You must pass a new group name." ) ;
if ( newName . Length > Limits . MaxGroupNameLength )
throw new PKError ( $"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters)." ) ;
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
await using var conn = await _db . Obtain ( ) ;
2021-08-27 15:03:47 +00:00
2020-08-16 10:10:54 +00:00
// Warn if there's already a group by this name
2020-08-29 11:46:27 +00:00
var existingGroup = await _repo . GetGroupByName ( conn , ctx . System . Id , newName ) ;
2021-08-27 15:03:47 +00:00
if ( existingGroup ! = null & & existingGroup . Id ! = target . Id )
{
2021-08-26 15:54:28 +00:00
var msg = $"{Emojis.Warn} You already have a group in your system with the name \" { existingGroup . Name } \ " (with ID `{existingGroup.Hid}`). Do you want to rename this group to that name too?" ;
2021-07-02 10:40:40 +00:00
if ( ! await ctx . PromptYesNo ( msg , "Rename" ) )
2021-08-26 15:54:28 +00:00
throw new PKError ( "Group rename cancelled." ) ;
2020-08-16 10:10:54 +00:00
}
2021-08-27 15:03:47 +00:00
await _repo . UpdateGroup ( conn , target . Id , new GroupPatch { Name = newName } ) ;
2020-07-06 17:50:39 +00:00
2020-08-20 19:43:17 +00:00
await ctx . Reply ( $"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}**." ) ;
}
public async Task GroupDisplayName ( Context ctx , PKGroup target )
{
2021-08-27 23:20:14 +00:00
var noDisplayNameSetMessage = "This group does not have a display name set." ;
if ( ctx . System ? . Id = = target . System )
noDisplayNameSetMessage + = $" To set one, type `pk;group {target.Reference()} displayname <display name>`." ;
2021-08-27 15:03:47 +00:00
2021-08-27 23:20:14 +00:00
// No perms check, display name isn't covered by member privacy
2020-08-20 19:43:17 +00:00
2021-09-13 05:22:40 +00:00
if ( ctx . MatchRaw ( ) )
{
if ( target . DisplayName = = null )
await ctx . Reply ( noDisplayNameSetMessage ) ;
else
await ctx . Reply ( $"```\n{target.DisplayName}\n```" ) ;
return ;
}
2021-09-13 06:33:34 +00:00
if ( ! ctx . HasNext ( false ) )
2020-08-20 19:43:17 +00:00
{
2021-08-27 23:20:14 +00:00
if ( target . DisplayName = = null )
await ctx . Reply ( noDisplayNameSetMessage ) ;
2021-04-13 09:25:05 +00:00
else
{
var eb = new EmbedBuilder ( )
. Field ( new ( "Name" , target . Name ) )
2021-08-27 23:20:14 +00:00
. Field ( new ( "Display Name" , target . DisplayName ) ) ;
2021-08-27 15:03:47 +00:00
2021-04-13 09:25:05 +00:00
if ( ctx . System ? . Id = = target . System )
2021-08-27 23:20:14 +00:00
eb . Description ( $"To change display name, type `pk;group {target.Reference()} displayname <display name>`."
2021-09-06 22:56:06 +00:00
+ $"To clear it, type `pk;group {target.Reference()} displayname -clear`."
+ $"To print the raw display name, type `pk;group {target.Reference()} displayname -raw`." ) ;
2021-08-27 15:03:47 +00:00
2021-04-13 09:25:05 +00:00
await ctx . Reply ( embed : eb . Build ( ) ) ;
}
2021-08-27 23:20:14 +00:00
return ;
2020-08-20 19:43:17 +00:00
}
2021-08-27 15:03:47 +00:00
2021-08-27 23:20:14 +00:00
ctx . CheckOwnGroup ( target ) ;
if ( await ctx . MatchClear ( "this group's display name" ) )
{
var patch = new GroupPatch { DisplayName = Partial < string > . Null ( ) } ;
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
await ctx . Reply ( $"{Emojis.Success} Group display name cleared." ) ;
}
else
{
2021-09-26 21:30:30 +00:00
var newDisplayName = ctx . RemainderOrNull ( skipFlags : false ) . NormalizeLineEndSpacing ( ) ;
2021-08-27 15:03:47 +00:00
var patch = new GroupPatch { DisplayName = Partial < string > . Present ( newDisplayName ) } ;
2020-08-29 11:46:27 +00:00
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
2020-08-20 19:43:17 +00:00
await ctx . Reply ( $"{Emojis.Success} Group display name changed." ) ;
}
2020-07-06 17:50:39 +00:00
}
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
public async Task GroupDescription ( Context ctx , PKGroup target )
{
2021-08-27 23:20:14 +00:00
if ( ! target . DescriptionPrivacy . CanAccess ( ctx . LookupContextFor ( target . System ) ) )
throw Errors . LookupNotAllowed ;
2020-07-06 17:50:39 +00:00
2021-08-27 23:20:14 +00:00
var noDescriptionSetMessage = "This group does not have a description set." ;
if ( ctx . System ? . Id = = target . System )
noDescriptionSetMessage + = $" To set one, type `pk;group {target.Reference()} description <description>`." ;
2021-05-24 19:18:57 +00:00
2021-09-13 05:22:40 +00:00
if ( ctx . MatchRaw ( ) )
2021-08-27 23:20:14 +00:00
{
2020-07-06 17:50:39 +00:00
if ( target . Description = = null )
2021-08-27 23:20:14 +00:00
await ctx . Reply ( noDescriptionSetMessage ) ;
2020-07-06 17:50:39 +00:00
else
2021-09-13 05:22:40 +00:00
await ctx . Reply ( $"```\n{target.Description}\n```" ) ;
2021-08-27 23:20:14 +00:00
return ;
2020-07-06 17:50:39 +00:00
}
2021-09-13 06:33:34 +00:00
if ( ! ctx . HasNext ( false ) )
2020-07-06 17:50:39 +00:00
{
2021-08-27 23:20:14 +00:00
if ( target . Description = = null )
await ctx . Reply ( noDescriptionSetMessage ) ;
else
2021-09-13 05:22:40 +00:00
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( "Group description" )
. Description ( target . Description )
. Field ( new ( "\u200B" , $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`."
+ ( ctx . System ? . Id = = target . System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : "" ) ) )
. Build ( ) ) ;
2021-08-27 23:20:14 +00:00
return ;
}
ctx . CheckOwnGroup ( target ) ;
2020-07-06 17:50:39 +00:00
2021-08-27 23:20:14 +00:00
if ( await ctx . MatchClear ( "this group's description" ) )
{
var patch = new GroupPatch { Description = Partial < string > . Null ( ) } ;
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
await ctx . Reply ( $"{Emojis.Success} Group description cleared." ) ;
}
else
{
2021-09-26 21:30:30 +00:00
var description = ctx . RemainderOrNull ( skipFlags : false ) . NormalizeLineEndSpacing ( ) ;
2020-07-06 17:50:39 +00:00
if ( description . IsLongerThan ( Limits . MaxDescriptionLength ) )
2021-09-13 05:13:57 +00:00
throw Errors . StringTooLongError ( "Description" , description . Length , Limits . MaxDescriptionLength ) ;
2021-08-27 15:03:47 +00:00
var patch = new GroupPatch { Description = Partial < string > . Present ( description ) } ;
2020-08-29 11:46:27 +00:00
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
await ctx . Reply ( $"{Emojis.Success} Group description changed." ) ;
}
}
2020-08-08 13:09:42 +00:00
public async Task GroupIcon ( Context ctx , PKGroup target )
{
async Task ClearIcon ( )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-27 15:03:47 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch { Icon = null } ) ) ;
2020-08-08 13:09:42 +00:00
await ctx . Reply ( $"{Emojis.Success} Group icon cleared." ) ;
}
async Task SetIcon ( ParsedImage img )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-27 15:03:47 +00:00
2021-08-23 20:53:58 +00:00
await AvatarUtils . VerifyAvatarOrThrow ( _client , img . Url ) ;
2020-08-08 13:09:42 +00:00
2021-08-27 15:03:47 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch { Icon = img . Url } ) ) ;
2020-08-08 13:09:42 +00:00
var msg = img . Source switch
{
AvatarSource . User = > $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set." ,
AvatarSource . Url = > $"{Emojis.Success} Group icon changed to the image at the given URL." ,
AvatarSource . Attachment = > $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working." ,
_ = > throw new ArgumentOutOfRangeException ( )
} ;
2021-08-27 15:03:47 +00:00
2020-08-08 13:09:42 +00:00
// The attachment's already right there, no need to preview it.
var hasEmbed = img . Source ! = AvatarSource . Attachment ;
2021-08-27 15:03:47 +00:00
await ( hasEmbed
? ctx . Reply ( msg , embed : new EmbedBuilder ( ) . Image ( new ( img . Url ) ) . Build ( ) )
2020-08-08 13:09:42 +00:00
: ctx . Reply ( msg ) ) ;
}
async Task ShowIcon ( )
{
2021-05-24 19:05:27 +00:00
if ( ! target . IconPrivacy . CanAccess ( ctx . LookupContextFor ( target . System ) ) )
throw Errors . LookupNotAllowed ;
2020-08-08 13:09:42 +00:00
if ( ( target . Icon ? . Trim ( ) ? ? "" ) . Length > 0 )
{
2020-12-25 11:56:46 +00:00
var eb = new EmbedBuilder ( )
. Title ( "Group icon" )
2021-08-01 16:51:54 +00:00
. Image ( new ( target . Icon . TryGetCleanCdnUrl ( ) ) ) ;
2021-08-27 15:03:47 +00:00
2020-08-08 13:09:42 +00:00
if ( target . System = = ctx . System ? . Id )
{
2020-12-25 11:56:46 +00:00
eb . Description ( $"To clear, use `pk;group {target.Reference()} icon -clear`." ) ;
2020-08-08 13:09:42 +00:00
}
await ctx . Reply ( embed : eb . Build ( ) ) ;
}
else
throw new PKSyntaxError ( "This group does not have an icon set. Set one by attaching an image to this command, or by passing an image URL or @mention." ) ;
}
2020-10-04 08:53:07 +00:00
if ( await ctx . MatchClear ( "this group's icon" ) )
2020-08-08 13:09:42 +00:00
await ClearIcon ( ) ;
2021-08-27 15:03:47 +00:00
else if ( await ctx . MatchImage ( ) is { } img )
2020-08-08 13:09:42 +00:00
await SetIcon ( img ) ;
else
await ShowIcon ( ) ;
}
2021-08-02 17:46:12 +00:00
public async Task GroupBannerImage ( Context ctx , PKGroup target )
{
async Task ClearBannerImage ( )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-27 15:03:47 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch { BannerImage = null } ) ) ;
2021-08-02 17:46:12 +00:00
await ctx . Reply ( $"{Emojis.Success} Group banner image cleared." ) ;
}
async Task SetBannerImage ( ParsedImage img )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-23 20:53:58 +00:00
await AvatarUtils . VerifyAvatarOrThrow ( _client , img . Url , isFullSizeImage : true ) ;
2021-08-02 17:46:12 +00:00
2021-08-27 15:03:47 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch { BannerImage = img . Url } ) ) ;
2021-08-02 17:46:12 +00:00
var msg = img . Source switch
{
AvatarSource . Url = > $"{Emojis.Success} Group banner image changed to the image at the given URL." ,
AvatarSource . Attachment = > $"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working." ,
AvatarSource . User = > throw new PKError ( "Cannot set a banner image to an user's avatar." ) ,
_ = > throw new ArgumentOutOfRangeException ( ) ,
} ;
// The attachment's already right there, no need to preview it.
var hasEmbed = img . Source ! = AvatarSource . Attachment ;
2021-08-27 15:03:47 +00:00
await ( hasEmbed
? ctx . Reply ( msg , embed : new EmbedBuilder ( ) . Image ( new ( img . Url ) ) . Build ( ) )
2021-08-02 17:46:12 +00:00
: ctx . Reply ( msg ) ) ;
}
async Task ShowBannerImage ( )
{
if ( ! target . DescriptionPrivacy . CanAccess ( ctx . LookupContextFor ( target . System ) ) )
throw Errors . LookupNotAllowed ;
if ( ( target . BannerImage ? . Trim ( ) ? ? "" ) . Length > 0 )
{
var eb = new EmbedBuilder ( )
. Title ( "Group banner image" )
. Image ( new ( target . BannerImage ) ) ;
if ( target . System = = ctx . System ? . Id )
{
eb . Description ( $"To clear, use `pk;group {target.Reference()} banner clear`." ) ;
}
await ctx . Reply ( embed : eb . Build ( ) ) ;
}
else
throw new PKSyntaxError ( "This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention." ) ;
}
if ( await ctx . MatchClear ( "this group's banner image" ) )
await ClearBannerImage ( ) ;
2021-08-27 15:03:47 +00:00
else if ( await ctx . MatchImage ( ) is { } img )
2021-08-02 17:46:12 +00:00
await SetBannerImage ( img ) ;
else
await ShowBannerImage ( ) ;
}
2021-03-28 10:02:41 +00:00
public async Task GroupColor ( Context ctx , PKGroup target )
{
var color = ctx . RemainderOrNull ( ) ;
if ( await ctx . MatchClear ( ) )
{
ctx . CheckOwnGroup ( target ) ;
2021-08-27 15:03:47 +00:00
var patch = new GroupPatch { Color = Partial < string > . Null ( ) } ;
2021-03-28 10:02:41 +00:00
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
2021-08-27 15:03:47 +00:00
2021-03-28 10:02:41 +00:00
await ctx . Reply ( $"{Emojis.Success} Group color cleared." ) ;
}
else if ( ! ctx . HasNext ( ) )
{
if ( target . Color = = null )
if ( ctx . System ? . Id = = target . System )
await ctx . Reply (
$"This group does not have a color set. To set one, type `pk;group {target.Reference()} color <color>`." ) ;
else
await ctx . Reply ( "This group does not have a color set." ) ;
else
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( "Group color" )
. Color ( target . Color . ToDiscordColor ( ) )
. Thumbnail ( new ( $"https://fakeimg.pl/256x256/{target.Color}/?text=%20" ) )
. Description ( $"This group's color is **#{target.Color}**."
+ ( ctx . System ? . Id = = target . System ? $" To clear it, type `pk;group {target.Reference()} color -clear`." : "" ) )
. Build ( ) ) ;
}
else
{
ctx . CheckOwnGroup ( target ) ;
if ( color . StartsWith ( "#" ) ) color = color . Substring ( 1 ) ;
if ( ! Regex . IsMatch ( color , "^[0-9a-fA-F]{6}$" ) ) throw Errors . InvalidColorError ( color ) ;
2021-08-27 15:03:47 +00:00
var patch = new GroupPatch { Color = Partial < string > . Present ( color . ToLowerInvariant ( ) ) } ;
2021-03-28 10:02:41 +00:00
await _db . Execute ( conn = > _repo . UpdateGroup ( conn , target . Id , patch ) ) ;
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( $"{Emojis.Success} Group color changed." )
. Color ( color . ToDiscordColor ( ) )
. Thumbnail ( new ( $"https://fakeimg.pl/256x256/{color}/?text=%20" ) )
. Build ( ) ) ;
}
}
2020-08-08 13:09:42 +00:00
2020-07-06 17:50:39 +00:00
public async Task ListSystemGroups ( Context ctx , PKSystem system )
{
if ( system = = null )
{
ctx . CheckSystem ( ) ;
system = ctx . System ;
}
2021-08-27 15:03:47 +00:00
2020-08-20 19:43:17 +00:00
ctx . CheckSystemPrivacy ( system , system . GroupListPrivacy ) ;
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
// TODO: integrate with the normal "search" system
await using var conn = await _db . Obtain ( ) ;
2020-07-18 11:53:02 +00:00
var pctx = LookupContext . ByNonOwner ;
2020-08-16 10:10:54 +00:00
if ( ctx . MatchFlag ( "a" , "all" ) )
{
if ( system . Id = = ctx . System . Id )
pctx = LookupContext . ByOwner ;
else
throw new PKError ( "You do not have permission to access this information." ) ;
}
2021-08-27 15:03:47 +00:00
2020-08-21 15:08:49 +00:00
var groups = ( await conn . QueryGroupList ( system . Id ) )
2020-08-16 10:10:54 +00:00
. Where ( g = > g . Visibility . CanAccess ( pctx ) )
2020-08-25 16:43:52 +00:00
. OrderBy ( g = > g . Name , StringComparer . InvariantCultureIgnoreCase )
2020-08-16 10:10:54 +00:00
. ToList ( ) ;
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
if ( groups . Count = = 0 )
{
if ( system . Id = = ctx . System ? . Id )
2020-08-20 19:43:17 +00:00
await ctx . Reply ( "This system has no groups. To create one, use the command `pk;group new <name>`." ) ;
2020-07-06 17:50:39 +00:00
else
2020-08-20 19:43:17 +00:00
await ctx . Reply ( "This system has no groups." ) ;
2021-08-27 15:03:47 +00:00
2020-07-06 17:50:39 +00:00
return ;
}
var title = system . Name ! = null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`" ;
2021-03-28 17:22:31 +00:00
await ctx . Paginate ( groups . ToAsyncEnumerable ( ) , groups . Count , 25 , title , ctx . System . Color , Renderer ) ;
2021-08-27 15:03:47 +00:00
2020-12-24 13:52:44 +00:00
Task Renderer ( EmbedBuilder eb , IEnumerable < ListedGroup > page )
2020-07-06 17:50:39 +00:00
{
2020-08-20 19:43:17 +00:00
eb . WithSimpleLineContent ( page . Select ( g = >
{
if ( g . DisplayName ! = null )
2020-11-15 14:42:27 +00:00
return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({g.DisplayName.EscapeMarkdown()}) ({" member ".ToQuantity(g.MemberCount)})" ;
2020-08-20 19:43:17 +00:00
else
2020-11-15 14:42:27 +00:00
return $"[`{g.Hid}`] **{g.Name.EscapeMarkdown()}** ({" member ".ToQuantity(g.MemberCount)})" ;
2020-08-20 19:43:17 +00:00
} ) ) ;
2020-12-24 13:52:44 +00:00
eb . Footer ( new ( $"{groups.Count} total." ) ) ;
2020-07-06 17:50:39 +00:00
return Task . CompletedTask ;
}
}
2020-06-29 21:51:12 +00:00
public async Task ShowGroupCard ( Context ctx , PKGroup target )
{
await using var conn = await _db . Obtain ( ) ;
var system = await GetGroupSystem ( ctx , target , conn ) ;
2020-11-22 16:57:54 +00:00
await ctx . Reply ( embed : await _embeds . CreateGroupEmbed ( ctx , system , target ) ) ;
2020-06-29 21:51:12 +00:00
}
2020-07-07 13:28:53 +00:00
public async Task AddRemoveMembers ( Context ctx , PKGroup target , AddRemoveOperation op )
{
ctx . CheckOwnGroup ( target ) ;
2020-11-23 04:11:34 +00:00
var members = ( await ctx . ParseMemberList ( ctx . System . Id ) )
. Select ( m = > m . Id )
2020-11-25 22:18:56 +00:00
. Distinct ( )
2020-11-23 04:11:34 +00:00
. ToList ( ) ;
2021-08-27 15:03:47 +00:00
2020-07-07 13:28:53 +00:00
await using var conn = await _db . Obtain ( ) ;
2021-08-27 15:03:47 +00:00
2020-08-16 10:10:54 +00:00
var existingMembersInGroup = ( await conn . QueryMemberList ( target . System ,
2021-08-27 15:03:47 +00:00
new DatabaseViewsExt . MemberListQueryOptions { GroupFilter = target . Id } ) )
2020-08-20 19:43:17 +00:00
. Select ( m = > m . Id . Value )
2020-11-25 22:18:56 +00:00
. Distinct ( )
2020-08-16 10:10:54 +00:00
. ToHashSet ( ) ;
2021-08-27 15:03:47 +00:00
2020-11-23 04:11:34 +00:00
List < MemberId > toAction ;
2020-07-07 13:28:53 +00:00
if ( op = = AddRemoveOperation . Add )
{
2020-11-23 04:11:34 +00:00
toAction = members
. Where ( m = > ! existingMembersInGroup . Contains ( m . Value ) )
2020-08-16 10:10:54 +00:00
. ToList ( ) ;
2020-11-23 04:11:34 +00:00
await _repo . AddMembersToGroup ( conn , target . Id , toAction ) ;
2020-07-07 13:28:53 +00:00
}
else if ( op = = AddRemoveOperation . Remove )
{
2020-11-23 04:11:34 +00:00
toAction = members
. Where ( m = > existingMembersInGroup . Contains ( m . Value ) )
2020-08-16 10:10:54 +00:00
. ToList ( ) ;
2020-11-23 04:11:34 +00:00
await _repo . RemoveMembersFromGroup ( conn , target . Id , toAction ) ;
2020-07-07 13:28:53 +00:00
}
2020-11-23 04:11:34 +00:00
else return ; // otherwise toAction "may be undefined"
2021-09-06 23:26:47 +00:00
await ctx . Reply ( GroupAddRemoveResponseService . GenerateResponse ( op , members . Count , 1 , toAction . Count , members . Count - toAction . Count ) ) ;
2020-07-07 13:28:53 +00:00
}
2020-07-07 17:34:23 +00:00
public async Task ListGroupMembers ( Context ctx , PKGroup target )
{
await using var conn = await _db . Obtain ( ) ;
2021-08-27 15:03:47 +00:00
2020-07-07 17:34:23 +00:00
var targetSystem = await GetGroupSystem ( ctx , target , conn ) ;
2020-08-20 19:43:17 +00:00
ctx . CheckSystemPrivacy ( targetSystem , target . ListPrivacy ) ;
2021-08-27 15:03:47 +00:00
2020-07-07 17:34:23 +00:00
var opts = ctx . ParseMemberListOptions ( ctx . LookupContextFor ( target . System ) ) ;
opts . GroupFilter = target . Id ;
2020-08-20 19:43:17 +00:00
var title = new StringBuilder ( $"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in " ) ;
2021-08-27 15:03:47 +00:00
if ( targetSystem . Name ! = null )
2020-07-07 17:34:23 +00:00
title . Append ( $"{targetSystem.Name} (`{targetSystem.Hid}`)" ) ;
else
title . Append ( $"`{targetSystem.Hid}`" ) ;
2021-08-27 15:03:47 +00:00
if ( opts . Search ! = null )
2020-07-07 17:34:23 +00:00
title . Append ( $" matching **{opts.Search}**" ) ;
2021-08-27 15:03:47 +00:00
2021-03-28 17:22:31 +00:00
await ctx . RenderMemberList ( ctx . LookupContextFor ( target . System ) , _db , target . System , title . ToString ( ) , target . Color , opts ) ;
2020-07-07 17:34:23 +00:00
}
2020-07-07 13:28:53 +00:00
public enum AddRemoveOperation
{
Add ,
Remove
}
2020-11-14 17:05:30 +00:00
2020-07-18 11:53:02 +00:00
public async Task GroupPrivacy ( Context ctx , PKGroup target , PrivacyLevel ? newValueFromCommand )
{
ctx . CheckSystem ( ) . CheckOwnGroup ( target ) ;
// Display privacy settings
if ( ! ctx . HasNext ( ) & & newValueFromCommand = = null )
{
2020-12-25 11:56:46 +00:00
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( $"Current privacy settings for {target.Name}" )
2021-08-27 15:03:47 +00:00
. Field ( new ( "Description" , target . DescriptionPrivacy . Explanation ( ) ) )
2020-12-25 11:56:46 +00:00
. Field ( new ( "Icon" , target . IconPrivacy . Explanation ( ) ) )
. Field ( new ( "Member list" , target . ListPrivacy . Explanation ( ) ) )
. Field ( new ( "Visibility" , target . Visibility . Explanation ( ) ) )
. Description ( $"To edit privacy settings, use the command:\n> pk;group **{target.Reference()}** privacy **<subject>** **<level>**\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`." )
2021-08-27 15:03:47 +00:00
. Build ( ) ) ;
2020-07-18 11:53:02 +00:00
return ;
}
async Task SetAll ( PrivacyLevel level )
{
2020-08-29 11:46:27 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch ( ) . WithAllPrivacy ( level ) ) ) ;
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
if ( level = = PrivacyLevel . Private )
2020-08-08 12:56:34 +00:00
await ctx . Reply ( $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card." ) ;
2021-08-27 15:03:47 +00:00
else
2020-08-08 12:56:34 +00:00
await ctx . Reply ( $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card." ) ;
2020-07-18 11:53:02 +00:00
}
async Task SetLevel ( GroupPrivacySubject subject , PrivacyLevel level )
{
2020-08-29 11:46:27 +00:00
await _db . Execute ( c = > _repo . UpdateGroup ( c , target . Id , new GroupPatch ( ) . WithPrivacy ( subject , level ) ) ) ;
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
var subjectName = subject switch
{
GroupPrivacySubject . Description = > "description privacy" ,
GroupPrivacySubject . Icon = > "icon privacy" ,
2020-08-20 19:43:17 +00:00
GroupPrivacySubject . List = > "member list" ,
2020-07-18 11:53:02 +00:00
GroupPrivacySubject . Visibility = > "visibility" ,
_ = > throw new ArgumentOutOfRangeException ( $"Unknown privacy subject {subject}" )
} ;
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
var explanation = ( subject , level ) switch
{
( GroupPrivacySubject . Description , PrivacyLevel . Private ) = > "This group's description is now hidden from other systems." ,
( GroupPrivacySubject . Icon , PrivacyLevel . Private ) = > "This group's icon is now hidden from other systems." ,
( GroupPrivacySubject . Visibility , PrivacyLevel . Private ) = > "This group is now hidden from group lists and member cards." ,
2020-08-20 19:43:17 +00:00
( GroupPrivacySubject . List , PrivacyLevel . Private ) = > "This group's member list is now hidden from other systems." ,
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
( GroupPrivacySubject . Description , PrivacyLevel . Public ) = > "This group's description is no longer hidden from other systems." ,
( GroupPrivacySubject . Icon , PrivacyLevel . Public ) = > "This group's icon is no longer hidden from other systems." ,
( GroupPrivacySubject . Visibility , PrivacyLevel . Public ) = > "This group is no longer hidden from group lists and member cards." ,
2020-08-20 19:43:17 +00:00
( GroupPrivacySubject . List , PrivacyLevel . Public ) = > "This group's member list is no longer hidden from other systems." ,
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
_ = > throw new InvalidOperationException ( $"Invalid subject/level tuple ({subject}, {level})" )
} ;
2021-08-27 15:03:47 +00:00
2020-07-18 11:53:02 +00:00
await ctx . Reply ( $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}" ) ;
}
if ( ctx . Match ( "all" ) | | newValueFromCommand ! = null )
await SetAll ( newValueFromCommand ? ? ctx . PopPrivacyLevel ( ) ) ;
else
await SetLevel ( ctx . PopGroupPrivacySubject ( ) , ctx . PopPrivacyLevel ( ) ) ;
}
2020-07-07 17:34:44 +00:00
2020-08-08 12:56:34 +00:00
public async Task DeleteGroup ( Context ctx , PKGroup target )
{
ctx . CheckOwnGroup ( target ) ;
await ctx . Reply ( $"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.Hid}`).\n**Note: this action is permanent.**" ) ;
if ( ! await ctx . ConfirmWithReply ( target . Hid ) )
throw new PKError ( $"Group deletion cancelled. Note that you must reply with your group ID (`{target.Hid}`) *verbatim*." ) ;
2020-08-29 11:46:27 +00:00
await _db . Execute ( conn = > _repo . DeleteGroup ( conn , target . Id ) ) ;
2021-08-27 15:03:47 +00:00
2020-08-08 12:56:34 +00:00
await ctx . Reply ( $"{Emojis.Success} Group deleted." ) ;
}
2021-08-27 15:03:47 +00:00
public async Task GroupFrontPercent ( Context ctx , PKGroup target )
2021-02-09 22:36:43 +00:00
{
await using var conn = await _db . Obtain ( ) ;
2021-08-27 15:03:47 +00:00
2021-02-09 22:36:43 +00:00
var targetSystem = await GetGroupSystem ( ctx , target , conn ) ;
ctx . CheckSystemPrivacy ( targetSystem , targetSystem . FrontHistoryPrivacy ) ;
2021-06-16 12:56:52 +00:00
var totalSwitches = await _db . Execute ( conn = > _repo . GetSwitchCount ( conn , targetSystem . Id ) ) ;
if ( totalSwitches = = 0 ) throw Errors . NoRegisteredSwitches ;
2021-02-09 22:36:43 +00:00
string durationStr = ctx . RemainderOrNull ( ) ? ? "30d" ;
2021-08-27 15:03:47 +00:00
2021-02-09 22:36:43 +00:00
var now = SystemClock . Instance . GetCurrentInstant ( ) ;
var rangeStart = DateUtils . ParseDateTime ( durationStr , true , targetSystem . Zone ) ;
if ( rangeStart = = null ) throw Errors . InvalidDateTime ( durationStr ) ;
if ( rangeStart . Value . ToInstant ( ) > now ) throw Errors . FrontPercentTimeInFuture ;
var title = new StringBuilder ( $"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in " ) ;
2021-08-27 15:03:47 +00:00
if ( targetSystem . Name ! = null )
2021-02-09 22:36:43 +00:00
title . Append ( $"{targetSystem.Name} (`{targetSystem.Hid}`)" ) ;
else
title . Append ( $"`{targetSystem.Hid}`" ) ;
2021-04-22 00:18:41 +00:00
var ignoreNoFronters = ctx . MatchFlag ( "fo" , "fronters-only" ) ;
2021-06-21 15:30:38 +00:00
var showFlat = ctx . MatchFlag ( "flat" ) ;
2021-02-09 22:36:43 +00:00
var frontpercent = await _db . Execute ( c = > _repo . GetFrontBreakdown ( c , targetSystem . Id , target . Id , rangeStart . Value . ToInstant ( ) , now ) ) ;
2021-06-21 15:30:38 +00:00
await ctx . Reply ( embed : await _embeds . CreateFrontPercentEmbed ( frontpercent , targetSystem , target , targetSystem . Zone , ctx . LookupContextFor ( targetSystem ) , title . ToString ( ) , ignoreNoFronters , showFlat ) ) ;
2021-02-09 22:36:43 +00:00
}
2020-08-29 11:46:27 +00:00
private async Task < PKSystem > GetGroupSystem ( Context ctx , PKGroup target , IPKConnection conn )
2020-06-29 21:51:12 +00:00
{
var system = ctx . System ;
if ( system ? . Id = = target . System )
return system ;
2020-08-29 11:46:27 +00:00
return await _repo . GetSystem ( conn , target . System ) ! ;
2020-06-29 21:51:12 +00:00
}
}
}