Пример #1
0
        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);
            }
Пример #2
0
        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.");
            }
        }
Пример #3
0
        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());
            }
        }
Пример #4
0
        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());
            }
        }
Пример #5
0
        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);
                }
            }
        }
Пример #6
0
        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);
            }
        }