Esempio n. 1
0
    public async Task Handle(int shardId, MessageCreateEvent evt)
    {
        if (evt.Author.Id == await _cache.GetOwnUser())
        {
            return;
        }
        if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply)
        {
            return;
        }
        if (IsDuplicateMessage(evt))
        {
            return;
        }

        if (!(await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.SendMessages))
        {
            return;
        }

        // spawn off saving the private channel into another thread
        // it is not a fatal error if this fails, and it shouldn't block message processing
        _ = _dmCache.TrySavePrivateChannel(evt);

        var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null;

        var channel = await _cache.GetChannel(evt.ChannelId);

        var rootChannel = await _cache.GetRootChannel(evt.ChannelId);

        // Log metrics and message info
        _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
        _lastMessageCache.AddMessage(evt);

        // Get message context from DB (tracking w/ metrics)
        MessageContext ctx;

        using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
            ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel.Id);

        // Try each handler until we find one that succeeds
        if (await TryHandleLogClean(evt, ctx))
        {
            return;
        }

        // Only do command/proxy handling if it's a user account
        if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true)
        {
            return;
        }

        if (await TryHandleCommand(shardId, evt, guild, channel, ctx))
        {
            return;
        }
        await TryHandleProxy(evt, guild, channel, ctx);
    }
Esempio n. 2
0
        public async Task Handle(Shard shard, MessageCreateEvent evt)
        {
            if (evt.Author.Id == shard.User?.Id)
            {
                return;
            }
            if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply)
            {
                return;
            }
            if (IsDuplicateMessage(evt))
            {
                return;
            }

            var guild = evt.GuildId != null?_cache.GetGuild(evt.GuildId.Value) : null;

            var channel     = _cache.GetChannel(evt.ChannelId);
            var rootChannel = _cache.GetRootChannel(evt.ChannelId);

            // Log metrics and message info
            _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
            _lastMessageCache.AddMessage(evt);

            // Get message context from DB (tracking w/ metrics)
            MessageContext ctx;

            await using (var conn = await _db.Obtain())
                using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
                    ctx = await _repo.GetMessageContext(conn, evt.Author.Id, evt.GuildId ?? default, rootChannel.Id);

            // Try each handler until we find one that succeeds
            if (await TryHandleLogClean(evt, ctx))
            {
                return;
            }

            // Only do command/proxy handling if it's a user account
            if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true)
            {
                return;
            }

            if (await TryHandleCommand(shard, evt, guild, channel, ctx))
            {
                return;
            }
            await TryHandleProxy(shard, evt, guild, channel, ctx);
        }
