Exemplo n.º 1
0
    public Embed CreateLoggedMessageEmbed(Message triggerMessage, Message proxiedMessage, string systemHid,
                                          PKMember member, string channelName, string oldContent = null)
    {
        // TODO: pronouns in ?-reacted response using this card
        var timestamp = DiscordUtils.SnowflakeToInstant(proxiedMessage.Id);
        var name      = proxiedMessage.Author.Username;
        // sometimes Discord will just... not return the avatar hash with webhook messages
        var avatar = proxiedMessage.Author.Avatar != null
            ? proxiedMessage.Author.AvatarUrl()
            : member.AvatarFor(LookupContext.ByNonOwner);

        var embed = new EmbedBuilder()
                    .Author(new Embed.EmbedAuthor($"#{channelName}: {name}", IconUrl: avatar))
                    .Thumbnail(new Embed.EmbedThumbnail(avatar))
                    .Description(proxiedMessage.Content?.NormalizeLineEndSpacing())
                    .Footer(new Embed.EmbedFooter(
                                $"System ID: {systemHid} | Member ID: {member.Hid} | Sender: {triggerMessage.Author.Username}#{triggerMessage.Author.Discriminator} ({triggerMessage.Author.Id}) | Message ID: {proxiedMessage.Id} | Original Message ID: {triggerMessage.Id}"))
                    .Timestamp(timestamp.ToDateTimeOffset().ToString("O"));

        if (oldContent != null)
        {
            embed.Field(new Embed.Field("Old message", oldContent?.NormalizeLineEndSpacing().Truncate(1000)));
        }

        return(embed.Build());
    }
Exemplo n.º 2
0
    public async Task Stats(Context ctx)
    {
        var timeBefore = SystemClock.Instance.GetCurrentInstant();
        var msg        = await ctx.Reply("...");

        var timeAfter  = SystemClock.Instance.GetCurrentInstant();
        var apiLatency = timeAfter - timeBefore;

        var embed = new EmbedBuilder();

        // todo: these will be inaccurate when the bot is actually multi-process

        var messagesReceived = _metrics.Snapshot.GetForContext("Bot").Meters
                               .FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name)?.Value;

        if (messagesReceived != null)
        {
            embed.Field(new Embed.Field("Messages processed",
                                        $"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)",
                                        true));
        }

        var messagesProxied = _metrics.Snapshot.GetForContext("Bot").Meters
                              .FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name)?.Value;

        if (messagesProxied != null)
        {
            embed.Field(new Embed.Field("Messages proxied",
                                        $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)",
                                        true));
        }

        var commandsRun = _metrics.Snapshot.GetForContext("Bot").Meters
                          .FirstOrDefault(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name)?.Value;

        if (commandsRun != null)
        {
            embed.Field(new Embed.Field("Commands executed",
                                        $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)",
                                        true));
        }

        var isCluster = _botConfig.Cluster != null && _botConfig.Cluster.TotalShards != ctx.Cluster.Shards.Count;

        var counts = await _repo.GetStats();

        var shards = await _shards.GetShards();

        var shardInfo = shards.Where(s => s.ShardId == ctx.ShardId).FirstOrDefault();

        // todo: if we're running multiple processes, it is not useful to get the CPU/RAM usage of just the current one
        var process     = Process.GetCurrentProcess();
        var memoryUsage = process.WorkingSet64;

        var now         = SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds();
        var shardUptime = Duration.FromSeconds(now - shardInfo?.LastConnection ?? 0);

        var shardTotal        = _botConfig.Cluster?.TotalShards ?? shards.Count();
        int shardClusterTotal = ctx.Cluster.Shards.Count;
        var shardUpTotal      = shards.Where(x => x.Up).Count();

        embed
        .Field(new Embed.Field("Current shard",
                               $"Shard #{ctx.ShardId} (of {shardTotal} total,"
                               + (isCluster ? $" {shardClusterTotal} in this cluster," : "") + $" {shardUpTotal} are up)"
                               , true))
        .Field(new Embed.Field("Shard uptime",
                               $"{shardUptime.FormatDuration()} ({shardInfo?.DisconnectionCount} disconnections)", true))
        .Field(new Embed.Field("CPU usage", $"{_cpu.LastCpuMeasure:P1}", true))
        .Field(new Embed.Field("Memory usage", $"{memoryUsage / 1024 / 1024} MiB", true))
        .Field(new Embed.Field("Latency",
                               $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo?.Latency} ms",
                               true));

        embed.Field(new("Total numbers", $" {counts.SystemCount:N0} systems,"
                        + $" {counts.MemberCount:N0} members,"
                        + $" {counts.GroupCount:N0} groups,"
                        + $" {counts.SwitchCount:N0} switches,"
                        + $" {counts.MessageCount:N0} messages"));

        embed
        .Footer(new(String.Join(" \u2022 ", new[] {
            $"PluralKit {BuildInfoService.Version}",
            (isCluster ? $"Cluster {_botConfig.Cluster.NodeIndex}" : ""),
            "https://github.com/xSke/PluralKit",
            "Last restarted:",
        })))
        .Timestamp(process.StartTime.ToString("O"));

        await ctx.Rest.EditMessage(msg.ChannelId, msg.Id,
                                   new MessageEditRequest { Content = "", Embeds = new[] { embed.Build() } });
    }
