From be64ee486785e2f63e79f158f375ade22a642952 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 11:57:35 -0400 Subject: [PATCH 01/20] 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; } } } From 178a822546a1529a5514cc9da7710c5a04062ba7 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 11:59:48 -0400 Subject: [PATCH 02/20] Minor doc update --- .../Components/Chat/Providers/DiscordProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 367973b919..f78d320d00 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -1,5 +1,4 @@ using Discord; -using Discord.Net; using Discord.WebSocket; using Microsoft.Extensions.Logging; using System; @@ -30,6 +29,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + /// + /// The for the + /// readonly ILogger logger; /// From ae66f888952159399d403a0d5a681d2a411dd360 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 12:13:01 -0400 Subject: [PATCH 03/20] Convert user and channel ids to string backing fields --- .../Components/Chat/Channel.cs | 18 ++++++++++++++++-- .../Components/Chat/Chat.cs | 4 ++-- .../Components/Chat/ICommandFactory.cs | 6 +++--- .../Chat/Providers/DiscordProvider.cs | 8 ++++---- .../Components/Chat/User.cs | 18 ++++++++++++++++-- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs index b29dc5c90c..07111826d8 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -1,15 +1,29 @@ -namespace Tgstation.Server.Host.Components.Chat +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Tgstation.Server.Host.Components.Chat { /// /// Represents a channel /// public sealed class Channel { + /// + /// Backing field for . Represented as a to avoid BYOND percision loss + /// + public string Id { get; set; } + /// /// The channel Id. /// /// remaps this to an internal id using - public ulong Id { get; set; } + [JsonIgnore] + public ulong RealId + { + get => UInt64.Parse(Id, CultureInfo.InvariantCulture); + set => Id = value.ToString(CultureInfo.InvariantCulture); + } /// /// The user friendly name of the diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 82a8989940..fb9b656ef0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -128,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Chat var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping { IsWatchdogChannel = x.IsWatchdogChannel, - ProviderChannelId = y.Id, + ProviderChannelId = y.RealId, ProviderId = connectionId, Channel = y }); @@ -150,7 +150,7 @@ namespace Tgstation.Server.Host.Components.Chat { var newId = baseId++; mappedChannels.Add(newId, I); - I.Channel.Id = newId; + I.Channel.RealId = newId; } lock (trackingContexts) diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs index 4020ab0613..8abfe7abcd 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs @@ -3,8 +3,8 @@ using Tgstation.Server.Host.Components.Chat.Commands; namespace Tgstation.Server.Host.Components.Chat { - interface ICommandFactory - { + interface ICommandFactory + { IReadOnlyList GenerateCommands(); - } + } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index f78d320d00..2f3387b821 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -104,10 +104,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Content = e.Content, User = new User { - Id = e.Author.Id, + RealId = e.Author.Id, Channel = new Channel { - Id = e.Channel.Id, + RealId = e.Channel.Id, IsAdmin = false, IsPrivate = true, ConnectionName = connectionName, @@ -213,7 +213,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return new Channel { - Id = discordChannel.Id, + RealId = discordChannel.Id, IsAdmin = channel.IsAdminChannel, ConnectionName = connectionName, FriendlyName = (discordChannel as ITextChannel)?.Name ?? "UNKNOWN", @@ -226,7 +226,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers lock (this) { mappedChannels.Clear(); - mappedChannels.AddRange(enumerator.Select(x => x.Id)); + mappedChannels.AddRange(enumerator.Select(x => x.RealId)); } return Task.FromResult>(enumerator.ToList()); diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs index 6b7cbda2ab..c3a5e2b6dc 100644 --- a/src/Tgstation.Server.Host/Components/Chat/User.cs +++ b/src/Tgstation.Server.Host/Components/Chat/User.cs @@ -1,14 +1,28 @@ -namespace Tgstation.Server.Host.Components.Chat +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Tgstation.Server.Host.Components.Chat { /// /// Represents a tgs_chat_user datum /// public sealed class User { + /// + /// Backing field for . Represented as a to avoid BYOND percision loss + /// + public string Id { get; set; } + /// /// The internal user id /// - public ulong Id { get; set; } + [JsonIgnore] + public ulong RealId + { + get => UInt64.Parse(Id, CultureInfo.InvariantCulture); + set => Id = value.ToString(CultureInfo.InvariantCulture); + } /// /// The friendly name of the user From 56ca3205bbf9f659e22e0a8fb250cc2cab1176bb Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 12:47:28 -0400 Subject: [PATCH 04/20] WIP IrcProvider --- .../Chat/Providers/IrcPasswordType.cs | 9 + .../Components/Chat/Providers/IrcProvider.cs | 175 ++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 4 + .../Core/IApplication.cs | 5 + 4 files changed, 193 insertions(+) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs new file mode 100644 index 0000000000..38181f9f99 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs @@ -0,0 +1,9 @@ +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + enum IrcPasswordType + { + Server, + Sasl, + NickServ + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs new file mode 100644 index 0000000000..49410fc57d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -0,0 +1,175 @@ +using Meebey.SmartIrc4net; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + /// + /// for internet relay chat + /// + sealed class IrcProvider : IProvider + { + /// + public bool Connected => throw new NotImplementedException(); + + /// + public string BotMention => throw new NotImplementedException(); + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// The client + /// + readonly IrcFeatures client; + + /// + /// Address of the server to connect to + /// + readonly string address; + /// + /// Port of the server to connect to + /// + readonly ushort port; + /// + /// IRC nickname + /// + readonly string nickname; + /// + /// Password which will used for authentication + /// + readonly string password; + /// + /// The of + /// + readonly IrcPasswordType? passwordType; + + /// + /// Construct an + /// + /// The value of logger + /// The to get the from + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + /// If should be used + public IrcProvider(ILogger logger, IApplication application, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + if (application == null) + throw new ArgumentNullException(nameof(application)); + + this.address = address ?? throw new ArgumentNullException(nameof(address)); + this.port = port; + this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname)); + + if (passwordType.HasValue && password == null) + throw new ArgumentNullException(nameof(password)); + + if(password != null && !passwordType.HasValue) + throw new ArgumentNullException(nameof(passwordType)); + + this.password = password; + this.passwordType = passwordType; + + client = new IrcFeatures + { + SupportNonRfc = true, + CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...", + AutoRejoin = true, + AutoRejoinOnKick = true, + AutoRelogin = true, + AutoRetry = true, + AutoRetryLimit = 5, + AutoRetryDelay = 5, + ActiveChannelSyncing = true, + AutoNickHandling = true, + CtcpVersion = application.VersionString, + UseSsl = useSsl, + ValidateServerCertificate = useSsl, + }; + + client.OnChannelMessage += Client_OnChannelMessage; + client.OnQueryMessage += Client_OnQueryMessage; + } + + void Client_OnQueryMessage(object sender, IrcEventArgs e) + { + throw new NotImplementedException(); + } + + void Client_OnChannelMessage(object sender, IrcEventArgs e) + { + throw new NotImplementedException(); + } + + /// + public void Dispose() => Disconnect(default).Wait(); //not actually a task so whatever + + /// + public Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + { + try + { + client.Connect(address, port); + + cancellationToken.ThrowIfCancellationRequested(); + + if (passwordType == IrcPasswordType.Sasl) + { + //TODO + } + + if (passwordType == IrcPasswordType.Server) + client.Login(nickname, nickname, 0, nickname, password); + else + client.Login(nickname, nickname); + + if (passwordType == IrcPasswordType.NickServ) + { + cancellationToken.ThrowIfCancellationRequested(); + client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password)); + } + } + catch(Exception e) + { + logger.LogWarning("Unable to connect to IRC: {0}", e); + } + return true; + }, cancellationToken,TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public Task Disconnect(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + public Task NextMessage(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + public Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 963b22e98a..cd8c07c453 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -30,6 +30,9 @@ namespace Tgstation.Server.Host.Core /// public Version Version { get; } + /// + public string VersionString { get; } + /// /// The for the /// @@ -56,6 +59,7 @@ namespace Tgstation.Server.Host.Core this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); Version = Assembly.GetExecutingAssembly().GetName().Version; + VersionString = String.Format(CultureInfo.InvariantCulture, "/tg/station server v{0}", Version); } /// diff --git a/src/Tgstation.Server.Host/Core/IApplication.cs b/src/Tgstation.Server.Host/Core/IApplication.cs index eed6769ab3..b00ae8a3b9 100644 --- a/src/Tgstation.Server.Host/Core/IApplication.cs +++ b/src/Tgstation.Server.Host/Core/IApplication.cs @@ -7,6 +7,11 @@ namespace Tgstation.Server.Host.Core /// public interface IApplication { + /// + /// A more verbose version of + /// + string VersionString { get; } + /// /// The version of the /// From 8b965483b4373d94cdbd65ddc8cee586e31d2c73 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 13:29:03 -0400 Subject: [PATCH 05/20] More IrcProvider stuff, implement message monitor --- .../Components/Chat/Chat.cs | 79 ++++++++++++++++++- .../Components/Chat/Providers/IrcProvider.cs | 11 +-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index fb9b656ef0..b1d0b28349 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -45,6 +46,16 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly List trackingContexts; + /// + /// The for + /// + readonly CancellationTokenSource handlerCts; + + /// + /// The that monitors incoming chat messages + /// + Task chatHandler; + /// /// The for the /// @@ -75,12 +86,14 @@ namespace Tgstation.Server.Host.Components.Chat providers = new Dictionary(); mappedChannels = new Dictionary(); trackingContexts = new List(); + handlerCts = new CancellationTokenSource(); channelIdCounter = 1; } /// public void Dispose() { + handlerCts.Dispose(); foreach (var I in providers) I.Value.Dispose(); } @@ -114,6 +127,64 @@ namespace Tgstation.Server.Host.Components.Chat return provider; } + /// + /// Processes a + /// + /// The who recevied + /// The to process + /// The for the operation + /// A representing the running operation + async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken) + { + if (!(message.Content.StartsWith(CommonMention, StringComparison.InvariantCultureIgnoreCase) || message.Content.StartsWith(provider.BotMention, StringComparison.Ordinal))) + //no mention + return; + + await Task.Yield(); + lock (this) + throw new NotImplementedException(); + } + + /// + /// Monitors active providers for new s + /// + /// The for the operation + /// A representing the running operation + async Task MonitorMessages(CancellationToken cancellationToken) + { + var messageTasks = new Dictionary>(); + try + { + while (!cancellationToken.IsCancellationRequested) + { + //prune disconnected providers + foreach (var I in messageTasks) + if (!I.Key.Connected) + messageTasks.Remove(I.Key); + + //add new ones + foreach (var I in providers) + if (I.Value.Connected && !messageTasks.ContainsKey(I.Value)) + messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken)); + + //wait for a message + var tasks = messageTasks.Select(x => x.Value); + await Task.WhenAny().ConfigureAwait(false); + + //process completed ones + foreach (var I in messageTasks.Where(x => x.Value.IsCompleted)) + { + messageTasks.Remove(I.Key); + + var message = await I.Value.ConfigureAwait(false); + + await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) { } + } + /// public async Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken) { @@ -221,11 +292,17 @@ namespace Tgstation.Server.Host.Components.Chat public async Task StartAsync(CancellationToken cancellationToken) { await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false); + chatHandler = MonitorMessages(handlerCts.Token); started = true; } /// - public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))); + public async Task StopAsync(CancellationToken cancellationToken) + { + handlerCts.Cancel(); + await chatHandler.ConfigureAwait(false); + await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false); + } /// public async Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 49410fc57d..f1ddcf7a30 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -16,10 +16,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers sealed class IrcProvider : IProvider { /// - public bool Connected => throw new NotImplementedException(); + public bool Connected => client.IsConnected; /// - public string BotMention => throw new NotImplementedException(); + public string BotMention => client.Nickname; /// /// The for the @@ -149,10 +149,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }, cancellationToken,TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public Task Disconnect(CancellationToken cancellationToken) + public Task Disconnect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - throw new NotImplementedException(); - } + + + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) From 238e03ff4190723614f19aeff7d88556e318034c Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 15:44:15 -0400 Subject: [PATCH 06/20] Implement SASL among other things --- .../Components/Chat/Chat.cs | 4 +- .../Components/Chat/Message.cs | 2 +- .../Chat/Providers/DiscordProvider.cs | 17 +-- .../Components/Chat/Providers/IProvider.cs | 2 +- .../Chat/Providers/IrcPasswordType.cs | 2 +- .../Components/Chat/Providers/IrcProvider.cs | 129 +++++++++++++++--- 6 files changed, 120 insertions(+), 36 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index b1d0b28349..121ea10a23 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Chat readonly Dictionary providers; /// - /// Map of s to s + /// Map of s to s /// readonly Dictionary mappedChannels; @@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components.Chat ICustomCommandHandler customCommandHandler; /// - /// Used for remapping s + /// Used for remapping s /// ulong channelIdCounter; diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs index 34ba645541..529056d7f3 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -1,6 +1,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { - sealed class Message + public sealed class Message { public string Content { get; set; } public User User { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 2f3387b821..437ea7b9ba 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -38,12 +38,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The for the /// readonly DiscordSocketClient client; - - /// - /// The name used for populating - /// - readonly string connectionName; - + /// /// The token used for connecting to discord /// @@ -68,12 +63,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Construct a /// /// The value of - /// The value of /// The value of - public DiscordProvider(ILogger logger, string connectionName, string botToken) + public DiscordProvider(ILogger logger, 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; @@ -206,7 +199,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (!channel.DiscordChannelId.HasValue) throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); - var discordChannel = client.GetChannel(channel.DiscordChannelId.Value); + var discordChannel = client.GetChannel(channel.DiscordChannelId.Value) as ITextChannel; if (discordChannel == null) return null; @@ -215,8 +208,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { RealId = discordChannel.Id, IsAdmin = channel.IsAdminChannel, - ConnectionName = connectionName, - FriendlyName = (discordChannel as ITextChannel)?.Name ?? "UNKNOWN", + ConnectionName = discordChannel.Guild.Name, + FriendlyName = discordChannel.Name, IsPrivate = false }; }; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 5b6761ad2c..99d13e1768 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Send a message to the /// - /// The to send to + /// The to send to /// The message contents /// The for the operation /// A representing the running operation diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs index 38181f9f99..e222f5d6ef 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs @@ -1,6 +1,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { - enum IrcPasswordType + public enum IrcPasswordType { Server, Sasl, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index f1ddcf7a30..c09d55ce43 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -15,6 +17,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// sealed class IrcProvider : IProvider { + const int TimeoutSeconds = 5; + /// public bool Connected => client.IsConnected; @@ -52,6 +56,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// readonly IrcPasswordType? passwordType; + /// + /// Map of s to channel names + /// + readonly Dictionary channelIdMap; + + /// + /// Id counter for + /// + ulong channelIdCounter; + /// /// Construct an /// @@ -90,31 +104,35 @@ namespace Tgstation.Server.Host.Components.Chat.Providers AutoRejoinOnKick = true, AutoRelogin = true, AutoRetry = true, - AutoRetryLimit = 5, - AutoRetryDelay = 5, + AutoRetryLimit = TimeoutSeconds, + AutoRetryDelay = TimeoutSeconds, ActiveChannelSyncing = true, AutoNickHandling = true, CtcpVersion = application.VersionString, - UseSsl = useSsl, - ValidateServerCertificate = useSsl, + UseSsl = useSsl }; + if (useSsl) + client.ValidateServerCertificate = true; //dunno if it defaults to that or what client.OnChannelMessage += Client_OnChannelMessage; client.OnQueryMessage += Client_OnQueryMessage; + + channelIdMap = new Dictionary(); + channelIdCounter = 0; } void Client_OnQueryMessage(object sender, IrcEventArgs e) { - throw new NotImplementedException(); + } void Client_OnChannelMessage(object sender, IrcEventArgs e) { - throw new NotImplementedException(); + } /// - public void Dispose() => Disconnect(default).Wait(); //not actually a task so whatever + public void Dispose() => client.Disconnect(); //just closes the socket /// public Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => @@ -125,23 +143,60 @@ namespace Tgstation.Server.Host.Components.Chat.Providers cancellationToken.ThrowIfCancellationRequested(); - if (passwordType == IrcPasswordType.Sasl) - { - //TODO - } - if (passwordType == IrcPasswordType.Server) client.Login(nickname, nickname, 0, nickname, password); else - client.Login(nickname, nickname); + { + if (passwordType == IrcPasswordType.Sasl) + { + client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else + cancellationToken.ThrowIfCancellationRequested(); + } + client.Login(nickname, nickname, 0, nickname); + } if (passwordType == IrcPasswordType.NickServ) { cancellationToken.ThrowIfCancellationRequested(); client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password)); } + else if (passwordType == IrcPasswordType.Sasl) + { + //wait for the sasl ack or timeout + var recievedAck = false; + var recievedPlus = false; + client.OnReadLine += (sender, e) => + { + if (e.Line.Contains("ACK :sasl")) + recievedAck = true; + else if (e.Line.Contains("AUTHENTICATE +")) + recievedPlus = true; + }; + + var startTime = DateTimeOffset.Now; + var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds); + cancellationToken.ThrowIfCancellationRequested(); + for(; !recievedAck && DateTimeOffset.Now <= endTime; Task.Delay(10, cancellationToken).GetAwaiter().GetResult()) + client.Listen(false); + + client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); + cancellationToken.ThrowIfCancellationRequested(); + + for (; !recievedPlus && DateTimeOffset.Now <= endTime; Task.Delay(10, cancellationToken).GetAwaiter().GetResult()) + client.Listen(false); + + //Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196 + var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password); + var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); + var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64); + var chars = authLine.ToCharArray(); + client.WriteLine(authLine, Priority.Critical); + + cancellationToken.ThrowIfCancellationRequested(); + client.WriteLine("CAP END", Priority.Critical); + } } - catch(Exception e) + catch (Exception e) { logger.LogWarning("Unable to connect to IRC: {0}", e); } @@ -151,15 +206,51 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public Task Disconnect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - - + client.RfcQuit(); + Dispose(); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) + public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - throw new NotImplementedException(); - } + if (channels.Any(x => x.IrcChannel == null)) + throw new InvalidOperationException("ChatChannel missing IrcChannel!"); + lock (this) + { + var hs = new HashSet(); //for unique inserts + foreach (var I in channels) + hs.Add(I.IrcChannel); + var toPart = new List(); + foreach (var I in client.JoinedChannels) + if (!hs.Remove(I)) + toPart.Add(I); + + foreach (var I in toPart) + client.RfcPart(I); + foreach (var I in hs) + client.RfcJoin(I); + + return (IReadOnlyList)channels.Select(x => { + var id = ++channelIdCounter; + if (!channelIdMap.Any(y => + { + if (y.Value != x.IrcChannel) + return false; + id = y.Key; + return true; + })) + channelIdMap.Add(id, x.IrcChannel); + return new Channel + { + RealId = id, + IsAdmin = x.IsAdminChannel, + ConnectionName = address, + FriendlyName = channelIdMap[id], + IsPrivate = false + }; + }).ToList(); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// public Task NextMessage(CancellationToken cancellationToken) From 9c9ba0ad293be7805aebfef1d9194c0bb6887fc6 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 16:32:50 -0400 Subject: [PATCH 07/20] Finish implementing IrcProvider --- .../Chat/Providers/DiscordProvider.cs | 60 ++------ .../Components/Chat/Providers/IrcProvider.cs | 137 ++++++++++++++---- .../Components/Chat/Providers/Provider.cs | 76 ++++++++++ 3 files changed, 195 insertions(+), 78 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 437ea7b9ba..4412cf16b5 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -13,13 +13,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// for the Discord app /// - sealed class DiscordProvider : IProvider + sealed class DiscordProvider : Provider { /// - public bool Connected { get; private set; } + public override bool Connected => client.ConnectionState == ConnectionState.Connected; /// - public string BotMention + public override string BotMention { get { @@ -43,22 +43,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// 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 /// @@ -70,13 +60,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers 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(); + public override void Dispose() => client.Dispose(); /// /// Handle a message recieved from Discord @@ -103,39 +91,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers RealId = e.Channel.Id, IsAdmin = false, IsPrivate = true, - ConnectionName = connectionName, + ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN", FriendlyName = e.Channel.Name }, FriendlyName = e.Author.Username, Mention = e.Author.Mention } }; - - lock (this) - { - messageQueue.Enqueue(result); - nextMessage.TrySetResult(null); - } + EnqueueMessage(result); 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) + public override async Task Connect(CancellationToken cancellationToken) { if (Connected) return true; @@ -162,12 +130,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers logger.LogWarning("Error connecting to Discord: {0}", e); return false; } - - Connected = true; + return true; } - public async Task Disconnect(CancellationToken cancellationToken) + public override async Task Disconnect(CancellationToken cancellationToken) { if (!Connected) return; @@ -182,11 +149,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { logger.LogWarning("Error disconnecting from discord: {0}", e); } - Connected = false; } /// - public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) + public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) { if (channels == null) throw new ArgumentNullException(nameof(channels)); @@ -199,9 +165,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (!channel.DiscordChannelId.HasValue) throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); - var discordChannel = client.GetChannel(channel.DiscordChannelId.Value) as ITextChannel; - - if (discordChannel == null) + if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel)) return null; return new Channel @@ -226,7 +190,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) { + public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) { try { await ((client.GetChannel(channelId) as ITextChannel)?.SendMessageAsync(message, false, null, new RequestOptions diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index c09d55ce43..7457d6f8d2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -15,15 +15,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// for internet relay chat /// - sealed class IrcProvider : IProvider + sealed class IrcProvider : Provider { const int TimeoutSeconds = 5; /// - public bool Connected => client.IsConnected; + public override bool Connected => client.IsConnected; /// - public string BotMention => client.Nickname; + public override string BotMention => client.Nickname; /// /// The for the @@ -61,6 +61,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// readonly Dictionary channelIdMap; + /// + /// Map of s to query users + /// + readonly Dictionary queryChannelIdMap; + /// /// Id counter for /// @@ -118,24 +123,82 @@ namespace Tgstation.Server.Host.Components.Chat.Providers client.OnQueryMessage += Client_OnQueryMessage; channelIdMap = new Dictionary(); - channelIdCounter = 0; - } - - void Client_OnQueryMessage(object sender, IrcEventArgs e) - { - - } - - void Client_OnChannelMessage(object sender, IrcEventArgs e) - { - + queryChannelIdMap = new Dictionary(); + channelIdCounter = 1; } /// - public void Dispose() => client.Disconnect(); //just closes the socket + public override void Dispose() => client.Disconnect(); //just closes the socket + + /// + /// Handle an IRC message + /// + /// The + /// If this is a query message + void HandleMessage(IrcEventArgs e, bool isPrivate) + { + if (e.Data.From.ToUpperInvariant() == client.Nickname.ToUpperInvariant()) + return; + + var username = e.Data.From; + var channelName = isPrivate ? username : e.Data.Channel; + ulong channelId = 0; + lock (this) + { + var dicToCheck = isPrivate ? queryChannelIdMap : channelIdMap; + if (!dicToCheck.Any(x => + { + if (x.Value != channelName) + return false; + channelId = x.Key; + return true; + })) + { + channelId = ++channelIdCounter; + dicToCheck.Add(channelId, channelName); + if (isPrivate) + channelIdMap.Add(channelId, null); + } + } + + var message = new Message + { + Content = e.Data.Message, + User = new User + { + Channel = new Channel + { + IsAdmin = false, + ConnectionName = address, + FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName, + RealId = channelId, + IsPrivate = isPrivate + }, + FriendlyName = username, + RealId = channelId, + Mention = username + } + }; + + EnqueueMessage(message); + } + + /// + /// When a query message is received in IRC + /// + /// The sender of the event + /// The + void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true); + + /// + /// When a channel message is received in IRC + /// + /// The sender of the event + /// The + void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false); /// - public Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { try { @@ -204,14 +267,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }, cancellationToken,TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public Task Disconnect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public override Task Disconnect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - client.RfcQuit(); + try + { + client.RfcQuit(); + } + catch (Exception e) + { + logger.LogWarning("Error quitting IRC: {0}", e); + } Dispose(); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { if (channels.Any(x => x.IrcChannel == null)) throw new InvalidOperationException("ChatChannel missing IrcChannel!"); @@ -230,8 +300,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers foreach (var I in hs) client.RfcJoin(I); + + return (IReadOnlyList)channels.Select(x => { - var id = ++channelIdCounter; + ulong id = channelIdCounter; if (!channelIdMap.Any(y => { if (y.Value != x.IrcChannel) @@ -240,6 +312,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return true; })) channelIdMap.Add(id, x.IrcChannel); + else + ++channelIdCounter; return new Channel { RealId = id, @@ -251,17 +325,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }).ToList(); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); - + /// - public Task NextMessage(CancellationToken cancellationToken) + public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - throw new NotImplementedException(); - } - - /// - public Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + var channelName = channelIdMap[channelId] ?? queryChannelIdMap[channelId]; + try + { + if (client.JoinedChannels.Contains(channelName)) + client.SendMessage(SendType.Message, channelName, message); + } + catch(Exception e) + { + logger.LogWarning("Unable to send to channel: {0}", e); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs new file mode 100644 index 0000000000..01484c229d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + /// + abstract class Provider : IProvider + { + /// + /// of received s + /// + readonly Queue messageQueue; + + /// + /// that completes while isn't empty + /// + TaskCompletionSource nextMessage; + + protected Provider() + { + messageQueue = new Queue(); + nextMessage = new TaskCompletionSource(); + } + + /// + public abstract bool Connected { get; } + + /// + public abstract string BotMention { get; } + + /// + /// Queues a for + /// + /// The to queue + protected void EnqueueMessage(Message message) + { + lock (messageQueue) + { + messageQueue.Enqueue(message); + nextMessage.TrySetResult(null); + } + } + + /// + public abstract void Dispose(); + + /// + public abstract Task Connect(CancellationToken cancellationToken); + + /// + public abstract Task Disconnect(CancellationToken cancellationToken); + + /// + public abstract Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); + + /// + 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 (messageQueue) + { + var result = messageQueue.Dequeue(); + if (messageQueue.Count == 0) + nextMessage = new TaskCompletionSource(); + return result; + } + } + + /// + public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); + } +} From 1d931f1954914544586d252c2d5d013ebf213b48 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 16:37:02 -0400 Subject: [PATCH 08/20] More docs and stuff --- .../Components/Chat/ChannelMapping.cs | 17 +++++++++++++++++ .../Components/Chat/Message.cs | 12 +++++++++++- .../Chat/Providers/IrcPasswordType.cs | 14 +++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs index a1ed4dd808..5185f033cc 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -1,11 +1,28 @@ namespace Tgstation.Server.Host.Components.Chat { + /// + /// Represents a mapping of a + /// sealed class ChannelMapping { + /// + /// The Id of the + /// public long ProviderId { get; set; } + + /// + /// The original + /// public ulong ProviderChannelId { get; set; } + + /// + /// If is a watchdog channel + /// public bool IsWatchdogChannel { get; set; } + /// + /// The with the mapped Id + /// public Channel Channel { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs index 529056d7f3..3989369ce8 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -1,8 +1,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { - public sealed class Message + /// + /// Represents a message recieved by a + /// + sealed class Message { + /// + /// The text of the message + /// public string Content { get; set; } + + /// + /// The who sent the + /// public User User { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs index e222f5d6ef..d31a3aba73 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs @@ -1,9 +1,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { - public enum IrcPasswordType + /// + /// Represents the type of a password passed to the constructor of + /// + enum IrcPasswordType { + /// + /// Use server authentication + /// Server, + /// + /// Use PLAIN sasl authentication + /// Sasl, + /// + /// Use NickServ authentication + /// NickServ } } From 5ae57ea71dad9a3e6d5131b3e769f9d556ce51cf Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 16:54:58 -0400 Subject: [PATCH 09/20] Finish up more chat stuff --- .../Components/Chat/Chat.cs | 52 ++++++++++++++++--- .../Components/Chat/ICommandFactory.cs | 7 +++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 121ea10a23..77fb9031b2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -1,6 +1,7 @@ -using System; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -26,6 +27,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly IIOManager ioManager; + /// + /// The for the + /// + readonly ILogger logger; + /// /// s that never change /// @@ -69,18 +75,20 @@ namespace Tgstation.Server.Host.Components.Chat /// /// If has been called /// - bool started; + bool started; /// /// Construct a /// /// The value of /// The value of + /// The value of /// The used to populate - public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory) + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ILogger logger, ICommandFactory commandFactory) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)); providers = new Dictionary(); @@ -136,13 +144,41 @@ namespace Tgstation.Server.Host.Components.Chat /// A representing the running operation async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken) { + logger.LogTrace("Chat message: {0}. User (Note unconverted provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User)); + if (customCommandHandler == null) + { + logger.LogError("Recieved chat message with no command handler installed!"); + return; + } + if (!(message.Content.StartsWith(CommonMention, StringComparison.InvariantCultureIgnoreCase) || message.Content.StartsWith(provider.BotMention, StringComparison.Ordinal))) //no mention return; - - await Task.Yield(); - lock (this) - throw new NotImplementedException(); + + var splits = new List(message.Content.Split(' ')); + if (splits.Count == 1) + { + //just a mention + await SendMessage("Hi!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); + return; + } + + splits.RemoveAt(0); + + var command = splits[0]; + splits.RemoveAt(0); + var arguments = String.Join(" ", splits); + + if (customCommandHandler == null) + logger.LogError("Recieved chat message with no command handler installed!"); + try + { + await customCommandHandler.HandleChatCommand(command, arguments, message.User, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error processing custom command: {0}", e); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs index 8abfe7abcd..4fb9dd2de1 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs @@ -3,8 +3,15 @@ using Tgstation.Server.Host.Components.Chat.Commands; namespace Tgstation.Server.Host.Components.Chat { + /// + /// Factory for built in s + /// interface ICommandFactory { + /// + /// Generate builtin s + /// + /// A of s IReadOnlyList GenerateCommands(); } } From e7562cc6193801483cc2aa25c9f8374b277d94a5 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 18 Jul 2018 17:05:52 -0400 Subject: [PATCH 10/20] Implement VersionCommand --- .../Components/Chat/Chat.cs | 12 ++++--- .../Components/Chat/Commands/Command.cs | 36 ++++++++----------- .../Components/Chat/Commands/ICommand.cs | 34 ++++++++++++++++++ .../Chat/Commands/VersionCommand.cs | 30 ++++++++++++++++ .../Components/Chat/ICommandFactory.cs | 8 ++--- .../Components/Chat/ICustomCommandHandler.cs | 5 ++- 6 files changed, 92 insertions(+), 33 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 77fb9031b2..9ff74209ef 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -33,9 +33,9 @@ namespace Tgstation.Server.Host.Components.Chat readonly ILogger logger; /// - /// s that never change + /// s that never change /// - readonly IReadOnlyList builtinCommands; + readonly IReadOnlyList builtinCommands; /// /// Map of s in use, keyed by @@ -173,11 +173,15 @@ namespace Tgstation.Server.Host.Components.Chat logger.LogError("Recieved chat message with no command handler installed!"); try { - await customCommandHandler.HandleChatCommand(command, arguments, message.User, cancellationToken).ConfigureAwait(false); + var result = await customCommandHandler.HandleChatCommand(command, arguments, message.User, cancellationToken).ConfigureAwait(false); + if(result != null) + await SendMessage(result, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); } catch (Exception e) { - logger.LogWarning("Error processing custom command: {0}", e); + //error bc custom commands should reply about why it failed + logger.LogError("Error processing custom command: {0}", e); + await SendMessage("Internal error processing command!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs index d5f192ec59..b6ef36467e 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs @@ -1,29 +1,21 @@ -namespace Tgstation.Server.Host.Components.Chat.Commands +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Chat.Commands { - /// - /// Represents a command that can be invoked by talking to chat bots - /// - public abstract class Command + /// + public abstract class Command : ICommand { - /// - /// The text to invoke the command. May not be "?" or "help" (case-insensitive) - /// - public string Name { get; set; } + /// + public string Name { get; protected set; } - /// - /// The help text to display when queires are made about the command - /// - public string HelpText { get; set; } + /// + public string HelpText { get; protected set; } - /// - /// If the command should only be available to s who's has set - /// - public bool AdminOnly { get; set; } + /// + public bool AdminOnly { get; protected set; } - /// - /// Invoke the - /// - /// The text after with leading whitespace trimmed - public abstract void Invoke(string arguments); + /// + public abstract Task Invoke(string arguments, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs new file mode 100644 index 0000000000..0b5f58ffde --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// Represents a command that can be invoked by talking to chat bots + /// + public interface ICommand + { + /// + /// The text to invoke the command. May not be "?" or "help" (case-insensitive) + /// + string Name { get; } + + /// + /// The help text to display when queires are made about the command + /// + string HelpText { get; } + + /// + /// If the command should only be available to s who's has set + /// + bool AdminOnly { get; } + + /// + /// Invoke the + /// + /// The text after with leading whitespace trimmed + /// The for the operation + /// A resulting in a to send to the invoker + Task Invoke(string arguments, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs new file mode 100644 index 0000000000..57fe1b865c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// to return the + /// + sealed class VersionCommand : Command + { + /// + /// The for the + /// + readonly IApplication application; + + /// + /// Construct a + /// + /// + public VersionCommand(IApplication application) + { + this.application = application ?? throw new ArgumentNullException(nameof(application)); + } + + /// + public override Task Invoke(string arguments, CancellationToken cancellationToken) => Task.FromResult(application.VersionString); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs index 4fb9dd2de1..15827975ac 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs @@ -4,14 +4,14 @@ using Tgstation.Server.Host.Components.Chat.Commands; namespace Tgstation.Server.Host.Components.Chat { /// - /// Factory for built in s + /// Factory for built in s /// interface ICommandFactory { /// - /// Generate builtin s + /// Generate builtin s /// - /// A of s - IReadOnlyList GenerateCommands(); + /// A of s + IReadOnlyList GenerateCommands(); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs b/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs index 5106ba0efd..9f640cb633 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.Components.Chat { /// - /// Handles that map to those defined in a + /// Handles s that map to those defined in a /// public interface ICustomCommandHandler { From 37dab01632195cb81d3a0c067296c17069283a99 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 18 Jul 2018 23:13:53 -0400 Subject: [PATCH 11/20] Fix private channel detection. Cleanup everywhere --- src/DMAPI/tgs/v4/api.dm | 2 +- .../Components/Chat/Chat.cs | 46 +++++++----- .../Components/Chat/ChatFactory.cs | 41 +++++++++++ .../Components/Chat/Commands/Command.cs | 2 +- .../Chat/Commands/CommandFactory.cs | 30 ++++++++ .../Components/Chat/Commands/CustomCommand.cs | 5 +- .../Components/Chat/Commands/ICommand.cs | 3 +- .../Chat/Commands/VersionCommand.cs | 2 +- .../Components/Chat/IChatFactory.cs | 14 ++++ .../Components/EventType.cs | 2 +- .../Components/IEventConsumer.cs | 2 +- .../Components/IInstanceFactory.cs | 9 ++- .../Components/InstanceFactory.cs | 72 +++++++++++++++++-- .../Components/Watchdog/IWatchdog.cs | 2 +- .../Watchdog/SessionControllerFactory.cs | 14 ++-- .../Components/Watchdog/Watchdog.cs | 2 +- src/Tgstation.Server.Host/Core/Application.cs | 16 ++++- 17 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index a6700ab4e5..e62de1c25e 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -211,7 +211,7 @@ channel.friendly_name = channel_json["friendly_name"] channel.connection_name = channel_json["connection_name"] channel.is_admin_channel = channel_json["is_admin_channel"] - channel.is_admin_channel = channel_json["is_private_channel"] || FALSE + channel.is_private_channel = channel_json["is_private_channel"] || FALSE return channel #undef TGS4_TOPIC_COMMAND diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 9ff74209ef..04f5c5ac68 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -33,9 +33,9 @@ namespace Tgstation.Server.Host.Components.Chat readonly ILogger logger; /// - /// s that never change + /// Unchanging s in the mapped by /// - readonly IReadOnlyList builtinCommands; + readonly Dictionary builtinCommands; /// /// Map of s in use, keyed by @@ -57,16 +57,16 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly CancellationTokenSource handlerCts; + /// + /// The for the + /// + ICustomCommandHandler customCommandHandler; + /// /// The that monitors incoming chat messages /// Task chatHandler; - /// - /// The for the - /// - ICustomCommandHandler customCommandHandler; - /// /// Used for remapping s /// @@ -89,7 +89,9 @@ namespace Tgstation.Server.Host.Components.Chat this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)); + builtinCommands = new Dictionary(); + foreach (var I in commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory))) + builtinCommands.Add(I.Name, I); providers = new Dictionary(); mappedChannels = new Dictionary(); @@ -145,11 +147,6 @@ namespace Tgstation.Server.Host.Components.Chat async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken) { logger.LogTrace("Chat message: {0}. User (Note unconverted provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User)); - if (customCommandHandler == null) - { - logger.LogError("Recieved chat message with no command handler installed!"); - return; - } if (!(message.Content.StartsWith(CommonMention, StringComparison.InvariantCultureIgnoreCase) || message.Content.StartsWith(provider.BotMention, StringComparison.Ordinal))) //no mention @@ -165,22 +162,33 @@ namespace Tgstation.Server.Host.Components.Chat splits.RemoveAt(0); - var command = splits[0]; + var command = splits[0].ToUpperInvariant(); splits.RemoveAt(0); var arguments = String.Join(" ", splits); - - if (customCommandHandler == null) - logger.LogError("Recieved chat message with no command handler installed!"); + try { - var result = await customCommandHandler.HandleChatCommand(command, arguments, message.User, cancellationToken).ConfigureAwait(false); + if (!builtinCommands.TryGetValue(command, out ICommand commandHandler)) + { + var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)); + await Task.WhenAll(tasks).ConfigureAwait(false); + commandHandler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == command).FirstOrDefault(); + } + + if (command == default) + { + await SendMessage("Invalid command! Type '?' or 'help' for available commands.", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); + return; + } + + var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false); if(result != null) await SendMessage(result, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); } catch (Exception e) { //error bc custom commands should reply about why it failed - logger.LogError("Error processing custom command: {0}", e); + logger.LogError("Error processing chat command: {0}", e); await SendMessage("Internal error processing command!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs new file mode 100644 index 0000000000..5a7b05ae0e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; +using System; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + sealed class ChatFactory : IChatFactory + { + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the + /// + readonly ICommandFactory commandFactory; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + public ChatFactory(IIOManager ioManager, ILoggerFactory loggerFactory, ICommandFactory commandFactory) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); + } + + /// + public IChat CreateChat() => new Chat(new ProviderFactory(), ioManager, loggerFactory.CreateLogger(), commandFactory); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs index b6ef36467e..94d7456133 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs @@ -16,6 +16,6 @@ namespace Tgstation.Server.Host.Components.Chat.Commands public bool AdminOnly { get; protected set; } /// - public abstract Task Invoke(string arguments, CancellationToken cancellationToken); + public abstract Task Invoke(string arguments, User user, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs new file mode 100644 index 0000000000..fd1e731c5e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + sealed class CommandFactory : ICommandFactory + { + /// + /// The for the + /// + readonly IApplication application; + + /// + /// Construct a + /// + /// The value of + public CommandFactory(IApplication application) + { + this.application = application ?? throw new ArgumentNullException(nameof(application)); + } + + /// + public IReadOnlyList GenerateCommands() => new List + { + new VersionCommand(application) + }; + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index 6221f531c1..64ba61c72d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace Tgstation.Server.Host.Components.Chat.Commands { @@ -24,10 +26,11 @@ namespace Tgstation.Server.Host.Components.Chat.Commands } /// - public override void Invoke(string arguments) + public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) { if (handler == null) throw new InvalidOperationException("SetHandler() has not been called!"); + return handler.HandleChatCommand(Name, arguments, user, cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs index 0b5f58ffde..8694606731 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs @@ -27,8 +27,9 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// Invoke the /// /// The text after with leading whitespace trimmed + /// The who invoked the command /// The for the operation /// A resulting in a to send to the invoker - Task Invoke(string arguments, CancellationToken cancellationToken); + Task Invoke(string arguments, User user, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs index 57fe1b865c..62e7f8228c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs @@ -25,6 +25,6 @@ namespace Tgstation.Server.Host.Components.Chat.Commands } /// - public override Task Invoke(string arguments, CancellationToken cancellationToken) => Task.FromResult(application.VersionString); + public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(application.VersionString); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs new file mode 100644 index 0000000000..734b6243f3 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs @@ -0,0 +1,14 @@ +namespace Tgstation.Server.Host.Components.Chat +{ + /// + /// For creating s + /// + interface IChatFactory + { + /// + /// Create a + /// + /// A new + IChat CreateChat(); + } +} diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/EventType.cs index 53e6c9c9bd..615c7ed3bb 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/EventType.cs @@ -3,7 +3,7 @@ /// /// Types of events /// - enum EventType + public enum EventType { /// /// Parameters: Reference name, commit sha diff --git a/src/Tgstation.Server.Host/Components/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/IEventConsumer.cs index 322860c7b3..986f9f2213 100644 --- a/src/Tgstation.Server.Host/Components/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/IEventConsumer.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components /// /// Consumes s and takes the appropriate actions /// - interface IEventConsumer + public interface IEventConsumer { /// /// Handle a given diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index d9d4f4cda8..600a71ecc0 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -1,4 +1,5 @@ -using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components { @@ -10,8 +11,10 @@ namespace Tgstation.Server.Host.Components /// /// Create an /// - /// The + /// The + /// The for the + /// The for the /// A new - IInstance CreateInstance(Host.Models.Instance metadata); + IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar, IReattachInfoHandler reattachInfoHandler); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 7939caed1b..e31d403ace 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -1,5 +1,11 @@ -using System; +using Byond.TopicSender; +using Microsoft.Extensions.Logging; +using System; +using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Components { @@ -16,22 +22,67 @@ namespace Tgstation.Server.Host.Components /// readonly IDatabaseContextFactory databaseContextFactory; + /// + /// The for the + /// + readonly IApplication application; + + /// + /// The for the + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the + /// + readonly IByondTopicSender byondTopicSender; + + /// + /// The for the + /// + readonly IServerUpdater serverUpdater; + + /// + /// The for the + /// + readonly ICryptographySuite cryptographySuite; + + /// + /// The for the + /// + readonly IExecutor executor; + + /// + /// The for the + /// + readonly ICommandFactory commandFactory; + /// /// Construct an /// /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory) + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.application = application ?? throw new ArgumentNullException(nameof(application)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite )); + this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); } /// - public IInstance CreateInstance(Models.Instance metadata) + public IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar, IReattachInfoHandler reattachInfoHandler) { //Create the ioManager for the instance - var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); //various other ioManagers @@ -42,10 +93,23 @@ namespace Tgstation.Server.Host.Components var codeModificationsIoMananger = new ResolvingIOManager(instanceIoManager, "CodeModifications"); var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, metadata); + var commandFactory = new CommandFactory(application); + var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory); + var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager); + IByond byond = null; + IConfiguration configuration = null; + + var chat = chatFactory.CreateChat(); + var sessionControllerFactory = new SessionControllerFactory(executor, byond, byondTopicSender, interopRegistrar, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata); + var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, metadata); + var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings); + + var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, watchdog, loggerFactory.CreateLogger()); throw new NotImplementedException(); + //return new Instance(metadata, repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory); } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 99e19e7ef0..c7533c58ca 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Runs and monitors the twin server controllers /// - public interface IWatchdog : IHostedService, IDisposable + public interface IWatchdog : IHostedService, IDisposable, IEventConsumer { /// /// If the watchdog is running diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index 4f2dc78885..696d51016c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -47,11 +47,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IApplication application; - /// - /// The for the - /// - readonly IInstance instance; - /// /// The for the /// @@ -67,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly ILoggerFactory loggerFactory; + /// + /// The for the + /// + readonly Models.Instance instance; + /// /// Construct a /// @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IInstance instance, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory) + public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory, Models.Instance instance) { this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); @@ -111,7 +111,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ChatChannelsJson = GuidJsonFile(), ChatCommandsJson = GuidJsonFile(), HostPath = application.HostingPath, - InstanceName = instance.GetMetadata().Name, + InstanceName = instance.Name, Revision = dmbProvider.CompileJob.RevisionInformation }; interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.TestMerges.Select(x => new TestMerge diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index c64d6212c7..6129f45488 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -15,7 +15,7 @@ using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Watchdog { /// - sealed class Watchdog : IWatchdog, IEventConsumer, ICustomCommandHandler + sealed class Watchdog : IWatchdog, ICustomCommandHandler { /// /// The time in milliseconds to wait from starting to start . Does not take responsiveness into account diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index cd8c07c453..3395876d70 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Byond.TopicSender; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -14,6 +15,9 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Reflection; using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Models; @@ -141,10 +145,18 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); - services.AddSingleton, PasswordHasher>(); + services.AddSingleton, PasswordHasher>(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(new ByondTopicSender + { + ReceiveTimeout = 5000, + SendTimeout = 5000 + }); + services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(); From d76b2d561fb5cdd21b5a74c2f29807e51958a429 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 19 Jul 2018 01:29:42 -0400 Subject: [PATCH 12/20] Stufff --- .../Components/IInstance.cs | 2 +- .../Components/IInstanceFactory.cs | 3 +- .../Components/Instance.cs | 41 --------- .../Components/InstanceFactory.cs | 10 ++- .../Components/InstanceManager.cs | 2 +- .../Components/ReattachInfoHandler.cs | 83 +++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 2 +- 7 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 54cd4468d6..9868430342 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components /// /// For interacting with the instance services /// - public interface IInstance : IHostedService, IReattachInfoHandler + public interface IInstance : IHostedService { /// /// The for the diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index 600a71ecc0..ea9ed59f44 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -13,8 +13,7 @@ namespace Tgstation.Server.Host.Components /// /// The /// The for the - /// The for the /// A new - IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar, IReattachInfoHandler reattachInfoHandler); + IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index b97f722e0c..d607053ab2 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -157,46 +157,5 @@ namespace Tgstation.Server.Host.Components timerTask = TimerLoop(newInterval.Value, timerCts.Token); } } - - /// - public Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) => - { - var instance = new Models.Instance { Id = metadata.Id }; - db.Instances.Attach(instance); - - Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo) - { - db.CompileJobs.Attach(wdInfo.Dmb.CompileJob); - return new Models.ReattachInformation - { - AccessIdentifier = wdInfo.AccessIdentifier, - ChatChannelsJson = wdInfo.ChatChannelsJson, - ChatCommandsJson = wdInfo.ChatCommandsJson, - CompileJob = wdInfo.Dmb.CompileJob, - IsPrimary = wdInfo.IsPrimary, - Port = wdInfo.Port, - ProcessId = wdInfo.ProcessId, - RebootState = wdInfo.RebootState - }; - } - - instance.WatchdogReattachInformation = new Models.WatchdogReattachInformation - { - Alpha = ConvertReattachInfo(reattachInformation.Alpha), - Bravo = ConvertReattachInfo(reattachInformation.Bravo), - AlphaIsActive = reattachInformation.AlphaIsActive, - }; - await db.Save(cancellationToken).ConfigureAwait(false); - }); - - /// - public async Task Load(CancellationToken cancellationToken) - { - Models.WatchdogReattachInformation result = null; - await databaseContextFactory.UseContext(async (db) => - result = await db.Instances.Where(x => x.Id == metadata.Id).Select(x => x.WatchdogReattachInformation).FirstAsync(cancellationToken).ConfigureAwait(false) - ).ConfigureAwait(false); - return new WatchdogReattachInformation(result, dmbFactory); - } } } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index e31d403ace..efbefbe737 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Components /// The for the /// readonly ICommandFactory commandFactory; - + /// /// Construct an /// @@ -64,23 +64,26 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of /// The value of /// The value of /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory) + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite )); this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); + this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); } /// - public IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar, IReattachInfoHandler reattachInfoHandler) + public IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar) { //Create the ioManager for the instance var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); @@ -103,6 +106,7 @@ namespace Tgstation.Server.Host.Components var chat = chatFactory.CreateChat(); var sessionControllerFactory = new SessionControllerFactory(executor, byond, byondTopicSender, interopRegistrar, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata); + var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, metadata); var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, metadata); var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings); diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index ff78b157b8..24dd107c18 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Components { if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - var instance = instanceFactory.CreateInstance(metadata); + var instance = instanceFactory.CreateInstance(metadata, this); lock (this) { if (instances.ContainsKey(metadata.Id)) diff --git a/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs new file mode 100644 index 0000000000..93c7e6aa91 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs @@ -0,0 +1,83 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class ReattachInfoHandler: IReattachInfoHandler + { + /// + /// The for the + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the + /// + readonly IDmbFactory dmbFactory; + + /// + /// The for the + /// + readonly Models.Instance metadata; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, Models.Instance metadata) + { + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); + this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + } + + /// + public Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) => + { + var instance = new Models.Instance { Id = metadata.Id }; + db.Instances.Attach(instance); + + Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo) + { + db.CompileJobs.Attach(wdInfo.Dmb.CompileJob); + return new Models.ReattachInformation + { + AccessIdentifier = wdInfo.AccessIdentifier, + ChatChannelsJson = wdInfo.ChatChannelsJson, + ChatCommandsJson = wdInfo.ChatCommandsJson, + CompileJob = wdInfo.Dmb.CompileJob, + IsPrimary = wdInfo.IsPrimary, + Port = wdInfo.Port, + ProcessId = wdInfo.ProcessId, + RebootState = wdInfo.RebootState + }; + } + + instance.WatchdogReattachInformation = new Models.WatchdogReattachInformation + { + Alpha = ConvertReattachInfo(reattachInformation.Alpha), + Bravo = ConvertReattachInfo(reattachInformation.Bravo), + AlphaIsActive = reattachInformation.AlphaIsActive, + }; + await db.Save(cancellationToken).ConfigureAwait(false); + }); + + /// + public async Task Load(CancellationToken cancellationToken) + { + Models.WatchdogReattachInformation result = null; + await databaseContextFactory.UseContext(async (db) => + result = await db.Instances.Where(x => x.Id == metadata.Id).Select(x => x.WatchdogReattachInformation).FirstAsync(cancellationToken).ConfigureAwait(false) + ).ConfigureAwait(false); + return new WatchdogReattachInformation(result, dmbFactory); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3395876d70..ae4489d387 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -148,7 +148,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton, PasswordHasher>(); services.AddSingleton(); services.AddSingleton(); - + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(new ByondTopicSender From 71ebb63398d179608b407126961f57100c45ac61 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 12:01:49 -0400 Subject: [PATCH 13/20] Fix some db stuff. Removes failing test --- src/Tgstation.Server.Api/Models/CompileJob.cs | 5 +++++ .../Models/Internal/CompileJob.cs | 5 ----- src/Tgstation.Server.Host/Components/DreamMaker.cs | 2 +- .../Components/Watchdog/SessionControllerFactory.cs | 4 ++-- src/Tgstation.Server.Host/Models/CompileJob.cs | 11 +++++++++-- src/Tgstation.Server.Host/Models/DatabaseContext.cs | 1 + .../Core/TestApplication.cs | 5 ++--- 7 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/CompileJob.cs b/src/Tgstation.Server.Api/Models/CompileJob.cs index 0d381b559b..44a3b7baad 100644 --- a/src/Tgstation.Server.Api/Models/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/CompileJob.cs @@ -14,5 +14,10 @@ namespace Tgstation.Server.Api.Models /// Git revision the compiler ran on. Not modifiable /// public RevisionInformation RevisionInformation { get; set; } + + /// + /// The the was made with + /// + public Version ByondVersion { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index 6255f1591c..7f717c121c 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -37,10 +37,5 @@ namespace Tgstation.Server.Api.Models.Internal /// Exit code of DM. If /// public int? ExitCode { get; set; } - - /// - /// The the was made with - /// - public Version ByondVersion { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index aaeb1204e3..5d3caa81d1 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components bool ddVerified; using (var byondLock = byond.UseExecutables(null)) { - job.ByondVersion = byondLock.Version; + job.ByondVersion = byondLock.Version.ToString(); await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index 696d51016c..bf472fcd33 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -141,7 +141,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { - var byondLock = currentByondLock ?? byond.UseExecutables(dmbProvider.CompileJob.ByondVersion); + var byondLock = currentByondLock ?? byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion)); try { //more sanitization here cause it uses the same scheme @@ -189,7 +189,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false); try { - var byondLock = byond.UseExecutables(reattachInformation.Dmb.CompileJob.ByondVersion); + var byondLock = byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion)); try { var session = executor.AttachToDreamDaemon(reattachInformation.ProcessId, byondLock); diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 6245765298..1fbcc9f498 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models @@ -23,6 +24,11 @@ namespace Tgstation.Server.Host.Models [Required] public RevisionInformation RevisionInformation { get; set; } + /// + /// The the was made with in string form + /// + public string ByondVersion { get; set; } + /// public Api.Models.CompileJob ToApi() => new Api.Models.CompileJob { @@ -33,7 +39,8 @@ namespace Tgstation.Server.Host.Models Id = Id, Job = Job.ToApi(), Output = Output, - RevisionInformation = RevisionInformation.ToApi() + RevisionInformation = RevisionInformation.ToApi(), + ByondVersion = Version.Parse(ByondVersion) }; } } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index ff966eba6f..1aa4170164 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -121,6 +121,7 @@ namespace Tgstation.Server.Host.Models var chatChannel = modelBuilder.Entity(); chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique(); chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique(); + chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade); } /// diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index 519d590b0d..6bc7d937d2 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -13,9 +13,8 @@ namespace Tgstation.Server.Host.Core.Tests public void ApplyUpdate(string updatePath) => throw new NotImplementedException(); public void RegisterForUpdate(Action action) => throw new NotImplementedException(); - - [TestMethod] - public async Task TestSuccessfulStartup() + + public static async Task TestSuccessfulStartup() { var dbName = Path.GetTempFileName(); try From cc8c0e20017fdf8e21aaa18d1e88bfe48e7b6bcb Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 14:42:24 -0400 Subject: [PATCH 14/20] Actually appveyor must have a database, no way this would work otherwise --- tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index 6bc7d937d2..980101a5ec 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -14,7 +14,8 @@ namespace Tgstation.Server.Host.Core.Tests public void RegisterForUpdate(Action action) => throw new NotImplementedException(); - public static async Task TestSuccessfulStartup() + [TestMethod] + public async Task TestSuccessfulStartup() { var dbName = Path.GetTempFileName(); try From 5cbc879bbc7a27f70c9cc36d8a5dcc965c58407b Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 15:12:43 -0400 Subject: [PATCH 15/20] And it was using SQLite too, you have no excuse --- tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index 980101a5ec..22833bacdb 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; @@ -22,6 +23,7 @@ namespace Tgstation.Server.Host.Core.Tests { using (var webHost = WebHost.CreateDefaultBuilder(new string[] { "Database:DatabaseType=Sqlite", "Database:ConnectionString=Data Source=" + dbName }) //force it to use sqlite .UseStartup() + .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) .Build() ) { From 64ef419ed0e48be83af57428f5982a1f72956616 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 15:34:14 -0400 Subject: [PATCH 16/20] Uhh, wow I think the chat system is done (barring commands) --- .../Components/Chat/Commands/BoolConverter.cs | 20 +++++++++++++++++++ .../{Command.cs => BuiltinCommand.cs} | 6 ++++-- .../Components/Chat/Commands/CustomCommand.cs | 17 +++++++++++++--- .../Chat/{ => Commands}/ICommandFactory.cs | 3 +-- .../Chat/Commands/VersionCommand.cs | 2 +- 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs rename src/Tgstation.Server.Host/Components/Chat/Commands/{Command.cs => BuiltinCommand.cs} (78%) rename src/Tgstation.Server.Host/Components/Chat/{ => Commands}/ICommandFactory.cs (78%) diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs new file mode 100644 index 0000000000..e3103e4298 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// for decoding bools returned by BYOND + /// + sealed class BoolConverter : JsonConverter + { + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0); + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1"; + + /// + public override bool CanConvert(Type objectType) => objectType == typeof(bool); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs similarity index 78% rename from src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs rename to src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs index 94d7456133..2b965fc957 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs @@ -3,8 +3,10 @@ using System.Threading.Tasks; namespace Tgstation.Server.Host.Components.Chat.Commands { - /// - public abstract class Command : ICommand + /// + /// s written in C# + /// + public abstract class BuiltinCommand : ICommand { /// public string Name { get; protected set; } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index 64ba61c72d..c3108af26f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; using System.Threading; using System.Threading.Tasks; @@ -7,8 +8,18 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// Represents a command made from DM code /// - public sealed class CustomCommand : Command + sealed class CustomCommand : ICommand { + /// + public string Name { get; set; } + + /// + public string HelpText { get; set; } + + /// + [JsonConverter(typeof(BoolConverter))] + public bool AdminOnly { get; set; } + /// /// The for the /// @@ -26,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands } /// - public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) + public Task Invoke(string arguments, User user, CancellationToken cancellationToken) { if (handler == null) throw new InvalidOperationException("SetHandler() has not been called!"); diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs similarity index 78% rename from src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs rename to src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs index 15827975ac..5520126604 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; -using Tgstation.Server.Host.Components.Chat.Commands; -namespace Tgstation.Server.Host.Components.Chat +namespace Tgstation.Server.Host.Components.Chat.Commands { /// /// Factory for built in s diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs index 62e7f8228c..74ea35ea27 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// to return the /// - sealed class VersionCommand : Command + sealed class VersionCommand : BuiltinCommand { /// /// The for the From 197bf8908bb5f89bfd6329c56d27c86e2c012d2f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 15:37:08 -0400 Subject: [PATCH 17/20] Adds the kek command Critical priority feature --- .../Components/Chat/Commands/CommandFactory.cs | 1 + .../Components/Chat/Commands/KekCommand.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs index fd1e731c5e..a9bd873534 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -24,6 +24,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// public IReadOnlyList GenerateCommands() => new List { + new KekCommand(), new VersionCommand(application) }; } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs new file mode 100644 index 0000000000..d608190e22 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// kek + /// + sealed class KekCommand : BuiltinCommand + { + /// + public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult("kek"); + } +} From bc7bea0d31fe0b79b8758df9d267b86495dd649a Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 15:44:07 -0400 Subject: [PATCH 18/20] Ignore post chat address punctuation --- src/Tgstation.Server.Host/Components/Chat/Chat.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 04f5c5ac68..7febaa1f3b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -148,11 +148,17 @@ namespace Tgstation.Server.Host.Components.Chat { logger.LogTrace("Chat message: {0}. User (Note unconverted provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User)); - if (!(message.Content.StartsWith(CommonMention, StringComparison.InvariantCultureIgnoreCase) || message.Content.StartsWith(provider.BotMention, StringComparison.Ordinal))) + var splits = new List(message.Content.Split(' ')); + var address = splits[0]; + if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ',')) + address = address.Substring(0, address.Length - 1); + + address = address.ToUpperInvariant(); + + if (address != CommonMention.ToUpperInvariant() && address != provider.BotMention.ToUpperInvariant()) //no mention return; - var splits = new List(message.Content.Split(' ')); if (splits.Count == 1) { //just a mention From 25d7f93c01027c63201b77b0689758e9ebd3b174 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 15:47:53 -0400 Subject: [PATCH 19/20] Implement builtin commands in a saner fashion --- .../Chat/Commands/BuiltinCommand.cs | 23 ------------------- .../Components/Chat/Commands/KekCommand.cs | 18 +++++++++++++-- .../Chat/Commands/VersionCommand.cs | 13 +++++++++-- 3 files changed, 27 insertions(+), 27 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs deleted file mode 100644 index 2b965fc957..0000000000 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/BuiltinCommand.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.Chat.Commands -{ - /// - /// s written in C# - /// - public abstract class BuiltinCommand : ICommand - { - /// - public string Name { get; protected set; } - - /// - public string HelpText { get; protected set; } - - /// - public bool AdminOnly { get; protected set; } - - /// - public abstract Task Invoke(string arguments, User user, CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs index d608190e22..7fa541ef37 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs @@ -6,9 +6,23 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// kek /// - sealed class KekCommand : BuiltinCommand + sealed class KekCommand : ICommand { + /// + /// kek + /// + const string Kek = "kek"; + /// - public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult("kek"); + public string Name => Kek; + + /// + public string HelpText => Kek; + + /// + public bool AdminOnly => false; + + /// + public Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(Kek); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs index 74ea35ea27..13b5a68f74 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs @@ -8,8 +8,17 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// to return the /// - sealed class VersionCommand : BuiltinCommand + sealed class VersionCommand : ICommand { + /// + public string Name => "version"; + + /// + public string HelpText => "Displays the tgstation server version"; + + /// + public bool AdminOnly => false; + /// /// The for the /// @@ -25,6 +34,6 @@ namespace Tgstation.Server.Host.Components.Chat.Commands } /// - public override Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(application.VersionString); + public Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(application.VersionString); } } From 349797b82d774a93b4209581073bac94362e4198 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Thu, 19 Jul 2018 16:20:34 -0400 Subject: [PATCH 20/20] Fix das build --- src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs | 1 + .../Components/Chat/Commands/CustomCommand.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs index 5a7b05ae0e..82f5cfc72c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using System; +using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Chat diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index c3108af26f..da23754a2d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// Represents a command made from DM code /// - sealed class CustomCommand : ICommand + public sealed class CustomCommand : ICommand { /// public string Name { get; set; }