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()); }
public async Task <Embed> CreateMessageInfoEmbed(FullMessage msg) { var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel); var ctx = LookupContext.ByNonOwner; Message serverMsg = null; try { serverMsg = await _rest.GetMessage(msg.Message.Channel, msg.Message.Mid); } catch (ForbiddenException) { // no permission, couldn't fetch, oh well } // 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 (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})*"; } // Put it all together var eb = new EmbedBuilder() .Author(new(msg.Member.NameFor(ctx), IconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx)))) .Description(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*") .Image(new(serverMsg?.Attachments?.FirstOrDefault()?.Url)) .Field(new("System", msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true)) .Field(new("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true)) .Field(new("Sent by", userStr, true)) .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O")); var roles = memberInfo?.Roles?.ToList(); if (roles != null && roles.Count > 0) { // TODO: what if role isn't in cache? figure out a fallback var rolesString = string.Join(", ", roles.Select(id => _cache.GetRole(id)) .OrderByDescending(role => role.Position) .Select(role => role.Name)); eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024))); } return(eb.Build()); }
public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, GuildMemberPartial member) => PermissionsFor(cache, channelId, userId, member.Roles);