Exemplo n.º 3
0
    public Task <Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group,
                                                DateTimeZone tz, LookupContext ctx, string embedTitle,
                                                bool ignoreNoFronters, bool showFlat)
    {
        var color = system.Color;

        if (group != null)
        {
            color = group.Color;
        }

        uint embedColor;

        try
        {
            embedColor = color?.ToDiscordColor() ?? DiscordUtils.Gray;
        }
        catch (ArgumentException)
        {
            embedColor = DiscordUtils.Gray;
        }

        var eb = new EmbedBuilder()
                 .Title(embedTitle)
                 .Color(embedColor);

        var footer =
            $"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)";

        Duration period;

        if (showFlat)
        {
            period  = Duration.FromTicks(breakdown.MemberSwitchDurations.Values.ToList().Sum(i => i.TotalTicks));
            footer += ". Showing flat list (percentages add up to 100%)";
            if (!ignoreNoFronters)
            {
                period += breakdown.NoFronterDuration;
            }
            else
            {
                footer += ", ignoring switch-outs";
            }
        }
        else if (ignoreNoFronters)
        {
            period  = breakdown.RangeEnd - breakdown.RangeStart - breakdown.NoFronterDuration;
            footer += ". Ignoring switch-outs";
        }
        else
        {
            period = breakdown.RangeEnd - breakdown.RangeStart;
        }

        eb.Footer(new Embed.EmbedFooter(footer));

        var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others"

        // We convert to a list of pairs so we can add the no-fronter value
        // Dictionary doesn't allow for null keys so we instead have a pair with a null key ;)
        var pairs = breakdown.MemberSwitchDurations.ToList();

        if (breakdown.NoFronterDuration != Duration.Zero && !ignoreNoFronters)
        {
            pairs.Add(new KeyValuePair <PKMember, Duration>(null, breakdown.NoFronterDuration));
        }

        var membersOrdered = pairs.OrderByDescending(pair => pair.Value).Take(maxEntriesToDisplay).ToList();

        foreach (var pair in membersOrdered)
        {
            var frac = pair.Value / period;
            eb.Field(new Embed.Field(pair.Key?.NameFor(ctx) ?? "*(no fronter)*",
                                     $"{frac * 100:F0}% ({pair.Value.FormatDuration()})"));
        }

        if (membersOrdered.Count > maxEntriesToDisplay)
        {
            eb.Field(new Embed.Field("(others)",
                                     membersOrdered.Skip(maxEntriesToDisplay)
                                     .Aggregate(Duration.Zero, (prod, next) => prod + next.Value)
                                     .FormatDuration(), true));
        }

        return(Task.FromResult(eb.Build()));
    }
