Esempio n. 1
0
        private async Task <Embed> CreateAutoproxyStatusEmbed(Context ctx)
        {
            var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy <member>** - Autoproxies as a specific member";
            var eb          = new EmbedBuilder()
                              .Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})");

            var fronters       = ctx.MessageContext.LastSwitchMembers;
            var relevantMember = ctx.MessageContext.AutoproxyMode switch
            {
                AutoproxyMode.Front => fronters.Length > 0 ? await _db.Execute(c => _repo.GetMember(c, fronters[0])) : null,
                AutoproxyMode.Member => await _db.Execute(c => _repo.GetMember(c, ctx.MessageContext.AutoproxyMember.Value)),
                _ => null
            };

            switch (ctx.MessageContext.AutoproxyMode)
            {
            case AutoproxyMode.Off:
                eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
                break;

            case AutoproxyMode.Front:
            {
                if (fronters.Length == 0)
                {
                    eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch.");
                }
                else
                {
                    if (relevantMember == null)
                    {
                        throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
                    }
                    eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
                }

                break;
            }

            // AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
            case AutoproxyMode.Member when relevantMember != null: {
                eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
                break;
            }

            case AutoproxyMode.Latch:
                eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`.");
                break;

            default: throw new ArgumentOutOfRangeException();
            }

            if (!ctx.MessageContext.AllowAutoproxy)
            {
                eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`."));
            }

            return(eb.Build());
        }
Esempio n. 2
0
        private async Task AvatarShow(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings?guildData)
        {
            var currentValue = location == AvatarLocation.Member ? target.AvatarUrl : guildData?.AvatarUrl;
            var canAccess    = location != AvatarLocation.Member || target.AvatarPrivacy.CanAccess(ctx.LookupContextFor(target));

            if (string.IsNullOrEmpty(currentValue) || !canAccess)
            {
                if (location == AvatarLocation.Member)
                {
                    if (target.System == ctx.System?.Id)
                    {
                        throw new PKSyntaxError("This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
                    }
                    throw new PKError("This member does not have an avatar set.");
                }

                if (location == AvatarLocation.Server)
                {
                    throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar.");
                }
            }

            var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
            var cmd   = location == AvatarLocation.Server ? "serveravatar" : "avatar";

            var eb = new EmbedBuilder()
                     .Title($"{target.NameFor(ctx)}'s {field}")
                     .Image(new(currentValue));

            if (target.System == ctx.System?.Id)
            {
                eb.Description($"To clear, use `pk;member {target.Reference()} {cmd} clear`.");
            }
            await ctx.Reply(embed : eb.Build());
        }
Esempio n. 3
0
    private async Task AvatarShow(AvatarLocation location, Context ctx, PKMember target,
                                  MemberGuildSettings?guildData)
    {
        // todo: this privacy code is really confusing
        // for now, we skip privacy flag/config parsing for this, but it would be good to fix that at some point

        var currentValue = location == AvatarLocation.Member ? target.AvatarUrl : guildData?.AvatarUrl;
        var canAccess    = location != AvatarLocation.Member ||
                           target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.System));

        if (string.IsNullOrEmpty(currentValue) || !canAccess)
        {
            if (location == AvatarLocation.Member)
            {
                if (target.System == ctx.System?.Id)
                {
                    throw new PKSyntaxError(
                              "This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
                }
                throw new PKError("This member does not have an avatar set.");
            }

            if (location == AvatarLocation.Server)
            {
                throw new PKError(
                          $"This member does not have a server avatar set. Type `pk;member {target.Reference(ctx)} avatar` to see their global avatar.");
            }
        }

        var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
        var cmd   = location == AvatarLocation.Server ? "serveravatar" : "avatar";

        var eb = new EmbedBuilder()
                 .Title($"{target.NameFor(ctx)}'s {field}")
                 .Image(new Embed.EmbedImage(currentValue?.TryGetCleanCdnUrl()));

        if (target.System == ctx.System?.Id)
        {
            eb.Description($"To clear, use `pk;member {target.Reference(ctx)} {cmd} clear`.");
        }
        await ctx.Reply(embed : eb.Build());
    }
Esempio n. 4
0
        public async Task GroupDisplayName(Context ctx, PKGroup target)
        {
            if (await ctx.MatchClear("this group's display name"))
            {
                ctx.CheckOwnGroup(target);

                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 if (!ctx.HasNext())
            {
                // No perms check, display name isn't covered by member privacy
                var eb = new EmbedBuilder()
                         .Field(new("Name", target.Name))
                         .Field(new("Display Name", target.DisplayName ?? "*(none)*"));

                if (ctx.System?.Id == target.System)
                {
                    eb.Description($"To change display name, type `pk;group {target.Reference()} displayname <display name>`.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`.");
                }

                await ctx.Reply(embed : eb.Build());
            }
            else
            {
                ctx.CheckOwnGroup(target);

                var newDisplayName = ctx.RemainderOrNull();

                var patch = new GroupPatch {
                    DisplayName = Partial <string> .Present(newDisplayName)
                };
                await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));

                await ctx.Reply($"{Emojis.Success} Group display name changed.");
            }
        }