Esempio n. 3
0
    private async Task ExecuteProxy(Message trigger, MessageContext ctx, AutoproxySettings autoproxySettings,
                                    ProxyMatch match, bool allowEveryone, bool allowEmbeds)
    {
        // Create reply embed
        var embeds  = new List <Embed>();
        var content = "";

        if (trigger.Type == Message.MessageType.Reply && trigger.MessageReference?.ChannelId == trigger.ChannelId)
        {
            var repliedTo = trigger.ReferencedMessage.Value;
            if (repliedTo != null)
            {
                if (trigger.Mentions.Length > 0 &&
                    repliedTo.Author.Id == trigger.Mentions[0].Id &&
                    !(trigger.Content.Contains($"<@{repliedTo.Author.Id}>") ||
                      trigger.Content.Contains($"<@!{repliedTo.Author.Id}>")))
                {
                    content = $"*<@{repliedTo.Author.Id}>*\n";
                }

                var(nickname, avatar) = await FetchReferencedMessageAuthorInfo(trigger, repliedTo);

                var embed = CreateReplyEmbed(match, trigger, repliedTo, nickname, avatar);
                if (embed != null)
                {
                    embeds.Add(embed);
                }
            }

            // TODO: have a clean error for when message can't be fetched instead of just being silent
        }

        // Send the webhook
        content += match.ProxyContent;
        if (!allowEmbeds)
        {
            content = content.BreakLinkEmbeds();
        }

        var messageChannel = await _cache.GetChannel(trigger.ChannelId);

        var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);

        var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
        var guild    = await _cache.GetGuild(trigger.GuildId.Value);

        var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest
        {
            GuildId       = trigger.GuildId !.Value,
            ChannelId     = rootChannel.Id,
            ThreadId      = threadId,
            Name          = await FixSameName(messageChannel.Id, ctx, match.Member),
            AvatarUrl     = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
            Content       = content,
            Attachments   = trigger.Attachments,
            FileSizeLimit = guild.FileSizeLimit(),
            Embeds        = embeds.ToArray(),
            Stickers      = trigger.StickerItems,
            AllowEveryone = allowEveryone
        });
        public async ValueTask LogMessage(MessageContext ctx, ProxyMatch proxy, Message trigger, ulong hookMessage)
        {
            var logChannel = await GetAndCheckLogChannel(ctx, trigger);

            if (logChannel == null)
            {
                return;
            }

            var triggerChannel = _cache.GetChannel(trigger.ChannelId);

            // Send embed!
            await using var conn = await _db.Obtain();

            var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value),
                                                        await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content,
                                                        triggerChannel);
            var url = $"https://discord.com/channels/{trigger.GuildId}/{trigger.ChannelId}/{hookMessage}";
            await _rest.CreateMessage(logChannel.Id, new() { Content = url, Embed = embed });
        }
        public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, ICollection <ulong>?userRoles)
        {
            var channel = cache.GetChannel(channelId);

            if (channel.GuildId == null)
            {
                return(PermissionSet.Dm);
            }

            var guild = cache.GetGuild(channel.GuildId.Value);

            return(PermissionsFor(guild, channel, userId, userRoles));
        }
Esempio n. 6
0
    public static async Task <Channel> GetRootChannel(this IDiscordCache cache, ulong channelOrThread)
    {
        var channel = await cache.GetChannel(channelOrThread);

        if (!channel.IsThread())
        {
            return(channel);
        }

        var parent = await cache.GetChannel(channel.ParentId !.Value);

        return(parent);
    }
Esempio n. 7
0
        public async Task Handle(Shard shard, MessageUpdateEvent evt)
        {
            if (evt.Author.Value?.Id == _client.User?.Id)
            {
                return;
            }

            // Edit message events sometimes arrive with missing data; double-check it's all there
            if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
            {
                return;
            }

            var channel = _cache.GetChannel(evt.ChannelId);

            if (channel.Type != Channel.ChannelType.GuildText)
            {
                return;
            }
            var guild = _cache.GetGuild(channel.GuildId !.Value);

            // Only react to the last message in the channel
            if (_lastMessageCache.GetLastMessage(evt.ChannelId) != evt.Id)
            {
                return;
            }

            // Just run the normal message handling code, with a flag to disable autoproxying
            MessageContext ctx;

            await using (var conn = await _db.Obtain())
                using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
                    ctx = await _repo.GetMessageContext(conn, evt.Author.Value !.Id, channel.GuildId !.Value, evt.ChannelId);

            // TODO: is this missing anything?
            var equivalentEvt = new MessageCreateEvent
            {
                Id          = evt.Id,
                ChannelId   = evt.ChannelId,
                GuildId     = channel.GuildId,
                Author      = evt.Author.Value,
                Member      = evt.Member.Value,
                Content     = evt.Content.Value,
                Attachments = evt.Attachments.Value ?? Array.Empty <Message.Attachment>()
            };
            var botPermissions = _bot.PermissionsIn(channel.Id);
            await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy : false, guild : guild, channel : channel, botPermissions : botPermissions);
        }
