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); }
private async Task OnEventReceived(int shardId, IGatewayEvent evt) { // we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent await _cache.HandleGatewayEvent(evt); var userId = await _cache.GetOwnUser(); await _cache.TryUpdateSelfMember(userId, evt); await OnEventReceivedInner(shardId, evt); }
private async Task <Webhook?> GetOrCreateWebhook(ulong channelId) { _logger.Debug("Webhook for channel {Channel} not found in cache, trying to fetch", channelId); _metrics.Measure.Meter.Mark(BotMetrics.WebhookCacheMisses); _logger.Debug("Finding webhook for channel {Channel}", channelId); var webhooks = await FetchChannelWebhooks(channelId); // If the channel has a webhook created by PK, just return that one var ourUserId = await _cache.GetOwnUser(); var ourWebhook = webhooks.FirstOrDefault(hook => IsWebhookMine(ourUserId, hook)); if (ourWebhook != null) { return(ourWebhook); } // We don't have one, so we gotta create a new one // but first, make sure we haven't hit the webhook cap yet... if (webhooks.Length >= 10) { throw new PKError( "This channel has the maximum amount of possible webhooks (10) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying."); } return(await DoCreateWebhook(channelId)); }
// todo: move this somewhere else private async Task <PermissionSet> GetPermissionsInLogChannel(Channel channel) { var guild = await _cache.TryGetGuild(channel.GuildId.Value); if (guild == null) { guild = await _rest.GetGuild(channel.GuildId.Value); } var guildMember = await _cache.TryGetSelfMember(channel.GuildId.Value); if (guildMember == null) { guildMember = await _rest.GetGuildMember(channel.GuildId.Value, await _cache.GetOwnUser()); } var perms = PermissionExtensions.PermissionsFor(guild, channel, await _cache.GetOwnUser(), guildMember); return(perms); }
private async Task OnEventReceived(int shardId, IGatewayEvent evt) { // we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent await _cache.HandleGatewayEvent(evt); var userId = await _cache.GetOwnUser(); await _cache.TryUpdateSelfMember(userId, evt); // HandleEvent takes a type parameter, automatically inferred by the event type // It will then look up an IEventHandler<TypeOfEvent> in the DI container and call that object's handler method // For registering new ones, see Modules.cs if (evt is MessageCreateEvent mc) { await HandleEvent(shardId, mc); } if (evt is MessageUpdateEvent mu) { await HandleEvent(shardId, mu); } if (evt is MessageDeleteEvent md) { await HandleEvent(shardId, md); } if (evt is MessageDeleteBulkEvent mdb) { await HandleEvent(shardId, mdb); } if (evt is MessageReactionAddEvent mra) { await HandleEvent(shardId, mra); } if (evt is InteractionCreateEvent ic) { await HandleEvent(shardId, ic); } }
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) { } }
public async Task Invite(Context ctx) { var clientId = _botConfig.ClientId ?? await _cache.GetOwnUser(); var permissions = PermissionSet.AddReactions | PermissionSet.AttachFiles | PermissionSet.EmbedLinks | PermissionSet.ManageMessages | PermissionSet.ManageWebhooks | PermissionSet.ReadMessageHistory | PermissionSet.SendMessages; var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(ulong)permissions}"; var botName = _botConfig.IsBetaBot ? "PluralKit Beta" : "PluralKit"; await ctx.Reply($"{Emojis.Success} Use this link to add {botName} to your server:\n<{invite}>"); }
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."); } try { guild = await _rest.GetGuild(guildId); } catch (ForbiddenException) { throw Errors.GuildNotFound(guildId); } if (guild != null) { senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); } if (guild == null || senderGuildUser == null) { throw Errors.GuildNotFound(guildId); } } var guildMember = await _rest.GetGuildMember(guild.Id, await _cache.GetOwnUser()); // Loop through every channel and group them by sets of permissions missing var permissionsMissing = new Dictionary <ulong, List <Channel> >(); var hiddenChannels = false; var missingEmojiPermissions = false; foreach (var channel in await _rest.GetGuildChannels(guild.Id)) { var botPermissions = PermissionExtensions.PermissionsFor(guild, channel, await _cache.GetOwnUser(), guildMember); var webhookPermissions = PermissionExtensions.EveryonePermissions(guild, channel); var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser); 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, show the user that some channels got ignored (so they don't get confused) hiddenChannels = true; 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 ((webhookPermissions & PermissionSet.UseExternalEmojis) == 0) { missingPermissionField |= (ulong)PermissionSet.UseExternalEmojis; missingEmojiPermissions = true; } // 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 Embed.Field($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000))); eb.Color(DiscordUtils.Red); } } var footer = ""; if (hiddenChannels) { footer += "Some channels were ignored as you do not have view access to them."; } if (missingEmojiPermissions) { if (hiddenChannels) { footer += " | "; } footer += "Use External Emojis permissions must be granted to the @everyone role / Default Permissions."; } if (footer.Length > 0) { eb.Footer(new Embed.EmbedFooter(footer)); } // Send! :) await ctx.Reply(embed : eb.Build()); }
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; } } }