Esempio n. 5
0
        public async Task <Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx)
        {
            // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));

            var name = member.NameFor(ctx);

            if (system.Name != null)
            {
                name = $"{name} ({system.Name})";
            }

            uint color;

            try
            {
                color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
            }
            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 = DiscordUtils.Gray;
            }

            await using var conn = await _db.Obtain();

            var guildSettings = guild != null ? await _repo.GetMemberGuild(conn, guild.Id, member.Id) : null;

            var guildDisplayName = guildSettings?.DisplayName;
            var avatar           = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx);

            var groups = await _repo.GetMemberGroups(conn, member.Id)
                         .Where(g => g.Visibility.CanAccess(ctx))
                         .OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
                         .ToListAsync();

            var eb = new EmbedBuilder()
                     // TODO: add URL of website when that's up
                     .Author(new(name, IconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)))
                     // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
                     .Color(color)
                     .Footer(new(
                                 $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}" : "")}"));

            var description = "";

            if (member.MemberVisibility == PrivacyLevel.Private)
            {
                description += "*(this member is hidden)*\n";
            }
            if (guildSettings?.AvatarUrl != null)
            {
                if (member.AvatarFor(ctx) != null)
                {
                    description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n";
                }
                else
                {
                    description += "*(this member has a server-specific avatar set)*\n";
                }
            }
            if (description != "")
            {
                eb.Description(description);
            }

            if (avatar != null)
            {
                eb.Thumbnail(new(avatar));
            }

            if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx))
            {
                eb.Field(new("Display Name", member.DisplayName.Truncate(1024), true));
            }
            if (guild != null && guildDisplayName != null)
            {
                eb.Field(new($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true));
            }
            if (member.BirthdayFor(ctx) != null)
            {
                eb.Field(new("Birthdate", member.BirthdayString, true));
            }
            if (member.PronounsFor(ctx) is {} pronouns&& !string.IsNullOrWhiteSpace(pronouns))
            {
                eb.Field(new("Pronouns", pronouns.Truncate(1024), true));
            }
            if (member.MessageCountFor(ctx) is {} count&& count > 0)
            {
                eb.Field(new("Message Count", member.MessageCount.ToString(), true));
            }
            if (member.HasProxyTags)
            {
                eb.Field(new("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true));
            }
            // --- For when this gets added to the member object itself or however they get added
            // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value)));
            // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value));
            // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
            if (!member.Color.EmptyOrNull())
            {
                eb.Field(new("Color", $"#{member.Color}", true));
            }

            if (groups.Count > 0)
            {
                // More than 5 groups show in "compact" format without ID
                var content = groups.Count > 5
                    ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name))
                    : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
                eb.Field(new($"Groups ({groups.Count})", content.Truncate(1000)));
            }

            if (member.DescriptionFor(ctx) is {} desc)
            {
                eb.Field(new("Description", member.Description.NormalizeLineEndSpacing(), false));
            }

            return(eb.Build());
        }