Esempio n. 8
0
    public async Task Handle(int shardId, MessageUpdateEvent evt)
    {
        if (evt.Author.Value?.Id == await _cache.GetOwnUser())
        {
            return;
        }

        // Edit message events sometimes arrive with missing data; double-check it's all there
        if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
        {
            return;
        }

        var channel = await _cache.GetChannel(evt.ChannelId);

        if (!DiscordUtils.IsValidGuildChannel(channel))
        {
            return;
        }
        var guild = await _cache.GetGuild(channel.GuildId !.Value);

        var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current;

        // Only react to the last message in the channel
        if (lastMessage?.Id != evt.Id)
        {
            return;
        }

        // Just run the normal message handling code, with a flag to disable autoproxying
        MessageContext ctx;

        using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
            ctx = await _repo.GetMessageContext(evt.Author.Value !.Id, channel.GuildId !.Value, evt.ChannelId);

        var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);

        var botPermissions = await _cache.PermissionsIn(channel.Id);

        try
        {
            await _proxy.HandleIncomingMessage(equivalentEvt, ctx, allowAutoproxy : false, guild : guild,
                                               channel : channel, botPermissions : botPermissions);
        }
        // Catch any failed proxy checks so they get ignored in the global error handler
        catch (ProxyService.ProxyChecksFailedException) { }
    }
Esempio n. 9
0
        private async Task ExecuteProxy(Shard shard, IPKConnection conn, Message trigger, MessageContext ctx,
                                        ProxyMatch match, bool allowEveryone, bool allowEmbeds)
        {
            // Create reply embed
            var embeds = new List <Embed>();

            if (trigger.Type == Message.MessageType.Reply && trigger.MessageReference?.ChannelId == trigger.ChannelId)
            {
                var repliedTo = trigger.ReferencedMessage.Value;
                if (repliedTo != null)
                {
                    var nickname = await FetchReferencedMessageAuthorNickname(trigger, repliedTo);

                    var embed = CreateReplyEmbed(match, trigger, repliedTo, nickname);
                    if (embed != null)
                    {
                        embeds.Add(embed);
                    }
                }

                // TODO: have a clean error for when message can't be fetched instead of just being silent
            }

            // Send the webhook
            var content = match.ProxyContent;

            if (!allowEmbeds)
            {
                content = content.BreakLinkEmbeds();
            }

            var messageChannel = _cache.GetChannel(trigger.ChannelId);
            var rootChannel    = _cache.GetRootChannel(trigger.ChannelId);
            var threadId       = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;

            var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest
            {
                GuildId       = trigger.GuildId !.Value,
                ChannelId     = rootChannel.Id,
                ThreadId      = threadId,
                Name          = match.Member.ProxyName(ctx),
                AvatarUrl     = match.Member.ProxyAvatar(ctx),
                Content       = content,
                Attachments   = trigger.Attachments,
                Embeds        = embeds.ToArray(),
                AllowEveryone = allowEveryone,
            });
Esempio n. 10
0
        public async Task Handle(Shard shard, MessageUpdateEvent evt)
        {
            if (evt.Author.Value?.Id == _client.User?.Id)
            {
                return;
            }

            // Edit message events sometimes arrive with missing data; double-check it's all there
            if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
            {
                return;
            }

            var channel = _cache.GetChannel(evt.ChannelId);

            if (!DiscordUtils.IsValidGuildChannel(channel))
            {
                return;
            }
            var guild       = _cache.GetGuild(channel.GuildId !.Value);
            var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId);

            // Only react to the last message in the channel
            if (lastMessage?.Id != evt.Id)
            {
                return;
            }

            // Just run the normal message handling code, with a flag to disable autoproxying
            MessageContext ctx;

            await using (var conn = await _db.Obtain())
                using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
                    ctx = await _repo.GetMessageContext(conn, evt.Author.Value !.Id, channel.GuildId !.Value, evt.ChannelId);

            var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);

            var botPermissions = _bot.PermissionsIn(channel.Id);
            await _proxy.HandleIncomingMessage(shard, equivalentEvt, ctx, allowAutoproxy : false, guild : guild, channel : channel, botPermissions : botPermissions);
        }
