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