Exemplo n.º 4
0
    public async Task <Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
    {
        // Fetch/render info for all accounts simultaneously
        var accounts = await _repo.GetSystemAccounts(system.Id);

        var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})");

        var countctx = LookupContext.ByNonOwner;

        if (cctx.MatchFlag("a", "all"))
        {
            if (system.Id == cctx.System.Id)
            {
                countctx = LookupContext.ByOwner;
            }
            else
            {
                throw Errors.LookupNotAllowed;
            }
        }

        var memberCount = await _repo.GetSystemMemberCount(system.Id, countctx == LookupContext.ByOwner?null : PrivacyLevel.Public);

        uint color;

        try
        {
            color = system.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
        }
        catch (ArgumentException)
        {
            // There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea
            color = DiscordUtils.Gray;
        }

        var eb = new EmbedBuilder()
                 .Title(system.Name)
                 .Thumbnail(new Embed.EmbedThumbnail(system.AvatarUrl.TryGetCleanCdnUrl()))
                 .Footer(new Embed.EmbedFooter(
                             $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}"))
                 .Color(color)
                 .Url($"https://dash.pluralkit.me/profile/s/{system.Hid}");

        if (system.DescriptionPrivacy.CanAccess(ctx))
        {
            eb.Image(new Embed.EmbedImage(system.BannerImage));
        }

        var latestSwitch = await _repo.GetLatestSwitch(system.Id);

        if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
        {
            var switchMembers =
                await _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id)).ToListAsync();

            if (switchMembers.Count > 0)
            {
                eb.Field(new Embed.Field("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None),
                                         string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))));
            }
        }

        if (system.Tag != null)
        {
            eb.Field(new Embed.Field("Tag", system.Tag.EscapeMarkdown(), true));
        }

        if (cctx.Guild != null)
        {
            var guildSettings = await _repo.GetSystemGuild(cctx.Guild.Id, system.Id);

            if (guildSettings.Tag != null && guildSettings.TagEnabled)
            {
                eb.Field(new Embed.Field($"Tag (in server '{cctx.Guild.Name}')", guildSettings.Tag
                                         .EscapeMarkdown(), true));
            }

            if (!guildSettings.TagEnabled)
            {
                eb.Field(new Embed.Field($"Tag (in server '{cctx.Guild.Name}')",
                                         "*(tag is disabled in this server)*"));
            }
        }

        if (system.PronounPrivacy.CanAccess(ctx) && system.Pronouns != null)
        {
            eb.Field(new Embed.Field("Pronouns", system.Pronouns, true));
        }

        if (!system.Color.EmptyOrNull())
        {
            eb.Field(new Embed.Field("Color", $"#{system.Color}", true));
        }

        eb.Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000), true));

        if (system.MemberListPrivacy.CanAccess(ctx))
        {
            if (memberCount > 0)
            {
                eb.Field(new Embed.Field($"Members ({memberCount})",
                                         $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true));
            }
            else
            {
                eb.Field(new Embed.Field($"Members ({memberCount})", "Add one with `pk;member new`!", true));
            }
        }

        if (system.DescriptionFor(ctx) is { } desc)
        {
            eb.Field(new Embed.Field("Description", desc.NormalizeLineEndSpacing().Truncate(1024)));
        }

        return(eb.Build());
    }