Esempio n. 11
0
    public async Task ShowBlacklisted(Context ctx)
    {
        await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");

        var blacklist = await ctx.Repository.GetGuild(ctx.Guild.Id);

        // Resolve all channels from the cache and order by position
        var channels = (await Task.WhenAll(blacklist.Blacklist
                                           .Select(id => _cache.TryGetChannel(id))))
                       .Where(c => c != null)
                       .OrderBy(c => c.Position)
                       .ToList();

        if (channels.Count == 0)
        {
            await ctx.Reply("This server has no blacklisted channels.");

            return;
        }

        await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25,
                           $"Blacklisted channels for {ctx.Guild.Name}",
                           null,
                           async (eb, l) =>
        {
            async Task <string> CategoryName(ulong?id) =>
            id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";

            ulong?lastCategory = null;

            var fieldValue = new StringBuilder();
            foreach (var channel in l)
            {
                if (lastCategory != channel !.ParentId && fieldValue.Length > 0)
                {
                    eb.Field(new Embed.Field(await CategoryName(lastCategory), fieldValue.ToString()));
                    fieldValue.Clear();
                }
Esempio n. 12
0
    public async ValueTask LogMessage(MessageContext ctx, PKMessage proxiedMessage, Message trigger,
                                      Message hookMessage, string oldContent = null)
    {
        var logChannelId = await GetAndCheckLogChannel(ctx, trigger, proxiedMessage);

        if (logChannelId == null)
        {
            return;
        }

        var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel);

        var system = await _repo.GetSystem(ctx.SystemId.Value);

        var member = await _repo.GetMember(proxiedMessage.Member !.Value);

        // Send embed!
        var embed = _embed.CreateLoggedMessageEmbed(trigger, hookMessage, system.Hid, member, triggerChannel.Name,
                                                    oldContent);
        var url =
            $"https://discord.com/channels/{proxiedMessage.Guild.Value}/{proxiedMessage.Channel}/{proxiedMessage.Mid}";
        await _rest.CreateMessage(logChannelId.Value, new MessageRequest { Content = url, Embeds = new[] { embed } });
    }
Esempio n. 13
0
        public async ValueTask LogMessage(MessageContext ctx, ProxyMatch proxy, Message trigger, ulong hookMessage)
        {
            if (ctx.SystemId == null || ctx.LogChannel == null || ctx.InLogBlacklist)
            {
                return;
            }

            // Find log channel and check if valid
            var logChannel = await FindLogChannel(trigger.GuildId !.Value, ctx.LogChannel.Value);

            if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText)
            {
                return;
            }

            var triggerChannel = _cache.GetChannel(trigger.ChannelId);

            // Check bot permissions
            var perms = _bot.PermissionsIn(logChannel.Id);

            if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
            {
                _logger.Information(
                    "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})",
                    ctx.LogChannel.Value, trigger.GuildId !.Value, perms);
                return;
            }

            // Send embed!
            await using var conn = await _db.Obtain();

            var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value),
                                                        await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content,
                                                        triggerChannel);
            var url = $"https://discord.com/channels/{trigger.GuildId}/{trigger.ChannelId}/{hookMessage}";
            await _rest.CreateMessage(logChannel.Id, new() { Content = url, Embed = embed });
        }
