From be64ee486785e2f63e79f158f375ade22a642952 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 11:57:35 -0400 Subject: [PATCH] Implement DiscordProvider and other things --- src/DMAPI/tgs.dm | 2 +- .../Models/ChatChannel.cs | 2 +- .../Components/Chat/Channel.cs | 2 +- .../Components/Chat/ChannelMapping.cs | 2 +- .../Components/Chat/Chat.cs | 16 +- .../Components/Chat/IChat.cs | 2 +- .../Components/Chat/Message.cs | 4 +- .../Chat/Providers/DiscordProvider.cs | 248 ++++++++++++++++++ .../Components/Chat/Providers/IProvider.cs | 7 +- .../Components/Chat/Response.cs | 2 +- .../Components/Chat/User.cs | 25 +- 11 files changed, 290 insertions(+), 22 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index c3daf1a380..628b17cac6 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -103,7 +103,7 @@ //represents a chat user /datum/tgs_chat_user - var/id //Internal user representation + var/id //Internal user representation, requires channel to be unique var/friendly_name //The user's public name var/mention //The text to use to ping this user in a message var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index 942154e506..d5131f4ff1 100644 --- a/src/Tgstation.Server.Api/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs @@ -13,7 +13,7 @@ /// /// The Discord channel ID /// - public long? DiscordChannelId { get; set; } + public ulong? DiscordChannelId { get; set; } /// /// If the is an admin channel diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs index c2eab84dd8..b29dc5c90c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -9,7 +9,7 @@ /// The channel Id. /// /// remaps this to an internal id using - public long Id { get; set; } + public ulong Id { get; set; } /// /// The user friendly name of the diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs index 243bf71206..a1ed4dd808 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -3,7 +3,7 @@ sealed class ChannelMapping { public long ProviderId { get; set; } - public long ProviderChannelId { get; set; } + public ulong ProviderChannelId { get; set; } public bool IsWatchdogChannel { get; set; } public Channel Channel { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 07a29865c9..82a8989940 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -13,6 +13,8 @@ namespace Tgstation.Server.Host.Components.Chat /// sealed class Chat : IChat { + const string CommonMention = "!tgs"; + /// /// The for the /// @@ -36,7 +38,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Map of s to s /// - readonly Dictionary mappedChannels; + readonly Dictionary mappedChannels; /// /// The active s for the @@ -51,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Used for remapping s /// - long channelIdCounter; + ulong channelIdCounter; /// /// If has been called @@ -71,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Chat builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)); providers = new Dictionary(); - mappedChannels = new Dictionary(); + mappedChannels = new Dictionary(); trackingContexts = new List(); channelIdCounter = 1; } @@ -131,11 +133,11 @@ namespace Tgstation.Server.Host.Components.Chat Channel = y }); - long baseId; + ulong baseId; lock (this) { baseId = channelIdCounter; - channelIdCounter += results.Count; + channelIdCounter += (ulong)results.Count; } Task task; @@ -185,7 +187,7 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) + public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) { if (message == null) throw new ArgumentNullException(nameof(message)); @@ -209,7 +211,7 @@ namespace Tgstation.Server.Host.Components.Chat /// public Task SendWatchdogMessage(string message, CancellationToken cancellationToken) { - List wdChannels; + List wdChannels; lock (mappedChannels) //so it doesn't change while we're using it wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList(); return SendMessage(message, wdChannels, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index 68c12aa1ed..15e6e939bc 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Chat /// The s of the s to send to /// The for the operation /// A representing the running operation - Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken); + Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken); /// /// Send a chat to configured watchdog channels diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs index e9b90e6560..34ba645541 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -2,7 +2,7 @@ { sealed class Message { - string Content { get; set; } - User User { get; set; } + public string Content { get; set; } + public User User { 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 new file mode 100644 index 0000000000..367973b919 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -0,0 +1,248 @@ +using Discord; +using Discord.Net; +using Discord.WebSocket; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + /// + /// for the Discord app + /// + sealed class DiscordProvider : IProvider + { + /// + public bool Connected { get; private set; } + + /// + public string BotMention + { + get + { + if (!Connected) + throw new InvalidOperationException("Provider not connected"); + return client.CurrentUser.Mention; + } + } + + readonly ILogger logger; + + /// + /// The for the + /// + readonly DiscordSocketClient client; + + /// + /// The name used for populating + /// + readonly string connectionName; + + /// + /// The token used for connecting to discord + /// + readonly string botToken; + + /// + /// of received s + /// + readonly Queue messageQueue; + + /// + /// of mapped s + /// + readonly List mappedChannels; + + /// + /// that completes while isn't empty + /// + TaskCompletionSource nextMessage; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + public DiscordProvider(ILogger logger, string connectionName, string botToken) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.connectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName)); + this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken)); + client = new DiscordSocketClient(); + client.MessageReceived += Client_MessageReceived; + nextMessage = new TaskCompletionSource(); + mappedChannels = new List(); + messageQueue = new Queue(); + } + + /// + public void Dispose() => client.Dispose(); + + /// + /// Handle a message recieved from Discord + /// + /// The + /// A representing the running operation + Task Client_MessageReceived(SocketMessage e) + { + if (e.Author.Id != client.CurrentUser.Id) + return Task.CompletedTask; + + var pm = e.Channel is IPrivateChannel; + + if (!pm && !mappedChannels.Contains(e.Channel.Id)) + return Task.CompletedTask; + + var result = new Message { + Content = e.Content, + User = new User + { + Id = e.Author.Id, + Channel = new Channel + { + Id = e.Channel.Id, + IsAdmin = false, + IsPrivate = true, + ConnectionName = connectionName, + FriendlyName = e.Channel.Name + }, + FriendlyName = e.Author.Username, + Mention = e.Author.Mention + } + }; + + lock (this) + { + messageQueue.Enqueue(result); + nextMessage.TrySetResult(null); + } + return Task.CompletedTask; + } + + /// + public async Task NextMessage(CancellationToken cancellationToken) + { + var cancelTcs = new TaskCompletionSource(); + using (cancellationToken.Register(() => cancelTcs.SetCanceled())) + await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false); + lock (this) + { + var result = messageQueue.Dequeue(); + if (messageQueue.Count == 0) + nextMessage = new TaskCompletionSource(); + return result; + } + } + + /// + public async Task Connect(CancellationToken cancellationToken) + { + if (Connected) + return true; + + try + { + await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + await client.StartAsync().ConfigureAwait(false); + + var channelsAvailable = new TaskCompletionSource(); + client.Ready += () => + { + channelsAvailable.SetResult(null); + return Task.CompletedTask; + }; + using (cancellationToken.Register(() => channelsAvailable.SetCanceled())) + await channelsAvailable.Task.ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error connecting to Discord: {0}", e); + return false; + } + + Connected = true; + return true; + } + + public async Task Disconnect(CancellationToken cancellationToken) + { + if (!Connected) + return; + + try + { + await client.StopAsync().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + await client.LogoutAsync().ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error disconnecting from discord: {0}", e); + } + Connected = false; + } + + /// + public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) + { + if (channels == null) + throw new ArgumentNullException(nameof(channels)); + + if (!Connected) + throw new InvalidOperationException("Provider not connected!"); + + Channel GetChannelForChatChannel(ChatChannel channel) + { + if (!channel.DiscordChannelId.HasValue) + throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); + + var discordChannel = client.GetChannel(channel.DiscordChannelId.Value); + + if (discordChannel == null) + return null; + + return new Channel + { + Id = discordChannel.Id, + IsAdmin = channel.IsAdminChannel, + ConnectionName = connectionName, + FriendlyName = (discordChannel as ITextChannel)?.Name ?? "UNKNOWN", + IsPrivate = false + }; + }; + + var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null); + + lock (this) + { + mappedChannels.Clear(); + mappedChannels.AddRange(enumerator.Select(x => x.Id)); + } + + return Task.FromResult>(enumerator.ToList()); + } + + /// + public async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) { + try + { + await ((client.GetChannel(channelId) as ITextChannel)?.SendMessageAsync(message, false, null, new RequestOptions + { + CancelToken = cancellationToken + }) ?? Task.CompletedTask).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error sending discord message: {0}", e); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 7abe357269..5b6761ad2c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -23,7 +23,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Get a resulting in the next the recieves or on a disconnect /// - Task NextMessage { get; } + /// The for the operation + /// A resulting in the next available + /// Note that private messages will come in the form of s not returned in + Task NextMessage(CancellationToken cancellationToken); /// /// Attempt to connect the @@ -54,6 +57,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The message contents /// The for the operation /// A representing the running operation - Task SendMessage(long channelId, string message, CancellationToken cancellationToken); + Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Response.cs b/src/Tgstation.Server.Host/Components/Chat/Response.cs index 855752e790..59be02b4ba 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Response.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Response.cs @@ -15,6 +15,6 @@ namespace Tgstation.Server.Host.Components.Chat /// /// The list of internal channel ids to send to /// - public List ChannelIds { get; set; } + public List ChannelIds { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs index 12eff58285..6b7cbda2ab 100644 --- a/src/Tgstation.Server.Host/Components/Chat/User.cs +++ b/src/Tgstation.Server.Host/Components/Chat/User.cs @@ -1,13 +1,28 @@ namespace Tgstation.Server.Host.Components.Chat { /// - /// + /// Represents a tgs_chat_user datum /// public sealed class User { - long Id { get; set; } - string FriendlyName { get; set; } - string Mention { get; set; } - Channel Channel { get; set; } + /// + /// The internal user id + /// + public ulong Id { get; set; } + + /// + /// The friendly name of the user + /// + public string FriendlyName { get; set; } + + /// + /// The text to mention the user + /// + public string Mention { get; set; } + + /// + /// The the user spoke from + /// + public Channel Channel { get; set; } } }