Exemplo n.º 5
0
    public async Task <Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent)
    {
        var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel);

        var ctx = LookupContext.ByNonOwner;

        var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid);

        // Need this whole dance to handle cases where:
        // - the user is deleted (userInfo == null)
        // - the bot's no longer in the server we're querying (channel == null)
        // - the member is no longer in the server we're querying (memberInfo == null)
        // TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s?
        GuildMemberPartial memberInfo = null;
        User userInfo = null;

        if (channel != null)
        {
            GuildMember member = null;
            try
            {
                member = await _rest.GetGuildMember(channel.GuildId !.Value, msg.Message.Sender);
            }
            catch (ForbiddenException)
            {
                // no permission, couldn't fetch, oh well
            }

            if (member != null)
            {
                // Don't do an extra request if we already have this info from the member lookup
                userInfo = member.User;
            }
            memberInfo = member;
        }

        if (userInfo == null)
        {
            userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender);
        }

        // Calculate string displayed under "Sent by"
        string userStr;

        if (showContent && memberInfo != null && memberInfo.Nick != null)
        {
            userStr = $"**Username:** {userInfo.NameAndMention()}\n**Nickname:** {memberInfo.Nick}";
        }
        else if (userInfo != null)
        {
            userStr = userInfo.NameAndMention();
        }
        else
        {
            userStr = $"*(deleted user {msg.Message.Sender})*";
        }

        var content = serverMsg?.Content?.NormalizeLineEndSpacing();

        if (content == null || !showContent)
        {
            content = "*(message contents deleted or inaccessible)*";
        }

        // Put it all together
        var eb = new EmbedBuilder()
                 .Author(new Embed.EmbedAuthor(msg.Member?.NameFor(ctx) ?? "(deleted member)",
                                               IconUrl: msg.Member?.AvatarFor(ctx).TryGetCleanCdnUrl()))
                 .Description(content)
                 .Image(showContent ? new Embed.EmbedImage(serverMsg?.Attachments?.FirstOrDefault()?.Url) : null)
                 .Field(new Embed.Field("System",
                                        msg.System == null
                    ? "*(deleted or unknown system)*"
                    : msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`"
                                        , true))
                 .Field(new Embed.Field("Member",
                                        msg.Member == null
                    ? "*(deleted member)*"
                    : $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)"
                                        , true))
                 .Field(new Embed.Field("Sent by", userStr, true))
                 .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O"));

        var roles = memberInfo?.Roles?.ToList();

        if (roles != null && roles.Count > 0 && showContent)
        {
            var rolesString = string.Join(", ", (await Task.WhenAll(roles
                                                                    .Select(async id =>
            {
                var role = await _cache.TryGetRole(id);
                if (role != null)
                {
                    return(role);
                }
                return(new Role {
                    Name = "*(unknown role)*", Position = 0
                });
            })))
                                          .OrderByDescending(role => role.Position)
                                          .Select(role => role.Name));
            eb.Field(new Embed.Field($"Account roles ({roles.Count})", rolesString.Truncate(1024)));
        }

        return(eb.Build());
    }
Exemplo n.º 6
0
    public async Task <Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
    {
        var pctx = ctx.LookupContextFor(system.Id);

        var countctx = LookupContext.ByNonOwner;

        if (ctx.MatchFlag("a", "all"))
        {
            if (system.Id == ctx.System.Id)
            {
                countctx = LookupContext.ByOwner;
            }
            else
            {
                throw Errors.LookupNotAllowed;
            }
        }

        var memberCount = await _repo.GetGroupMemberCount(target.Id, countctx == LookupContext.ByOwner?null : PrivacyLevel.Public);

        var nameField = target.NamePrivacy.Get(pctx, target.Name, target.DisplayName ?? target.Name);

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

        uint color;

        try
        {
            color = target.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
        }
        catch (ArgumentException)
        {
            // There's no API for group colors yet, but defaulting to a blank color regardless
            color = DiscordUtils.Gray;
        }

        var eb = new EmbedBuilder()
                 .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}"))
                 .Color(color);

        eb.Footer(new Embed.EmbedFooter($"System ID: {system.Hid} | Group ID: {target.Hid}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}"));

        if (target.DescriptionPrivacy.CanAccess(pctx))
        {
            eb.Image(new Embed.EmbedImage(target.BannerImage));
        }

        if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null)
        {
            eb.Field(new Embed.Field("Display Name", target.DisplayName, true));
        }

        if (!target.Color.EmptyOrNull())
        {
            eb.Field(new Embed.Field("Color", $"#{target.Color}", true));
        }

        if (target.ListPrivacy.CanAccess(pctx))
        {
            if (memberCount == 0 && pctx == LookupContext.ByOwner)
            {
                // Only suggest the add command if this is actually the owner lol
                eb.Field(new Embed.Field("Members (0)",
                                         $"Add one with `pk;group {target.Reference(ctx)} add <member>`!"));
            }
            else
            {
                var name = pctx == LookupContext.ByOwner
                    ? target.Reference(ctx)
                    : target.Hid;
                eb.Field(new Embed.Field($"Members ({memberCount})", $"(see `pk;group {name} list`)"));
            }
        }

        if (target.DescriptionFor(pctx) is { } desc)
        {
            eb.Field(new Embed.Field("Description", desc));
        }

        if (target.IconFor(pctx) is { } icon)
        {
            eb.Thumbnail(new Embed.EmbedThumbnail(icon.TryGetCleanCdnUrl()));
        }

        return(eb.Build());
    }
Exemplo n.º 7
0
    public async Task <Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx, DateTimeZone zone)
    {
        // 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
            // this is now fixed in the API, but might still have some remnants in the database
            // so we just default to a blank color, yolo
            color = DiscordUtils.Gray;
        }

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

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

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

        var eb = new EmbedBuilder()
                 .Author(new Embed.EmbedAuthor(name, IconUrl: avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}"))
                 // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
                 .Color(color)
                 .Footer(new Embed.EmbedFooter(
                             $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}"));

        if (member.DescriptionPrivacy.CanAccess(ctx))
        {
            eb.Image(new Embed.EmbedImage(member.BannerImage));
        }

        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.TryGetCleanCdnUrl()}) 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 Embed.EmbedThumbnail(avatar.TryGetCleanCdnUrl()));
        }

        if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx))
        {
            eb.Field(new Embed.Field("Display Name", member.DisplayName.Truncate(1024), true));
        }
        if (guild != null && guildDisplayName != null)
        {
            eb.Field(new Embed.Field($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true));
        }
        if (member.BirthdayFor(ctx) != null)
        {
            eb.Field(new Embed.Field("Birthdate", member.BirthdayString, true));
        }
        if (member.PronounsFor(ctx) is { } pronouns&& !string.IsNullOrWhiteSpace(pronouns))
        {
            eb.Field(new Embed.Field("Pronouns", pronouns.Truncate(1024), true));
        }
        if (member.MessageCountFor(ctx) is { } count&& count > 0)
        {
            eb.Field(new Embed.Field("Message Count", member.MessageCount.ToString(), true));
        }
        if (member.HasProxyTags)
        {
            eb.Field(new Embed.Field("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 Embed.Field("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 Embed.Field($"Groups ({groups.Count})", content.Truncate(1000)));
        }

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

        return(eb.Build());
    }
Exemplo n.º 8
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.");
            }

            try
            {
                guild = await _rest.GetGuild(guildId);
            }
            catch (ForbiddenException)
            {
                throw Errors.GuildNotFound(guildId);
            }

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

        var guildMember = await _rest.GetGuildMember(guild.Id, await _cache.GetOwnUser());

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

        foreach (var channel in await _rest.GetGuildChannels(guild.Id))
        {
            var botPermissions     = PermissionExtensions.PermissionsFor(guild, channel, await _cache.GetOwnUser(), guildMember);
            var webhookPermissions = PermissionExtensions.EveryonePermissions(guild, channel);
            var userPermissions    = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser);

            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, show the user that some channels got ignored (so they don't get confused)
                hiddenChannels = true;
                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 ((webhookPermissions & PermissionSet.UseExternalEmojis) == 0)
            {
                missingPermissionField |= (ulong)PermissionSet.UseExternalEmojis;
                missingEmojiPermissions = true;
            }

            // 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 Embed.Field($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)));
                eb.Color(DiscordUtils.Red);
            }
        }

        var footer = "";

        if (hiddenChannels)
        {
            footer += "Some channels were ignored as you do not have view access to them.";
        }
        if (missingEmojiPermissions)
        {
            if (hiddenChannels)
            {
                footer += " | ";
            }
            footer +=
                "Use External Emojis permissions must be granted to the @everyone role / Default Permissions.";
        }

        if (footer.Length > 0)
        {
            eb.Footer(new Embed.EmbedFooter(footer));
        }

        // Send! :)
        await ctx.Reply(embed : eb.Build());
    }
Exemplo n.º 9
0
    private async Task <Embed> CreateAutoproxyStatusEmbed(Context ctx, AutoproxySettings settings)
    {
        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 = settings.AutoproxyMode switch
        {
            AutoproxyMode.Front => fronters.Length > 0 ? await ctx.Repository.GetMember(fronters[0]) : null,
            AutoproxyMode.Member when settings.AutoproxyMember.HasValue => await ctx.Repository.GetMember(settings.AutoproxyMember.Value),
            _ => null
        };

        switch (settings.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;
        }

        case AutoproxyMode.Member:
        {
            if (relevantMember == null)
            {
                // just pretend autoproxy is off if the member was deleted
                // ideally we would set it to off in the database though...
                eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
            }
            else
            {
                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 Embed.Field("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`."));
        }

        return(eb.Build());
    }