2020-07-06 19:50:39 +02:00
using System.Text ;
2021-03-28 12:02:41 +02:00
using System.Text.RegularExpressions ;
2020-07-18 13:30:54 +02:00
2021-11-26 21:10:56 -05:00
using Myriad.Builders ;
using Myriad.Types ;
2021-11-25 15:33:02 -05:00
using Newtonsoft.Json.Linq ;
2020-06-29 23:51:12 +02:00
using PluralKit.Core ;
2021-11-26 21:10:56 -05:00
namespace PluralKit.Bot ;
public class Groups
2020-06-29 23:51:12 +02:00
{
2021-11-26 21:10:56 -05:00
public enum AddRemoveOperation
2020-06-29 23:51:12 +02:00
{
2021-11-26 21:10:56 -05:00
Add ,
Remove
}
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
private readonly HttpClient _client ;
private readonly DispatchService _dispatch ;
private readonly EmbedService _embeds ;
2024-02-11 03:53:46 +01:00
private readonly AvatarHostingService _avatarHosting ;
2021-08-27 11:03:47 -04:00
2022-01-22 03:05:01 -05:00
public Groups ( EmbedService embeds , HttpClient client ,
2024-02-11 03:53:46 +01:00
DispatchService dispatch , AvatarHostingService avatarHosting )
2021-11-26 21:10:56 -05:00
{
_embeds = embeds ;
_client = client ;
_dispatch = dispatch ;
2024-02-11 03:53:46 +01:00
_avatarHosting = avatarHosting ;
2021-11-26 21:10:56 -05:00
}
2020-08-16 12:10:54 +02:00
2025-10-08 03:26:40 +00:00
public async Task CreateGroup ( Context ctx , string groupName , bool confirmYes = false )
2021-11-26 21:10:56 -05:00
{
ctx . CheckSystem ( ) ;
// Check group name length
if ( groupName . Length > Limits . MaxGroupNameLength )
throw new PKError ( $"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters)." ) ;
// Check group cap
2022-01-22 03:05:01 -05:00
var existingGroupCount = await ctx . Repository . GetSystemGroupCount ( ctx . System . Id ) ;
2021-11-29 21:35:21 -05:00
var groupLimit = ctx . Config . GroupLimitOverride ? ? Limits . MaxGroupCount ;
2021-11-26 21:10:56 -05:00
if ( existingGroupCount > = groupLimit )
throw new PKError (
2024-01-27 00:59:30 +00:00
$"System has reached the maximum number of groups ({groupLimit}). If you need to add more groups, you can either delete existing groups, or ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>" ) ;
2021-11-26 21:10:56 -05:00
// Warn if there's already a group by this name
2022-01-22 03:05:01 -05:00
var existingGroup = await ctx . Repository . GetGroupByName ( ctx . System . Id , groupName ) ;
2021-11-26 21:10:56 -05:00
if ( existingGroup ! = null )
{
var msg =
2024-04-28 15:46:06 +12:00
$"{Emojis.Warn} You already have a group in your system with the name \" { existingGroup . Name } \ " (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to create another group with the same name?" ;
2025-10-08 03:26:40 +00:00
if ( ! await ctx . PromptYesNo ( msg , "Create" , flagValue : confirmYes ) )
2021-11-26 21:10:56 -05:00
throw new PKError ( "Group creation cancelled." ) ;
}
2021-08-27 11:03:47 -04:00
2022-01-22 03:05:01 -05:00
// todo: this is supposed to be a transaction, but it's not used in any useful way
// consider removing it?
using var conn = await ctx . Database . Obtain ( ) ;
var newGroup = await ctx . Repository . CreateGroup ( ctx . System . Id , groupName ) ;
2021-08-27 11:03:47 -04:00
2021-12-01 11:48:49 -05:00
var dispatchData = new JObject ( ) ;
dispatchData . Add ( "name" , groupName ) ;
if ( ctx . Config . GroupDefaultPrivate )
{
var patch = new GroupPatch ( ) . WithAllPrivacy ( PrivacyLevel . Private ) ;
2022-01-22 03:05:01 -05:00
await ctx . Repository . UpdateGroup ( newGroup . Id , patch , conn ) ;
2021-12-01 11:48:49 -05:00
dispatchData . Merge ( patch . ToJson ( ) ) ;
}
_ = _dispatch . Dispatch ( newGroup . Id , new UpdateDispatchData
{
Event = DispatchEvent . CREATE_GROUP ,
EventData = dispatchData
} ) ;
2021-11-25 15:33:02 -05:00
2022-02-05 09:26:14 -05:00
var reference = newGroup . Reference ( ctx ) ;
2021-11-26 21:10:56 -05:00
var eb = new EmbedBuilder ( )
. Description (
2024-04-28 15:46:06 +12:00
$"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.DisplayHid(ctx.Config)}`**.\nBelow are a couple of useful commands:" )
2024-12-31 08:09:18 -07:00
. Field ( new Embed . Field ( "View the group card" , $"> {ctx.DefaultPrefix}group **{reference}**" ) )
2021-11-26 21:10:56 -05:00
. Field ( new Embed . Field ( "Add members to the group" ,
2024-12-31 08:09:18 -07:00
$"> {ctx.DefaultPrefix}group **{reference}** add **MemberName**\n> {ctx.DefaultPrefix}group **{reference}** add **Member1** **Member2** **Member3** (and so on...)" ) )
2021-11-26 21:10:56 -05:00
. Field ( new Embed . Field ( "Set the description" ,
2024-12-31 08:09:18 -07:00
$"> {ctx.DefaultPrefix}group **{reference}** description **This is my new group, and here is the description!**" ) )
2021-11-26 21:10:56 -05:00
. Field ( new Embed . Field ( "Set the group icon" ,
2024-12-31 08:09:18 -07:00
$"> {ctx.DefaultPrefix}group **{reference}** icon\n*(with an image attached)*" ) ) ;
2025-01-05 04:50:58 -07:00
var replyStr = $"{Emojis.Success} Group created!" ;
2021-11-26 21:10:56 -05:00
if ( existingGroupCount > = Limits . WarnThreshold ( groupLimit ) )
2025-01-05 04:50:58 -07:00
replyStr + = $"\n{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Once you reach this limit, you will be unable to create new groups until existing groups are deleted, or you can ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>" ;
await ctx . Reply ( replyStr , eb . Build ( ) ) ;
2021-11-26 21:10:56 -05:00
}
2021-09-13 02:46:40 -04:00
2025-10-08 03:26:40 +00:00
public async Task RenameGroup ( Context ctx , PKGroup target , string? newName , bool confirmYes = false )
2021-11-26 21:10:56 -05:00
{
ctx . CheckOwnGroup ( target ) ;
// Check group name length
if ( newName . Length > Limits . MaxGroupNameLength )
throw new PKError (
$"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters)." ) ;
2020-06-29 23:51:12 +02:00
2021-11-26 21:10:56 -05:00
// Warn if there's already a group by this name
2022-01-22 03:05:01 -05:00
var existingGroup = await ctx . Repository . GetGroupByName ( ctx . System . Id , newName ) ;
2021-11-26 21:10:56 -05:00
if ( existingGroup ! = null & & existingGroup . Id ! = target . Id )
2020-07-06 19:50:39 +02:00
{
2021-11-26 21:10:56 -05:00
var msg =
2024-04-28 15:46:06 +12:00
$"{Emojis.Warn} You already have a group in your system with the name \" { existingGroup . Name } \ " (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to rename this group to that name too?" ;
2025-10-08 03:26:40 +00:00
if ( ! await ctx . PromptYesNo ( msg , "Rename" , flagValue : confirmYes ) )
2021-11-26 21:10:56 -05:00
throw new PKError ( "Group rename cancelled." ) ;
}
2021-08-27 11:03:47 -04:00
2022-01-22 03:05:01 -05:00
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { Name = newName } ) ;
2021-08-27 11:03:47 -04:00
2022-08-27 11:25:44 +02:00
await ctx . Reply ( $"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}** (using {newName.Length}/{Limits.MaxGroupNameLength} characters)." ) ;
2021-11-26 21:10:56 -05:00
}
2020-08-16 12:10:54 +02:00
2025-10-01 00:51:45 +00:00
public async Task ShowGroupDisplayName ( Context ctx , PKGroup target , ReplyFormat format )
2021-11-26 21:10:56 -05:00
{
2025-01-04 05:40:41 -07:00
var noDisplayNameSetMessage = "This group does not have a display name set" +
( ctx . System ? . Id = = target . System
? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} displayname <display name>`."
: " or name is private." ) ;
2020-07-06 19:50:39 +02:00
2025-01-04 05:40:41 -07:00
// Whether displayname is shown or not should depend on if group name privacy is set.
// If name privacy is on then displayname should look like name.
2020-08-20 21:43:17 +02:00
2025-01-04 05:40:41 -07:00
// if we're doing a raw or plaintext query check for null
if ( format ! = ReplyFormat . Standard )
if ( target . DisplayName = = null | | ! target . NamePrivacy . CanAccess ( ctx . DirectLookupContextFor ( target . System ) ) )
2024-10-03 02:23:33 -06:00
{
2021-11-26 21:10:56 -05:00
await ctx . Reply ( noDisplayNameSetMessage ) ;
2024-10-03 02:23:33 -06:00
return ;
}
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( $"```\n{target.DisplayName}\n```" ) ;
return ;
}
if ( format = = ReplyFormat . Plaintext )
{
var eb = new EmbedBuilder ( )
2024-11-07 19:01:47 -07:00
. Description ( $"Showing displayname for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
2024-10-03 02:23:33 -06:00
await ctx . Reply ( target . DisplayName , embed : eb . Build ( ) ) ;
2021-11-26 21:10:56 -05:00
return ;
}
2021-08-27 19:20:14 -04:00
2025-10-01 00:51:45 +00:00
var showDisplayName = target . NamePrivacy . CanAccess ( ctx . LookupContextFor ( target . System ) ) & & target . DisplayName ! = null ;
2025-01-04 05:40:41 -07:00
2025-10-01 00:51:45 +00:00
var eb2 = new EmbedBuilder ( )
. Title ( "Group names" )
. Field ( new Embed . Field ( "Name" , target . NameFor ( ctx ) ) )
. Field ( new Embed . Field ( "Display Name" , showDisplayName ? target . DisplayName : "*(no displayname set or name is private)*" ) ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
var reference = target . Reference ( ctx ) ;
2022-02-05 09:26:14 -05:00
2025-10-01 00:51:45 +00:00
if ( ctx . System ? . Id = = target . System )
eb2 . Description (
$"To change display name, type `{ctx.DefaultPrefix}group {reference} displayname <display name>`.\n"
+ $"To clear it, type `{ctx.DefaultPrefix}group {reference} displayname -clear`.\n"
+ $"To print the raw display name, type `{ctx.DefaultPrefix}group {reference} displayname -raw`." ) ;
2022-08-27 11:25:44 +02:00
2025-10-01 00:51:45 +00:00
if ( ctx . System ? . Id = = target . System & & showDisplayName )
eb2 . Footer ( new Embed . EmbedFooter ( $"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters." ) ) ;
2021-11-26 21:10:56 -05:00
2025-10-01 00:51:45 +00:00
await ctx . Reply ( embed : eb2 . Build ( ) ) ;
}
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
public async Task ClearGroupDisplayName ( Context ctx , PKGroup target )
{
2021-11-26 21:10:56 -05:00
ctx . CheckOwnGroup ( target ) ;
2025-10-01 00:51:45 +00:00
var patch = new GroupPatch { DisplayName = Partial < string > . Null ( ) } ;
await ctx . Repository . UpdateGroup ( target . Id , patch ) ;
2021-08-27 19:20:14 -04:00
2025-10-01 00:51:45 +00:00
var replyStr = $"{Emojis.Success} Group display name cleared." ;
if ( target . NamePrivacy = = PrivacyLevel . Private )
replyStr + = $"\n{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**." ;
await ctx . Reply ( replyStr ) ;
2021-11-26 21:10:56 -05:00
}
2020-07-06 19:50:39 +02:00
2025-10-01 00:51:45 +00:00
public async Task ChangeGroupDisplayName ( Context ctx , PKGroup target , string newDisplayName )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
ctx . CheckOwnGroup ( target ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
if ( newDisplayName . Length > Limits . MaxGroupNameLength )
throw new PKError ( $"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)." ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
var patch = new GroupPatch { DisplayName = Partial < string > . Present ( newDisplayName ) } ;
await ctx . Repository . UpdateGroup ( target . Id , patch ) ;
await ctx . Reply ( $"{Emojis.Success} Group display name changed (using {newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)." ) ;
}
2024-10-03 02:23:33 -06:00
2025-10-01 00:51:45 +00:00
public async Task ShowGroupDescription ( Context ctx , PKGroup target , ReplyFormat format )
{
var noDescriptionSetMessage = "This group does not have a description set" +
( ctx . System ? . Id = = target . System
? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description <description>`."
: "." ) ;
// if we're doing a raw or plaintext query check for null
if ( format ! = ReplyFormat . Standard )
2021-11-26 21:10:56 -05:00
if ( target . Description = = null )
2024-10-03 02:23:33 -06:00
{
2021-11-26 21:10:56 -05:00
await ctx . Reply ( noDescriptionSetMessage ) ;
2024-10-03 02:23:33 -06:00
return ;
}
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( $"```\n{target.Description}\n```" ) ;
return ;
}
if ( format = = ReplyFormat . Plaintext )
{
var eb = new EmbedBuilder ( )
2024-11-07 19:01:47 -07:00
. Description ( $"Showing description for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
2024-10-03 02:23:33 -06:00
await ctx . Reply ( target . Description , embed : eb . Build ( ) ) ;
2021-11-26 21:10:56 -05:00
return ;
2020-07-06 19:50:39 +02:00
}
2025-10-01 00:51:45 +00:00
if ( target . Description = = null )
2020-08-08 15:09:42 +02:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noDescriptionSetMessage ) ;
2021-11-26 21:10:56 -05:00
return ;
}
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
var eb2 = new EmbedBuilder ( )
. Title ( "Group description" )
. Description ( target . Description ) ;
2020-08-08 15:09:42 +02:00
2025-10-01 00:51:45 +00:00
var reference = target . Reference ( ctx ) ;
if ( ctx . System ? . Id = = target . System )
eb2 . Field ( new Embed . Field ( "\u200B" ,
$"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`."
+ $" To clear it, type `{ctx.DefaultPrefix}group {reference} description -clear`."
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." ) ) ;
2021-11-26 21:10:56 -05:00
else
2025-10-01 00:51:45 +00:00
eb2 . Field ( new Embed . Field ( "\u200B" ,
$"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`."
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." ) ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
await ctx . Reply ( embed : eb2 . Build ( ) ) ;
}
2020-08-08 15:09:42 +02:00
2025-10-01 00:51:45 +00:00
public async Task ClearGroupDescription ( Context ctx , PKGroup target )
{
ctx . CheckOwnGroup ( target ) ;
var patch = new GroupPatch { Description = Partial < string > . Null ( ) } ;
await ctx . Repository . UpdateGroup ( target . Id , patch ) ;
await ctx . Reply ( $"{Emojis.Success} Group description cleared." ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
public async Task ChangeGroupDescription ( Context ctx , PKGroup target , string newDescription )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
ctx . CheckOwnGroup ( target ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
if ( newDescription . IsLongerThan ( Limits . MaxDescriptionLength ) )
throw Errors . StringTooLongError ( "Description" , newDescription . Length , Limits . MaxDescriptionLength ) ;
2020-08-08 15:09:42 +02:00
2025-10-01 00:51:45 +00:00
var patch = new GroupPatch { Description = Partial < string > . Present ( newDescription ) } ;
await ctx . Repository . UpdateGroup ( target . Id , patch ) ;
2020-08-08 15:09:42 +02:00
2025-10-01 00:51:45 +00:00
await ctx . Reply ( $"{Emojis.Success} Group description changed (using {newDescription.Length}/{Limits.MaxDescriptionLength} characters)." ) ;
}
public async Task ShowGroupIcon ( Context ctx , PKGroup target , ReplyFormat format )
{
var noIconSetMessage = "This group does not have an avatar set" +
( ctx . System ? . Id = = target . System
? ". Set one by attaching an image to this command, or by passing an image URL or @mention."
: "." ) ;
2021-11-26 21:10:56 -05:00
2025-10-01 00:51:45 +00:00
ctx . CheckSystemPrivacy ( target . System , target . IconPrivacy ) ;
2021-11-26 21:10:56 -05:00
2025-10-01 00:51:45 +00:00
// if we're doing a raw or plaintext query check for null
if ( format ! = ReplyFormat . Standard )
if ( ( target . Icon ? . Trim ( ) ? ? "" ) . Length = = 0 )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noIconSetMessage ) ;
return ;
}
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( $"`{target.Icon.TryGetCleanCdnUrl()}`" ) ;
return ;
}
if ( format = = ReplyFormat . Plaintext )
{
var ebP = new EmbedBuilder ( )
. Description ( $"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
await ctx . Reply ( text : $"<{target.Icon.TryGetCleanCdnUrl()}>" , embed : ebP . Build ( ) ) ;
return ;
2020-08-08 15:09:42 +02:00
}
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
if ( ( target . Icon ? . Trim ( ) ? ? "" ) . Length = = 0 )
2021-08-02 13:46:12 -04:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noIconSetMessage ) ;
return ;
2021-11-26 21:10:56 -05:00
}
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
var ebS = new EmbedBuilder ( )
. Title ( "Group icon" )
. Image ( new Embed . EmbedImage ( target . Icon . TryGetCleanCdnUrl ( ) ) ) ;
if ( target . System = = ctx . System ? . Id )
ebS . Description ( $"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} icon -clear`." ) ;
await ctx . Reply ( embed : ebS . Build ( ) ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-02 13:46:12 -04:00
2025-10-08 03:26:40 +00:00
public async Task ClearGroupIcon ( Context ctx , PKGroup target , bool confirmYes )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
ctx . CheckOwnGroup ( target ) ;
2025-10-08 03:26:40 +00:00
await ctx . ConfirmClear ( "this group's icon" , confirmYes ) ;
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { Icon = null } ) ;
await ctx . Reply ( $"{Emojis.Success} Group icon cleared." ) ;
}
public async Task ChangeGroupIcon ( Context ctx , PKGroup target , ParsedImage img )
{
ctx . CheckOwnGroup ( target ) ;
img = await _avatarHosting . TryRehostImage ( img , AvatarHostingService . RehostedImageType . Avatar , ctx . Author . Id , ctx . System ) ;
await _avatarHosting . VerifyAvatarOrThrow ( img . Url ) ;
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { Icon = img . CleanUrl ? ? img . Url } ) ;
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
var msg = img . Source switch
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
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 . HostedCdn = > $"{Emojis.Success} Group icon changed to attached image." ,
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 ( )
} ;
// The attachment's already right there, no need to preview it.
var hasEmbed = img . Source ! = AvatarSource . Attachment & & img . Source ! = AvatarSource . HostedCdn ;
await ( hasEmbed
? ctx . Reply ( msg , new EmbedBuilder ( ) . Image ( new Embed . EmbedImage ( img . Url ) ) . Build ( ) )
: ctx . Reply ( msg ) ) ;
}
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
public async Task ShowGroupBanner ( Context ctx , PKGroup target , ReplyFormat format )
{
var noBannerSetMessage = "This group does not have a banner image set" +
( ctx . System ? . Id = = target . System
? ". Set one by attaching an image to this command, or by passing an image URL or @mention."
: "." ) ;
2021-08-02 13:46:12 -04:00
2025-10-01 00:51:45 +00:00
ctx . CheckSystemPrivacy ( target . System , target . BannerPrivacy ) ;
2021-11-26 21:10:56 -05:00
2025-10-01 00:51:45 +00:00
// if we're doing a raw or plaintext query check for null
if ( format ! = ReplyFormat . Standard )
if ( ( target . BannerImage ? . Trim ( ) ? ? "" ) . Length = = 0 )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noBannerSetMessage ) ;
return ;
}
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( $"`{target.BannerImage.TryGetCleanCdnUrl()}`" ) ;
return ;
}
if ( format = = ReplyFormat . Plaintext )
{
var ebP = new EmbedBuilder ( )
. Description ( $"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
await ctx . Reply ( text : $"<{target.BannerImage.TryGetCleanCdnUrl()}>" , embed : ebP . Build ( ) ) ;
return ;
2021-08-02 13:46:12 -04:00
}
2025-10-01 00:51:45 +00:00
if ( ( target . BannerImage ? . Trim ( ) ? ? "" ) . Length = = 0 )
2021-03-28 12:02:41 +02:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noBannerSetMessage ) ;
return ;
2021-11-26 21:10:56 -05:00
}
2021-03-28 12:02:41 +02:00
2025-10-01 00:51:45 +00:00
var ebS = new EmbedBuilder ( )
. Title ( "Group banner image" )
. Image ( new Embed . EmbedImage ( target . BannerImage . TryGetCleanCdnUrl ( ) ) ) ;
if ( target . System = = ctx . System ? . Id )
ebS . Description ( $"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} banner clear`." ) ;
await ctx . Reply ( embed : ebS . Build ( ) ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-27 11:03:47 -04:00
2025-10-08 03:26:40 +00:00
public async Task ClearGroupBanner ( Context ctx , PKGroup target , bool confirmYes )
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
ctx . CheckOwnGroup ( target ) ;
2025-10-08 03:26:40 +00:00
await ctx . ConfirmClear ( "this group's banner image" , confirmYes ) ;
2021-03-28 12:02:41 +02:00
2025-10-01 00:51:45 +00:00
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { BannerImage = null } ) ;
await ctx . Reply ( $"{Emojis.Success} Group banner image cleared." ) ;
}
public async Task ChangeGroupBanner ( Context ctx , PKGroup target , ParsedImage img )
{
ctx . CheckOwnGroup ( target ) ;
img = await _avatarHosting . TryRehostImage ( img , AvatarHostingService . RehostedImageType . Banner , ctx . Author . Id , ctx . System ) ;
await _avatarHosting . VerifyAvatarOrThrow ( img . Url , true ) ;
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { BannerImage = img . CleanUrl ? ? img . Url } ) ;
var msg = img . Source switch
2021-11-26 21:10:56 -05:00
{
2025-10-01 00:51:45 +00:00
AvatarSource . Url = > $"{Emojis.Success} Group banner image changed to the image at the given URL." ,
AvatarSource . HostedCdn = > $"{Emojis.Success} Group banner image changed to attached image." ,
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 & & img . Source ! = AvatarSource . HostedCdn ;
await ( hasEmbed
? ctx . Reply ( msg , new EmbedBuilder ( ) . Image ( new Embed . EmbedImage ( img . Url ) ) . Build ( ) )
: ctx . Reply ( msg ) ) ;
}
public async Task ShowGroupColor ( Context ctx , PKGroup target , ReplyFormat format )
{
var noColorSetMessage = "This group does not have a color set" +
( ctx . System ? . Id = = target . System
? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color <color>`."
: "." ) ;
// if we're doing a raw or plaintext query check for null
if ( format ! = ReplyFormat . Standard )
2021-11-26 21:10:56 -05:00
if ( target . Color = = null )
2025-10-01 00:51:45 +00:00
{
await ctx . Reply ( noColorSetMessage ) ;
return ;
}
if ( format = = ReplyFormat . Raw )
{
await ctx . Reply ( "```\n#" + target . Color + "\n```" ) ;
2022-06-13 14:35:18 -04:00
return ;
}
2025-10-01 00:51:45 +00:00
if ( format = = ReplyFormat . Plaintext )
2022-06-13 14:35:18 -04:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( target . Color ) ;
return ;
2021-03-28 12:02:41 +02:00
}
2025-10-01 00:51:45 +00:00
if ( target . Color = = null )
2020-07-06 19:50:39 +02:00
{
2025-10-01 00:51:45 +00:00
await ctx . Reply ( noColorSetMessage ) ;
return ;
}
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
var eb = new EmbedBuilder ( )
. Title ( "Group color" )
. Color ( target . Color . ToDiscordColor ( ) )
. Thumbnail ( new Embed . EmbedThumbnail ( $"attachment://color.gif" ) )
. Description ( $"This group's color is **#{target.Color}**." ) ;
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
if ( ctx . System ? . Id = = target . System )
eb . Description ( eb . Build ( ) . Description + $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`." ) ;
2020-07-06 19:50:39 +02:00
2025-10-01 00:51:45 +00:00
await ctx . Reply ( embed : eb . Build ( ) , files : [ MiscUtils . GenerateColorPreview ( target . Color ) ] ) ;
}
public async Task ClearGroupColor ( Context ctx , PKGroup target )
{
ctx . CheckOwnGroup ( target ) ;
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch { Color = Partial < string > . Null ( ) } ) ;
await ctx . Reply ( $"{Emojis.Success} Group color cleared." ) ;
}
public async Task ChangeGroupColor ( Context ctx , PKGroup target , string color )
{
ctx . CheckOwnGroup ( target ) ;
if ( color . StartsWith ( "#" ) ) color = color . Substring ( 1 ) ;
if ( ! Regex . IsMatch ( color , "^[0-9a-fA-F]{6}$" ) ) throw Errors . InvalidColorError ( color ) ;
var patch = new GroupPatch { Color = Partial < string > . Present ( color . ToLowerInvariant ( ) ) } ;
await ctx . Repository . UpdateGroup ( target . Id , patch ) ;
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( $"{Emojis.Success} Group color changed." )
. Color ( color . ToDiscordColor ( ) )
. Thumbnail ( new Embed . EmbedThumbnail ( $"attachment://color.gif" ) )
. Build ( ) ,
files : [ MiscUtils . GenerateColorPreview ( color ) ] ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-27 11:03:47 -04:00
2025-10-08 03:26:40 +00:00
public async Task ListSystemGroups ( Context ctx , PKSystem system , string? query , IHasListOptions flags , bool all )
2021-11-26 21:10:56 -05:00
{
if ( system = = null )
{
ctx . CheckSystem ( ) ;
system = ctx . System ;
}
2021-08-27 11:03:47 -04:00
2021-12-07 01:32:29 -05:00
ctx . CheckSystemPrivacy ( system . Id , system . GroupListPrivacy ) ;
2020-07-06 19:50:39 +02:00
2022-01-14 22:30:02 -05:00
// explanation of privacy lookup here:
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
// - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list
2025-09-30 18:45:35 +00:00
var opts = flags . GetListOptions ( ctx , system . Id ) ;
opts . Search = query ;
2022-01-14 22:30:02 -05:00
await ctx . RenderGroupList (
ctx . LookupContextFor ( system . Id ) ,
system . Id ,
2024-04-28 15:46:06 +12:00
GetEmbedTitle ( ctx , system , opts ) ,
2022-01-14 22:30:02 -05:00
system . Color ,
2025-10-08 03:26:40 +00:00
opts ,
all
2022-01-14 22:30:02 -05:00
) ;
}
2021-11-26 21:10:56 -05:00
2024-04-28 15:46:06 +12:00
private string GetEmbedTitle ( Context ctx , PKSystem target , ListOptions opts )
2022-01-14 22:30:02 -05:00
{
var title = new StringBuilder ( "Groups of " ) ;
2021-11-26 21:10:56 -05:00
2024-10-03 00:59:31 -06:00
if ( target . NameFor ( ctx ) ! = null )
title . Append ( $"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)" ) ;
2022-01-14 22:30:02 -05:00
else
2024-04-28 15:46:06 +12:00
title . Append ( $"`{target.DisplayHid(ctx.Config)}`" ) ;
2020-06-29 23:51:12 +02:00
2022-01-14 22:30:02 -05:00
if ( opts . Search ! = null )
title . Append ( $" matching **{opts.Search}**" ) ;
2021-11-26 21:10:56 -05:00
2022-01-14 22:30:02 -05:00
return title . ToString ( ) ;
2021-11-26 21:10:56 -05:00
}
2020-07-07 15:28:53 +02:00
2025-10-08 03:26:40 +00:00
public async Task ShowGroupCard ( Context ctx , PKGroup target , bool showEmbed , bool all )
2021-11-26 21:10:56 -05:00
{
var system = await GetGroupSystem ( ctx , target ) ;
2025-09-26 18:47:54 +00:00
if ( showEmbed )
2025-09-07 10:16:50 +12:00
{
2025-10-08 03:26:40 +00:00
await ctx . Reply ( text : EmbedService . LEGACY_EMBED_WARNING , embed : await _embeds . CreateGroupEmbed ( ctx , system , target , all ) ) ;
2025-09-07 10:16:50 +12:00
return ;
}
2025-10-08 03:26:40 +00:00
await ctx . Reply ( components : await _embeds . CreateGroupMessageComponents ( ctx , system , target , all ) ) ;
2021-11-26 21:10:56 -05:00
}
2021-08-27 11:03:47 -04:00
2025-10-01 00:51:45 +00:00
public async Task ShowGroupPrivacy ( Context ctx , PKGroup target )
2021-11-26 21:10:56 -05:00
{
ctx . CheckSystem ( ) . CheckOwnGroup ( target ) ;
2020-07-07 19:34:23 +02:00
2025-10-01 00:51:45 +00:00
await ctx . Reply ( embed : new EmbedBuilder ( )
. Title ( $"Current privacy settings for {target.Name}" )
. Field ( new Embed . Field ( "Name" , target . NamePrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Description" , target . DescriptionPrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Banner" , target . BannerPrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Icon" , target . IconPrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Member list" , target . ListPrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Metadata (creation date)" , target . MetadataPrivacy . Explanation ( ) ) )
. Field ( new Embed . Field ( "Visibility" , target . Visibility . Explanation ( ) ) )
. Description (
$"To edit privacy settings, use the command:\n> {ctx.DefaultPrefix}group **{target.Reference(ctx)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`." )
. Build ( ) ) ;
}
2020-11-14 12:05:30 -05:00
2025-10-01 00:51:45 +00:00
public async Task SetAllGroupPrivacy ( Context ctx , PKGroup target , PrivacyLevel level )
{
ctx . CheckOwnGroup ( target ) ;
2020-07-18 13:53:02 +02:00
2025-10-01 00:51:45 +00:00
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch ( ) . WithAllPrivacy ( level ) ) ;
2020-07-07 19:34:44 +02:00
2025-10-01 00:51:45 +00:00
if ( level = = PrivacyLevel . Private )
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-11-26 21:10:56 -05:00
else
2025-10-01 00:51:45 +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." ) ;
}
public async Task SetGroupPrivacy ( Context ctx , PKGroup target , GroupPrivacySubject subject , PrivacyLevel level )
{
ctx . CheckOwnGroup ( target ) ;
await ctx . Repository . UpdateGroup ( target . Id , new GroupPatch ( ) . WithPrivacy ( subject , level ) ) ;
var subjectName = subject switch
{
GroupPrivacySubject . Name = > "name privacy" ,
GroupPrivacySubject . Description = > "description privacy" ,
GroupPrivacySubject . Banner = > "banner privacy" ,
GroupPrivacySubject . Icon = > "icon privacy" ,
GroupPrivacySubject . List = > "member list" ,
GroupPrivacySubject . Metadata = > "metadata" ,
GroupPrivacySubject . Visibility = > "visibility" ,
_ = > throw new ArgumentOutOfRangeException ( $"Unknown privacy subject {subject}" )
} ;
var explanation = ( subject , level ) switch
{
( GroupPrivacySubject . Name , PrivacyLevel . Private ) = >
"This group's name is now hidden from other systems, and will be replaced by the group's display name." ,
( GroupPrivacySubject . Description , PrivacyLevel . Private ) = >
"This group's description is now hidden from other systems." ,
( GroupPrivacySubject . Banner , PrivacyLevel . Private ) = >
"This group's banner 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." ,
( GroupPrivacySubject . Metadata , PrivacyLevel . Private ) = >
"This group's metadata (eg. creation date) is now hidden from other systems." ,
( GroupPrivacySubject . List , PrivacyLevel . Private ) = >
"This group's member list is now hidden from other systems." ,
( GroupPrivacySubject . Name , PrivacyLevel . Public ) = >
"This group's name is no longer hidden from other systems." ,
( GroupPrivacySubject . Description , PrivacyLevel . Public ) = >
"This group's description is no longer hidden from other systems." ,
( GroupPrivacySubject . Banner , PrivacyLevel . Public ) = >
"This group's banner 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." ,
( GroupPrivacySubject . Metadata , PrivacyLevel . Public ) = >
"This group's metadata (eg. creation date) is no longer hidden from other systems." ,
( GroupPrivacySubject . List , PrivacyLevel . Public ) = >
"This group's member list is no longer hidden from other systems." ,
_ = > throw new InvalidOperationException ( $"Invalid subject/level tuple ({subject}, {level})" )
} ;
var replyStr = $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}" ;
if ( subject = = GroupPrivacySubject . Name & & level = = PrivacyLevel . Private & & target . DisplayName = = null )
replyStr + = $"\n{Emojis.Warn} This group does not have a display name set, and name privacy **will not take effect**." ;
await ctx . Reply ( replyStr ) ;
2021-11-26 21:10:56 -05:00
}
2020-08-08 14:56:34 +02:00
2021-11-26 21:10:56 -05:00
public async Task DeleteGroup ( Context ctx , PKGroup target )
{
ctx . CheckOwnGroup ( target ) ;
2020-08-08 14:56:34 +02:00
2021-11-26 21:10:56 -05:00
await ctx . Reply (
2024-05-01 21:13:12 +12:00
$"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.DisplayHid(ctx.Config)}`).\n**Note: this action is permanent.**" ) ;
if ( ! await ctx . ConfirmWithReply ( target . Hid , treatAsHid : true ) )
2021-11-26 21:10:56 -05:00
throw new PKError (
2024-05-01 21:13:12 +12:00
$"Group deletion cancelled. Note that you must reply with your group ID (`{target.DisplayHid(ctx.Config)}`) *verbatim*." ) ;
2021-08-27 11:03:47 -04:00
2022-01-22 03:05:01 -05:00
await ctx . Repository . DeleteGroup ( target . Id ) ;
2020-08-08 14:56:34 +02:00
2021-11-26 21:10:56 -05:00
await ctx . Reply ( $"{Emojis.Success} Group deleted." ) ;
}
2021-02-09 23:36:43 +01:00
2022-08-27 13:52:50 +02:00
public async Task DisplayId ( Context ctx , PKGroup target )
{
2024-04-28 15:46:06 +12:00
await ctx . Reply ( target . DisplayHid ( ctx . Config ) ) ;
2022-08-27 13:52:50 +02:00
}
2021-11-26 21:10:56 -05:00
private async Task < PKSystem > GetGroupSystem ( Context ctx , PKGroup target )
{
var system = ctx . System ;
if ( system ? . Id = = target . System )
return system ;
2022-01-22 03:05:01 -05:00
return await ctx . Repository . GetSystem ( target . System ) ! ;
2020-06-29 23:51:12 +02:00
}
}