Esempio n. 1
0
        private async ValueTask <bool> TryHandleCommand(Shard shard, MessageCreateEvent evt, Guild?guild, Channel channel, MessageContext ctx)
        {
            var content = evt.Content;

            if (content == null)
            {
                return(false);
            }

            // Check for command prefix
            if (!HasCommandPrefix(content, shard.User?.Id ?? default, out var cmdStart))
            {
                return(false);
            }

            // Trim leading whitespace from command without actually modifying the string
            // This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string
            var trimStartLengthDiff = content.Substring(cmdStart).Length - content.Substring(cmdStart).TrimStart().Length;

            cmdStart += trimStartLengthDiff;

            try
            {
                var system = ctx.SystemId != null ? await _db.Execute(c => _repo.GetSystem(c, ctx.SystemId.Value)) : null;

                await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.PermissionsIn(channel.Id)));
            }
            catch (PKError)
            {
                // Only permission errors will ever bubble this far and be caught here instead of Context.Execute
                // so we just catch and ignore these. TODO: this may need to change.
            }

            return(true);
        }
Esempio n. 2
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. 3
0
        private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, FullMessage msg)
        {
            if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
            {
                return;
            }

            // Can only delete your own message
            if (msg.Message.Sender != evt.UserId)
            {
                return;
            }

            try
            {
                await _rest.DeleteMessage(evt.ChannelId, evt.MessageId);
            }
            catch (NotFoundException)
            {
                // Message was deleted by something/someone else before we got to it
            }

            await _db.Execute(c => _repo.DeleteMessage(c, evt.MessageId));
        }
Esempio n. 4
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. 5
0
        // TODO: should this class take the Scope by dependency injection instead?
        // Would allow us to create a centralized "chain of handlers" where this class could just be registered as an entry in

        public void Enrich(Scope scope, Shard shard, MessageCreateEvent evt)
        {
            scope.AddBreadcrumb(evt.Content, "event.message", data: new Dictionary <string, string>
            {
                { "user", evt.Author.Id.ToString() },
                { "channel", evt.ChannelId.ToString() },
                { "guild", evt.GuildId.ToString() },
                { "message", evt.Id.ToString() },
            });
            scope.SetTag("shard", shard.ShardInfo.ShardId.ToString());

            // Also report information about the bot's permissions in the channel
            // We get a lot of permission errors so this'll be useful for determining problems
            var perms = _bot.PermissionsIn(evt.ChannelId);

            scope.AddBreadcrumb(perms.ToPermissionString(), "permissions");
        }
Esempio n. 6
0
        public ILogEventEnricher GetEnricher(Shard shard, IGatewayEvent evt)
        {
            var props = new List <LogEventProperty>
            {
                new("ShardId", new ScalarValue(shard.ShardId)),
            };

            var(guild, channel) = GetGuildChannelId(evt);
            var user    = GetUserId(evt);
            var message = GetMessageId(evt);

            if (guild != null)
            {
                props.Add(new("GuildId", new ScalarValue(guild.Value)));
            }

            if (channel != null)
            {
                props.Add(new("ChannelId", new ScalarValue(channel.Value)));

                if (_cache.TryGetChannel(channel.Value, out _))
                {
                    var botPermissions = _bot.PermissionsIn(channel.Value);
                    props.Add(new("BotPermissions", new ScalarValue(botPermissions)));
                }
            }

            if (message != null)
            {
                props.Add(new("MessageId", new ScalarValue(message.Value)));
            }

            if (user != null)
            {
                props.Add(new("UserId", new ScalarValue(user.Value)));
            }

            if (evt is MessageCreateEvent mce)
            {
                props.Add(new("UserPermissions", new ScalarValue(_cache.PermissionsFor(mce))));
            }

            return(new Inner(props));
        }
        private async Task <Channel?> GetAndCheckLogChannel(MessageContext ctx, Message trigger, PKMessage original = null)
        {
            var guildId       = trigger.GuildId != null ? trigger.GuildId !.Value : original.Guild.Value;
            var logChannelId  = ctx.LogChannel;
            var isBlacklisted = ctx.InLogBlacklist;

            if (original != null)
            {
                // we're editing a message, get log channel info from the database
                var guild = await _db.Execute(c => _repo.GetGuild(c, original.Guild.Value));

                logChannelId  = guild.LogChannel;
                isBlacklisted = guild.Blacklist.Any(x => x == logChannelId);
            }

            if (ctx.SystemId == null || logChannelId == null || isBlacklisted)
            {
                return(null);
            }


            // Find log channel and check if valid
            var logChannel = await FindLogChannel(guildId, logChannelId.Value);

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

            // 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(null);
            }

            return(logChannel);
        }
Esempio n. 8
0
        private async ValueTask <bool> TryHandleProxy(Shard shard, MessageCreateEvent evt, Guild guild, Channel channel, MessageContext ctx)
        {
            var botPermissions = _bot.PermissionsIn(channel.Id);

            try
            {
                return(await _proxy.HandleIncomingMessage(shard, evt, ctx, guild, channel, allowAutoproxy : ctx.AllowAutoproxy, botPermissions));
            }
            catch (PKError e)
            {
                // User-facing errors, print to the channel properly formatted
                if (botPermissions.HasFlag(PermissionSet.SendMessages))
                {
                    await _rest.CreateMessage(evt.ChannelId,
                                              new MessageRequest { Content = $"{Emojis.Error} {e.Message}" });
                }
            }

            return(false);
        }
Esempio n. 9
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. 10
0
        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());
        }