public async ValueTask LogMessage(MessageContext ctx, ProxyMatch proxy, DiscordMessage 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.Channel.GuildId, ctx.LogChannel.Value); if (logChannel == null || logChannel.Type != ChannelType.Text) { return; } // Check bot permissions if (!trigger.Channel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) { _logger.Information( "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", ctx.LogChannel.Value, trigger.Channel.GuildId, trigger.Channel.BotPermissions()); return; } // Send embed! await using var conn = await _db.Obtain(); var embed = _embed.CreateLoggedMessageEmbed(await conn.QuerySystem(ctx.SystemId.Value), await conn.QueryMember(proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, trigger.Channel); var url = $"https://discord.com/channels/{trigger.Channel.GuildId}/{trigger.ChannelId}/{hookMessage}"; await logChannel.SendMessageFixedAsync(content : url, embed : embed); }
private async Task ExecuteProxy(DiscordClient shard, IPKConnection conn, DiscordMessage trigger, MessageContext ctx, ProxyMatch match, bool allowEveryone, bool allowEmbeds) { // Create reply embed var embeds = new List <DiscordEmbed>(); if (trigger.Reference?.Channel?.Id == trigger.ChannelId) { var repliedTo = await FetchReplyOriginalMessage(trigger.Reference); if (repliedTo != null) { var embed = CreateReplyEmbed(repliedTo); 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 proxyMessage = await _webhookExecutor.ExecuteWebhook(trigger.Channel, FixSingleCharacterName(match.Member.ProxyName(ctx)), match.Member.ProxyAvatar(ctx), content, trigger.Attachments, embeds, allowEveryone); await HandleProxyExecutedActions(shard, conn, ctx, trigger, proxyMessage, match); }
private bool TryMatchAutoproxy(MessageContext ctx, IReadOnlyCollection <ProxyMember> members, string messageContent, out ProxyMatch match) { match = default; // Find the member we should autoproxy (null if none) var member = ctx.AutoproxyMode switch { AutoproxyMode.Member when ctx.AutoproxyMember != null => members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx.LastMessage) => members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value), _ => null }; if (member == null) { return(false); } match = new ProxyMatch { Content = messageContent, Member = member, // We're autoproxying, so not using any proxy tags here // we just find the first pair of tags (if any), otherwise null ProxyTags = member.ProxyTags.FirstOrDefault() }; return(true); }
public bool TryMatch(IEnumerable <ProxyMember> members, string?input, out ProxyMatch result) { result = default; // Null input is valid and is equivalent to empty string if (input == null) { return(false); } // If the message starts with a @mention, and then proceeds to have proxy tags, // extract the mention and place it inside the inner message // eg. @Ske [text] => [@Ske text] var leadingMention = ExtractLeadingMention(ref input); // "Flatten" list of members to a list of tag-member pairs // Then order them by "tag specificity" // (prefix+suffix length desc = inner message asc = more specific proxy first) var tags = members .SelectMany(member => member.ProxyTags.Select(tag => (tag, member))) .OrderByDescending(p => p.tag.ProxyString.Length); // Iterate now-ordered list of tags and try matching each one foreach (var(tag, member) in tags) { result.ProxyTags = tag; result.Member = member; // Skip blank tags (shouldn't ever happen in practice) if (tag.Prefix == null && tag.Suffix == null) { continue; } // Can we match with these tags? if (TryMatchTagsInner(input, tag, out result.Content)) { // If we extracted a leading mention before, add that back now if (leadingMention != null) { result.Content = $"{leadingMention} {result.Content}"; } return(true); } // (if not, keep going) } // We couldn't match anything :( return(false); }
private async Task ExecuteProxy(IPKConnection conn, DiscordMessage trigger, MessageContext ctx, ProxyMatch match, bool allowEveryone, bool allowEmbeds) { // Send the webhook var content = match.ProxyContent; if (!allowEmbeds) { content = content.BreakLinkEmbeds(); } var id = await _webhookExecutor.ExecuteWebhook(trigger.Channel, match.Member.ProxyName(ctx), match.Member.ProxyAvatar(ctx), content, trigger.Attachments, allowEveryone); Task SaveMessage() => _repo.AddMessage(conn, new PKMessage { Channel = trigger.ChannelId, Guild = trigger.Channel.GuildId, Member = match.Member.Id, Mid = id, OriginalMid = trigger.Id, Sender = trigger.Author.Id }); Task LogMessage() => _logChannel.LogMessage(ctx, match, trigger, id).AsTask(); async Task DeleteMessage() { // Wait a second or so before deleting the original message await Task.Delay(MessageDeletionDelay); try { await trigger.DeleteAsync(); } catch (NotFoundException) { // If it's already deleted, we just log and swallow the exception _logger.Warning("Attempted to delete already deleted proxy trigger message {Message}", trigger.Id); } } // Run post-proxy actions (simultaneously; order doesn't matter) // Note that only AddMessage is using our passed-in connection, careful not to pass it elsewhere and run into conflicts await Task.WhenAll( DeleteMessage(), SaveMessage(), LogMessage() ); }
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, });
private async Task HandleProxyExecutedActions(DiscordClient shard, IPKConnection conn, MessageContext ctx, DiscordMessage triggerMessage, DiscordMessage proxyMessage, ProxyMatch match) { Task SaveMessageInDatabase() => _repo.AddMessage(conn, new PKMessage { Channel = triggerMessage.ChannelId, Guild = triggerMessage.Channel.GuildId, Member = match.Member.Id, Mid = proxyMessage.Id, OriginalMid = triggerMessage.Id, Sender = triggerMessage.Author.Id }); Task LogMessageToChannel() => _logChannel.LogMessage(shard, ctx, match, triggerMessage, proxyMessage.Id).AsTask(); async Task DeleteProxyTriggerMessage() { // Wait a second or so before deleting the original message await Task.Delay(MessageDeletionDelay); try { await triggerMessage.DeleteAsync(); } catch (NotFoundException) { _logger.Debug("Trigger message {TriggerMessageId} was already deleted when we attempted to; deleting proxy message {ProxyMessageId} also", triggerMessage.Id, proxyMessage.Id); await HandleTriggerAlreadyDeleted(proxyMessage); // Swallow the exception, we don't need it } } // Run post-proxy actions (simultaneously; order doesn't matter) // Note that only AddMessage is using our passed-in connection, careful not to pass it elsewhere and run into conflicts await Task.WhenAll( DeleteProxyTriggerMessage(), SaveMessageInDatabase(), LogMessageToChannel() ); }
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 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 bool TryMatchTags(IReadOnlyCollection <ProxyMember> members, string messageContent, bool hasAttachments, out ProxyMatch match) { if (!_parser.TryMatch(members, messageContent, out match)) { return(false); } // Edge case: If we got a match with blank inner text, we'd normally just send w/ attachments // However, if there are no attachments, the user probably intended something else, so we "un-match" and proceed to autoproxy return(hasAttachments || match.Content.Trim().Length > 0); }
public bool TryMatch(MessageContext ctx, IReadOnlyCollection <ProxyMember> members, out ProxyMatch match, string messageContent, bool hasAttachments, bool allowAutoproxy) { if (TryMatchTags(members, messageContent, hasAttachments, out match)) { return(true); } if (allowAutoproxy && TryMatchAutoproxy(ctx, members, messageContent, out match)) { return(true); } return(false); }