Esempio n. 6
0
    public async Task Avatar(Context ctx, PKSystem target)
    {
        async Task ClearIcon()
        {
            ctx.CheckOwnSystem(target);

            await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = null });

            await ctx.Reply($"{Emojis.Success} System icon cleared.");
        }

        async Task SetIcon(ParsedImage img)
        {
            ctx.CheckOwnSystem(target);

            await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);

            await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.Url });

            var msg = img.Source switch
            {
                AvatarSource.User =>
                $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.",
                AvatarSource.Url => $"{Emojis.Success} System icon changed to the image at the given URL.",
                AvatarSource.Attachment =>
                $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.",
                _ => throw new ArgumentOutOfRangeException()
            };

            // The attachment's already right there, no need to preview it.
            var hasEmbed = img.Source != AvatarSource.Attachment;

            await(hasEmbed
                ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
                : ctx.Reply(msg));
        }

        async Task ShowIcon()
        {
            if ((target.AvatarUrl?.Trim() ?? "").Length > 0)
            {
                var eb = new EmbedBuilder()
                         .Title("System icon")
                         .Image(new Embed.EmbedImage(target.AvatarUrl.TryGetCleanCdnUrl()));
                if (target.Id == ctx.System?.Id)
                {
                    eb.Description("To clear, use `pk;system icon clear`.");
                }
                await ctx.Reply(embed : eb.Build());
            }
            else
            {
                throw new PKSyntaxError(
                          "This system does not have an icon set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
            }
        }

        if (target != null && target?.Id != ctx.System?.Id)
        {
            await ShowIcon();

            return;
        }

        if (await ctx.MatchClear("your system's icon"))
        {
            await ClearIcon();
        }
        else if (await ctx.MatchImage() is { } img)
        {
            await SetIcon(img);
        }
Esempio n. 7
0
        public async Task PermCheckGuild(Context ctx)
        {
            Guild guild;
            GuildMemberPartial senderGuildUser = null;

            if (ctx.Guild != null && !ctx.HasNext())
            {
                guild           = ctx.Guild;
                senderGuildUser = ctx.Member;
            }
            else
            {
                var guildIdStr = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a server ID or run this command in a server.");
                if (!ulong.TryParse(guildIdStr, out var guildId))
                {
                    throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
                }

                guild = await _rest.GetGuild(guildId);

                if (guild != null)
                {
                    senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id);
                }
                if (guild == null || senderGuildUser == null)
                {
                    throw Errors.GuildNotFound(guildId);
                }
            }

            var requiredPermissions = new []
            {
                PermissionSet.ViewChannel,
                PermissionSet.SendMessages,
                PermissionSet.AddReactions,
                PermissionSet.AttachFiles,
                PermissionSet.EmbedLinks,
                PermissionSet.ManageMessages,
                PermissionSet.ManageWebhooks
            };

            // Loop through every channel and group them by sets of permissions missing
            var permissionsMissing = new Dictionary <ulong, List <Channel> >();
            var hiddenChannels     = 0;

            foreach (var channel in await _rest.GetGuildChannels(guild.Id))
            {
                var botPermissions  = _bot.PermissionsIn(channel.Id);
                var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser.Roles);

                if ((userPermissions & PermissionSet.ViewChannel) == 0)
                {
                    // If the user can't see this channel, don't calculate permissions for it
                    // (to prevent info-leaking, mostly)
                    // Instead, count how many hidden channels and show the user (so they don't get confused)
                    hiddenChannels++;
                    continue;
                }

                // We use a bitfield so we can set individual permission bits in the loop
                // TODO: Rewrite with proper bitfield math
                ulong missingPermissionField = 0;
                foreach (var requiredPermission in requiredPermissions)
                {
                    if ((botPermissions & requiredPermission) == 0)
                    {
                        missingPermissionField |= (ulong)requiredPermission;
                    }
                }

                // If we're not missing any permissions, don't bother adding it to the dict
                // This means we can check if the dict is empty to see if all channels are proxyable
                if (missingPermissionField != 0)
                {
                    permissionsMissing.TryAdd(missingPermissionField, new List <Channel>());
                    permissionsMissing[missingPermissionField].Add(channel);
                }
            }

            // Generate the output embed
            var eb = new EmbedBuilder()
                     .Title($"Permission check for **{guild.Name}**");

            if (permissionsMissing.Count == 0)
            {
                eb.Description($"No errors found, all channels proxyable :)").Color(DiscordUtils.Green);
            }
            else
            {
                foreach (var(missingPermissionField, channels) in permissionsMissing)
                {
                    // Each missing permission field can have multiple missing channels
                    // so we extract them all and generate a comma-separated list
                    var missingPermissionNames = ((PermissionSet)missingPermissionField).ToPermissionString();

                    var channelsList = string.Join("\n", channels
                                                   .OrderBy(c => c.Position)
                                                   .Select(c => $"#{c.Name}"));
                    eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)));
                    eb.Color(DiscordUtils.Red);
                }
            }

            if (hiddenChannels > 0)
            {
                eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them."));
            }

            // Send! :)
            await ctx.Reply(embed : eb.Build());
        }
