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