diff --git a/UpgradeLog.htm b/UpgradeLog.htm
new file mode 100644
index 0000000000..1b7f8648c7
Binary files /dev/null and b/UpgradeLog.htm differ
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index eee991a8aa..c3daf1a380 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
@@ -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/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm
index cce546d7c4..a6700ab4e5 100644
--- a/src/DMAPI/tgs/v4/api.dm
+++ b/src/DMAPI/tgs/v4/api.dm
@@ -203,14 +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.server_name = I["server_name"]
- channel.provider_name = I["provider_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)
diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs
index a927090029..942154e506 100644
--- a/src/Tgstation.Server.Api/Models/ChatChannel.cs
+++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs
@@ -13,11 +13,16 @@
///
/// The Discord channel ID
///
- public long DiscordChannelId { get; set; }
+ public long? DiscordChannelId { get; set; }
///
/// 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..2a23052ad8 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, CanList = 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..c4336d76dd 100644
--- a/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs
+++ b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs
@@ -13,24 +13,40 @@ 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,
+ ///
+ /// User can create new
+ ///
+ Create = 64,
+ ///
+ /// User can delete
+ ///
+ Delete = 128
}
}
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 @@
-
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..c2eab84dd8
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs
@@ -0,0 +1,34 @@
+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; }
+
+ ///
+ /// The name of the connection the belongs to
+ ///
+ public string ConnectionName { get; set; }
+
+ ///
+ /// If this is considered a channel for admin commands
+ ///
+ 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/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
new file mode 100644
index 0000000000..243bf71206
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
@@ -0,0 +1,11 @@
+namespace Tgstation.Server.Host.Components.Chat
+{
+ sealed class ChannelMapping
+ {
+ 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
new file mode 100644
index 0000000000..07a29865c9
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+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;
+
+namespace Tgstation.Server.Host.Components.Chat
+{
+ ///
+ sealed class Chat : IChat
+ {
+ ///
+ /// The for the
+ ///
+ readonly IProviderFactory providerFactory;
+
+ ///
+ /// The for the
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// s that never change
+ ///
+ readonly IReadOnlyList builtinCommands;
+
+ ///
+ /// Map of s in use, keyed by
+ ///
+ readonly Dictionary providers;
+
+ ///
+ /// Map of s to s
+ ///
+ readonly Dictionary mappedChannels;
+
+ ///
+ /// The active s for the
+ ///
+ readonly List trackingContexts;
+
+ ///
+ /// The for the
+ ///
+ ICustomCommandHandler customCommandHandler;
+
+ ///
+ /// Used for remapping s
+ ///
+ long channelIdCounter;
+
+ ///
+ /// If has been called
+ ///
+ bool started;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ /// 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;
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var I in providers)
+ 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));
+ 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;
+ var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
+ {
+ IsWatchdogChannel = x.IsWatchdogChannel,
+ ProviderChannelId = y.Id,
+ ProviderId = connectionId,
+ Channel = y
+ });
+
+ long baseId;
+ lock (this)
+ {
+ 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)
+ {
+ 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);
+ }
+
+ ///
+ 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.Value)
+ {
+ 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.Value && started)
+ await provider.Connect(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken)
+ {
+ 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)
+ {
+ 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 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.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken)));
+
+ ///
+ public async Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
+ {
+ 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;
+ }
+
+ ///
+ public bool Connected(long connectionId)
+ {
+ 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));
+ }
+
+ ///
+ public Task DeleteConnection(long connectionId, CancellationToken cancellationToken) => RemoveProvider(connectionId, true, cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs b/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs
deleted file mode 100644
index e7bc242642..0000000000
--- a/src/Tgstation.Server.Host/Components/Chat/ChatResponse.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Collections.Generic;
-
-namespace Tgstation.Server.Host.Components.Chat
-{
- sealed class ChatResponse
- {
- public string Message { get; set; }
- public List ChannelIds { get; set; }
- }
-}
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..d5f192ec59
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs
@@ -0,0 +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
new file mode 100644
index 0000000000..6221f531c1
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs
@@ -0,0 +1,33 @@
+using System;
+
+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)
+ 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 f73805eb2b..68c12aa1ed 100644
--- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs
@@ -10,33 +10,45 @@ 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
+ /// Registers a to use
///
- bool DiscordConnected { get; }
+ /// A
+ void RegisterCommandHandler(ICustomCommandHandler customCommandHandler);
///
- /// 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);
+ ///
+ /// 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
///
+ /// 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 +75,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/IChatJsonTrackingContext.cs
deleted file mode 100644
index 8926b33291..0000000000
--- a/src/Tgstation.Server.Host/Components/Chat/IChatJsonTrackingContext.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-
-namespace Tgstation.Server.Host.Components.Chat
-{
- ///
- /// Represents a tracking of dynamic chat json files
- ///
- public interface IChatJsonTrackingContext : IDisposable
- {
- }
-}
\ No newline at end of file
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
new file mode 100644
index 0000000000..778b6d0d3c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs
@@ -0,0 +1,29 @@
+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
+{
+ ///
+ /// Represents a tracking of dynamic chat json files
+ ///
+ 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/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/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/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..7abe357269
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs
@@ -0,0 +1,59 @@
+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);
+
+ ///
+ /// 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
+ ///
+ /// 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/Response.cs b/src/Tgstation.Server.Host/Components/Chat/Response.cs
new file mode 100644
index 0000000000..855752e790
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Response.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Components.Chat
+{
+ ///
+ /// Represents a chat message requested by DD
+ ///
+ sealed class Response
+ {
+ ///
+ /// The message string
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// The list of internal channel ids to send to
+ ///
+ public List ChannelIds { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/User.cs b/src/Tgstation.Server.Host/Components/Chat/User.cs
new file mode 100644
index 0000000000..12eff58285
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/User.cs
@@ -0,0 +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; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs
index 75ac5c271b..aaeb1204e3 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));
}
///
@@ -86,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
{
@@ -102,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);
@@ -119,10 +132,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())
{
@@ -169,7 +182,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)
@@ -211,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;
@@ -266,18 +281,18 @@ 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)
//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);
@@ -285,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;
@@ -297,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 887d8ffe19..53e6c9c9bd 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
+ /// Parameters: Origin commit sha
///
- CompileStart,
+ CompileStart = 9,
///
/// No parameters
///
- CompileCancelled,
+ CompileCancelled = 10,
///
- /// Parameters: Error string
+ /// Parameters: "1" if compile succeeded and api validation failed, "0" otherwise
///
- 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,
}
}
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/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/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs
index 43913c7e54..4937f7e170 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;
@@ -101,15 +102,20 @@ namespace Tgstation.Server.Host.Components.Watchdog
readonly ISession session;
///
- /// The for the
+ /// The for the
///
- readonly IChatJsonTrackingContext chatJsonTrackingContext;
+ readonly IJsonTrackingContext chatJsonTrackingContext;
///
/// The for the
///
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, IJsonTrackingContext 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,9 +262,29 @@ 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 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}", 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)
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs
index 19de65758f..4f2dc78885 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,10 +91,11 @@ 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));
}
///
- 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)
@@ -133,11 +141,11 @@ 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
{
- 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
@@ -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
{
@@ -159,7 +167,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
catch
{
- byondLock.Dispose();
+ if (currentByondLock == null)
+ byondLock.Dispose();
throw;
}
}
@@ -186,7 +195,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
{
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
index d2a32643e2..c64d6212c7 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
@@ -1,8 +1,10 @@
-using Microsoft.Extensions.Logging;
+using Byond.TopicSender;
+using Microsoft.Extensions.Logging;
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;
@@ -13,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
@@ -36,7 +38,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
@@ -68,6 +70,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
readonly IDatabaseContextFactory databaseContextFactory;
+ ///
+ /// The for the
+ ///
+ readonly IByondTopicSender byondTopicSender;
+
///
/// The for the
///
@@ -104,10 +111,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 +123,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;
@@ -123,6 +132,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
serverUpdater.RegisterForUpdate(() => releaseServers = true);
+ chat.RegisterCommandHandler(this);
+
AlphaIsActive = true;
ActiveLaunchParameters = initialLaunchParameters;
releaseServers = false;
@@ -354,7 +365,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))
@@ -371,46 +381,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, 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);
- 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
- {
- if (alphaServerTask != null)
- if (alphaServerTask.Status == TaskStatus.RanToCompletion)
- alphaServer = await alphaServerTask.ConfigureAwait(false);
- else
- {
- alphaStartCts.Cancel();
- try
- {
- alphaServer = await alphaServerTask.ConfigureAwait(false);
- }
- catch { }
- }
- throw;
- }
+ await Task.WhenAll(alphaServerTask, bravoServerTask).ConfigureAwait(false);
async Task CheckLaunch(ISessionController controller, string serverName)
{
@@ -466,7 +456,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;
}
@@ -531,6 +521,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)
@@ -540,11 +531,42 @@ 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;
- 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);
+ }
+
+ ///
+ public async Task HandleChatCommand(string commandName, string 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!";
}
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs
index ad068c4b31..6c948960d0 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;
@@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
/// The for the
///
- readonly ISessionControllerFactory sessionManagerFactory;
+ readonly ISessionControllerFactory sessionControllerFactory;
///
/// The for the
@@ -39,6 +40,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
readonly IDatabaseContextFactory databaseContextFactory;
+ ///
+ /// The for the
+ ///
+ readonly IByondTopicSender byondTopicSender;
+
///
/// The for the
///
@@ -49,24 +55,26 @@ 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, 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));
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, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value);
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
new file mode 100644
index 0000000000..2a19a797e3
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -0,0 +1,204 @@
+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;
+using Microsoft.EntityFrameworkCore;
+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;
+using Z.EntityFramework.Plus;
+
+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));
+ }
+
+ 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)
+ {
+ 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 = "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
+ {
+ Name = model.Name,
+ ConnectionString = model.ConnectionString,
+ Enabled = model.Enabled,
+ Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).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);
+ await DatabaseContext.Save(default).ConfigureAwait(false);
+ throw;
+ }
+ }
+ catch (InvalidOperationException e)
+ {
+ return BadRequest(new { message = e.Message });
+ }
+ 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();
+ }
+
+ ///
+ [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);
+ }
+
+ ///
+ [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();
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 375e4dace8..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
@@ -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();
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
///
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
///
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index 8dfd920fb5..aa1f136bcd 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -34,19 +34,21 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+