public async Task Avatar(Context ctx) { ctx.CheckSystem(); async Task ClearIcon() { await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { AvatarUrl = null })); await ctx.Reply($"{Emojis.Success} System icon cleared."); } async Task SetIcon(ParsedImage img) { if (img.Url.Length > Limits.MaxUriLength) { throw Errors.InvalidUrl(img.Url); } await AvatarUtils.VerifyAvatarOrThrow(img.Url); await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.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, embed: new DiscordEmbedBuilder().WithImageUrl(img.Url).Build()) : ctx.Reply(msg)); } async Task ShowIcon() { if ((ctx.System.AvatarUrl?.Trim() ?? "").Length > 0) { var eb = new DiscordEmbedBuilder() .WithTitle("System icon") .WithImageUrl(ctx.System.AvatarUrl) .WithDescription("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 (ctx.MatchClear()) { await ClearIcon(); } else if (await ctx.MatchImage() is {} img) { await SetIcon(img); }
public async Task Description(Context ctx, PKMember target) { if (await ctx.MatchClear("this member's description")) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Description = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member description cleared."); } else if (!ctx.HasNext()) { if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System))) { throw Errors.LookupNotAllowed; } if (target.Description == null) { if (ctx.System?.Id == target.System) { await ctx.Reply($"This member does not have a description set. To set one, type `pk;member {target.Reference()} description <description>`."); } else { await ctx.Reply("This member does not have a description set."); } } else if (ctx.MatchFlag("r", "raw")) { await ctx.Reply($"```\n{target.Description}\n```"); } else { await ctx.Reply(embed : new EmbedBuilder() .Title("Member description") .Description(target.Description) .Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : ""))) .Build()); } } else { ctx.CheckOwnMember(target); var description = ctx.RemainderOrNull().NormalizeLineEndSpacing(); if (description.IsLongerThan(Limits.MaxDescriptionLength)) { throw Errors.DescriptionTooLongError(description.Length); } var patch = new MemberPatch { Description = Partial <string> .Present(description) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member description changed."); } }
public async Task Color(Context ctx, PKMember target) { var color = ctx.RemainderOrNull(); if (await ctx.MatchClear()) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Color = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member color cleared."); } else if (!ctx.HasNext()) { // if (!target.ColorPrivacy.CanAccess(ctx.LookupContextFor(target.System))) // throw Errors.LookupNotAllowed; if (target.Color == null) { if (ctx.System?.Id == target.System) { await ctx.Reply( $"This member does not have a color set. To set one, type `pk;member {target.Reference()} color <color>`."); } else { await ctx.Reply("This member does not have a color set."); } } else { await ctx.Reply(embed : new EmbedBuilder() .Title("Member color") .Color(target.Color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) .Description($"This member's color is **#{target.Color}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} color -clear`." : "")) .Build()); } } else { ctx.CheckOwnMember(target); if (color.StartsWith("#")) { color = color.Substring(1); } if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) { throw Errors.InvalidColorError(color); } var patch = new MemberPatch { Color = Partial <string> .Present(color.ToLowerInvariant()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply(embed : new EmbedBuilder() .Title($"{Emojis.Success} Member color changed.") .Color(color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) .Build()); } }
public async Task Color(Context ctx, PKMember target) { var color = ctx.RemainderOrNull(); if (MatchClear(ctx)) { CheckEditMemberPermission(ctx, target); target.Color = null; await _data.SaveMember(target); await ctx.Reply($"{Emojis.Success} Member color cleared."); } else if (!ctx.HasNext()) { // if (!target.ColorPrivacy.CanAccess(ctx.LookupContextFor(target.System))) // throw Errors.LookupNotAllowed; if (target.Color == null) { if (ctx.System?.Id == target.System) { await ctx.Reply( $"This member does not have a color set. To set one, type `pk;member {target.Hid} color <color>`."); } else { await ctx.Reply("This member does not have a color set."); } } else { await ctx.Reply(embed : new DiscordEmbedBuilder() .WithTitle("Member color") .WithColor(target.Color.ToDiscordColor().Value) .WithThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") .WithDescription($"This member's color is **#{target.Color}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Hid} color -clear`." : "")) .Build()); } } else { CheckEditMemberPermission(ctx, target); if (color.StartsWith("#")) { color = color.Substring(1); } if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) { throw Errors.InvalidColorError(color); } target.Color = color.ToLower(); await _data.SaveMember(target); await ctx.Reply(embed : new DiscordEmbedBuilder() .WithTitle($"{Emojis.Success} Member color changed.") .WithColor(target.Color.ToDiscordColor().Value) .WithThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20") .Build()); } }
public static async Task VerifyAvatarOrThrow(string url) { if (url.Length > Limits.MaxUriLength) { throw Errors.UrlTooLong(url); } // List of MIME types we consider acceptable var acceptableMimeTypes = new[] { "image/jpeg", "image/gif", "image/png" // TODO: add image/webp once ImageSharp supports this }; using (var client = new HttpClient()) { Uri uri; try { uri = new Uri(url); if (!uri.IsAbsoluteUri || (uri.Scheme != "http" && uri.Scheme != "https")) { throw Errors.InvalidUrl(url); } } catch (UriFormatException) { throw Errors.InvalidUrl(url); } var response = await client.GetAsync(uri); if (!response.IsSuccessStatusCode) // Check status code { throw Errors.AvatarServerError(response.StatusCode); } if (response.Content.Headers.ContentLength == null) // Check presence of content length { throw Errors.AvatarNotAnImage(null); } if (response.Content.Headers.ContentLength > Limits.AvatarFileSizeLimit) // Check content length { throw Errors.AvatarFileSizeLimit(response.Content.Headers.ContentLength.Value); } if (!acceptableMimeTypes.Contains(response.Content.Headers.ContentType.MediaType)) // Check MIME type { throw Errors.AvatarNotAnImage(response.Content.Headers.ContentType.MediaType); } // Parse the image header in a worker var stream = await response.Content.ReadAsStreamAsync(); var image = await Task.Run(() => Image.Identify(stream)); if (image == null) { throw Errors.AvatarInvalid; } if (image.Width > Limits.AvatarDimensionLimit || image.Height > Limits.AvatarDimensionLimit) // Check image size { throw Errors.AvatarDimensionsTooLarge(image.Width, image.Height); } } }
public async Task HandleMessageAsync(GuildConfig guild, CachedAccount account, IMessage message) { // Bail early if this isn't in a guild channel if (!(message.Channel is ITextChannel channel)) { return; } // Find a member with proxy tags matching the message var match = GetProxyTagMatch(message.Content, account.System, account.Members); // O(n) lookup since n is small (max ~100 in prod) and we're more constrained by memory (for a dictionary) here var systemSettingsForGuild = account.SettingsForGuild(channel.GuildId); // If we didn't get a match by proxy tags, try to get one by autoproxy // Also try if we *did* get a match, but there's no inner text. This happens if someone sends a message that // is equal to someone else's tags, and messages like these should be autoproxied if possible if (match == null || (match.InnerText.Trim().Length == 0 && message.Attachments.Count == 0)) { match = await GetAutoproxyMatch(account, systemSettingsForGuild, message, channel); } // If we still haven't found any, just yeet if (match == null) { return; } // And make sure the channel's not blacklisted from proxying. if (guild.Blacklist.Contains(channel.Id)) { return; } // Make sure the system hasn't blacklisted the guild either if (!systemSettingsForGuild.ProxyEnabled) { return; } // We know message.Channel can only be ITextChannel as PK doesn't work in DMs/groups // Afterwards we ensure the bot has the right permissions, otherwise bail early if (!await EnsureBotPermissions(channel)) { return; } // Can't proxy a message with no content and no attachment if (match.InnerText.Trim().Length == 0 && message.Attachments.Count == 0) { return; } var memberSettingsForGuild = account.SettingsForMemberGuild(match.Member.Id, channel.GuildId); // Get variables in order and all var proxyName = match.Member.ProxyName(match.System.Tag, memberSettingsForGuild.DisplayName); var avatarUrl = match.Member.AvatarUrl ?? match.System.AvatarUrl; // If the name's too long (or short), bail if (proxyName.Length < 2) { throw Errors.ProxyNameTooShort(proxyName); } if (proxyName.Length > Limits.MaxProxyNameLength) { throw Errors.ProxyNameTooLong(proxyName); } // Add the proxy tags into the proxied message if that option is enabled // Also check if the member has any proxy tags - some cases autoproxy can return a member with no tags var messageContents = (match.Member.KeepProxy && match.ProxyTags.HasValue) ? $"{match.ProxyTags.Value.Prefix}{match.InnerText}{match.ProxyTags.Value.Suffix}" : match.InnerText; // Sanitize @everyone, but only if the original user wouldn't have permission to messageContents = SanitizeEveryoneMaybe(message, messageContents); // Execute the webhook itself var hookMessageId = await _webhookExecutor.ExecuteWebhook( channel, proxyName, avatarUrl, messageContents, message.Attachments ); // Store the message in the database, and log it in the log channel (if applicable) await _data.AddMessage(message.Author.Id, hookMessageId, channel.GuildId, message.Channel.Id, message.Id, match.Member); await _logChannel.LogMessage(match.System, match.Member, hookMessageId, message.Id, message.Channel as IGuildChannel, message.Author, match.InnerText, guild); // Wait a second or so before deleting the original message await Task.Delay(1000); try { await message.DeleteAsync(); } catch (HttpException) { // If it's already deleted, we just log and swallow the exception _logger.Warning("Attempted to delete already deleted proxy trigger message {Message}", message.Id); } }