From 882e05ac6e40c768359e15b7fe0cfa647310d97f Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 17 Aug 2020 23:08:13 -0400 Subject: [PATCH] Discord improvements - Channel 0 can be mapped to indicate all unmapped channels the bot has access to. - Discord bot now "types" when responding to a command --- .../Components/Chat/ChatManager.cs | 10 +- .../Components/Chat/Message.cs | 11 +- .../Chat/Providers/DiscordProvider.cs | 193 ++++++++++++------ 3 files changed, 150 insertions(+), 64 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 49ffccb072..25807b4d2f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -21,7 +21,10 @@ namespace Tgstation.Server.Host.Components.Chat #pragma warning disable CA1506 sealed class ChatManager : IChatManager, IRestartHandler { - const string CommonMention = "!tgs"; + /// + /// The common bot mention. + /// + public const string CommonMention = "!tgs"; /// /// The for the @@ -415,7 +418,11 @@ namespace Tgstation.Server.Host.Components.Chat { // prune disconnected providers foreach (var I in messageTasks.Where(x => !x.Key.Disposed).ToList()) + { messageTasks.Remove(I.Key); + if (I.Value.IsCompleted) + (await I.Value.ConfigureAwait(false))?.Context?.Dispose(); + } // add new ones Task updatedTask; @@ -439,6 +446,7 @@ namespace Tgstation.Server.Host.Components.Chat foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList()) { var message = await I.Value.ConfigureAwait(false); + using var messageContext = message?.Context; var messageNumber = Interlocked.Increment(ref messagesProcessed); using (LogContext.PushProperty("ChatMessage", messageNumber)) await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs index 5f661fd4f7..ef87a84d34 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Components.Chat.Providers +using System; + +namespace Tgstation.Server.Host.Components.Chat.Providers { /// /// Represents a message recieved by a @@ -14,5 +16,10 @@ /// The who sent the /// public ChatUser User { get; set; } + + /// + /// The that should be d once the is processed. + /// + public IDisposable Context { get; set; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 4488b7319a..d624c960d4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -106,53 +106,76 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (e.Author.Id == client.CurrentUser.Id) return; - if (e.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase)) + IDisposable typingState = null; + void StartTyping() => typingState = e.Channel.EnterTypingState(); + try { - // DCT: None available - await SendMessage( - e.Channel.Id, - "https://youtu.be/LrNu-SuFF_o", - default) - .ConfigureAwait(false); - } - - var pm = e.Channel is IPrivateChannel; - - if (!pm && !mappedChannels.Contains(e.Channel.Id)) - { - var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id); - if (mentionedUs) + if (e.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase)) { - Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username); + StartTyping(); // DCT: None available - await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false); + await SendMessage( + e.Channel.Id, + "https://youtu.be/LrNu-SuFF_o", + default) + .ConfigureAwait(false); + return; } - return; - } + var pm = e.Channel is IPrivateChannel; + var shouldNotAnswer = !pm; + if (shouldNotAnswer) + lock (mappedChannels) + shouldNotAnswer = !mappedChannels.Contains(e.Channel.Id); - var result = new Message - { - Content = NormalizeMentions(e.Content), - User = new ChatUser + var content = NormalizeMentions(e.Content); + var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) + || (!shouldNotAnswer && content.Split(' ').First().Equals(ChatManager.CommonMention, StringComparison.OrdinalIgnoreCase)); + if (mentionedUs) + StartTyping(); + + if (shouldNotAnswer) { - RealId = e.Author.Id, - Channel = new ChannelRepresentation + if (mentionedUs) { - RealId = e.Channel.Id, - IsPrivateChannel = pm, - ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN", - FriendlyName = e.Channel.Name + Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username); - // isAdmin and Tag populated by manager - }, - FriendlyName = e.Author.Username, - Mention = NormalizeMentions(e.Author.Mention) + // DCT: None available + await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false); + } + + return; } - }; - EnqueueMessage(result); + var result = new Message + { + Content = content, + User = new ChatUser + { + RealId = e.Author.Id, + Channel = new ChannelRepresentation + { + RealId = e.Channel.Id, + IsPrivateChannel = pm, + ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN", + FriendlyName = e.Channel.Name + + // isAdmin and Tag populated by manager + }, + FriendlyName = e.Author.Username, + Mention = NormalizeMentions(e.Author.Mention) + }, + Context = typingState + }; + + EnqueueMessage(result); + typingState = null; + } + finally + { + typingState?.Dispose(); + } } /// @@ -236,29 +259,47 @@ namespace Tgstation.Server.Host.Components.Chat.Providers throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); var channelId = channelFromDB.DiscordChannelId.Value; - var discordChannel = client.GetChannel(channelId); - if (discordChannel is ITextChannel textChannel) + ulong discordChannelId; + string connectionName; + string friendlyName; + if (channelId == 0) { - var channelModel = new ChannelRepresentation + connectionName = client.CurrentUser.Username; + friendlyName = "(Unmapped accessible channels)"; + discordChannelId = 0; + } + else + { + var discordChannel = client.GetChannel(channelId); + if (!(discordChannel is ITextChannel textChannel)) { - RealId = discordChannel.Id, - IsAdminChannel = channelFromDB.IsAdminChannel == true, - ConnectionName = textChannel.Guild.Name, - FriendlyName = textChannel.Name, - IsPrivateChannel = false, - Tag = channelFromDB.Tag - }; - Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName); - return channelModel; + Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType()); + return null; + } + + discordChannelId = textChannel.Id; + connectionName = textChannel.Guild.Name; + friendlyName = textChannel.Name; } - Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType()); - return null; + var channelModel = new ChannelRepresentation + { + RealId = discordChannelId, + IsAdminChannel = channelFromDB.IsAdminChannel == true, + ConnectionName = connectionName, + FriendlyName = friendlyName, + IsPrivateChannel = false, + Tag = channelFromDB.Tag + }; + Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName); + return channelModel; } - var enumerator = channels.Select(x => GetModelChannelFromDBChannel(x)).Where(x => x != null).ToList(); + var enumerator = channels + .Select(x => GetModelChannelFromDBChannel(x)) + .Where(x => x != null).ToList(); - lock (client) + lock (mappedChannels) { mappedChannels.Clear(); mappedChannels.AddRange(enumerator.Select(x => x.RealId)); @@ -270,21 +311,51 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) { + var requestOptions = new RequestOptions + { + CancelToken = cancellationToken, + Timeout = 10000 // prevent stupid long hold ups from this + }; + + Task SendToChannel(IMessageChannel channel) => channel.SendMessageAsync( + message, + false, + null, + requestOptions); + try { + if (channelId == 0) + { + var unmappedTextChannels = client + .Guilds + .SelectMany(x => x.TextChannels); + + lock (mappedChannels) + unmappedTextChannels = unmappedTextChannels.Where(x => !mappedChannels.Contains(x.Id)); + + // discord API confirmed weak boned: https://stackoverflow.com/a/52462336 + var channelCount = 0UL; + var tasks = unmappedTextChannels + .Select(x => + { + ++channelCount; + return SendToChannel(x); + }); + + if (channelCount > 0) + { + Logger.LogTrace("Dispatched to {0} unmapped channels...", channelCount); + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + return; + } + if (!(client.GetChannel(channelId) is IMessageChannel channel)) return; - await channel.SendMessageAsync( - message, - false, - null, - new RequestOptions - { - CancelToken = cancellationToken, - Timeout = 10000 // prevent stupid long hold ups from this - }) - .ConfigureAwait(false); + await SendToChannel(channel).ConfigureAwait(false); } catch (Exception e) {