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); }
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); }
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)); }
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); }
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); }
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) { } }
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, });
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); }
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(); }
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 } }); }
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 }); }
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; } } }
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; } } }