From ce10cb80730d656ae1997f2177ed46b464ceadc0 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:03:25 -0400 Subject: [PATCH 01/32] Properly use byondTopicSender sanitization --- .../Components/Watchdog/ISessionController.cs | 2 +- .../Components/Watchdog/Watchdog.cs | 14 +++++++++++--- .../Components/Watchdog/WatchdogFactory.cs | 14 +++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs index 949e1b53d9..faa997cbf1 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Sends a command to DreamDaemon through /world/Topic() /// - /// The command to send + /// The sanitized command to send /// The for the operation /// A resulting in the result of /world/Topic() Task SendCommand(string command, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index d2a32643e2..52e2aa19cb 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Byond.TopicSender; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -68,6 +69,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IDatabaseContextFactory databaseContextFactory; + /// + /// The for the + /// + readonly IByondTopicSender byondTopicSender; + /// /// The for the /// @@ -104,10 +110,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The initial value of /// The containing the value of /// The value of - public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, DreamDaemonLaunchParameters initialLaunchParameters, Models.Instance instance, bool autoStart) + public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, DreamDaemonLaunchParameters initialLaunchParameters, Models.Instance instance, bool autoStart) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); @@ -115,6 +122,7 @@ namespace Tgstation.Server.Host.Components.Watchdog this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); instanceId = instance?.Id ?? throw new ArgumentNullException(nameof(instance)); this.autoStart = autoStart; @@ -540,7 +548,7 @@ namespace Tgstation.Server.Host.Components.Watchdog foreach (var I in parameters) { builder.Append("&"); - builder.Append(I); + builder.Append(byondTopicSender.SanitizeString(I)); } var activeServer = AlphaIsActive ? alphaServer : bravoServer; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index ad068c4b31..e14419c9e0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Byond.TopicSender; +using Microsoft.Extensions.Logging; using System; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; @@ -39,6 +40,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IDatabaseContextFactory databaseContextFactory; + /// + /// The for the + /// + readonly IByondTopicSender byondTopicSender; + /// /// The for the /// @@ -54,8 +60,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The value of - public WatchdogFactory(IChat chat, ISessionControllerFactory sessionManagerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, Models.Instance instance) + public WatchdogFactory(IChat chat, ISessionControllerFactory sessionManagerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, Models.Instance instance) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.sessionManagerFactory = sessionManagerFactory ?? throw new ArgumentNullException(nameof(sessionManagerFactory)); @@ -63,10 +70,11 @@ namespace Tgstation.Server.Host.Components.Watchdog this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionManagerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, settings, instance, settings.AutoStart.Value); + public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionManagerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value); } } From d581019c6b94beae4d174930b657c601b79f8109 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:04:08 -0400 Subject: [PATCH 02/32] Removes unneccessary cast --- src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 52e2aa19cb..c5998416ca 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Watchdog public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; } /// - public RebootState? RebootState => Running ? (RebootState?)(AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null; + public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null; /// /// The for the From a3251cf4f77b39fd8a6b9f3de6fd8948922c6ff6 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:12:58 -0400 Subject: [PATCH 03/32] Properly specify EventType enum values --- .../Components/EventType.cs | 64 +++++-------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/EventType.cs index 887d8ffe19..fa8b5a2b29 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/EventType.cs @@ -8,97 +8,65 @@ /// /// Parameters: Reference name, commit sha /// - RepoResetOrigin, + RepoResetOrigin = 0, /// /// Parameters: Reference name, commit sha /// - RepoCheckout, + RepoCheckout = 1, /// /// No parameters /// - RepoFetch, + RepoFetch = 2, /// /// Parameters: Comma separated list in form of "#{Pull Request Number} @ {7 character SHA} /// - RepoMergePullRequests, + RepoMergePullRequests = 3, /// /// Parameters: Current version, new version /// - ByondChangeStart, + ByondChangeStart = 4, /// /// No parameters /// - ByondChangeCancelled, + ByondChangeCancelled = 5, /// /// Parameters: Error string /// - ByondFail, + ByondFail = 6, /// /// No parameters /// - ByondStageComplete, + ByondStageComplete = 7, /// /// No parameters /// - ByondChangeComplete, + ByondChangeComplete = 8, /// /// Parameters: Commit sha, parameter of /// - CompileStart, + CompileStart = 9, /// /// No parameters /// - CompileCancelled, + CompileCancelled = 10, /// /// Parameters: Error string /// - CompileFailure, + CompileFailure = 11, /// /// No parameters /// - CompileComplete, - - /// - /// Parameters: Access token - /// - DDLaunched, + CompileComplete = 12, + /// /// Parameters: Exit code /// - DDCrash, + DDOtherCrash = 13, /// /// No parameters /// - DDExit, - /// - /// Parameters: Exit code - /// - DDOtherCrash, - /// - /// No parameters - /// - DDOtherExit, - /// - /// No parameters - /// - DDRestart, - /// - /// No parameters - /// - DDBeginGracefulRestart, - /// - /// No parameters - /// - DDBeginGracefulShutdown, - /// - /// No parameters - /// - DDCancelGraceful, - /// - /// No parameters - /// - DDTerminated, + DDOtherExit = 14, } } From f87c27704023886142b3172130d51b1d93a48854 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:13:37 -0400 Subject: [PATCH 04/32] Add more missing topic sanitization to SessionController --- .../Components/Watchdog/SessionController.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 43913c7e54..0792b0736c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -254,9 +254,18 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public Task SendCommand(string command, CancellationToken cancellationToken) => byondTopicSender.SendTopic(new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), String.Format(CultureInfo.InvariantCulture, "?{0}={1}&{2}={3}", InteropConstants.DMInteropAccessIdentifier, reattachInformation.AccessIdentifier, InteropConstants.DMParameterCommand, command), cancellationToken); + public Task SendCommand(string command, CancellationToken cancellationToken) => byondTopicSender.SendTopic( + new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), + String.Format(CultureInfo.InvariantCulture, + "?{0}={1}&{2}={3}", + byondTopicSender.SanitizeString(InteropConstants.DMInteropAccessIdentifier), + byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier), + byondTopicSender.SanitizeString(InteropConstants.DMParameterCommand), + //intentionally don't sanitize command, that's up to the caller + command), + cancellationToken); - async Task SetPortImpl(ushort port, CancellationToken cancellationToken) => await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", InteropConstants.DMTopicChangePort, InteropConstants.DMParameterNewPort, port), cancellationToken).ConfigureAwait(false) == InteropConstants.DMResponseSuccess; + async Task SetPortImpl(ushort port, CancellationToken cancellationToken) => await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChangePort), byondTopicSender.SanitizeString(InteropConstants.DMParameterNewPort), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false) == InteropConstants.DMResponseSuccess; /// public async Task ClosePort(CancellationToken cancellationToken) From 649effdcc68ef7cf220f3e2d0b1d3799a9e13b0e Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:14:28 -0400 Subject: [PATCH 05/32] Handle chat responses in Watchdog.HandleEvent --- .../Components/Watchdog/Watchdog.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index c5998416ca..8110f17efc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -539,6 +540,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) { + string results; using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { if (!Running) @@ -552,8 +554,24 @@ namespace Tgstation.Server.Host.Components.Watchdog } var activeServer = AlphaIsActive ? alphaServer : bravoServer; - await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false); + results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false); } + + if (results == null) + return; + + List responses; + try + { + responses = JsonConvert.DeserializeObject>(results); + } + catch + { + logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results); + return; + } + + await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false); } } } From 0c3aebce620d84b0de7da4cce9b1b473a36fc542 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:16:12 -0400 Subject: [PATCH 06/32] Document ChatResponse --- .../Components/Chat/ChatResponse.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs b/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs index e7bc242642..67f8cf6782 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs @@ -2,9 +2,19 @@ namespace Tgstation.Server.Host.Components.Chat { + /// + /// Represents a chat message requested by DD + /// sealed class ChatResponse { + /// + /// The message string + /// public string Message { get; set; } + + /// + /// The list of internal channel ids to send to + /// public List ChannelIds { get; set; } } } From bf0e37058d763b163c742d8cb1655e95303c7ccb Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:16:38 -0400 Subject: [PATCH 07/32] Change values of some builtin DMAPI events --- src/DMAPI/tgs.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index eee991a8aa..4e9749084b 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -44,8 +44,8 @@ //EVENT CODES -#define TGS_EVENT_PORT_SWAP 1 //before a port change is about to happen, extra parameter is new port -#define TGS_EVENT_REBOOT_MODE_CHANGE 2 //before a reboot mode change, extras parameters are the current and new reboot mode enums +#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port +#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums //OTHER ENUMS From 1a9dfd11e9055c9508e2a04815cd92f03a58782f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:18:43 -0400 Subject: [PATCH 08/32] Add eventConsumer and logger fieds to DreamMaker --- .../Components/DreamMaker.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index 75ac5c271b..c83a8e0e23 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -60,6 +61,14 @@ namespace Tgstation.Server.Host.Components /// The for /// readonly IApplication application; + /// + /// The for + /// + readonly IEventConsumer eventConsumer; + /// + /// The for + /// + readonly ILogger logger; /// /// Construct @@ -70,8 +79,9 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// - public DreamMaker(IByond byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application) + /// The value of + /// The value of + public DreamMaker(IByond byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, ILogger logger) { this.byond = byond; this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -79,6 +89,8 @@ namespace Tgstation.Server.Host.Components this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.application = application ?? throw new ArgumentNullException(nameof(application)); + this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// From ff63c512f74b38ef01f619519fcf2bf7374d09e1 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:19:16 -0400 Subject: [PATCH 09/32] Remove unecessary namespace specifications --- src/Tgstation.Server.Host/Components/DreamMaker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index c83a8e0e23..b68260c224 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -131,10 +131,10 @@ namespace Tgstation.Server.Host.Components /// Compiles a .dme with DreamMaker /// /// The path to the DreamMaker executable - /// The for the operation + /// The for the operation /// The for the operation /// A representing the running operation - async Task RunDreamMaker(string dreamMakerPath, Host.Models.CompileJob job, CancellationToken cancellationToken) + async Task RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken) { using (var dm = new Process()) { @@ -181,7 +181,7 @@ namespace Tgstation.Server.Host.Components /// /// Adds server side includes to the .dme being compiled /// - /// The for the operation + /// The for the operation /// The for the operation /// A representing the running operation async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken) From e5d8d461ded78b88ac0ec88a820ef6245258feef Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:27:28 -0400 Subject: [PATCH 10/32] Catch and log exceptions thrown by byondTopicSender --- .../Components/Watchdog/SessionController.cs | 45 +++++++++++++------ .../Watchdog/SessionControllerFactory.cs | 14 ++++-- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 0792b0736c..4d61522620 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -1,5 +1,6 @@ using Byond.TopicSender; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; @@ -110,6 +111,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IChat chat; + /// + /// The for the + /// + readonly ILogger logger; + /// /// The waits on when DreamDaemon currently has it's ports closed /// @@ -147,15 +153,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The used to construct /// The value of /// The value of - public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IChatJsonTrackingContext chatJsonTrackingContext, IChat chat) + /// The value of + public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IChatJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger logger) { this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); - this.session = session ?? throw new ArgumentNullException(nameof(session)); - this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); if (interopRegistrar == null) throw new ArgumentNullException(nameof(interopRegistrar)); + this.session = session ?? throw new ArgumentNullException(nameof(session)); + this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); interopContext = interopRegistrar.Register(reattachInformation.AccessIdentifier, this); @@ -254,16 +262,27 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public Task SendCommand(string command, CancellationToken cancellationToken) => byondTopicSender.SendTopic( - new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), - String.Format(CultureInfo.InvariantCulture, - "?{0}={1}&{2}={3}", - byondTopicSender.SanitizeString(InteropConstants.DMInteropAccessIdentifier), - byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier), - byondTopicSender.SanitizeString(InteropConstants.DMParameterCommand), - //intentionally don't sanitize command, that's up to the caller - command), - cancellationToken); + public async Task SendCommand(string command, CancellationToken cancellationToken) + { + try + { + return await byondTopicSender.SendTopic( + new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), + String.Format(CultureInfo.InvariantCulture, + "?{0}={1}&{2}={3}", + byondTopicSender.SanitizeString(InteropConstants.DMInteropAccessIdentifier), + byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier), + byondTopicSender.SanitizeString(InteropConstants.DMParameterCommand), + //intentionally don't sanitize command, that's up to the caller + command), + cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message); + return null; + } + } async Task SetPortImpl(ushort port, CancellationToken cancellationToken) => await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChangePort), byondTopicSender.SanitizeString(InteropConstants.DMParameterNewPort), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false) == InteropConstants.DMResponseSuccess; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index 19de65758f..18be1f76af 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -1,4 +1,5 @@ using Byond.TopicSender; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Globalization; @@ -61,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IChat chat; + /// + /// The for the + /// + readonly ILoggerFactory loggerFactory; + /// /// Construct a /// @@ -73,7 +79,8 @@ 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) + /// 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) { this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); @@ -84,6 +91,7 @@ namespace Tgstation.Server.Host.Components.Watchdog this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -149,7 +157,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IsPrimary = primaryDirectory, Port = portToUse.Value, ProcessId = session.ProcessId - }, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat); + }, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat, loggerFactory.CreateLogger()); } catch { @@ -186,7 +194,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var session = executor.AttachToDreamDaemon(reattachInformation.ProcessId, byondLock); try { - return new SessionController(reattachInformation, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat); + return new SessionController(reattachInformation, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat, loggerFactory.CreateLogger()); } catch { From 70ab0e5b50c94c30cf84a0626a11c35989fd5bf7 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:28:58 -0400 Subject: [PATCH 11/32] sessionManagerFactory -> sessionControllerFactory --- .../Components/Watchdog/WatchdogFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index e14419c9e0..6c948960d0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the /// - readonly ISessionControllerFactory sessionManagerFactory; + readonly ISessionControllerFactory sessionControllerFactory; /// /// The for the @@ -55,17 +55,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// Construct a /// /// The value of - /// 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 WatchdogFactory(IChat chat, ISessionControllerFactory sessionManagerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, Models.Instance instance) + public WatchdogFactory(IChat chat, ISessionControllerFactory sessionControllerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, Models.Instance instance) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); - this.sessionManagerFactory = sessionManagerFactory ?? throw new ArgumentNullException(nameof(sessionManagerFactory)); + this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler)); @@ -75,6 +75,6 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionManagerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value); + public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value); } } From 3024d632ea788e3bb03b8336442a5ae6ba003dac Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:41:51 -0400 Subject: [PATCH 12/32] Add sanitization to DD launch parameters --- .../Components/Watchdog/SessionControllerFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index 18be1f76af..b5ea4e60f0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -144,8 +144,8 @@ namespace Tgstation.Server.Host.Components.Watchdog var byondLock = byond.UseExecutables(dmbProvider.CompileJob.ByondVersion); try { - var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", application.Version, interopJsonFile, InteropConstants.DMParamHostVersion, InteropConstants.DMParamInfoJson); - + //more sanitization here cause it uses the same scheme + var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(InteropConstants.DMParamHostVersion), byondTopicSender.SanitizeString(InteropConstants.DMParamInfoJson)); var session = executor.RunDreamDaemon(launchParameters, byondLock, dmbProvider, parameters, !primaryPort, !primaryDirectory); try From 5a9a9cb42245534355c0411d812695b76811d688 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:47:40 -0400 Subject: [PATCH 13/32] Add passing through active byond lock from DreamMaker --- src/Tgstation.Server.Host/Components/DreamMaker.cs | 7 ++++--- .../Components/Watchdog/ISessionControllerFactory.cs | 3 ++- .../Components/Watchdog/SessionControllerFactory.cs | 7 ++++--- src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index b68260c224..1816ad5b00 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -98,9 +98,10 @@ namespace Tgstation.Server.Host.Components /// /// The timeout in seconds for validation /// The for the operation + /// The current /// The for the operation /// A resulting in if the DMAPI was successfully validated, otherwise - async Task VerifyApi(int timeout, Models.CompileJob job, CancellationToken cancellationToken) + async Task VerifyApi(int timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken) { var launchParameters = new DreamDaemonLaunchParameters { @@ -114,7 +115,7 @@ namespace Tgstation.Server.Host.Components var provider = new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension)))); var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); - using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, true, true, true, cancellationToken).ConfigureAwait(false)) + using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false)) { var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken); @@ -278,7 +279,7 @@ namespace Tgstation.Server.Host.Components Status = CompilerStatus.Verifying; - ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, cancellationToken).ConfigureAwait(false); + ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, byondLock, cancellationToken).ConfigureAwait(false); } if (!ddVerified) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs index 32faa37b0e..9f307f44c0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs @@ -14,12 +14,13 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The to use /// The to use + /// The current if any /// If the of should be used /// If the of should be used /// If the should only validate the DMAPI then exit /// The for the operation /// A resulting in a new - Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken); + Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken); /// /// Create a from an existing DreamDaemon instance diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index b5ea4e60f0..4f2dc78885 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) + public async Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) { var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort; if (!portToUse.HasValue) @@ -141,7 +141,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { - var byondLock = byond.UseExecutables(dmbProvider.CompileJob.ByondVersion); + var byondLock = currentByondLock ?? byond.UseExecutables(dmbProvider.CompileJob.ByondVersion); try { //more sanitization here cause it uses the same scheme @@ -167,7 +167,8 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch { - byondLock.Dispose(); + if (currentByondLock == null) + byondLock.Dispose(); throw; } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 8110f17efc..d41b375a8a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -390,14 +390,14 @@ namespace Tgstation.Server.Host.Components.Watchdog try { if (!doReattach || reattachInfo.Alpha == null) - alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, true, true, false, alphaStartCts.Token); + alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token); else alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken); //do a few seconds of delay so that any backends the servers use know that alpha came first await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false); Task bravoServerTask; if (!doReattach || reattachInfo.Bravo == null) - bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, false, false, false, cancellationToken); + bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken); else bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken); From f9f142bfb8150d2dc0026e6394bc1e72fe2f5912 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:49:03 -0400 Subject: [PATCH 14/32] Throw an aggregate exception if both alpha and bravo servers fail to start --- src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index d41b375a8a..add29a5cf6 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -404,7 +404,7 @@ namespace Tgstation.Server.Host.Components.Watchdog bravoServer = await bravoServerTask.ConfigureAwait(false); alphaServer = await alphaServerTask.ConfigureAwait(false); } - catch + catch (Exception e) { if (alphaServerTask != null) if (alphaServerTask.Status == TaskStatus.RanToCompletion) @@ -416,7 +416,10 @@ namespace Tgstation.Server.Host.Components.Watchdog { alphaServer = await alphaServerTask.ConfigureAwait(false); } - catch { } + catch (Exception e2) + { + throw new AggregateException(e, e2); + } } throw; } From 3fa9ed0b71edc79dba10a6f7b27804e4efd9ba05 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 11:58:59 -0400 Subject: [PATCH 15/32] Add compiler logging and events --- .../Components/DreamMaker.cs | 17 +++++++++++++---- .../Components/EventType.cs | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index 1816ad5b00..aaeb1204e3 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -224,6 +224,8 @@ namespace Tgstation.Server.Host.Components /// public async Task Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) { + logger.LogTrace("Begin Compile"); + await eventConsumer.HandleEvent(EventType.CompileStart, new List{ repository.Origin }, cancellationToken).ConfigureAwait(false); try { Status = CompilerStatus.Copying; @@ -284,13 +286,13 @@ namespace Tgstation.Server.Host.Components if (!ddVerified) //server never validated or compile failed - await CleanupFailedCompile().ConfigureAwait(false); + await Task.WhenAll(CleanupFailedCompile(), eventConsumer.HandleEvent(EventType.CompileFailure, new List { job.ExitCode == 0 ? "1" : "0" }, cancellationToken)).ConfigureAwait(false); else { job.DMApiValidated = true; Status = CompilerStatus.Duplicating; - + //duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); @@ -298,8 +300,10 @@ namespace Tgstation.Server.Host.Components //symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken); - await configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken).ConfigureAwait(false); - await symATask.ConfigureAwait(false); + var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); + + await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); + await eventConsumer.HandleEvent(EventType.CompileComplete, null, cancellationToken).ConfigureAwait(false); } await compileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false); return job; @@ -310,6 +314,11 @@ namespace Tgstation.Server.Host.Components throw; } } + catch (OperationCanceledException) + { + await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false); + throw; + } finally { Status = CompilerStatus.Idle; diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/EventType.cs index fa8b5a2b29..53e6c9c9bd 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/EventType.cs @@ -44,7 +44,7 @@ ByondChangeComplete = 8, /// - /// Parameters: Commit sha, parameter of + /// Parameters: Origin commit sha /// CompileStart = 9, /// @@ -52,7 +52,7 @@ /// CompileCancelled = 10, /// - /// Parameters: Error string + /// Parameters: "1" if compile succeeded and api validation failed, "0" otherwise /// CompileFailure = 11, /// From 15dd20c76eda29017bf072ee98d0f306730a8fa0 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:00:46 -0400 Subject: [PATCH 16/32] Use Task.WhenAll in Watchdog.LaunchNoLock instead of weird exception handling Remove uneeded verbosity on var declaration --- .../Components/Watchdog/Watchdog.cs | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index add29a5cf6..c0f07beaa5 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -363,7 +363,6 @@ namespace Tgstation.Server.Host.Components.Watchdog return null; } - Task chatTask; //this is necessary, the monitor could be in it's sleep loop trying to restart if (startMonitor && await StopMonitor().ConfigureAwait(false)) @@ -380,49 +379,26 @@ namespace Tgstation.Server.Host.Components.Watchdog if (alphaServer != null || bravoServer != null) throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!"); - WatchdogReattachInformation reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null; + var reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null; var doesntNeedNewDmb = doReattach && reattachInfo.Alpha != null && reattachInfo.Bravo != null; var dmbToUse = doesntNeedNewDmb ? null : await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false); Task alphaServerTask = null; try { - try - { - if (!doReattach || reattachInfo.Alpha == null) - alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token); - else - alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken); - //do a few seconds of delay so that any backends the servers use know that alpha came first - await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false); - Task bravoServerTask; - if (!doReattach || reattachInfo.Bravo == null) - bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken); - else - bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken); + if (!doReattach || reattachInfo.Alpha == null) + alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token); + else + alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken); + //do a few seconds of delay so that any backends the servers use know that alpha came first + await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false); + Task bravoServerTask; + if (!doReattach || reattachInfo.Bravo == null) + bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken); + else + bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken); - bravoServer = await bravoServerTask.ConfigureAwait(false); - alphaServer = await alphaServerTask.ConfigureAwait(false); - } - catch (Exception e) - { - if (alphaServerTask != null) - if (alphaServerTask.Status == TaskStatus.RanToCompletion) - alphaServer = await alphaServerTask.ConfigureAwait(false); - else - { - alphaStartCts.Cancel(); - try - { - alphaServer = await alphaServerTask.ConfigureAwait(false); - } - catch (Exception e2) - { - throw new AggregateException(e, e2); - } - } - throw; - } + await Task.WhenAll(alphaServerTask, bravoServerTask).ConfigureAwait(false); async Task CheckLaunch(ISessionController controller, string serverName) { @@ -478,7 +454,7 @@ namespace Tgstation.Server.Host.Components.Watchdog catch { if (alphaServer == null && bravoServer == null) - dmbToUse.Dispose(); //guaranteed to not be null here + dmbToUse.Dispose(); //guaranteed to not be null here DisposeAndNullControllers(); throw; } From 598d08b374d3f40696d2935b0ad8d355c4318f7c Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:02:18 -0400 Subject: [PATCH 17/32] Add skeleton chat implementation --- .../Components/Chat/Chat.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Chat.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs new file mode 100644 index 0000000000..1b5a0fdf09 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + sealed class Chat : IChat + { + /// + public bool IrcConnected => throw new System.NotImplementedException(); + + /// + public bool DiscordConnected => throw new System.NotImplementedException(); + + /// + public Task ChangeChannels(IEnumerable newChannels, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task ChangeSettings(Api.Models.Internal.ChatSettings newSettings, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task SendWatchdogMessage(string message, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + /// + public Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } +} From cf2429f82e9c7fe9fb960d9a8a99d6b96d282871 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:06:21 -0400 Subject: [PATCH 18/32] Add Discord.Net nuget package --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 8dfd920fb5..9c8124a0ee 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -34,6 +34,7 @@ + @@ -60,4 +61,8 @@ + + + + From 63f9b2c2a566c4a6d56b42c4b742460ba6676ffd Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:13:06 -0400 Subject: [PATCH 19/32] Update several dependencies --- .../Tgstation.Server.Host.csproj | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 9c8124a0ee..6aa781f294 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -36,18 +36,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + From a58aa5877d08e321f162bece3b6f89bbb44e8f5d Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:51:03 -0400 Subject: [PATCH 20/32] Remove Octokit from Tgstation.Server.Host.Watchdog dependencies --- .../Tgstation.Server.Host.Watchdog.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index 157f22838f..70e1a609ed 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -18,7 +18,6 @@ - From 805d63c6d4302c589a3a0fdea07ce4ce62114015 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 12:52:09 -0400 Subject: [PATCH 21/32] Fix HttpStatusCode casts --- .../Controllers/DreamDaemonController.cs | 4 ++-- src/Tgstation.Server.Host/Controllers/DreamMakerController.cs | 2 +- src/Tgstation.Server.Host/Controllers/InstanceController.cs | 2 +- src/Tgstation.Server.Host/Controllers/JobController.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 375e4dace8..168d3a46da 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Controllers var instance = instanceManager.GetInstance(Instance); if (instance.Watchdog.Running) - return StatusCode(HttpStatusCode.Gone); + return StatusCode((int)HttpStatusCode.Gone); await jobManager.RegisterOperation(new Models.Job { @@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Controllers var instance = instanceManager.GetInstance(Instance); if (!instance.Watchdog.Running) - return StatusCode(HttpStatusCode.Gone); + return StatusCode((int)HttpStatusCode.Gone); await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false); return Ok(); diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 3e5844e7ef..b936a07f87 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Controllers //alias for cancelling the latest job var job = await DatabaseContext.CompileJobs.OrderByDescending(x => x.Job.StartedAt).Select(x => new Job { Id = x.Job.Id, StoppedAt = x.Job.StoppedAt }).FirstAsync(cancellationToken).ConfigureAwait(false); if (job.StoppedAt != null) - return StatusCode(HttpStatusCode.Gone); + return StatusCode((int)HttpStatusCode.Gone); await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false); return Ok(); } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 3bb4ab83c5..d5665f6a4b 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Controllers { var originalModel = await DatabaseContext.Instances.Where(x => x.Id == model.Id).FirstAsync(cancellationToken).ConfigureAwait(false); if (originalModel == default(Models.Instance)) - return StatusCode(HttpStatusCode.Gone); + return StatusCode((int)HttpStatusCode.Gone); throw new NotImplementedException(); } diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 1dcab2f526..a84d50c8f5 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Controllers return Forbid(); if(job.StoppedAt != null) - return StatusCode(HttpStatusCode.Gone); + return StatusCode((int)HttpStatusCode.Gone); await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false); return Ok(); From d7b8689576640eebcb3575f52df5698e3c218488 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 15:58:14 -0400 Subject: [PATCH 22/32] Work on implementing chat --- .../Models/ChatChannel.cs | 5 + .../Models/ChatProvider.cs | 17 ++ .../Models/ChatSettings.cs | 14 +- .../Models/Internal/ChatSettings.cs | 43 ++--- .../Rights/ChatSettingsRights.cs | 28 +-- .../Components/Chat/Channel.cs | 14 ++ .../Components/Chat/ChannelMapping.cs | 9 + .../Components/Chat/Chat.cs | 162 +++++++++++++++--- .../Components/Chat/IChat.cs | 22 ++- ...kingContext.cs => IJsonTrackingContext.cs} | 2 +- .../Components/Chat/IProviderFactory.cs | 18 ++ .../Components/Chat/Message.cs | 8 + .../Components/Chat/ProviderFactory.cs | 27 +++ .../Components/Chat/Providers/IProvider.cs | 52 ++++++ .../Chat/{ChatResponse.cs => Response.cs} | 2 +- .../Components/Chat/User.cs | 10 ++ .../Components/Watchdog/SessionController.cs | 6 +- .../Components/Watchdog/Watchdog.cs | 4 +- .../Models/ChatChannel.cs | 2 +- .../Models/ChatSettings.cs | 7 +- 20 files changed, 355 insertions(+), 97 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/ChatProvider.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Channel.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs rename src/Tgstation.Server.Host/Components/Chat/{IChatJsonTrackingContext.cs => IJsonTrackingContext.cs} (73%) create mode 100644 src/Tgstation.Server.Host/Components/Chat/IProviderFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Message.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs rename src/Tgstation.Server.Host/Components/Chat/{ChatResponse.cs => Response.cs} (93%) create mode 100644 src/Tgstation.Server.Host/Components/Chat/User.cs diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index a927090029..c7ab3b9b32 100644 --- a/src/Tgstation.Server.Api/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs @@ -19,5 +19,10 @@ /// If the is an admin channel /// public bool IsAdminChannel { get; set; } + + /// + /// If the is a watchdog channel + /// + public bool IsWatchdogChannel { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/ChatProvider.cs b/src/Tgstation.Server.Api/Models/ChatProvider.cs new file mode 100644 index 0000000000..f41d9de9c5 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/ChatProvider.cs @@ -0,0 +1,17 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// Represents a chat service provider + /// + public enum ChatProvider + { + /// + /// Internet relay chat + /// + Irc, + /// + /// Superior chat service + /// + Discord + } +} diff --git a/src/Tgstation.Server.Api/Models/ChatSettings.cs b/src/Tgstation.Server.Api/Models/ChatSettings.cs index 2900a7e055..aab5431e9a 100644 --- a/src/Tgstation.Server.Api/Models/ChatSettings.cs +++ b/src/Tgstation.Server.Api/Models/ChatSettings.cs @@ -6,22 +6,10 @@ namespace Tgstation.Server.Api.Models /// public sealed class ChatSettings : Internal.ChatSettings { - /// - /// If the IRC connection is established - /// - [Permissions(DenyWrite = true)] - bool IrcConnected { get; set; } - - /// - /// If the Discord connection is established - /// - [Permissions(DenyWrite = true)] - bool DiscordConnected { get; set; } - /// /// Channels the Discord bot should listen/announce in /// - [Permissions(WriteRight = ChatSettingsRights.SetChannels)] + [Permissions(WriteRight = ChatSettingsRights.WriteChannels)] public List Channels { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs index a672895d3c..3357c507b1 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal @@ -8,44 +6,39 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Manage the server chat bots /// - [Model(RightsType.ChatSettings, RequiresInstance = true)] + [Model(RightsType.ChatSettings, RequiresInstance = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)] public class ChatSettings { /// - /// If the IRC client is enabled + /// The settings id /// - [Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)] - public bool IrcEnabled { get; set; } + [Permissions(DenyWrite = true)] + public long Id { get; set; } /// - /// The IRC server name + /// The name of the connection /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] + [Permissions(WriteRight = ChatSettingsRights.WriteName)] [Required] - public string IrcHost { get; set; } + public string Name { get; set; } /// - /// The IRC server port + /// If the connection is enabled /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] - public ushort IrcPort { get; set; } + [Permissions(WriteRight = ChatSettingsRights.WriteEnabled)] + public bool Enabled { get; set; } /// - /// The IRC server NickServ password + /// The used for the connection /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] - public string IrcNickServPassword { get; set; } + [Permissions(WriteRight = ChatSettingsRights.WriteProvider)] + public ChatProvider Provider { get; set; } /// - /// If the Discord bot is enabled + /// The information used to connect to the /// - [Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)] - public bool DiscordEnabled { get; set; } - - /// - /// The Discord bot token - /// - [Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)] - public string DiscordBotToken { get; set; } + [Permissions(ReadRight = ChatSettingsRights.ReadConnectionString, WriteRight = ChatSettingsRights.ReadConnectionString)] + [Required] + public string ConnectionString { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs index d27723c28c..e8803311ac 100644 --- a/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs +++ b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs @@ -13,24 +13,32 @@ namespace Tgstation.Server.Api.Rights /// None = 0, /// - /// User can enable/disable the IRC client + /// User can change /// - SetIrcEnabled = 1, + WriteEnabled = 1, /// - /// User can change the IRC settings + /// User can change /// - SetIrcSettings = 2, + WriteProvider = 2, /// - /// User can change the chat channels + /// User can change /// - SetChannels = 4, + WriteChannels = 4, /// - /// User can enable/disable the Discord bot + /// User can change /// - SetDiscordEnabled = 8, + WriteConnectionString = 8, /// - /// User can change the Discord settings + /// User can read /// - SetDiscordSettings = 16, + ReadConnectionString = 16, + /// + /// User can read all chat settings except + /// + Read = 32, + /// + /// User can change + /// + WriteName = 32 } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs new file mode 100644 index 0000000000..d9916a1014 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -0,0 +1,14 @@ +namespace Tgstation.Server.Host.Components.Chat +{ + sealed class Channel + { + public long Id { get; set; } + + public string FriendlyName { get; set; } + + public string ConnectionName { get; set; } + + public bool IsAdminChannel { get; set; } + public bool IsPrivateChannel { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs new file mode 100644 index 0000000000..cdf098287a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -0,0 +1,9 @@ +namespace Tgstation.Server.Host.Components.Chat +{ + sealed class ChannelMapping + { + public long ProviderId { get; set; } + public long ProviderChannelId { get; set; } + public bool IsWatchdogChannel { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 1b5a0fdf09..86cc30b99c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -1,59 +1,175 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Chat.Providers; +using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Chat { /// sealed class Chat : IChat { - /// - public bool IrcConnected => throw new System.NotImplementedException(); + /// + /// The for the + /// + readonly IProviderFactory providerFactory; - /// - public bool DiscordConnected => throw new System.NotImplementedException(); + /// + /// The for the + /// + readonly IIOManager ioManager; - /// - public Task ChangeChannels(IEnumerable newChannels, CancellationToken cancellationToken) + /// + /// Map of s in use, keyed by + /// + readonly Dictionary providers; + + /// + /// Map of s to s + /// + readonly Dictionary mappedChannels; + + /// + /// Used for remapping s + /// + long channelIdCounter; + + /// + /// Construct a + /// + /// The value of + /// The value of + public Chat(IProviderFactory providerFactory, IIOManager ioManager) { - throw new System.NotImplementedException(); + this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + + providers = new Dictionary(); + mappedChannels = new Dictionary(); + channelIdCounter = 1; } /// - public Task ChangeSettings(Api.Models.Internal.ChatSettings newSettings, CancellationToken cancellationToken) + public void Dispose() { - throw new System.NotImplementedException(); + foreach (var I in providers) + I.Value.Dispose(); + } + + /// + public async Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken) + { + if (newChannels == null) + throw new ArgumentNullException(nameof(newChannels)); + IProvider provider; + lock (providers) + if (!providers.TryGetValue(connectionId, out provider)) + return; + var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false); + if (results == null) //aborted + return; + var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping + { + IsWatchdogChannel = x.IsWatchdogChannel, + ProviderChannelId = y.Id, + ProviderId = connectionId + }); + + long baseId; + lock (this) + { + baseId = channelIdCounter; + channelIdCounter += results.Count; + } + lock (mappedChannels) + { + lock (providers) + if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again + return; + foreach (var I in mappings) + mappedChannels.Add(baseId++, I); + } + } + + /// + public async Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken) + { + if (newSettings == null) + throw new ArgumentNullException(nameof(newSettings)); + IProvider provider; + lock (providers) + { + //raw settings changes forces a rebuild of the provider + if (providers.TryGetValue(newSettings.Id, out provider)) + { + providers.Remove(newSettings.Id); + provider.Dispose(); + } + if (newSettings.Enabled) + { + provider = providerFactory.CreateProvider(newSettings); + providers.Add(newSettings.Id, provider); + } + } + lock (mappedChannels) + foreach (var channelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key)) + mappedChannels.Remove(channelId); + if (newSettings.Enabled) + await provider.Connect(cancellationToken).ConfigureAwait(false); } /// public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (channelIds == null) + throw new ArgumentNullException(nameof(channelIds)); + + return Task.WhenAll(channelIds.Select(x => + { + ChannelMapping channelMapping; + lock(mappedChannels) + if (!mappedChannels.TryGetValue(x, out channelMapping)) + return Task.CompletedTask; + IProvider provider; + lock (providers) + if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) + return Task.CompletedTask; + return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken); + })); } /// public Task SendWatchdogMessage(string message, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + 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); } /// - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))); + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + ioManager.ResolvePath("."); + throw new NotImplementedException(); } /// - public Task StopAsync(CancellationToken cancellationToken) + public bool Connected(long connectionId) { - throw new System.NotImplementedException(); - } - - /// - public Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); + lock (providers) + return providers.TryGetValue(connectionId, out var provider) && provider.Connected; } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index f73805eb2b..26eacbfd4f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -10,33 +10,31 @@ namespace Tgstation.Server.Host.Components.Chat /// /// For managing connected chat services /// - public interface IChat : IHostedService + public interface IChat : IHostedService, IDisposable { /// - /// If the IRC client is connected + /// If a given set of is connected /// - bool IrcConnected { get; } + /// The of the connection + /// if it is connected, otherwise + bool Connected(long connectionId); /// - /// If the Discord client is connected - /// - bool DiscordConnected { get; } - - /// - /// Change chat settings + /// Change chat settings. If the is not currently in use, a new connection will be made instead /// /// The new /// The for the operation - /// A representing the running operation + /// A representing the running operation. Will complete immediately if the property of is Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken); /// /// Change chat channels /// + /// The of the connection /// An of the new list of s /// The for the operation /// A representing the running operation - Task ChangeChannels(IEnumerable newChannels, CancellationToken cancellationToken); + Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken); /// /// Send a chat to a given set of @@ -63,6 +61,6 @@ namespace Tgstation.Server.Host.Components.Chat /// The name of the chat commands json /// The for the operation /// A resulting in a tied to the lifetime of the json trackings - Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken); + Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatJsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs similarity index 73% rename from src/Tgstation.Server.Host/Components/Chat/IChatJsonTrackingContext.cs rename to src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs index 8926b33291..5988619602 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatJsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Represents a tracking of dynamic chat json files /// - public interface IChatJsonTrackingContext : IDisposable + public interface IJsonTrackingContext : IDisposable { } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/IProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/IProviderFactory.cs new file mode 100644 index 0000000000..b4b47f2d5b --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/IProviderFactory.cs @@ -0,0 +1,18 @@ +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Chat.Providers; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + /// Factory for s + /// + interface IProviderFactory + { + /// + /// Create a + /// + /// The for the new provider + /// A new + IProvider CreateProvider(ChatSettings settings); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs new file mode 100644 index 0000000000..e9b90e6560 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -0,0 +1,8 @@ +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + sealed class Message + { + string Content { get; set; } + User User { get; set; } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs new file mode 100644 index 0000000000..b16cd3617c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Chat.Providers; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + sealed class ProviderFactory : IProviderFactory + { + /// + public IProvider CreateProvider(Api.Models.Internal.ChatSettings settings) + { + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + switch (settings.Provider) + { + case ChatProvider.Irc: + throw new NotImplementedException(); + case ChatProvider.Discord: + throw new NotImplementedException(); + default: + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs new file mode 100644 index 0000000000..278ffa5813 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Chat.Providers +{ + /// + /// For interacting with a chat service + /// + interface IProvider : IDisposable + { + /// + /// If the + /// + bool Connected { get; } + + /// + /// The that indicates the was mentioned + /// + string BotMention { get; } + + /// + /// Get a resulting in the next the recieves or on a disconnect + /// + Task NextMessage { get; } + + /// + /// Attempt to connect the + /// + /// The for the operation + /// A resulting in on success, otherwise + Task Connect(CancellationToken cancellationToken); + + /// + /// Get the s for given + /// + /// The s to map + /// The for the operation + /// A resulting in a of the s representing + Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); + + /// + /// Send a message to the + /// + /// The to send to + /// The message contents + /// The for the operation + /// A representing the running operation + Task SendMessage(long channelId, string message, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs b/src/Tgstation.Server.Host/Components/Chat/Response.cs similarity index 93% rename from src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs rename to src/Tgstation.Server.Host/Components/Chat/Response.cs index 67f8cf6782..855752e790 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Response.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Represents a chat message requested by DD /// - sealed class ChatResponse + sealed class Response { /// /// The message string diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs new file mode 100644 index 0000000000..cc081dfaa9 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/User.cs @@ -0,0 +1,10 @@ +namespace Tgstation.Server.Host.Components.Chat +{ + class User + { + long Id { get; set; } + string FriendlyName { get; set; } + string Mention { get; set; } + Channel channel { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 4d61522620..4937f7e170 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -102,9 +102,9 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly ISession session; /// - /// The for the + /// The for the /// - readonly IChatJsonTrackingContext chatJsonTrackingContext; + readonly IJsonTrackingContext chatJsonTrackingContext; /// /// The for the @@ -154,7 +154,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IChatJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger logger) + public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger logger) { this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index c0f07beaa5..70ce656dc0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -539,10 +539,10 @@ namespace Tgstation.Server.Host.Components.Watchdog if (results == null) return; - List responses; + List responses; try { - responses = JsonConvert.DeserializeObject>(results); + responses = JsonConvert.DeserializeObject>(results); } catch { diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs index 8f57e1e885..3bd53864fd 100644 --- a/src/Tgstation.Server.Host/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -9,7 +9,7 @@ public long Id { get; set; } /// - /// The + /// The /// public long ChatSettingsId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/ChatSettings.cs b/src/Tgstation.Server.Host/Models/ChatSettings.cs index 5d3df56af5..1152181268 100644 --- a/src/Tgstation.Server.Host/Models/ChatSettings.cs +++ b/src/Tgstation.Server.Host/Models/ChatSettings.cs @@ -5,12 +5,7 @@ namespace Tgstation.Server.Host.Models { /// public sealed class ChatSettings : Api.Models.Internal.ChatSettings - { - /// - /// The row Id - /// - public long Id { get; set; } - + { /// /// The /// From 36e2c414b6294d1c85d8cf8baf08de4360f986c7 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 15:58:27 -0400 Subject: [PATCH 23/32] Add Cyberboss.SmartIrc4Net.Standard --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 6aa781f294..aa1f136bcd 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -34,6 +34,7 @@ + @@ -61,8 +62,4 @@ - - - - From 2c57a059fae16374ada104105354def95e3afb03 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 13 Jul 2018 17:04:01 -0400 Subject: [PATCH 24/32] Bunch more work on chat stuff --- .../Components/Chat/Channel.cs | 2 +- .../Components/Chat/ChannelMapping.cs | 2 + .../Components/Chat/Chat.cs | 72 +++++++++++++++++-- .../Components/Chat/Commands/Command.cs | 11 +++ .../Components/Chat/Commands/CustomCommand.cs | 23 ++++++ .../Components/Chat/IChat.cs | 6 ++ .../Components/Chat/ICommandFactory.cs | 10 +++ .../Components/Chat/ICustomCommandHandler.cs | 22 ++++++ .../Components/Chat/IJsonTrackingContext.cs | 6 ++ .../Components/Chat/JsonTrackingContext.cs | 63 ++++++++++++++++ .../Components/Chat/User.cs | 2 +- .../Components/Watchdog/Watchdog.cs | 19 ++++- 12 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs create mode 100644 src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs index d9916a1014..33b0e6f7db 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -1,6 +1,6 @@ namespace Tgstation.Server.Host.Components.Chat { - sealed class Channel + public sealed class Channel { public long Id { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs index cdf098287a..243bf71206 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -5,5 +5,7 @@ public long ProviderId { get; set; } public long 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 86cc30b99c..9f3e8e4324 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -1,9 +1,12 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Core; @@ -22,6 +25,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly IIOManager ioManager; + /// + /// s that never change + /// + readonly IReadOnlyList builtinCommands; + /// /// Map of s in use, keyed by /// @@ -32,6 +40,13 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly Dictionary mappedChannels; + readonly List trackingContexts; + + /// + /// The for the + /// + ICustomCommandHandler customCommandHandler; + /// /// Used for remapping s /// @@ -42,13 +57,16 @@ namespace Tgstation.Server.Host.Components.Chat /// /// The value of /// The value of - public Chat(IProviderFactory providerFactory, IIOManager ioManager) + /// The used to populate + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)); providers = new Dictionary(); mappedChannels = new Dictionary(); + trackingContexts = new List(); channelIdCounter = 1; } @@ -68,6 +86,12 @@ namespace Tgstation.Server.Host.Components.Chat lock (providers) if (!providers.TryGetValue(connectionId, out provider)) return; + lock (mappedChannels) + foreach (var kvp in mappedChannels.Where(x => x.Value.ProviderId == connectionId)) + { + mappedChannels.Remove(kvp.Key); + + } var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false); if (results == null) //aborted return; @@ -75,7 +99,8 @@ namespace Tgstation.Server.Host.Components.Chat { IsWatchdogChannel = x.IsWatchdogChannel, ProviderChannelId = y.Id, - ProviderId = connectionId + ProviderId = connectionId, + Channel = y }); long baseId; @@ -84,14 +109,24 @@ namespace Tgstation.Server.Host.Components.Chat baseId = channelIdCounter; channelIdCounter += results.Count; } + + Task task; lock (mappedChannels) { lock (providers) if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again return; foreach (var I in mappings) - mappedChannels.Add(baseId++, I); + { + var newId = baseId++; + mappedChannels.Add(newId, I); + I.Channel.Id = newId; + } + + lock (trackingContexts) + task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken))); } + await task.ConfigureAwait(false); } /// @@ -159,10 +194,25 @@ namespace Tgstation.Server.Host.Components.Chat public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// - public Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) + public async Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) { - ioManager.ResolvePath("."); - throw new NotImplementedException(); + if (customCommandHandler == null) + throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!"); + JsonTrackingContext context = null; + context = new JsonTrackingContext(ioManager, customCommandHandler, () => + { + lock (trackingContexts) + trackingContexts.Remove(context); + }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName)); + Task task; + lock (trackingContexts) + { + trackingContexts.Add(context); + lock (mappedChannels) + task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken))); + } + await task.ConfigureAwait(false); + return context; } /// @@ -171,5 +221,13 @@ namespace Tgstation.Server.Host.Components.Chat lock (providers) return providers.TryGetValue(connectionId, out var provider) && provider.Connected; } + + /// + public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler) + { + if (this.customCommandHandler != null) + throw new InvalidOperationException("RegisterCommandHandler() already called!"); + this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs new file mode 100644 index 0000000000..c607f04b45 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs @@ -0,0 +1,11 @@ +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + abstract class Command + { + public string Name { get; set; } + public string HelpText { get; set; } + public bool AdminOnly { get; set; } + + public abstract void Invoke(string arguments); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs new file mode 100644 index 0000000000..e2d9b5a4a1 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -0,0 +1,23 @@ +using System; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + sealed class CustomCommand : Command + { + ICustomCommandHandler handler; + + public void SetHandler(ICustomCommandHandler handler) + { + if (this.handler != null) + throw new InvalidOperationException("SetHandler() already called!"); + this.handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + /// + public override void Invoke(string arguments) + { + if (handler == null) + throw new InvalidOperationException("SetHandler() has not been called!"); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index 26eacbfd4f..a96d4b4fea 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -19,6 +19,12 @@ namespace Tgstation.Server.Host.Components.Chat /// if it is connected, otherwise bool Connected(long connectionId); + /// + /// Registers a to use + /// + /// A + void RegisterCommandHandler(ICustomCommandHandler customCommandHandler); + /// /// Change chat settings. If the is not currently in use, a new connection will be made instead /// diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs new file mode 100644 index 0000000000..4020ab0613 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Tgstation.Server.Host.Components.Chat.Commands; + +namespace Tgstation.Server.Host.Components.Chat +{ + interface ICommandFactory + { + IReadOnlyList GenerateCommands(); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs b/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs new file mode 100644 index 0000000000..5106ba0efd --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + /// Handles that map to those defined in a + /// + public interface ICustomCommandHandler + { + /// + /// Handle a chat command + /// + /// The command name + /// Everything typed after minus leading spaces + /// The sending + /// The for the operation + /// A resulting in the response text to send back + Task HandleChatCommand(string commandName, string arguments, User sender, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs index 5988619602..a4273ee79c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Chat.Commands; namespace Tgstation.Server.Host.Components.Chat { @@ -7,5 +11,7 @@ namespace Tgstation.Server.Host.Components.Chat /// public interface IJsonTrackingContext : IDisposable { + Task> GetCustomCommands(CancellationToken cancellationToken); + Task SetChannels(IEnumerable channels, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs new file mode 100644 index 0000000000..c231ea3ef7 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat +{ + /// + sealed class JsonTrackingContext : IJsonTrackingContext + { + readonly IIOManager ioManager; + readonly ICustomCommandHandler customCommandHandler; + readonly Action onDispose; + + readonly string commandsPath; + readonly string channelsPath; + + readonly SemaphoreSlim channelsSemaphore; + + public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, Action onDispose, string commandsPath, string channelsPath) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); + this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath)); + this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath)); + + channelsSemaphore = new SemaphoreSlim(1); + } + + /// + public void Dispose() => onDispose(); + + /// + public async Task> GetCustomCommands(CancellationToken cancellationToken) + { + try + { + var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false); + var resultJson = Encoding.UTF8.GetString(resultBytes); + var result = JsonConvert.DeserializeObject>(resultJson); + foreach (var I in result) + I.SetHandler(customCommandHandler); + return result; + } + catch + { + return new List(); + } + } + + /// + public async Task SetChannels(IEnumerable channels, CancellationToken cancellationToken) + { + using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false)) + await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels)), cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs index cc081dfaa9..7b716dc419 100644 --- a/src/Tgstation.Server.Host/Components/Chat/User.cs +++ b/src/Tgstation.Server.Host/Components/Chat/User.cs @@ -1,6 +1,6 @@ namespace Tgstation.Server.Host.Components.Chat { - class User + public sealed class User { long Id { get; set; } string FriendlyName { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 70ce656dc0..f4a19fad3d 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 + sealed class Watchdog : IWatchdog, IEventConsumer, ICustomCommandHandler { /// /// The time in milliseconds to wait from starting to start . Does not take responsiveness into account @@ -132,6 +132,8 @@ namespace Tgstation.Server.Host.Components.Watchdog serverUpdater.RegisterForUpdate(() => releaseServers = true); + chat.RegisterCommandHandler(this); + AlphaIsActive = true; ActiveLaunchParameters = initialLaunchParameters; releaseServers = false; @@ -552,5 +554,20 @@ namespace Tgstation.Server.Host.Components.Watchdog await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false); } + + /// + public async Task HandleChatCommand(string commandName, IEnumerable arguments, Chat.User sender, CancellationToken cancellationToken) + { + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + if (!Running) + return "ERROR: Server offline!"; + + var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChatCommand), byondTopicSender.SanitizeString(InteropConstants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments))); + + var activeServer = AlphaIsActive ? alphaServer : bravoServer; + return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!"; + } + } } } From 88d6ff42fbfea32bc2705a9c0826c92a669f8661 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 16 Jul 2018 01:23:51 -0400 Subject: [PATCH 25/32] Fixes and comments --- UpgradeLog.htm | Bin 0 -> 41384 bytes .../Components/Chat/Channel.cs | 19 ++++++++++++++++-- .../Components/Chat/Commands/Command.cs | 6 +++--- .../Components/Chat/Commands/CustomCommand.cs | 2 +- .../Components/Watchdog/Watchdog.cs | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 UpgradeLog.htm diff --git a/UpgradeLog.htm b/UpgradeLog.htm new file mode 100644 index 0000000000000000000000000000000000000000..1b7f8648c7794d8114bd8e89b04958735798ae8d GIT binary patch literal 41384 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 SOY%JaB&srrXUO#d_5Kfj4hm2J literal 0 HcmV?d00001 diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs index 33b0e6f7db..8a6cfc1d66 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -1,14 +1,29 @@ namespace Tgstation.Server.Host.Components.Chat { + /// + /// Represents a channel + /// public sealed class Channel { + /// + /// The channel Id. + /// + /// remaps this to an internal id using public long Id { get; set; } + /// + /// The user friendly name of the + /// public string FriendlyName { get; set; } - public string ConnectionName { get; set; } - + /// + /// If this is considered a channel for admin commands + /// public bool IsAdminChannel { get; set; } + + /// + /// If this i + /// public bool IsPrivateChannel { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs index c607f04b45..8ad981aee0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs @@ -1,11 +1,11 @@ namespace Tgstation.Server.Host.Components.Chat.Commands { - abstract class Command - { + public abstract class Command + { public string Name { get; set; } public string HelpText { get; set; } public bool AdminOnly { get; set; } public abstract void Invoke(string arguments); - } + } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index e2d9b5a4a1..94e8af328f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -2,7 +2,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands { - sealed class CustomCommand : Command + public sealed class CustomCommand : Command { ICustomCommandHandler handler; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index f4a19fad3d..c64d6212c7 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -556,7 +556,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task HandleChatCommand(string commandName, IEnumerable arguments, Chat.User sender, CancellationToken cancellationToken) + public async Task HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { From 9bbe7135f0517b801fefa8a76680bfe67fa5b5c5 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 16 Jul 2018 15:52:12 -0400 Subject: [PATCH 26/32] Get things building again --- src/DMAPI/tgs.dm | 3 +- .../Models/ChatChannel.cs | 2 +- .../Components/Chat/Channel.cs | 13 ++-- .../Components/Chat/Chat.cs | 67 ++++++++++++++----- .../Components/Chat/Commands/Command.cs | 18 +++++ .../Components/Chat/Commands/CustomCommand.cs | 10 +++ .../Components/Chat/IChat.cs | 8 +++ .../Components/Chat/IJsonTrackingContext.cs | 12 ++++ .../Components/Chat/Providers/IProvider.cs | 7 ++ .../Components/Chat/User.cs | 5 +- 10 files changed, 121 insertions(+), 24 deletions(-) diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 4e9749084b..c3daf1a380 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -97,8 +97,7 @@ /datum/tgs_chat_channel var/id //internal channel representation var/friendly_name //user friendly channel name - var/server_name //server name the channel resides on - var/provider_name //chat provider for the channel + var/connection_name //the name of the configured chat connection var/is_admin_channel //if the server operator has marked this channel for game admins only var/is_private_channel //if this is a private chat channel diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index c7ab3b9b32..942154e506 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 long? 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 8a6cfc1d66..c2eab84dd8 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs @@ -17,13 +17,18 @@ public string FriendlyName { get; set; } /// - /// If this is considered a channel for admin commands + /// The name of the connection the belongs to /// - public bool IsAdminChannel { get; set; } + public string ConnectionName { get; set; } /// - /// If this i + /// If this is considered a channel for admin commands /// - public bool IsPrivateChannel { get; set; } + public bool IsAdmin { get; set; } + + /// + /// If this is a 1-to-1 chat channel + /// + public bool IsPrivate { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 9f3e8e4324..862096ae44 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -1,8 +1,6 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; @@ -40,6 +38,9 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly Dictionary mappedChannels; + /// + /// The active s for the + /// readonly List trackingContexts; /// @@ -52,6 +53,11 @@ namespace Tgstation.Server.Host.Components.Chat /// long channelIdCounter; + /// + /// If has been called + /// + bool started; + /// /// Construct a /// @@ -77,21 +83,43 @@ namespace Tgstation.Server.Host.Components.Chat I.Value.Dispose(); } + /// + /// Remove a from and optionally updating the as well + /// + /// The of the to delete + /// If should be update + /// The for the operation + /// A resulting in the being removed if it exists, otherwise + async Task RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken) + { + IProvider provider; + lock (providers) + if (!providers.TryGetValue(connectionId, out provider)) + return null; + Task task; + lock (mappedChannels) + { + foreach (var kvp in mappedChannels.Where(x => x.Value.ProviderId == connectionId)) + mappedChannels.Remove(kvp.Key); + + if (updateTrackings) + lock (trackingContexts) + task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken))); + else + task = Task.CompletedTask; + } + await task.ConfigureAwait(false); + return provider; + } + /// public async Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken) { if (newChannels == null) throw new ArgumentNullException(nameof(newChannels)); - IProvider provider; - lock (providers) - if (!providers.TryGetValue(connectionId, out provider)) - return; - lock (mappedChannels) - foreach (var kvp in mappedChannels.Where(x => x.Value.ProviderId == connectionId)) - { - mappedChannels.Remove(kvp.Key); - - } + var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false); + if (provider == null) + return; var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false); if (results == null) //aborted return; @@ -152,7 +180,7 @@ namespace Tgstation.Server.Host.Components.Chat lock (mappedChannels) foreach (var channelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key)) mappedChannels.Remove(channelId); - if (newSettings.Enabled) + if (newSettings.Enabled && started) await provider.Connect(cancellationToken).ConfigureAwait(false); } @@ -188,10 +216,14 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))); + public async Task StartAsync(CancellationToken cancellationToken) + { + await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false); + started = true; + } /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))); /// public async Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken) @@ -229,5 +261,8 @@ namespace Tgstation.Server.Host.Components.Chat throw new InvalidOperationException("RegisterCommandHandler() already called!"); this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); } + + /// + public Task DeleteConnection(long connectionId, CancellationToken cancellationToken) => RemoveProvider(connectionId, true, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs index 8ad981aee0..d5f192ec59 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs @@ -1,11 +1,29 @@ namespace Tgstation.Server.Host.Components.Chat.Commands { + /// + /// Represents a command that can be invoked by talking to chat bots + /// public abstract class Command { + /// + /// The text to invoke the command. May not be "?" or "help" (case-insensitive) + /// public string Name { get; set; } + + /// + /// The help text to display when queires are made about the command + /// public string HelpText { get; set; } + + /// + /// If the command should only be available to s who's has set + /// public bool AdminOnly { get; set; } + /// + /// Invoke the + /// + /// The text after with leading whitespace trimmed public abstract void Invoke(string arguments); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index 94e8af328f..6221f531c1 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -2,10 +2,20 @@ namespace Tgstation.Server.Host.Components.Chat.Commands { + /// + /// Represents a command made from DM code + /// public sealed class CustomCommand : Command { + /// + /// The for the + /// ICustomCommandHandler handler; + /// + /// Set a new + /// + /// The value of public void SetHandler(ICustomCommandHandler handler) { if (this.handler != null) diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index a96d4b4fea..68c12aa1ed 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -33,6 +33,14 @@ namespace Tgstation.Server.Host.Components.Chat /// A representing the running operation. Will complete immediately if the property of is Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken); + /// + /// Disconnects and deletes a given connection + /// + /// The of the connection + /// The for the operation + /// A representing the running operation + Task DeleteConnection(long connectionId, CancellationToken cancellationToken); + /// /// Change chat channels /// diff --git a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs index a4273ee79c..778b6d0d3c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs @@ -11,7 +11,19 @@ namespace Tgstation.Server.Host.Components.Chat /// public interface IJsonTrackingContext : IDisposable { + /// + /// Read s from the + /// + /// The for the operation + /// A resulting in a of s in the Task> GetCustomCommands(CancellationToken cancellationToken); + + /// + /// Writes information about connected to the + /// + /// The s to write out + /// The for the operation + /// A representing the running operation Task SetChannels(IEnumerable channels, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 278ffa5813..7abe357269 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -32,6 +32,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// A resulting in on success, otherwise Task Connect(CancellationToken cancellationToken); + /// + /// Gracefully disconnects the provider. Implies a call to + /// + /// The for the operation + /// A representing the running operation + Task Disconnect(CancellationToken cancellationToken); + /// /// Get the s for given /// diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs index 7b716dc419..12eff58285 100644 --- a/src/Tgstation.Server.Host/Components/Chat/User.cs +++ b/src/Tgstation.Server.Host/Components/Chat/User.cs @@ -1,10 +1,13 @@ namespace Tgstation.Server.Host.Components.Chat { + /// + /// + /// public sealed class User { long Id { get; set; } string FriendlyName { get; set; } string Mention { get; set; } - Channel channel { get; set; } + Channel Channel { get; set; } } } From 4ac2606c65fb9a95c12ac2265a67b29e834fe92b Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 16 Jul 2018 16:14:18 -0400 Subject: [PATCH 27/32] ChatController Create --- .../Rights/ChatSettingsRights.cs | 10 +- .../Controllers/ChatController.cs | 97 +++++++++++++++++++ .../Controllers/DreamDaemonController.cs | 4 +- .../Models/DatabaseContext.cs | 4 +- .../Models/IDatabaseContext.cs | 5 + 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/Tgstation.Server.Host/Controllers/ChatController.cs diff --git a/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs index e8803311ac..c4336d76dd 100644 --- a/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs +++ b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs @@ -39,6 +39,14 @@ namespace Tgstation.Server.Api.Rights /// /// User can change /// - WriteName = 32 + WriteName = 32, + /// + /// User can create new + /// + Create = 64, + /// + /// User can delete + /// + Delete = 128 } } diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs new file mode 100644 index 0000000000..22f1d75cb4 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for managing + /// + [TgsAuthorize] + public sealed class ChatController : ModelController + { + /// + /// The for the + /// + readonly IInstanceManager instanceManager; + + /// + /// Construct a + /// + /// The for the + /// The for the + /// The value of + public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory) + { + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + } + + /// + [TgsAuthorize(ChatSettingsRights.Create)] + public override async Task Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + if (String.IsNullOrWhiteSpace(model.Name)) + return BadRequest(new { message = "Name cannot be null or whitespace!" }); + + if (String.IsNullOrWhiteSpace(model.ConnectionString)) + return BadRequest(new { message = "ConnectionString cannot be null or whitespace!" }); + + //try to update das db first + var dbModel = new Models.ChatSettings + { + Name = model.Name, + ConnectionString = model.ConnectionString, + Enabled = model.Enabled, + Channels = model.Channels?.Select(x => new Models.ChatChannel + { + DiscordChannelId = x.DiscordChannelId, + IrcChannel = x.IrcChannel, + IsAdminChannel = x.IsAdminChannel, + IsWatchdogChannel = x.IsWatchdogChannel + }).ToList() ?? new List(), + InstanceId = Instance.Id, + Provider = model.Provider, + }; + DatabaseContext.ChatSettings.Add(dbModel); + DatabaseContext.ChatChannels.AddRange(dbModel.Channels); + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + + try + { + try + { + //try to create it + var instance = instanceManager.GetInstance(Instance); + await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false); + + if (dbModel.Channels.Count > 0) + await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false); + } + catch + { + //undo the add + DatabaseContext.ChatSettings.Remove(dbModel); + DatabaseContext.ChatChannels.RemoveRange(dbModel.Channels); + await DatabaseContext.Save(default).ConfigureAwait(false); + throw; + } + } + catch (InvalidOperationException e) + { + return BadRequest(new { message = e.Message }); + } + return Json(dbModel); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 168d3a46da..c1a4a6b673 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -18,10 +18,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// for managing + /// for managing the /// [Route("/" + nameof(DreamDaemon))] - public sealed class DreamDaemonController : ModelController + public sealed class DreamDaemonController : ModelController { /// /// The for the diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index 554118c0f4..ff966eba6f 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -52,9 +52,7 @@ namespace Tgstation.Server.Host.Models /// public DbSet InstanceUsers { get; set; } - /// - /// The s in the - /// + /// public DbSet ChatChannels { get; set; } /// diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs index e08826dcc9..9e8d906905 100644 --- a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs @@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Models /// DbSet ChatSettings { get; set; } + /// + /// The in the + /// + DbSet ChatChannels { get; set; } + /// /// The in the /// From 9e58ccbc30288c7d58306d95eef569013fe08efb Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 16 Jul 2018 16:17:27 -0400 Subject: [PATCH 28/32] ChatController.Delete --- .../Controllers/ChatController.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 22f1d75cb4..6e30d57aee 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -9,6 +9,7 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; +using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers { @@ -82,7 +83,6 @@ namespace Tgstation.Server.Host.Controllers { //undo the add DatabaseContext.ChatSettings.Remove(dbModel); - DatabaseContext.ChatChannels.RemoveRange(dbModel.Channels); await DatabaseContext.Save(default).ConfigureAwait(false); throw; } @@ -93,5 +93,18 @@ namespace Tgstation.Server.Host.Controllers } return Json(dbModel); } + + /// + [TgsAuthorize(ChatSettingsRights.Delete)] + public override async Task Delete([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + var instance = instanceManager.GetInstance(Instance); + await Task.WhenAll(instance.Chat.DeleteConnection(model.Id, cancellationToken), DatabaseContext.ChatSettings.Where(x => x.Id == model.Id).DeleteAsync(cancellationToken)).ConfigureAwait(false); + + return Ok(); + } } } From 9ec064c6029448c452f373576b3cb20a5e3c932a Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 16 Jul 2018 16:26:07 -0400 Subject: [PATCH 29/32] Chat controller list --- .../Models/Internal/ChatSettings.cs | 2 +- .../Controllers/ChatController.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs index 3357c507b1..db9ba2db13 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Manage the server chat bots /// - [Model(RightsType.ChatSettings, RequiresInstance = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)] + [Model(RightsType.ChatSettings, RequiresInstance = true, CanList = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)] public class ChatSettings { /// diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 6e30d57aee..3288e5d7f7 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; @@ -106,5 +107,22 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } + + /// + [TgsAuthorize(ChatSettingsRights.Read)] + public override async Task List(CancellationToken cancellationToken) + { + var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels); + + var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false); + + var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0; + + if (!connectionStrings) + foreach (var I in results) + I.ConnectionString = null; + + return Json(results); + } } } From 99e10f905e1f2e80bde590e645deeec4291bad7f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 16 Jul 2018 17:02:07 -0400 Subject: [PATCH 30/32] ChatController.Update --- .../Models/Internal/ChatSettings.cs | 4 +- .../Components/Chat/Chat.cs | 4 +- .../Controllers/ChatController.cs | 94 +++++++++++++++++-- 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs index db9ba2db13..2a23052ad8 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs @@ -26,13 +26,13 @@ namespace Tgstation.Server.Api.Models.Internal /// If the connection is enabled /// [Permissions(WriteRight = ChatSettingsRights.WriteEnabled)] - public bool Enabled { get; set; } + public bool? Enabled { get; set; } /// /// The used for the connection /// [Permissions(WriteRight = ChatSettingsRights.WriteProvider)] - public ChatProvider Provider { get; set; } + public ChatProvider? Provider { get; set; } /// /// The information used to connect to the diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 862096ae44..07a29865c9 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -171,7 +171,7 @@ namespace Tgstation.Server.Host.Components.Chat providers.Remove(newSettings.Id); provider.Dispose(); } - if (newSettings.Enabled) + if (newSettings.Enabled.Value) { provider = providerFactory.CreateProvider(newSettings); providers.Add(newSettings.Id, provider); @@ -180,7 +180,7 @@ namespace Tgstation.Server.Host.Components.Chat lock (mappedChannels) foreach (var channelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key)) mappedChannels.Remove(channelId); - if (newSettings.Enabled && started) + if (newSettings.Enabled.Value && started) await provider.Connect(cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 3288e5d7f7..2a19a797e3 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -36,6 +39,14 @@ namespace Tgstation.Server.Host.Controllers this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } + static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel + { + DiscordChannelId = api.DiscordChannelId, + IrcChannel = api.IrcChannel, + IsAdminChannel = api.IsAdminChannel, + IsWatchdogChannel = api.IsWatchdogChannel + }; + /// [TgsAuthorize(ChatSettingsRights.Create)] public override async Task Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken) @@ -44,10 +55,16 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (String.IsNullOrWhiteSpace(model.Name)) - return BadRequest(new { message = "Name cannot be null or whitespace!" }); + return BadRequest(new { message = "name cannot be null or whitespace!" }); if (String.IsNullOrWhiteSpace(model.ConnectionString)) - return BadRequest(new { message = "ConnectionString cannot be null or whitespace!" }); + return BadRequest(new { message = "connection_string cannot be null or whitespace!" }); + + if (!model.Provider.HasValue) + return BadRequest(new { message = "provider cannot be null!" }); + + if (!model.Enabled.HasValue) + return BadRequest(new { message = "enabled cannot be null!" }); //try to update das db first var dbModel = new Models.ChatSettings @@ -55,13 +72,7 @@ namespace Tgstation.Server.Host.Controllers Name = model.Name, ConnectionString = model.ConnectionString, Enabled = model.Enabled, - Channels = model.Channels?.Select(x => new Models.ChatChannel - { - DiscordChannelId = x.DiscordChannelId, - IrcChannel = x.IrcChannel, - IsAdminChannel = x.IsAdminChannel, - IsWatchdogChannel = x.IsWatchdogChannel - }).ToList() ?? new List(), + Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List(), InstanceId = Instance.Id, Provider = model.Provider, }; @@ -124,5 +135,70 @@ namespace Tgstation.Server.Host.Controllers return Json(results); } + + /// + [TgsAuthorize(ChatSettingsRights.WriteChannels | ChatSettingsRights.WriteConnectionString | ChatSettingsRights.WriteEnabled | ChatSettingsRights.WriteName | ChatSettingsRights.WriteProvider)] + public override async Task Update([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels); + + var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + + if (current == default) + return StatusCode((int)HttpStatusCode.Gone); + + var userRights = (ChatSettingsRights)AuthenticationContext.GetRight(RightsType.ChatSettings); + + bool anySettingsModified = false; + + bool CheckModified(Expression> expression, ChatSettingsRights requiredRight) + { + var memberSelectorExpression = (MemberExpression)expression.Body; + var property = (PropertyInfo)memberSelectorExpression.Member; + + var newVal = property.GetValue(model); + if (newVal == null) + return false; + if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal) + return true; + + property.SetValue(current, newVal); + anySettingsModified = true; + return false; + }; + + if (!CheckModified(x => x.ConnectionString, ChatSettingsRights.WriteConnectionString) + || !CheckModified(x => x.Enabled, ChatSettingsRights.WriteEnabled) + || !CheckModified(x => x.Name, ChatSettingsRights.WriteName) + || !CheckModified(x => x.Provider, ChatSettingsRights.WriteProvider) + || (model.Channels != null && !userRights.HasFlag(ChatSettingsRights.WriteChannels))) + return Forbid(); + + if (model.Channels != null) + { + DatabaseContext.ChatChannels.RemoveRange(current.Channels); + var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList(); + DatabaseContext.ChatChannels.AddRange(dbChannels); + current.Channels = dbChannels; + } + + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + + var chat = instanceManager.GetInstance(Instance).Chat; + + if (anySettingsModified) + //have to rebuild the thing first + await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); + + if (model.Channels != null) + await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false); + + if(userRights.HasFlag(ChatSettingsRights.Read)) + return Json(current); + return Ok(); + } } } From 4298c81d070f1f4452c483f1223f0a5770975738 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 16 Jul 2018 17:49:54 -0400 Subject: [PATCH 31/32] Fix the shit --- src/DMAPI/tgs/v4/api.dm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index cce546d7c4..68c95bec92 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -206,8 +206,7 @@ var/datum/tgs_chat_channel/channel = new channel.id = I["id"] channel.friendly_name = I["friendly_name"] - channel.server_name = I["server_name"] - channel.provider_name = I["provider_name"] + channel.connection_name = I["connection_name"] channel.is_admin_channel = I["is_admin_channel"] channel.is_private_channel = FALSE //tgs will never send us pm channels . += channel From dc0e4f123202218fbe73d301feacf96b4e6553ed Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 16 Jul 2018 19:45:08 -0400 Subject: [PATCH 32/32] Unify this shit --- src/DMAPI/tgs/v4/api.dm | 17 ++++++++++------- src/DMAPI/tgs/v4/commands.dm | 9 +-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 68c95bec92..a6700ab4e5 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -203,13 +203,16 @@ //no caching cause tgs may change this var/list/json = json_decode(file2text(chat_channels_json_path)) for(var/I in json) - var/datum/tgs_chat_channel/channel = new - channel.id = I["id"] - channel.friendly_name = I["friendly_name"] - channel.connection_name = I["connection_name"] - channel.is_admin_channel = I["is_admin_channel"] - channel.is_private_channel = FALSE //tgs will never send us pm channels - . += channel + . += DecodeChannel(I) + +/datum/tgs_api/v4/proc/DecodeChannel(channel_json) + var/datum/tgs_chat_channel/channel = new + channel.id = channel_json["id"] + 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 + return channel #undef TGS4_TOPIC_COMMAND #undef TGS4_TOPIC_TOKEN diff --git a/src/DMAPI/tgs/v4/commands.dm b/src/DMAPI/tgs/v4/commands.dm index 6034affe19..48ca6e98de 100644 --- a/src/DMAPI/tgs/v4/commands.dm +++ b/src/DMAPI/tgs/v4/commands.dm @@ -30,14 +30,7 @@ u.id = user["id"] u.friendly_name = user["friendly_name"] u.mention = user["mention"] - var/datum/tgs_chat_channel/channel = new - u.channel = channel - var/channel_json = user["channel"] - channel.id = channel_json["id"] - channel.friendly_name = channel_json["friendly_name"] - channel.server_name = channel_json["server_name"] - channel.is_admin_channel = channel_json["is_admin_channel"] - channel.is_private_channel = channel_json["is_private_channel"] + u.channel = DecodeChannel(user["channel"]) var/datum/tgs_chat_command/sc = custom_commands[command] var/result = sc.Run(u, params)