Esempio n. 14
0
        private async ValueTask TryHandleProxyMessageReactions(MessageReactionAddEvent evt)
        {
            // Sometimes we get events from users that aren't in the user cache
            // We just ignore all of those for now, should be quite rare...
            if (!_cache.TryGetUser(evt.UserId, out var user))
            {
                return;
            }

            var channel = _cache.GetChannel(evt.ChannelId);

            // check if it's a command message first
            // since this can happen in DMs as well
            if (evt.Emoji.Name == "\u274c")
            {
                await using var conn = await _db.Obtain();

                var commandMsg = await _commandMessageService.GetCommandMessage(conn, evt.MessageId);

                if (commandMsg != null)
                {
                    await HandleCommandDeleteReaction(evt, commandMsg);

                    return;
                }
            }

            // Only proxies in guild text channels
            if (channel.Type != Channel.ChannelType.GuildText)
            {
                return;
            }

            // Ignore reactions from bots (we can't DM them anyway)
            if (user.Bot)
            {
                return;
            }

            switch (evt.Emoji.Name)
            {
            // Message deletion
            case "\u274C":     // Red X
            {
                await using var conn = await _db.Obtain();

                var msg = await _repo.GetMessage(conn, evt.MessageId);

                if (msg != null)
                {
                    await HandleProxyDeleteReaction(evt, msg);
                }

                break;
            }

            case "\u2753":     // Red question mark
            case "\u2754":     // White question mark
            {
                await using var conn = await _db.Obtain();

                var msg = await _repo.GetMessage(conn, evt.MessageId);

                if (msg != null)
                {
                    await HandleQueryReaction(evt, msg);
                }

                break;
            }

            case "\U0001F514": // Bell
            case "\U0001F6CE": // Bellhop bell
            case "\U0001F3D3": // Ping pong paddle (lol)
            case "\u23F0":     // Alarm clock
            case "\u2757":     // Exclamation mark
            {
                await using var conn = await _db.Obtain();

                var msg = await _repo.GetMessage(conn, evt.MessageId);

                if (msg != null)
                {
                    await HandlePingReaction(evt, msg);
                }
                break;
            }
            }
        }
Esempio n. 15
0
    private async ValueTask TryHandleProxyMessageReactions(MessageReactionAddEvent evt)
    {
        // ignore any reactions added by *us*
        if (evt.UserId == await _cache.GetOwnUser())
        {
            return;
        }

        // Ignore reactions from bots (we can't DM them anyway)
        // note: this used to get from cache since this event does not contain Member in DMs
        // but we aren't able to get DMs from bots anyway, so it's not really needed
        if (evt.GuildId != null && evt.Member.User.Bot)
        {
            return;
        }

        var channel = await _cache.GetChannel(evt.ChannelId);

        // check if it's a command message first
        // since this can happen in DMs as well
        if (evt.Emoji.Name == "\u274c")
        {
            // in DMs, allow deleting any PK message
            if (channel.GuildId == null)
            {
                await HandleCommandDeleteReaction(evt, null);

                return;
            }

            var commandMsg = await _commandMessageService.GetCommandMessage(evt.MessageId);

            if (commandMsg != null)
            {
                await HandleCommandDeleteReaction(evt, commandMsg);

                return;
            }
        }

        // Proxied messages only exist in guild text channels, so skip checking if we're elsewhere
        if (!DiscordUtils.IsValidGuildChannel(channel))
        {
            return;
        }

        switch (evt.Emoji.Name)
        {
        // Message deletion
        case "\u274C":     // Red X
        {
            var msg = await _db.Execute(c => _repo.GetMessage(c, evt.MessageId));

            if (msg != null)
            {
                await HandleProxyDeleteReaction(evt, msg);
            }

            break;
        }

        case "\u2753":     // Red question mark
        case "\u2754":     // White question mark
        {
            var msg = await _db.Execute(c => _repo.GetMessage(c, evt.MessageId));

            if (msg != null)
            {
                await HandleQueryReaction(evt, msg);
            }

            break;
        }

        case "\U0001F514": // Bell
        case "\U0001F6CE": // Bellhop bell
        case "\U0001F3D3": // Ping pong paddle (lol)
        case "\u23F0":     // Alarm clock
        case "\u2757":     // Exclamation mark
        {
            var msg = await _db.Execute(c => _repo.GetMessage(c, evt.MessageId));

            if (msg != null)
            {
                await HandlePingReaction(evt, msg);
            }
            break;
        }
        }
    }