2019-05-16 23:23:09 +00:00
using System.Linq ;
2019-05-11 21:56:56 +00:00
using System.Text.RegularExpressions ;
2019-04-27 14:30:34 +00:00
using System.Threading.Tasks ;
2019-05-16 23:23:09 +00:00
using Discord ;
2019-05-13 20:44:49 +00:00
using NodaTime ;
2019-10-05 05:41:00 +00:00
using PluralKit.Bot.CommandSystem ;
2019-07-09 22:19:18 +00:00
using PluralKit.Core ;
2019-04-27 14:30:34 +00:00
namespace PluralKit.Bot.Commands
{
2019-10-05 05:41:00 +00:00
public class MemberCommands
2019-04-27 14:30:34 +00:00
{
2019-10-26 17:45:30 +00:00
private IDataStore _data ;
2019-10-05 05:41:00 +00:00
private EmbedService _embeds ;
private ProxyCacheService _proxyCache ;
2019-04-27 14:30:34 +00:00
2019-10-26 17:45:30 +00:00
public MemberCommands ( IDataStore data , EmbedService embeds , ProxyCacheService proxyCache )
2019-10-05 05:41:00 +00:00
{
2019-10-26 17:45:30 +00:00
_data = data ;
2019-10-05 05:41:00 +00:00
_embeds = embeds ;
_proxyCache = proxyCache ;
}
2019-04-27 14:30:34 +00:00
2019-10-05 05:41:00 +00:00
public async Task NewMember ( Context ctx ) {
if ( ctx . System = = null ) throw Errors . NoSystemError ;
var memberName = ctx . RemainderOrNull ( ) ? ? throw new PKSyntaxError ( "You must pass a member name." ) ;
2019-04-29 18:28:53 +00:00
// Hard name length cap
if ( memberName . Length > Limits . MaxMemberNameLength ) throw Errors . MemberNameTooLongError ( memberName . Length ) ;
2019-04-27 14:30:34 +00:00
// Warn if there's already a member by this name
2019-10-26 17:45:30 +00:00
var existingMember = await _data . GetMemberByName ( ctx . System , memberName ) ;
2019-04-27 14:30:34 +00:00
if ( existingMember ! = null ) {
2019-10-18 11:14:36 +00:00
var msg = await ctx . Reply ( $"{Emojis.Warn} You already have a member in your system with the name \" { existingMember . Name . SanitizeMentions ( ) } \ " (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?" ) ;
2019-10-05 05:41:00 +00:00
if ( ! await ctx . PromptYesNo ( msg ) ) throw new PKError ( "Member creation cancelled." ) ;
2019-04-27 14:30:34 +00:00
}
2019-10-20 07:16:57 +00:00
// Enforce per-system member limit
2019-10-26 17:45:30 +00:00
var memberCount = await _data . GetSystemMemberCount ( ctx . System ) ;
2019-10-20 07:16:57 +00:00
if ( memberCount > = Limits . MaxMemberCount )
throw Errors . MemberLimitReachedError ;
2019-04-27 14:30:34 +00:00
// Create the member
2019-10-26 17:45:30 +00:00
var member = await _data . CreateMember ( ctx . System , memberName ) ;
2019-10-20 07:16:57 +00:00
memberCount + + ;
2019-04-27 14:30:34 +00:00
2019-04-29 17:43:09 +00:00
// Send confirmation and space hint
2019-10-18 11:14:36 +00:00
await ctx . Reply ( $"{Emojis.Success} Member \" { memberName . SanitizeMentions ( ) } \ " (`{member.Hid}`) registered! See the user guide for commands for editing this member: https://pluralkit.me/guide#member-management" ) ;
2019-10-20 07:16:57 +00:00
if ( memberName . Contains ( " " ) )
await ctx . Reply ( $"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \" double quotes \ " when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`)." ) ;
if ( memberCount > = Limits . MaxMemberCount )
await ctx . Reply ( $"{Emojis.Warn} You have reached the per-system member limit ({Limits.MaxMemberCount}). You will be unable to create additional members until existing members are deleted." ) ;
else if ( memberCount > = Limits . MaxMembersWarnThreshold )
await ctx . Reply ( $"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {Limits.MaxMemberCount} members). Please review your member list for unused or duplicate members." ) ;
2019-10-05 05:41:00 +00:00
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-04-27 14:30:34 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task RenameMember ( Context ctx , PKMember target ) {
2019-04-29 17:43:09 +00:00
// TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean?
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
var newName = ctx . RemainderOrNull ( ) ;
2019-04-29 17:43:09 +00:00
2019-04-29 18:28:53 +00:00
// Hard name length cap
if ( newName . Length > Limits . MaxMemberNameLength ) throw Errors . MemberNameTooLongError ( newName . Length ) ;
2019-04-29 17:43:09 +00:00
// Warn if there's already a member by this name
2019-10-26 17:45:30 +00:00
var existingMember = await _data . GetMemberByName ( ctx . System , newName ) ;
2019-04-29 17:43:09 +00:00
if ( existingMember ! = null ) {
2019-10-18 11:14:36 +00:00
var msg = await ctx . Reply ( $"{Emojis.Warn} You already have a member in your system with the name \" { existingMember . Name . SanitizeMentions ( ) } \ " (`{existingMember.Hid}`). Do you want to rename this member to that name too?" ) ;
2019-10-05 05:41:00 +00:00
if ( ! await ctx . PromptYesNo ( msg ) ) throw new PKError ( "Member renaming cancelled." ) ;
2019-04-29 17:43:09 +00:00
}
2019-05-11 21:56:56 +00:00
// Rename the member
2019-10-05 05:41:00 +00:00
target . Name = newName ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-04-29 17:43:09 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member renamed." ) ;
if ( newName . Contains ( " " ) ) await ctx . Reply ( $"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \" double quotes \ " when using commands referring to it." ) ;
2019-10-18 11:14:36 +00:00
if ( target . DisplayName ! = null ) await ctx . Reply ( $"{Emojis.Note} Note that this member has a display name set ({target.DisplayName.SanitizeMentions()}), and will be proxied using that name instead." ) ;
2019-08-12 15:49:07 +00:00
2019-10-05 05:41:00 +00:00
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-04-29 17:43:09 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberDescription ( Context ctx , PKMember target ) {
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-04-29 17:43:09 +00:00
2019-10-05 05:41:00 +00:00
var description = ctx . RemainderOrNull ( ) ;
2019-05-11 21:56:56 +00:00
if ( description . IsLongerThan ( Limits . MaxDescriptionLength ) ) throw Errors . DescriptionTooLongError ( description . Length ) ;
2019-04-29 18:33:21 +00:00
2019-10-05 05:41:00 +00:00
target . Description = description ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-04-29 18:33:21 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member description {(description == null ? " cleared " : " changed ")}." ) ;
2019-04-29 18:33:21 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberPronouns ( Context ctx , PKMember target ) {
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-04-29 18:33:21 +00:00
2019-10-05 05:41:00 +00:00
var pronouns = ctx . RemainderOrNull ( ) ;
2019-05-11 21:56:56 +00:00
if ( pronouns . IsLongerThan ( Limits . MaxPronounsLength ) ) throw Errors . MemberPronounsTooLongError ( pronouns . Length ) ;
2019-04-29 18:36:09 +00:00
2019-10-05 05:41:00 +00:00
target . Pronouns = pronouns ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-04-29 18:36:09 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member pronouns {(pronouns == null ? " cleared " : " changed ")}." ) ;
2019-04-29 18:36:09 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberColor ( Context ctx , PKMember target )
2019-05-11 21:56:56 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
var color = ctx . RemainderOrNull ( ) ;
2019-05-11 21:56:56 +00:00
if ( color ! = null )
{
if ( color . StartsWith ( "#" ) ) color = color . Substring ( 1 ) ;
2019-07-19 12:54:40 +00:00
if ( ! Regex . IsMatch ( color , "^[0-9a-fA-F]{6}$" ) ) throw Errors . InvalidColorError ( color ) ;
2019-05-11 21:56:56 +00:00
}
2019-10-05 05:41:00 +00:00
target . Color = color ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-05-11 21:56:56 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member color {(color == null ? " cleared " : " changed ")}." ) ;
2019-05-11 21:56:56 +00:00
}
2019-05-11 22:44:02 +00:00
2019-10-05 05:41:00 +00:00
public async Task MemberBirthday ( Context ctx , PKMember target )
2019-05-13 20:44:49 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-05-13 20:44:49 +00:00
LocalDate ? date = null ;
2019-10-05 05:41:00 +00:00
var birthday = ctx . RemainderOrNull ( ) ;
2019-05-13 20:44:49 +00:00
if ( birthday ! = null )
{
date = PluralKit . Utils . ParseDate ( birthday , true ) ;
if ( date = = null ) throw Errors . BirthdayParseError ( birthday ) ;
}
2019-10-05 05:41:00 +00:00
target . Birthday = date ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-05-13 20:44:49 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member birthdate {(date == null ? " cleared " : $" changed to { target . BirthdayString } ")}." ) ;
2019-05-13 20:44:49 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberProxy ( Context ctx , PKMember target )
2019-05-13 20:56:22 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-10-28 19:15:27 +00:00
ProxyTag ParseProxyTags ( string exampleProxy )
{
// // Make sure there's one and only one instance of "text" in the example proxy given
var prefixAndSuffix = exampleProxy . Split ( "text" ) ;
if ( prefixAndSuffix . Length < 2 ) throw Errors . ProxyMustHaveText ;
if ( prefixAndSuffix . Length > 2 ) throw Errors . ProxyMultipleText ;
return new ProxyTag ( prefixAndSuffix [ 0 ] , prefixAndSuffix [ 1 ] ) ;
}
2019-10-05 05:41:00 +00:00
2019-12-21 20:42:06 +00:00
async Task < bool > WarnOnConflict ( ProxyTag newTag )
{
var conflicts = ( await _data . GetConflictingProxies ( ctx . System , newTag ) )
. Where ( m = > m . Id ! = target . Id )
. ToList ( ) ;
if ( conflicts . Count < = 0 ) return true ;
var conflictList = conflicts . Select ( m = > $"- **{m.Name}**" ) ;
var msg = await ctx . Reply (
$"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?" ) ;
return await ctx . PromptYesNo ( msg ) ;
}
2019-10-28 19:15:27 +00:00
// "Sub"command: no arguments clearing
if ( ! ctx . HasNext ( ) )
2019-05-13 20:56:22 +00:00
{
2019-10-28 19:15:27 +00:00
// If we already have multiple tags, this would clear everything, so prompt that
2019-10-30 07:44:17 +00:00
if ( target . ProxyTags . Count > 1 )
{
var msg = await ctx . Reply (
$"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?" ) ;
if ( ! await ctx . PromptYesNo ( msg ) )
throw Errors . GenericCancelled ( ) ;
}
2019-12-21 20:42:06 +00:00
2019-10-28 19:15:27 +00:00
target . ProxyTags = new ProxyTag [ ] { } ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-10-28 19:15:27 +00:00
await ctx . Reply ( $"{Emojis.Success} Proxy tags cleared." ) ;
}
// Subcommand: "add"
else if ( ctx . Match ( "add" ) )
{
2019-11-02 01:57:55 +00:00
if ( ! ctx . HasNext ( ) ) throw new PKSyntaxError ( "You must pass an example proxy to add (eg. `[text]` or `J:text`)." ) ;
2019-10-28 19:15:27 +00:00
var tagToAdd = ParseProxyTags ( ctx . RemainderOrNull ( ) ) ;
if ( target . ProxyTags . Contains ( tagToAdd ) )
throw Errors . ProxyTagAlreadyExists ( tagToAdd , target ) ;
2019-12-21 20:42:06 +00:00
if ( ! await WarnOnConflict ( tagToAdd ) )
throw Errors . GenericCancelled ( ) ;
2019-10-28 19:15:27 +00:00
// It's not guaranteed the list's mutable, so we force it to be
target . ProxyTags = target . ProxyTags . ToList ( ) ;
target . ProxyTags . Add ( tagToAdd ) ;
2019-10-27 23:37:53 +00:00
2019-10-28 19:15:27 +00:00
await _data . SaveMember ( target ) ;
await ctx . Reply ( $"{Emojis.Success} Added proxy tags `{tagToAdd.ProxyString.SanitizeMentions()}`." ) ;
2019-05-13 20:56:22 +00:00
}
2019-10-28 19:15:27 +00:00
// Subcommand: "remove"
else if ( ctx . Match ( "remove" ) )
{
2019-11-02 01:57:55 +00:00
if ( ! ctx . HasNext ( ) ) throw new PKSyntaxError ( "You must pass a proxy tag to remove (eg. `[text]` or `J:text`)." ) ;
2019-10-28 19:15:27 +00:00
var tagToRemove = ParseProxyTags ( ctx . RemainderOrNull ( ) ) ;
if ( ! target . ProxyTags . Contains ( tagToRemove ) )
throw Errors . ProxyTagDoesNotExist ( tagToRemove , target ) ;
// It's not guaranteed the list's mutable, so we force it to be
target . ProxyTags = target . ProxyTags . ToList ( ) ;
target . ProxyTags . Remove ( tagToRemove ) ;
await _data . SaveMember ( target ) ;
await ctx . Reply ( $"{Emojis.Success} Removed proxy tags `{tagToRemove.ProxyString.SanitizeMentions()}`." ) ;
}
// Subcommand: bare proxy tag given
else
{
2019-11-02 01:57:55 +00:00
if ( ! ctx . HasNext ( ) ) throw new PKSyntaxError ( "You must pass an example proxy to set (eg. `[text]` or `J:text`)." ) ;
2019-10-28 19:15:27 +00:00
var requestedTag = ParseProxyTags ( ctx . RemainderOrNull ( ) ) ;
// This is mostly a legacy command, so it's gonna error out if there's
// already more than one proxy tag.
if ( target . ProxyTags . Count > 1 )
throw Errors . LegacyAlreadyHasProxyTag ( requestedTag , target ) ;
2019-12-21 20:42:06 +00:00
if ( ! await WarnOnConflict ( requestedTag ) )
throw Errors . GenericCancelled ( ) ;
2019-10-28 19:15:27 +00:00
target . ProxyTags = new [ ] { requestedTag } ;
await _data . SaveMember ( target ) ;
await ctx . Reply ( $"{Emojis.Success} Member proxy tags set to `{requestedTag.ProxyString.SanitizeMentions()}`." ) ;
}
2019-10-05 05:41:00 +00:00
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-05-13 20:56:22 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberDelete ( Context ctx , PKMember target )
2019-05-13 21:08:44 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-08-12 15:49:07 +00:00
2019-10-18 11:14:36 +00:00
await ctx . Reply ( $"{Emojis.Warn} Are you sure you want to delete \" { target . Name . SanitizeMentions ( ) } \ "? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__" ) ;
2019-10-05 05:41:00 +00:00
if ( ! await ctx . ConfirmWithReply ( target . Hid ) ) throw Errors . MemberDeleteCancelled ;
2019-10-26 17:45:30 +00:00
await _data . DeleteMember ( target ) ;
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member deleted." ) ;
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-05-13 21:08:44 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberAvatar ( Context ctx , PKMember target )
2019-07-16 18:17:04 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
2019-07-16 18:17:04 +00:00
2019-10-05 05:41:00 +00:00
if ( await ctx . MatchUser ( ) is IUser user )
{
if ( user . AvatarId = = null ) throw Errors . UserHasNoAvatar ;
target . AvatarUrl = user . GetAvatarUrl ( ImageFormat . Png , size : 256 ) ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-08-12 15:49:07 +00:00
2019-10-05 05:41:00 +00:00
var embed = new EmbedBuilder ( ) . WithImageUrl ( target . AvatarUrl ) . Build ( ) ;
await ctx . Reply (
$"{Emojis.Success} Member avatar changed to {user.Username}'s avatar! {Emojis.Warn} Please note that if {user.Username} changes their avatar, the webhook's avatar will need to be re-set." , embed : embed ) ;
2019-07-16 18:17:04 +00:00
2019-10-05 05:41:00 +00:00
}
else if ( ctx . RemainderOrNull ( ) is string url )
{
await Utils . VerifyAvatarOrThrow ( url ) ;
target . AvatarUrl = url ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-05-16 23:23:09 +00:00
2019-10-05 05:41:00 +00:00
var embed = new EmbedBuilder ( ) . WithImageUrl ( url ) . Build ( ) ;
await ctx . Reply ( $"{Emojis.Success} Member avatar changed." , embed : embed ) ;
}
else if ( ctx . Message . Attachments . FirstOrDefault ( ) is Attachment attachment )
{
await Utils . VerifyAvatarOrThrow ( attachment . Url ) ;
target . AvatarUrl = attachment . Url ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-05-16 23:23:09 +00:00
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working." ) ;
}
else
{
target . AvatarUrl = null ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-10-05 05:41:00 +00:00
await ctx . Reply ( $"{Emojis.Success} Member avatar cleared." ) ;
}
2019-08-12 15:49:07 +00:00
2019-10-05 05:41:00 +00:00
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-05-16 23:23:09 +00:00
}
2019-10-05 05:41:00 +00:00
public async Task MemberDisplayName ( Context ctx , PKMember target )
2019-08-09 08:12:38 +00:00
{
2019-10-05 05:41:00 +00:00
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
var newDisplayName = ctx . RemainderOrNull ( ) ;
2019-10-27 20:57:31 +00:00
2019-10-05 05:41:00 +00:00
target . DisplayName = newDisplayName ;
2019-10-26 17:45:30 +00:00
await _data . SaveMember ( target ) ;
2019-08-09 08:12:38 +00:00
var successStr = $"{Emojis.Success} " ;
if ( newDisplayName ! = null )
{
successStr + =
2019-10-18 11:14:36 +00:00
$"Member display name changed. This member will now be proxied using the name \" { newDisplayName . SanitizeMentions ( ) } \ "." ;
2019-08-09 08:12:38 +00:00
}
else
{
2019-10-27 20:57:31 +00:00
successStr + = $"Member display name cleared. This member will now be proxied using their member name \" { target . Name . SanitizeMentions ( ) } \ "." ;
2019-08-09 08:12:38 +00:00
}
2019-10-05 05:41:00 +00:00
await ctx . Reply ( successStr ) ;
2019-08-12 15:49:07 +00:00
2019-10-05 05:41:00 +00:00
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
2019-05-11 22:44:02 +00:00
}
2019-05-16 23:23:09 +00:00
2019-10-30 08:26:50 +00:00
public async Task MemberKeepProxy ( Context ctx , PKMember target )
{
if ( ctx . System = = null ) throw Errors . NoSystemError ;
if ( target . System ! = ctx . System . Id ) throw Errors . NotOwnMemberError ;
bool newValue ;
if ( ctx . Match ( "on" , "enabled" , "true" , "yes" ) ) newValue = true ;
else if ( ctx . Match ( "off" , "disabled" , "false" , "no" ) ) newValue = false ;
else if ( ctx . HasNext ( ) ) throw new PKSyntaxError ( "You must pass either \"on\" or \"off\"." ) ;
else newValue = ! target . KeepProxy ;
target . KeepProxy = newValue ;
await _data . SaveMember ( target ) ;
if ( newValue )
await ctx . Reply ( $"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying." ) ;
else
await ctx . Reply ( $"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying." ) ;
await _proxyCache . InvalidateResultsForSystem ( ctx . System ) ;
}
2019-10-05 05:41:00 +00:00
public async Task ViewMember ( Context ctx , PKMember target )
2019-04-27 14:30:34 +00:00
{
2019-10-26 17:45:30 +00:00
var system = await _data . GetSystemById ( target . System ) ;
2019-10-05 05:41:00 +00:00
await ctx . Reply ( embed : await _embeds . CreateMemberEmbed ( system , target ) ) ;
2019-04-27 14:30:34 +00:00
}
}
}