diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index c3daf1a380..628b17cac6 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -103,7 +103,7 @@
//represents a chat user
/datum/tgs_chat_user
- var/id //Internal user representation
+ var/id //Internal user representation, requires channel to be unique
var/friendly_name //The user's public name
var/mention //The text to use to ping this user in a message
var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from
diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm
index a6700ab4e5..e62de1c25e 100644
--- a/src/DMAPI/tgs/v4/api.dm
+++ b/src/DMAPI/tgs/v4/api.dm
@@ -211,7 +211,7 @@
channel.friendly_name = channel_json["friendly_name"]
channel.connection_name = channel_json["connection_name"]
channel.is_admin_channel = channel_json["is_admin_channel"]
- channel.is_admin_channel = channel_json["is_private_channel"] || FALSE
+ channel.is_private_channel = channel_json["is_private_channel"] || FALSE
return channel
#undef TGS4_TOPIC_COMMAND
diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs
index 942154e506..d5131f4ff1 100644
--- a/src/Tgstation.Server.Api/Models/ChatChannel.cs
+++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs
@@ -13,7 +13,7 @@
///
/// The Discord channel ID
///
- public long? DiscordChannelId { get; set; }
+ public ulong? DiscordChannelId { get; set; }
///
/// If the is an admin channel
diff --git a/src/Tgstation.Server.Api/Models/CompileJob.cs b/src/Tgstation.Server.Api/Models/CompileJob.cs
index 0d381b559b..44a3b7baad 100644
--- a/src/Tgstation.Server.Api/Models/CompileJob.cs
+++ b/src/Tgstation.Server.Api/Models/CompileJob.cs
@@ -14,5 +14,10 @@ namespace Tgstation.Server.Api.Models
/// Git revision the compiler ran on. Not modifiable
///
public RevisionInformation RevisionInformation { get; set; }
+
+ ///
+ /// The the was made with
+ ///
+ public Version ByondVersion { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
index 6255f1591c..7f717c121c 100644
--- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
@@ -37,10 +37,5 @@ namespace Tgstation.Server.Api.Models.Internal
/// Exit code of DM. If
///
public int? ExitCode { get; set; }
-
- ///
- /// The the was made with
- ///
- public Version ByondVersion { get; set; }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs
index c2eab84dd8..07111826d8 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs
@@ -1,15 +1,29 @@
-namespace Tgstation.Server.Host.Components.Chat
+using Newtonsoft.Json;
+using System;
+using System.Globalization;
+
+namespace Tgstation.Server.Host.Components.Chat
{
///
/// Represents a channel
///
public sealed class Channel
{
+ ///
+ /// Backing field for . Represented as a to avoid BYOND percision loss
+ ///
+ public string Id { get; set; }
+
///
/// The channel Id.
///
/// remaps this to an internal id using
- public long Id { get; set; }
+ [JsonIgnore]
+ public ulong RealId
+ {
+ get => UInt64.Parse(Id, CultureInfo.InvariantCulture);
+ set => Id = value.ToString(CultureInfo.InvariantCulture);
+ }
///
/// The user friendly name of the
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
index 243bf71206..5185f033cc 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
@@ -1,11 +1,28 @@
namespace Tgstation.Server.Host.Components.Chat
{
+ ///
+ /// Represents a mapping of a
+ ///
sealed class ChannelMapping
{
+ ///
+ /// The Id of the
+ ///
public long ProviderId { get; set; }
- public long ProviderChannelId { get; set; }
+
+ ///
+ /// The original
+ ///
+ public ulong ProviderChannelId { get; set; }
+
+ ///
+ /// If is a watchdog channel
+ ///
public bool IsWatchdogChannel { get; set; }
+ ///
+ /// The with the mapped Id
+ ///
public Channel Channel { get; set; }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
index 07a29865c9..7febaa1f3b 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
@@ -1,4 +1,6 @@
-using System;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -13,6 +15,8 @@ namespace Tgstation.Server.Host.Components.Chat
///
sealed class Chat : IChat
{
+ const string CommonMention = "!tgs";
+
///
/// The for the
///
@@ -24,9 +28,14 @@ namespace Tgstation.Server.Host.Components.Chat
readonly IIOManager ioManager;
///
- /// s that never change
+ /// The for the
///
- readonly IReadOnlyList builtinCommands;
+ readonly ILogger logger;
+
+ ///
+ /// Unchanging s in the mapped by
+ ///
+ readonly Dictionary builtinCommands;
///
/// Map of s in use, keyed by
@@ -34,9 +43,9 @@ namespace Tgstation.Server.Host.Components.Chat
readonly Dictionary providers;
///
- /// Map of s to s
+ /// Map of s to s
///
- readonly Dictionary mappedChannels;
+ readonly Dictionary mappedChannels;
///
/// The active s for the
@@ -44,41 +53,57 @@ namespace Tgstation.Server.Host.Components.Chat
readonly List trackingContexts;
///
- /// The for the
+ /// The for
+ ///
+ readonly CancellationTokenSource handlerCts;
+
+ ///
+ /// The for the
///
ICustomCommandHandler customCommandHandler;
///
- /// Used for remapping s
+ /// The that monitors incoming chat messages
///
- long channelIdCounter;
+ Task chatHandler;
+
+ ///
+ /// Used for remapping s
+ ///
+ ulong channelIdCounter;
///
/// If has been called
///
- bool started;
+ bool started;
///
/// Construct a
///
/// The value of
/// The value of
+ /// The value of
/// The used to populate
- public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory)
+ public Chat(IProviderFactory providerFactory, IIOManager ioManager, ILogger logger, ICommandFactory commandFactory)
{
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
- builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ builtinCommands = new Dictionary();
+ foreach (var I in commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)))
+ builtinCommands.Add(I.Name, I);
providers = new Dictionary();
- mappedChannels = new Dictionary();
+ mappedChannels = new Dictionary();
trackingContexts = new List();
+ handlerCts = new CancellationTokenSource();
channelIdCounter = 1;
}
///
public void Dispose()
{
+ handlerCts.Dispose();
foreach (var I in providers)
I.Value.Dispose();
}
@@ -112,6 +137,108 @@ namespace Tgstation.Server.Host.Components.Chat
return provider;
}
+ ///
+ /// Processes a
+ ///
+ /// The who recevied
+ /// The to process
+ /// The for the operation
+ /// A representing the running operation
+ async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
+ {
+ logger.LogTrace("Chat message: {0}. User (Note unconverted provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
+
+ var splits = new List(message.Content.Split(' '));
+ var address = splits[0];
+ if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
+ address = address.Substring(0, address.Length - 1);
+
+ address = address.ToUpperInvariant();
+
+ if (address != CommonMention.ToUpperInvariant() && address != provider.BotMention.ToUpperInvariant())
+ //no mention
+ return;
+
+ if (splits.Count == 1)
+ {
+ //just a mention
+ await SendMessage("Hi!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ splits.RemoveAt(0);
+
+ var command = splits[0].ToUpperInvariant();
+ splits.RemoveAt(0);
+ var arguments = String.Join(" ", splits);
+
+ try
+ {
+ if (!builtinCommands.TryGetValue(command, out ICommand commandHandler))
+ {
+ var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ commandHandler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == command).FirstOrDefault();
+ }
+
+ if (command == default)
+ {
+ await SendMessage("Invalid command! Type '?' or 'help' for available commands.", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
+ if(result != null)
+ await SendMessage(result, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ //error bc custom commands should reply about why it failed
+ logger.LogError("Error processing chat command: {0}", e);
+ await SendMessage("Internal error processing command!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Monitors active providers for new s
+ ///
+ /// The for the operation
+ /// A representing the running operation
+ async Task MonitorMessages(CancellationToken cancellationToken)
+ {
+ var messageTasks = new Dictionary>();
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ //prune disconnected providers
+ foreach (var I in messageTasks)
+ if (!I.Key.Connected)
+ messageTasks.Remove(I.Key);
+
+ //add new ones
+ foreach (var I in providers)
+ if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
+ messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
+
+ //wait for a message
+ var tasks = messageTasks.Select(x => x.Value);
+ await Task.WhenAny().ConfigureAwait(false);
+
+ //process completed ones
+ foreach (var I in messageTasks.Where(x => x.Value.IsCompleted))
+ {
+ messageTasks.Remove(I.Key);
+
+ var message = await I.Value.ConfigureAwait(false);
+
+ await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (OperationCanceledException) { }
+ }
+
///
public async Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken)
{
@@ -126,16 +253,16 @@ namespace Tgstation.Server.Host.Components.Chat
var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
{
IsWatchdogChannel = x.IsWatchdogChannel,
- ProviderChannelId = y.Id,
+ ProviderChannelId = y.RealId,
ProviderId = connectionId,
Channel = y
});
- long baseId;
+ ulong baseId;
lock (this)
{
baseId = channelIdCounter;
- channelIdCounter += results.Count;
+ channelIdCounter += (ulong)results.Count;
}
Task task;
@@ -148,7 +275,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
var newId = baseId++;
mappedChannels.Add(newId, I);
- I.Channel.Id = newId;
+ I.Channel.RealId = newId;
}
lock (trackingContexts)
@@ -185,7 +312,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
///
- public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken)
+ public Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
@@ -209,7 +336,7 @@ namespace Tgstation.Server.Host.Components.Chat
///
public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
{
- List wdChannels;
+ List wdChannels;
lock (mappedChannels) //so it doesn't change while we're using it
wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
return SendMessage(message, wdChannels, cancellationToken);
@@ -219,11 +346,17 @@ namespace Tgstation.Server.Host.Components.Chat
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
+ chatHandler = MonitorMessages(handlerCts.Token);
started = true;
}
///
- public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken)));
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ handlerCts.Cancel();
+ await chatHandler.ConfigureAwait(false);
+ await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
+ }
///
public async Task TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs
new file mode 100644
index 0000000000..82f5cfc72c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.Logging;
+using System;
+using Tgstation.Server.Host.Components.Chat.Commands;
+using Tgstation.Server.Host.Core;
+
+namespace Tgstation.Server.Host.Components.Chat
+{
+ ///
+ sealed class ChatFactory : IChatFactory
+ {
+ ///
+ /// The for the
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// The for the
+ ///
+ readonly ILoggerFactory loggerFactory;
+
+ ///
+ /// The for the
+ ///
+ readonly ICommandFactory commandFactory;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ /// The value of
+ public ChatFactory(IIOManager ioManager, ILoggerFactory loggerFactory, ICommandFactory commandFactory)
+ {
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
+ this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
+ }
+
+ ///
+ public IChat CreateChat() => new Chat(new ProviderFactory(), ioManager, loggerFactory.CreateLogger(), commandFactory);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs
new file mode 100644
index 0000000000..e3103e4298
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/BoolConverter.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+using System;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ /// for decoding bools returned by BYOND
+ ///
+ sealed class BoolConverter : JsonConverter
+ {
+ ///
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0);
+
+ ///
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1";
+
+ ///
+ public override bool CanConvert(Type objectType) => objectType == typeof(bool);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs
deleted file mode 100644
index d5f192ec59..0000000000
--- a/src/Tgstation.Server.Host/Components/Chat/Commands/Command.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-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/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs
new file mode 100644
index 0000000000..a9bd873534
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using Tgstation.Server.Host.Core;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ sealed class CommandFactory : ICommandFactory
+ {
+ ///
+ /// The for the
+ ///
+ readonly IApplication application;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ public CommandFactory(IApplication application)
+ {
+ this.application = application ?? throw new ArgumentNullException(nameof(application));
+ }
+
+ ///
+ public IReadOnlyList GenerateCommands() => new List
+ {
+ new KekCommand(),
+ new VersionCommand(application)
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs
index 6221f531c1..da23754a2d 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs
@@ -1,12 +1,25 @@
-using System;
+using Newtonsoft.Json;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components.Chat.Commands
{
///
/// Represents a command made from DM code
///
- public sealed class CustomCommand : Command
+ public sealed class CustomCommand : ICommand
{
+ ///
+ public string Name { get; set; }
+
+ ///
+ public string HelpText { get; set; }
+
+ ///
+ [JsonConverter(typeof(BoolConverter))]
+ public bool AdminOnly { get; set; }
+
///
/// The for the
///
@@ -24,10 +37,11 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
}
///
- public override void Invoke(string arguments)
+ public Task Invoke(string arguments, User user, CancellationToken cancellationToken)
{
if (handler == null)
throw new InvalidOperationException("SetHandler() has not been called!");
+ return handler.HandleChatCommand(Name, arguments, user, cancellationToken);
}
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs
new file mode 100644
index 0000000000..8694606731
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs
@@ -0,0 +1,35 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ /// Represents a command that can be invoked by talking to chat bots
+ ///
+ public interface ICommand
+ {
+ ///
+ /// The text to invoke the command. May not be "?" or "help" (case-insensitive)
+ ///
+ string Name { get; }
+
+ ///
+ /// The help text to display when queires are made about the command
+ ///
+ string HelpText { get; }
+
+ ///
+ /// If the command should only be available to s who's has set
+ ///
+ bool AdminOnly { get; }
+
+ ///
+ /// Invoke the
+ ///
+ /// The text after with leading whitespace trimmed
+ /// The who invoked the command
+ /// The for the operation
+ /// A resulting in a to send to the invoker
+ Task Invoke(string arguments, User user, CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs
new file mode 100644
index 0000000000..5520126604
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommandFactory.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ /// Factory for built in s
+ ///
+ interface ICommandFactory
+ {
+ ///
+ /// Generate builtin s
+ ///
+ /// A of s
+ IReadOnlyList GenerateCommands();
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs
new file mode 100644
index 0000000000..7fa541ef37
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/KekCommand.cs
@@ -0,0 +1,28 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ /// kek
+ ///
+ sealed class KekCommand : ICommand
+ {
+ ///
+ /// kek
+ ///
+ const string Kek = "kek";
+
+ ///
+ public string Name => Kek;
+
+ ///
+ public string HelpText => Kek;
+
+ ///
+ public bool AdminOnly => false;
+
+ ///
+ public Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(Kek);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs
new file mode 100644
index 0000000000..13b5a68f74
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/VersionCommand.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host.Core;
+
+namespace Tgstation.Server.Host.Components.Chat.Commands
+{
+ ///
+ /// to return the
+ ///
+ sealed class VersionCommand : ICommand
+ {
+ ///
+ public string Name => "version";
+
+ ///
+ public string HelpText => "Displays the tgstation server version";
+
+ ///
+ public bool AdminOnly => false;
+
+ ///
+ /// The for the
+ ///
+ readonly IApplication application;
+
+ ///
+ /// Construct a
+ ///
+ ///
+ public VersionCommand(IApplication application)
+ {
+ this.application = application ?? throw new ArgumentNullException(nameof(application));
+ }
+
+ ///
+ public Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(application.VersionString);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs
index 68c12aa1ed..15e6e939bc 100644
--- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// The s of the s to send to
/// The for the operation
/// A representing the running operation
- Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken);
+ Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken);
///
/// Send a chat to configured watchdog channels
diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs
new file mode 100644
index 0000000000..734b6243f3
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs
@@ -0,0 +1,14 @@
+namespace Tgstation.Server.Host.Components.Chat
+{
+ ///
+ /// For creating s
+ ///
+ interface IChatFactory
+ {
+ ///
+ /// Create a
+ ///
+ /// A new
+ IChat CreateChat();
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs
deleted file mode 100644
index 4020ab0613..0000000000
--- a/src/Tgstation.Server.Host/Components/Chat/ICommandFactory.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-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
index 5106ba0efd..9f640cb633 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ICustomCommandHandler.cs
@@ -1,11 +1,10 @@
-using System.Collections.Generic;
-using System.Threading;
+using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components.Chat
{
///
- /// Handles that map to those defined in a
+ /// Handles s that map to those defined in a
///
public interface ICustomCommandHandler
{
diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs
index e9b90e6560..3989369ce8 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Message.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs
@@ -1,8 +1,18 @@
namespace Tgstation.Server.Host.Components.Chat.Providers
{
+ ///
+ /// Represents a message recieved by a
+ ///
sealed class Message
{
- string Content { get; set; }
- User User { get; set; }
+ ///
+ /// The text of the message
+ ///
+ public string Content { get; set; }
+
+ ///
+ /// The who sent the
+ ///
+ public User User { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
new file mode 100644
index 0000000000..4412cf16b5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
@@ -0,0 +1,207 @@
+using Discord;
+using Discord.WebSocket;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api.Models;
+
+namespace Tgstation.Server.Host.Components.Chat.Providers
+{
+ ///
+ /// for the Discord app
+ ///
+ sealed class DiscordProvider : Provider
+ {
+ ///
+ public override bool Connected => client.ConnectionState == ConnectionState.Connected;
+
+ ///
+ public override string BotMention
+ {
+ get
+ {
+ if (!Connected)
+ throw new InvalidOperationException("Provider not connected");
+ return client.CurrentUser.Mention;
+ }
+ }
+
+ ///
+ /// The for the
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// The for the
+ ///
+ readonly DiscordSocketClient client;
+
+ ///
+ /// The token used for connecting to discord
+ ///
+ readonly string botToken;
+
+ ///
+ /// of mapped s
+ ///
+ readonly List mappedChannels;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ public DiscordProvider(ILogger logger, string botToken)
+ {
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
+ client = new DiscordSocketClient();
+ client.MessageReceived += Client_MessageReceived;
+ mappedChannels = new List();
+ }
+
+ ///
+ public override void Dispose() => client.Dispose();
+
+ ///
+ /// Handle a message recieved from Discord
+ ///
+ /// The
+ /// A representing the running operation
+ Task Client_MessageReceived(SocketMessage e)
+ {
+ if (e.Author.Id != client.CurrentUser.Id)
+ return Task.CompletedTask;
+
+ var pm = e.Channel is IPrivateChannel;
+
+ if (!pm && !mappedChannels.Contains(e.Channel.Id))
+ return Task.CompletedTask;
+
+ var result = new Message {
+ Content = e.Content,
+ User = new User
+ {
+ RealId = e.Author.Id,
+ Channel = new Channel
+ {
+ RealId = e.Channel.Id,
+ IsAdmin = false,
+ IsPrivate = true,
+ ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
+ FriendlyName = e.Channel.Name
+ },
+ FriendlyName = e.Author.Username,
+ Mention = e.Author.Mention
+ }
+ };
+ EnqueueMessage(result);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override async Task Connect(CancellationToken cancellationToken)
+ {
+ if (Connected)
+ return true;
+
+ try
+ {
+ await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await client.StartAsync().ConfigureAwait(false);
+
+ var channelsAvailable = new TaskCompletionSource