Esempio n. 8
0
    public async Task PermCheckChannel(Context ctx)
    {
        if (!ctx.HasNext())
        {
            throw new PKSyntaxError("You need to specify a channel.");
        }

        var error = "Channel not found or you do not have permissions to access it.";

        // todo: this breaks if channel is not in cache and bot does not have View Channel permissions
        var channel = await ctx.MatchChannel();

        if (channel == null || channel.GuildId == null)
        {
            throw new PKError(error);
        }

        var guild = await _rest.GetGuildOrNull(channel.GuildId.Value);

        if (guild == null)
        {
            throw new PKError(error);
        }

        var guildMember = await _rest.GetGuildMember(channel.GuildId.Value, await _cache.GetOwnUser());

        if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
        {
            throw new PKError(error);
        }

        var botPermissions     = PermissionExtensions.PermissionsFor(guild, channel, await _cache.GetOwnUser(), guildMember);
        var webhookPermissions = PermissionExtensions.EveryonePermissions(guild, channel);

        // We use a bitfield so we can set individual permission bits
        ulong missingPermissions = 0;

        foreach (var requiredPermission in requiredPermissions)
        {
            if ((botPermissions & requiredPermission) == 0)
            {
                missingPermissions |= (ulong)requiredPermission;
            }
        }

        if ((webhookPermissions & PermissionSet.UseExternalEmojis) == 0)
        {
            missingPermissions |= (ulong)PermissionSet.UseExternalEmojis;
        }

        // Generate the output embed
        var eb = new EmbedBuilder()
                 .Title($"Permission check for **{channel.Name}**");

        if (missingPermissions == 0)
        {
            eb.Description("No issues found, channel is proxyable :)");
        }
        else
        {
            var missing = "";

            foreach (var permission in requiredPermissions)
            {
                if (((ulong)permission & missingPermissions) == (ulong)permission)
                {
                    missing += $"\n- **{permission.ToPermissionString()}**";
                }
            }

            if (((ulong)PermissionSet.UseExternalEmojis & missingPermissions) ==
                (ulong)PermissionSet.UseExternalEmojis)
            {
                missing += $"\n- **{PermissionSet.UseExternalEmojis.ToPermissionString()}**";
            }

            eb.Description($"Missing permissions:\n{missing}");
        }

        await ctx.Reply(embed : eb.Build());
    }
Esempio n. 9
0
        public async Task GroupIcon(Context ctx, PKGroup target)
        {
            async Task ClearIcon()
            {
                ctx.CheckOwnGroup(target);

                await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {
                    Icon = null
                }));

                await ctx.Reply($"{Emojis.Success} Group icon cleared.");
            }

            async Task SetIcon(ParsedImage img)
            {
                ctx.CheckOwnGroup(target);

                await AvatarUtils.VerifyAvatarOrThrow(img.Url);

                await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {
                    Icon = img.Url
                }));

                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()
                };

                // The attachment's already right there, no need to preview it.
                var hasEmbed = img.Source != AvatarSource.Attachment;

                await(hasEmbed
                    ? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
                    : ctx.Reply(msg));
            }

            async Task ShowIcon()
            {
                if ((target.Icon?.Trim() ?? "").Length > 0)
                {
                    var eb = new EmbedBuilder()
                             .Title("Group icon")
                             .Image(new(target.Icon));

                    if (target.System == ctx.System?.Id)
                    {
                        eb.Description($"To clear, use `pk;group {target.Reference()} icon -clear`.");
                    }

                    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.");
                }
            }

            if (await ctx.MatchClear("this group's icon"))
            {
                await ClearIcon();
            }
            else if (await ctx.MatchImage() is {} img)
            {
                await SetIcon(img);
            }
Esempio n. 10
0
    public async Task GroupDisplayName(Context ctx, PKGroup target)
    {
        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(ctx)} displayname <display name>`.";
        }

        // No perms check, display name isn't covered by member privacy

        if (ctx.MatchRaw())
        {
            if (target.DisplayName == null)
            {
                await ctx.Reply(noDisplayNameSetMessage);
            }
            else
            {
                await ctx.Reply($"```\n{target.DisplayName}\n```");
            }
            return;
        }

        if (!ctx.HasNext(false))
        {
            if (target.DisplayName == null)
            {
                await ctx.Reply(noDisplayNameSetMessage);
            }
            else
            {
                var eb = new EmbedBuilder()
                         .Field(new Embed.Field("Name", target.Name))
                         .Field(new Embed.Field("Display Name", target.DisplayName));

                var reference = target.Reference(ctx);

                if (ctx.System?.Id == target.System)
                {
                    eb.Description(
                        $"To change display name, type `pk;group {reference} displayname <display name>`."
                        + $"To clear it, type `pk;group {reference} displayname -clear`."
                        + $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
                }

                await ctx.Reply(embed : eb.Build());
            }

            return;
        }

        ctx.CheckOwnGroup(target);

        if (await ctx.MatchClear("this group's display name"))
        {
            var patch = new GroupPatch {
                DisplayName = Partial <string> .Null()
            };
            await ctx.Repository.UpdateGroup(target.Id, patch);

            await ctx.Reply($"{Emojis.Success} Group display name cleared.");

            if (target.NamePrivacy == PrivacyLevel.Private)
            {
                await ctx.Reply($"{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**.");
            }
        }
        else
        {
            var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();

            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.");
        }
    }