mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Merge branch 'V4' into WatchdogErrorStates
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/// <summary>
|
||||
/// The Discord channel ID
|
||||
/// </summary>
|
||||
public long? DiscordChannelId { get; set; }
|
||||
public ulong? DiscordChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="ChatChannel"/> is an admin channel
|
||||
|
||||
@@ -14,5 +14,10 @@ namespace Tgstation.Server.Api.Models
|
||||
/// Git revision the compiler ran on. Not modifiable
|
||||
/// </summary>
|
||||
public RevisionInformation RevisionInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Byond.Version"/> the <see cref="CompileJob"/> was made with
|
||||
/// </summary>
|
||||
public Version ByondVersion { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,5 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// Exit code of DM. If <see langword="null"/>
|
||||
/// </summary>
|
||||
public int? ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Byond.Version"/> the <see cref="CompileJob"/> was made with
|
||||
/// </summary>
|
||||
public Version ByondVersion { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a <see cref="Providers.IProvider"/> channel
|
||||
/// </summary>
|
||||
public sealed class Channel
|
||||
{
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="RealId"/>. Represented as a <see cref="string"/> to avoid BYOND percision loss
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Providers.IProvider"/> channel Id.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="Chat"/> remaps this to an internal id using <see cref="ChannelMapping"/></remarks>
|
||||
public long Id { get; set; }
|
||||
[JsonIgnore]
|
||||
public ulong RealId
|
||||
{
|
||||
get => UInt64.Parse(Id, CultureInfo.InvariantCulture);
|
||||
set => Id = value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The user friendly name of the <see cref="Channel"/>
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a mapping of a <see cref="Channel.RealId"/>
|
||||
/// </summary>
|
||||
sealed class ChannelMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// The Id of the <see cref="Providers.IProvider"/>
|
||||
/// </summary>
|
||||
public long ProviderId { get; set; }
|
||||
public long ProviderChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original <see cref="Components.Chat.Channel.RealId"/>
|
||||
/// </summary>
|
||||
public ulong ProviderChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="Channel"/> is a watchdog channel
|
||||
/// </summary>
|
||||
public bool IsWatchdogChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Components.Chat.Channel"/> with the mapped Id
|
||||
/// </summary>
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
sealed class Chat : IChat
|
||||
{
|
||||
const string CommonMention = "!tgs";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProviderFactory"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
@@ -24,9 +28,14 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Command"/>s that never change
|
||||
/// The <see cref="ILogger"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
readonly IReadOnlyList<Command> builtinCommands;
|
||||
readonly ILogger<Chat> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Unchanging <see cref="ICommand"/>s in the <see cref="Chat"/> mapped by <see cref="ICommand.Name"/>
|
||||
/// </summary>
|
||||
readonly Dictionary<string, ICommand> builtinCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="IProvider"/>s in use, keyed by <see cref="ChatSettings.Id"/>
|
||||
@@ -34,9 +43,9 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
readonly Dictionary<long, IProvider> providers;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="Channel.Id"/>s to <see cref="ChannelMapping"/>s
|
||||
/// Map of <see cref="Channel.RealId"/>s to <see cref="ChannelMapping"/>s
|
||||
/// </summary>
|
||||
readonly Dictionary<long, ChannelMapping> mappedChannels;
|
||||
readonly Dictionary<ulong, ChannelMapping> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// The active <see cref="IJsonTrackingContext"/>s for the <see cref="Chat"/>
|
||||
@@ -44,41 +53,57 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
readonly List<IJsonTrackingContext> trackingContexts;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="Chat"/>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="chatHandler"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource handlerCts;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="ChangeChannels(long, IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/>
|
||||
/// </summary>
|
||||
ICustomCommandHandler customCommandHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Used for remapping <see cref="Channel.Id"/>s
|
||||
/// The <see cref="Task"/> that monitors incoming chat messages
|
||||
/// </summary>
|
||||
long channelIdCounter;
|
||||
Task chatHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Used for remapping <see cref="Channel.RealId"/>s
|
||||
/// </summary>
|
||||
ulong channelIdCounter;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="StartAsync(CancellationToken)"/> has been called
|
||||
/// </summary>
|
||||
bool started;
|
||||
bool started;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="Chat"/>
|
||||
/// </summary>
|
||||
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="commandFactory">The <see cref="ICommandFactory"/> used to populate <see cref="builtinCommands"/></param>
|
||||
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory)
|
||||
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ILogger<Chat> 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<string, ICommand>();
|
||||
foreach (var I in commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)))
|
||||
builtinCommands.Add(I.Name, I);
|
||||
|
||||
providers = new Dictionary<long, IProvider>();
|
||||
mappedChannels = new Dictionary<long, ChannelMapping>();
|
||||
mappedChannels = new Dictionary<ulong, ChannelMapping>();
|
||||
trackingContexts = new List<IJsonTrackingContext>();
|
||||
handlerCts = new CancellationTokenSource();
|
||||
channelIdCounter = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a <paramref name="message"/>
|
||||
/// </summary>
|
||||
/// <param name="provider">The <see cref="IProvider"/> who recevied <paramref name="message"/></param>
|
||||
/// <param name="message">The <see cref="Message"/> to process</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
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<string>(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<ulong> { 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<ulong> { 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<ulong> { 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<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitors active providers for new <see cref="Message"/>s
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task MonitorMessages(CancellationToken cancellationToken)
|
||||
{
|
||||
var messageTasks = new Dictionary<IProvider, Task<Message>>();
|
||||
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) { }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken)
|
||||
public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
@@ -209,7 +336,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <inheritdoc />
|
||||
public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
|
||||
{
|
||||
List<long> wdChannels;
|
||||
List<ulong> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ChatFactory : IChatFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ChatFactory"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILoggerFactory"/> for the <see cref="ChatFactory"/>
|
||||
/// </summary>
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICommandFactory"/> for the <see cref="ChatFactory"/>
|
||||
/// </summary>
|
||||
readonly ICommandFactory commandFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ChatFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
/// <param name="commandFactory">The value of <see cref="commandFactory"/></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IChat CreateChat() => new Chat(new ProviderFactory(), ioManager, loggerFactory.CreateLogger<Chat>(), commandFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="JsonConverter"/> for decoding bools returned by BYOND
|
||||
/// </summary>
|
||||
sealed class BoolConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(bool);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command that can be invoked by talking to chat bots
|
||||
/// </summary>
|
||||
public abstract class Command
|
||||
{
|
||||
/// <summary>
|
||||
/// The text to invoke the command. May not be "?" or "help" (case-insensitive)
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The help text to display when queires are made about the command
|
||||
/// </summary>
|
||||
public string HelpText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the command should only be available to <see cref="User"/>s who's <see cref="User.Channel"/> has <see cref="Channel.IsAdmin"/> set
|
||||
/// </summary>
|
||||
public bool AdminOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the <see cref="Command"/>
|
||||
/// </summary>
|
||||
/// <param name="arguments">The text after <see cref="Name"/> with leading whitespace trimmed</param>
|
||||
public abstract void Invoke(string arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class CommandFactory : ICommandFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IApplication"/> for the <see cref="CommandFactory"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="CommandFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
public CommandFactory(IApplication application)
|
||||
{
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ICommand> GenerateCommands() => new List<ICommand>
|
||||
{
|
||||
new KekCommand(),
|
||||
new VersionCommand(application)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command made from DM code
|
||||
/// </summary>
|
||||
public sealed class CustomCommand : Command
|
||||
public sealed class CustomCommand : ICommand
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HelpText { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool AdminOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="CustomCommand"/>
|
||||
/// </summary>
|
||||
@@ -24,10 +37,11 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Invoke(string arguments)
|
||||
public Task<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command that can be invoked by talking to chat bots
|
||||
/// </summary>
|
||||
public interface ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The text to invoke the command. May not be "?" or "help" (case-insensitive)
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The help text to display when queires are made about the command
|
||||
/// </summary>
|
||||
string HelpText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the command should only be available to <see cref="User"/>s who's <see cref="User.Channel"/> has <see cref="Channel.IsAdmin"/> set
|
||||
/// </summary>
|
||||
bool AdminOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the <see cref="ICommand"/>
|
||||
/// </summary>
|
||||
/// <param name="arguments">The text after <see cref="Name"/> with leading whitespace trimmed</param>
|
||||
/// <param name="user">The <see cref="User"/> who invoked the command</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="string"/> to send to the invoker</returns>
|
||||
Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for built in <see cref="ICommand"/>s
|
||||
/// </summary>
|
||||
interface ICommandFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate builtin <see cref="ICommand"/>s
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="IReadOnlyList{T}"/> of <see cref="ICommand"/>s</returns>
|
||||
IReadOnlyList<ICommand> GenerateCommands();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// kek
|
||||
/// </summary>
|
||||
sealed class KekCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// kek
|
||||
/// </summary>
|
||||
const string Kek = "kek";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => Kek;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HelpText => Kek;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AdminOnly => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(Kek);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ICommand"/> to return the <see cref="IApplication.VersionString"/>
|
||||
/// </summary>
|
||||
sealed class VersionCommand : ICommand
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "version";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HelpText => "Displays the tgstation server version";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AdminOnly => false;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApplication"/> for the <see cref="VersionCommand"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="VersionCommand"/>
|
||||
/// </summary>
|
||||
/// <param name="application"></param>
|
||||
public VersionCommand(IApplication application)
|
||||
{
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(application.VersionString);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <param name="channelIds">The <see cref="Models.ChatChannel.Id"/>s of the <see cref="Host.Models.ChatChannel"/>s to send to</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken);
|
||||
Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat <paramref name="message"/> to configured watchdog channels
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// For creating <see cref="IChat"/>s
|
||||
/// </summary>
|
||||
interface IChatFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="IChat"/>
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IChat"/></returns>
|
||||
IChat CreateChat();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
interface ICommandFactory
|
||||
{
|
||||
IReadOnlyList<Command> GenerateCommands();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <see cref="Commands.Command"/> that map to those defined in a <see cref="IJsonTrackingContext"/>
|
||||
/// Handles <see cref="Commands.ICommand"/>s that map to those defined in a <see cref="IJsonTrackingContext"/>
|
||||
/// </summary>
|
||||
public interface ICustomCommandHandler
|
||||
{
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a message recieved by a <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
sealed class Message
|
||||
{
|
||||
string Content { get; set; }
|
||||
User User { get; set; }
|
||||
/// <summary>
|
||||
/// The text of the message
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Components.Chat.User"/> who sent the <see cref="Message"/>
|
||||
/// </summary>
|
||||
public User User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IProvider"/> for the Discord app
|
||||
/// </summary>
|
||||
sealed class DiscordProvider : Provider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool Connected => client.ConnectionState == ConnectionState.Connected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string BotMention
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Connected)
|
||||
throw new InvalidOperationException("Provider not connected");
|
||||
return client.CurrentUser.Mention;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
readonly ILogger<DiscordProvider> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
readonly DiscordSocketClient client;
|
||||
|
||||
/// <summary>
|
||||
/// The token used for connecting to discord
|
||||
/// </summary>
|
||||
readonly string botToken;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="List{T}"/> of mapped <see cref="ITextChannel"/> <see cref="IEntity{TId}.Id"/>s
|
||||
/// </summary>
|
||||
readonly List<ulong> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="botToken">The value of <see cref="botToken"/></param>
|
||||
public DiscordProvider(ILogger<DiscordProvider> 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<ulong>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose() => client.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Handle a message recieved from Discord
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="SocketMessage"/></param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> 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<object>();
|
||||
client.Ready += () =>
|
||||
{
|
||||
channelsAvailable.SetResult(null);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
|
||||
await channelsAvailable.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error connecting to Discord: {0}", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override async Task Disconnect(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await client.StopAsync().ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await client.LogoutAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error disconnecting from discord: {0}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
|
||||
{
|
||||
if (channels == null)
|
||||
throw new ArgumentNullException(nameof(channels));
|
||||
|
||||
if (!Connected)
|
||||
throw new InvalidOperationException("Provider not connected!");
|
||||
|
||||
Channel GetChannelForChatChannel(ChatChannel channel)
|
||||
{
|
||||
if (!channel.DiscordChannelId.HasValue)
|
||||
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
|
||||
|
||||
if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
|
||||
return null;
|
||||
|
||||
return new Channel
|
||||
{
|
||||
RealId = discordChannel.Id,
|
||||
IsAdmin = channel.IsAdminChannel,
|
||||
ConnectionName = discordChannel.Guild.Name,
|
||||
FriendlyName = discordChannel.Name,
|
||||
IsPrivate = false
|
||||
};
|
||||
};
|
||||
|
||||
var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null);
|
||||
|
||||
lock (this)
|
||||
{
|
||||
mappedChannels.Clear();
|
||||
mappedChannels.AddRange(enumerator.Select(x => x.RealId));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<Channel>>(enumerator.ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) {
|
||||
try
|
||||
{
|
||||
await ((client.GetChannel(channelId) as ITextChannel)?.SendMessageAsync(message, false, null, new RequestOptions
|
||||
{
|
||||
CancelToken = cancellationToken
|
||||
}) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error sending discord message: {0}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <summary>
|
||||
/// Get a <see cref="Task{TResult}"/> resulting in the next <see cref="Message"/> the <see cref="IProvider"/> recieves or <see langword="null"/> on a disconnect
|
||||
/// </summary>
|
||||
Task<Message> NextMessage { get; }
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/></returns>
|
||||
/// <remarks>Note that private messages will come in the form of <see cref="Channel"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/></remarks>
|
||||
Task<Message> NextMessage(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to connect the <see cref="IProvider"/>
|
||||
@@ -50,10 +53,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <summary>
|
||||
/// Send a message to the <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="channelId">The <see cref="Channel.Id"/> to send to</param>
|
||||
/// <param name="channelId">The <see cref="Channel.RealId"/> to send to</param>
|
||||
/// <param name="message">The message contents</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(long channelId, string message, CancellationToken cancellationToken);
|
||||
Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the type of a password passed to the constructor of <see cref="IrcProvider"/>
|
||||
/// </summary>
|
||||
enum IrcPasswordType
|
||||
{
|
||||
/// <summary>
|
||||
/// Use server authentication
|
||||
/// </summary>
|
||||
Server,
|
||||
/// <summary>
|
||||
/// Use PLAIN sasl authentication
|
||||
/// </summary>
|
||||
Sasl,
|
||||
/// <summary>
|
||||
/// Use NickServ authentication
|
||||
/// </summary>
|
||||
NickServ
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
using Meebey.SmartIrc4net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IProvider"/> for internet relay chat
|
||||
/// </summary>
|
||||
sealed class IrcProvider : Provider
|
||||
{
|
||||
const int TimeoutSeconds = 5;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Connected => client.IsConnected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string BotMention => client.Nickname;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="IrcProvider"/>
|
||||
/// </summary>
|
||||
readonly ILogger<IrcProvider> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IrcFeatures"/> client
|
||||
/// </summary>
|
||||
readonly IrcFeatures client;
|
||||
|
||||
/// <summary>
|
||||
/// Address of the server to connect to
|
||||
/// </summary>
|
||||
readonly string address;
|
||||
/// <summary>
|
||||
/// Port of the server to connect to
|
||||
/// </summary>
|
||||
readonly ushort port;
|
||||
/// <summary>
|
||||
/// IRC nickname
|
||||
/// </summary>
|
||||
readonly string nickname;
|
||||
/// <summary>
|
||||
/// Password which will used for authentication
|
||||
/// </summary>
|
||||
readonly string password;
|
||||
/// <summary>
|
||||
/// The <see cref="IrcPasswordType"/> of <see cref="password"/>
|
||||
/// </summary>
|
||||
readonly IrcPasswordType? passwordType;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="Channel.RealId"/>s to channel names
|
||||
/// </summary>
|
||||
readonly Dictionary<ulong, string> channelIdMap;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="Channel.RealId"/>s to query users
|
||||
/// </summary>
|
||||
readonly Dictionary<ulong, string> queryChannelIdMap;
|
||||
|
||||
/// <summary>
|
||||
/// Id counter for <see cref="channelIdMap"/>
|
||||
/// </summary>
|
||||
ulong channelIdCounter;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="IrcProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of logger</param>
|
||||
/// <param name="application">The <see cref="IApplication"/> to get the <see cref="IApplication.VersionString"/> from</param>
|
||||
/// <param name="address">The value of <see cref="address"/></param>
|
||||
/// <param name="port">The value of <see cref="port"/></param>
|
||||
/// <param name="nickname">The value of <see cref="nickname"/></param>
|
||||
/// <param name="password">The value of <see cref="password"/></param>
|
||||
/// <param name="passwordType">The value of <see cref="passwordType"/></param>
|
||||
/// <param name="useSsl">If <see cref="IrcConnection.UseSsl"/> should be used</param>
|
||||
public IrcProvider(ILogger<IrcProvider> logger, IApplication application, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
if (application == null)
|
||||
throw new ArgumentNullException(nameof(application));
|
||||
|
||||
this.address = address ?? throw new ArgumentNullException(nameof(address));
|
||||
this.port = port;
|
||||
this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
|
||||
|
||||
if (passwordType.HasValue && password == null)
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
|
||||
if(password != null && !passwordType.HasValue)
|
||||
throw new ArgumentNullException(nameof(passwordType));
|
||||
|
||||
this.password = password;
|
||||
this.passwordType = passwordType;
|
||||
|
||||
client = new IrcFeatures
|
||||
{
|
||||
SupportNonRfc = true,
|
||||
CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
|
||||
AutoRejoin = true,
|
||||
AutoRejoinOnKick = true,
|
||||
AutoRelogin = true,
|
||||
AutoRetry = true,
|
||||
AutoRetryLimit = TimeoutSeconds,
|
||||
AutoRetryDelay = TimeoutSeconds,
|
||||
ActiveChannelSyncing = true,
|
||||
AutoNickHandling = true,
|
||||
CtcpVersion = application.VersionString,
|
||||
UseSsl = useSsl
|
||||
};
|
||||
if (useSsl)
|
||||
client.ValidateServerCertificate = true; //dunno if it defaults to that or what
|
||||
|
||||
client.OnChannelMessage += Client_OnChannelMessage;
|
||||
client.OnQueryMessage += Client_OnQueryMessage;
|
||||
|
||||
channelIdMap = new Dictionary<ulong, string>();
|
||||
queryChannelIdMap = new Dictionary<ulong, string>();
|
||||
channelIdCounter = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose() => client.Disconnect(); //just closes the socket
|
||||
|
||||
/// <summary>
|
||||
/// Handle an IRC message
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="IrcEventArgs"/></param>
|
||||
/// <param name="isPrivate">If this is a query message</param>
|
||||
void HandleMessage(IrcEventArgs e, bool isPrivate)
|
||||
{
|
||||
if (e.Data.From.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
|
||||
return;
|
||||
|
||||
var username = e.Data.From;
|
||||
var channelName = isPrivate ? username : e.Data.Channel;
|
||||
ulong channelId = 0;
|
||||
lock (this)
|
||||
{
|
||||
var dicToCheck = isPrivate ? queryChannelIdMap : channelIdMap;
|
||||
if (!dicToCheck.Any(x =>
|
||||
{
|
||||
if (x.Value != channelName)
|
||||
return false;
|
||||
channelId = x.Key;
|
||||
return true;
|
||||
}))
|
||||
{
|
||||
channelId = ++channelIdCounter;
|
||||
dicToCheck.Add(channelId, channelName);
|
||||
if (isPrivate)
|
||||
channelIdMap.Add(channelId, null);
|
||||
}
|
||||
}
|
||||
|
||||
var message = new Message
|
||||
{
|
||||
Content = e.Data.Message,
|
||||
User = new User
|
||||
{
|
||||
Channel = new Channel
|
||||
{
|
||||
IsAdmin = false,
|
||||
ConnectionName = address,
|
||||
FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
|
||||
RealId = channelId,
|
||||
IsPrivate = isPrivate
|
||||
},
|
||||
FriendlyName = username,
|
||||
RealId = channelId,
|
||||
Mention = username
|
||||
}
|
||||
};
|
||||
|
||||
EnqueueMessage(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a query message is received in IRC
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of the event</param>
|
||||
/// <param name="e">The <see cref="IrcEventArgs"/></param>
|
||||
void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
|
||||
|
||||
/// <summary>
|
||||
/// When a channel message is received in IRC
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of the event</param>
|
||||
/// <param name="e">The <see cref="IrcEventArgs"/></param>
|
||||
void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Connect(address, port);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (passwordType == IrcPasswordType.Server)
|
||||
client.Login(nickname, nickname, 0, nickname, password);
|
||||
else
|
||||
{
|
||||
if (passwordType == IrcPasswordType.Sasl)
|
||||
{
|
||||
client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
client.Login(nickname, nickname, 0, nickname);
|
||||
}
|
||||
|
||||
if (passwordType == IrcPasswordType.NickServ)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
|
||||
}
|
||||
else if (passwordType == IrcPasswordType.Sasl)
|
||||
{
|
||||
//wait for the sasl ack or timeout
|
||||
var recievedAck = false;
|
||||
var recievedPlus = false;
|
||||
client.OnReadLine += (sender, e) =>
|
||||
{
|
||||
if (e.Line.Contains("ACK :sasl"))
|
||||
recievedAck = true;
|
||||
else if (e.Line.Contains("AUTHENTICATE +"))
|
||||
recievedPlus = true;
|
||||
};
|
||||
|
||||
var startTime = DateTimeOffset.Now;
|
||||
var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
for(; !recievedAck && DateTimeOffset.Now <= endTime; Task.Delay(10, cancellationToken).GetAwaiter().GetResult())
|
||||
client.Listen(false);
|
||||
|
||||
client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
for (; !recievedPlus && DateTimeOffset.Now <= endTime; Task.Delay(10, cancellationToken).GetAwaiter().GetResult())
|
||||
client.Listen(false);
|
||||
|
||||
//Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
|
||||
var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
|
||||
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
|
||||
var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
|
||||
var chars = authLine.ToCharArray();
|
||||
client.WriteLine(authLine, Priority.Critical);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
client.WriteLine("CAP END", Priority.Critical);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Unable to connect to IRC: {0}", e);
|
||||
}
|
||||
return true;
|
||||
}, cancellationToken,TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Disconnect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
client.RfcQuit();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error quitting IRC: {0}", e);
|
||||
}
|
||||
Dispose();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
if (channels.Any(x => x.IrcChannel == null))
|
||||
throw new InvalidOperationException("ChatChannel missing IrcChannel!");
|
||||
lock (this)
|
||||
{
|
||||
var hs = new HashSet<string>(); //for unique inserts
|
||||
foreach (var I in channels)
|
||||
hs.Add(I.IrcChannel);
|
||||
var toPart = new List<string>();
|
||||
foreach (var I in client.JoinedChannels)
|
||||
if (!hs.Remove(I))
|
||||
toPart.Add(I);
|
||||
|
||||
foreach (var I in toPart)
|
||||
client.RfcPart(I);
|
||||
foreach (var I in hs)
|
||||
client.RfcJoin(I);
|
||||
|
||||
|
||||
|
||||
return (IReadOnlyList<Channel>)channels.Select(x => {
|
||||
ulong id = channelIdCounter;
|
||||
if (!channelIdMap.Any(y =>
|
||||
{
|
||||
if (y.Value != x.IrcChannel)
|
||||
return false;
|
||||
id = y.Key;
|
||||
return true;
|
||||
}))
|
||||
channelIdMap.Add(id, x.IrcChannel);
|
||||
else
|
||||
++channelIdCounter;
|
||||
return new Channel
|
||||
{
|
||||
RealId = id,
|
||||
IsAdmin = x.IsAdminChannel,
|
||||
ConnectionName = address,
|
||||
FriendlyName = channelIdMap[id],
|
||||
IsPrivate = false
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
var channelName = channelIdMap[channelId] ?? queryChannelIdMap[channelId];
|
||||
try
|
||||
{
|
||||
if (client.JoinedChannels.Contains(channelName))
|
||||
client.SendMessage(SendType.Message, channelName, message);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
logger.LogWarning("Unable to send to channel: {0}", e);
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
abstract class Provider : IProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Queue{T}"/> of received <see cref="Message"/>s
|
||||
/// </summary>
|
||||
readonly Queue<Message> messageQueue;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TaskCompletionSource{TResult}"/> that completes while <see cref="messageQueue"/> isn't empty
|
||||
/// </summary>
|
||||
TaskCompletionSource<object> nextMessage;
|
||||
|
||||
protected Provider()
|
||||
{
|
||||
messageQueue = new Queue<Message>();
|
||||
nextMessage = new TaskCompletionSource<object>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool Connected { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string BotMention { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queues a <paramref name="message"/> for <see cref="NextMessage(CancellationToken)"/>
|
||||
/// </summary>
|
||||
/// <param name="message">The <see cref="Message"/> to queue</param>
|
||||
protected void EnqueueMessage(Message message)
|
||||
{
|
||||
lock (messageQueue)
|
||||
{
|
||||
messageQueue.Enqueue(message);
|
||||
nextMessage.TrySetResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<bool> Connect(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task Disconnect(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> NextMessage(CancellationToken cancellationToken)
|
||||
{
|
||||
var cancelTcs = new TaskCompletionSource<object>();
|
||||
using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
|
||||
await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
|
||||
lock (messageQueue)
|
||||
{
|
||||
var result = messageQueue.Dequeue();
|
||||
if (messageQueue.Count == 0)
|
||||
nextMessage = new TaskCompletionSource<object>();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,6 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// The list of internal channel ids to send <see cref="Message"/> to
|
||||
/// </summary>
|
||||
public List<long> ChannelIds { get; set; }
|
||||
public List<ulong> ChannelIds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Represents a tgs_chat_user datum
|
||||
/// </summary>
|
||||
public sealed class User
|
||||
{
|
||||
long Id { get; set; }
|
||||
string FriendlyName { get; set; }
|
||||
string Mention { get; set; }
|
||||
Channel Channel { get; set; }
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="RealId"/>. Represented as a <see cref="string"/> to avoid BYOND percision loss
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal user id
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ulong RealId
|
||||
{
|
||||
get => UInt64.Parse(Id, CultureInfo.InvariantCulture);
|
||||
set => Id = value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The friendly name of the user
|
||||
/// </summary>
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text to mention the user
|
||||
/// </summary>
|
||||
public string Mention { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Components.Chat.Channel"/> the user spoke from
|
||||
/// </summary>
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components
|
||||
bool ddVerified;
|
||||
using (var byondLock = byond.UseExecutables(null))
|
||||
{
|
||||
job.ByondVersion = byondLock.Version;
|
||||
job.ByondVersion = byondLock.Version.ToString();
|
||||
|
||||
await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Types of events
|
||||
/// </summary>
|
||||
enum EventType
|
||||
public enum EventType
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters: Reference name, commit sha
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// Consumes <see cref="EventType"/>s and takes the appropriate actions
|
||||
/// </summary>
|
||||
interface IEventConsumer
|
||||
public interface IEventConsumer
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle a given <paramref name="eventType"/>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// For interacting with the instance services
|
||||
/// </summary>
|
||||
public interface IInstance : IHostedService, IReattachInfoHandler
|
||||
public interface IInstance : IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IRepositoryManager"/> for the <see cref="IInstance"/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
@@ -10,8 +11,9 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// Create an <see cref="IInstance"/>
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Host.Models.Instance"/></param>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/></param>
|
||||
/// <param name="interopRegistrar">The <see cref="IInteropRegistrar"/> for the <see cref="IInstance"/></param>
|
||||
/// <returns>A new <see cref="IInstance"/></returns>
|
||||
IInstance CreateInstance(Host.Models.Instance metadata);
|
||||
IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar);
|
||||
}
|
||||
}
|
||||
@@ -157,46 +157,5 @@ namespace Tgstation.Server.Host.Components
|
||||
timerTask = TimerLoop(newInterval.Value, timerCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
|
||||
{
|
||||
var instance = new Models.Instance { Id = metadata.Id };
|
||||
db.Instances.Attach(instance);
|
||||
|
||||
Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo)
|
||||
{
|
||||
db.CompileJobs.Attach(wdInfo.Dmb.CompileJob);
|
||||
return new Models.ReattachInformation
|
||||
{
|
||||
AccessIdentifier = wdInfo.AccessIdentifier,
|
||||
ChatChannelsJson = wdInfo.ChatChannelsJson,
|
||||
ChatCommandsJson = wdInfo.ChatCommandsJson,
|
||||
CompileJob = wdInfo.Dmb.CompileJob,
|
||||
IsPrimary = wdInfo.IsPrimary,
|
||||
Port = wdInfo.Port,
|
||||
ProcessId = wdInfo.ProcessId,
|
||||
RebootState = wdInfo.RebootState
|
||||
};
|
||||
}
|
||||
|
||||
instance.WatchdogReattachInformation = new Models.WatchdogReattachInformation
|
||||
{
|
||||
Alpha = ConvertReattachInfo(reattachInformation.Alpha),
|
||||
Bravo = ConvertReattachInfo(reattachInformation.Bravo),
|
||||
AlphaIsActive = reattachInformation.AlphaIsActive,
|
||||
};
|
||||
await db.Save(cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WatchdogReattachInformation> Load(CancellationToken cancellationToken)
|
||||
{
|
||||
Models.WatchdogReattachInformation result = null;
|
||||
await databaseContextFactory.UseContext(async (db) =>
|
||||
result = await db.Instances.Where(x => x.Id == metadata.Id).Select(x => x.WatchdogReattachInformation).FirstAsync(cancellationToken).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
return new WatchdogReattachInformation(result, dmbFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
using System;
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
@@ -16,22 +22,70 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApplication"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILoggerFactory"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IByondTopicSender"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly IByondTopicSender byondTopicSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IServerUpdater"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly IServerUpdater serverUpdater;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICryptographySuite"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly ICryptographySuite cryptographySuite;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IExecutor"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly IExecutor executor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICommandFactory"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly ICommandFactory commandFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory)
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
|
||||
/// <param name="serverUpdater">The value of <see cref="serverUpdater"/></param>
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
|
||||
/// <param name="executor">The value of <see cref="executor"/></param>
|
||||
/// <param name="commandFactory">The value of <see cref="commandFactory"/></param>
|
||||
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
|
||||
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite ));
|
||||
this.executor = executor ?? throw new ArgumentNullException(nameof(executor));
|
||||
this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IInstance CreateInstance(Models.Instance metadata)
|
||||
public IInstance CreateInstance(Models.Instance metadata, IInteropRegistrar interopRegistrar)
|
||||
{
|
||||
//Create the ioManager for the instance
|
||||
|
||||
var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);
|
||||
|
||||
//various other ioManagers
|
||||
@@ -42,10 +96,24 @@ namespace Tgstation.Server.Host.Components
|
||||
var codeModificationsIoMananger = new ResolvingIOManager(instanceIoManager, "CodeModifications");
|
||||
|
||||
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, metadata);
|
||||
var commandFactory = new CommandFactory(application);
|
||||
var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory);
|
||||
|
||||
var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager);
|
||||
|
||||
IByond byond = null;
|
||||
IConfiguration configuration = null;
|
||||
|
||||
var chat = chatFactory.CreateChat();
|
||||
var sessionControllerFactory = new SessionControllerFactory(executor, byond, byondTopicSender, interopRegistrar, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata);
|
||||
var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, metadata);
|
||||
var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, metadata);
|
||||
var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings);
|
||||
|
||||
var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, watchdog, loggerFactory.CreateLogger<DreamMaker>());
|
||||
|
||||
throw new NotImplementedException();
|
||||
//return new Instance(metadata, repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
if (metadata == null)
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
var instance = instanceFactory.CreateInstance(metadata);
|
||||
var instance = instanceFactory.CreateInstance(metadata, this);
|
||||
lock (this)
|
||||
{
|
||||
if (instances.ContainsKey(metadata.Id))
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ReattachInfoHandler: IReattachInfoHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="ReattachInfoHandler"/>
|
||||
/// </summary>
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDmbFactory"/> for the <see cref="ReattachInfoHandler"/>
|
||||
/// </summary>
|
||||
readonly IDmbFactory dmbFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance"/> for the <see cref="ReattachInfoHandler"/>
|
||||
/// </summary>
|
||||
readonly Models.Instance metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ReattachInfoHandler"/>
|
||||
/// </summary>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
|
||||
/// <param name="metadata">The value of <see cref="metadata"/></param>
|
||||
public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, Models.Instance metadata)
|
||||
{
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
|
||||
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
|
||||
{
|
||||
var instance = new Models.Instance { Id = metadata.Id };
|
||||
db.Instances.Attach(instance);
|
||||
|
||||
Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo)
|
||||
{
|
||||
db.CompileJobs.Attach(wdInfo.Dmb.CompileJob);
|
||||
return new Models.ReattachInformation
|
||||
{
|
||||
AccessIdentifier = wdInfo.AccessIdentifier,
|
||||
ChatChannelsJson = wdInfo.ChatChannelsJson,
|
||||
ChatCommandsJson = wdInfo.ChatCommandsJson,
|
||||
CompileJob = wdInfo.Dmb.CompileJob,
|
||||
IsPrimary = wdInfo.IsPrimary,
|
||||
Port = wdInfo.Port,
|
||||
ProcessId = wdInfo.ProcessId,
|
||||
RebootState = wdInfo.RebootState
|
||||
};
|
||||
}
|
||||
|
||||
instance.WatchdogReattachInformation = new Models.WatchdogReattachInformation
|
||||
{
|
||||
Alpha = ConvertReattachInfo(reattachInformation.Alpha),
|
||||
Bravo = ConvertReattachInfo(reattachInformation.Bravo),
|
||||
AlphaIsActive = reattachInformation.AlphaIsActive,
|
||||
};
|
||||
await db.Save(cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WatchdogReattachInformation> Load(CancellationToken cancellationToken)
|
||||
{
|
||||
Models.WatchdogReattachInformation result = null;
|
||||
await databaseContextFactory.UseContext(async (db) =>
|
||||
result = await db.Instances.Where(x => x.Id == metadata.Id).Select(x => x.WatchdogReattachInformation).FirstAsync(cancellationToken).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
return new WatchdogReattachInformation(result, dmbFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// Runs and monitors the twin server controllers
|
||||
/// </summary>
|
||||
public interface IWatchdog : IHostedService, IDisposable
|
||||
public interface IWatchdog : IHostedService, IDisposable, IEventConsumer
|
||||
{
|
||||
/// <summary>
|
||||
/// If the watchdog is running
|
||||
|
||||
@@ -47,11 +47,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IInstance"/> for the <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
readonly IInstance instance;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
@@ -67,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance"/> for the <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
readonly Models.Instance instance;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
@@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IInstance instance, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory)
|
||||
public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory, Models.Instance instance)
|
||||
{
|
||||
this.executor = executor ?? throw new ArgumentNullException(nameof(executor));
|
||||
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
|
||||
@@ -111,7 +111,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
ChatChannelsJson = GuidJsonFile(),
|
||||
ChatCommandsJson = GuidJsonFile(),
|
||||
HostPath = application.HostingPath,
|
||||
InstanceName = instance.GetMetadata().Name,
|
||||
InstanceName = instance.Name,
|
||||
Revision = dmbProvider.CompileJob.RevisionInformation
|
||||
};
|
||||
interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.TestMerges.Select(x => new TestMerge
|
||||
@@ -141,7 +141,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var byondLock = currentByondLock ?? byond.UseExecutables(dmbProvider.CompileJob.ByondVersion);
|
||||
var byondLock = currentByondLock ?? byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion));
|
||||
try
|
||||
{
|
||||
//more sanitization here cause it uses the same scheme
|
||||
@@ -189,7 +189,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var byondLock = byond.UseExecutables(reattachInformation.Dmb.CompileJob.ByondVersion);
|
||||
var byondLock = byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion));
|
||||
try
|
||||
{
|
||||
var session = executor.AttachToDreamDaemon(reattachInformation.ProcessId, byondLock);
|
||||
|
||||
@@ -15,7 +15,7 @@ using Tgstation.Server.Host.Core;
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Watchdog : IWatchdog, IEventConsumer, ICustomCommandHandler
|
||||
sealed class Watchdog : IWatchdog, ICustomCommandHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The time in milliseconds to wait from starting <see cref="alphaServer"/> to start <see cref="bravoServer"/>. Does not take responsiveness into account
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
@@ -14,6 +15,9 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.Models;
|
||||
@@ -30,6 +34,9 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <inheritdoc />
|
||||
public Version Version { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VersionString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IConfiguration"/> for the <see cref="Application"/>
|
||||
/// </summary>
|
||||
@@ -56,6 +63,7 @@ namespace Tgstation.Server.Host.Core
|
||||
this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
VersionString = String.Format(CultureInfo.InvariantCulture, "/tg/station server v{0}", Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -137,9 +145,17 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
services.AddSingleton<ICryptographySuite, CryptographySuite>();
|
||||
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
|
||||
services.AddSingleton<IPasswordHasher<User>, PasswordHasher<User>>();
|
||||
services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
|
||||
services.AddSingleton<ITokenFactory, TokenFactory>();
|
||||
services.AddSingleton<ISystemIdentityFactory, SystemIdentityFactory>();
|
||||
|
||||
services.AddSingleton<IExecutor, Executor>();
|
||||
services.AddSingleton<ICommandFactory, CommandFactory>();
|
||||
services.AddSingleton<IByondTopicSender>(new ByondTopicSender
|
||||
{
|
||||
ReceiveTimeout = 5000,
|
||||
SendTimeout = 5000
|
||||
});
|
||||
|
||||
services.AddSingleton<InstanceFactory>();
|
||||
services.AddSingleton<IInstanceFactory>(x => x.GetRequiredService<InstanceFactory>());
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace Tgstation.Server.Host.Core
|
||||
/// </summary>
|
||||
public interface IApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// A more verbose version of <see cref="Version"/>
|
||||
/// </summary>
|
||||
string VersionString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="Application"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
@@ -23,6 +24,11 @@ namespace Tgstation.Server.Host.Models
|
||||
[Required]
|
||||
public RevisionInformation RevisionInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Version"/> the <see cref="CompileJob"/> was made with in string form
|
||||
/// </summary>
|
||||
public string ByondVersion { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Api.Models.CompileJob ToApi() => new Api.Models.CompileJob
|
||||
{
|
||||
@@ -33,7 +39,8 @@ namespace Tgstation.Server.Host.Models
|
||||
Id = Id,
|
||||
Job = Job.ToApi(),
|
||||
Output = Output,
|
||||
RevisionInformation = RevisionInformation.ToApi()
|
||||
RevisionInformation = RevisionInformation.ToApi(),
|
||||
ByondVersion = Version.Parse(ByondVersion)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace Tgstation.Server.Host.Models
|
||||
var chatChannel = modelBuilder.Entity<ChatChannel>();
|
||||
chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
|
||||
chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
|
||||
chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.IO;
|
||||
@@ -13,7 +14,7 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
public void ApplyUpdate(string updatePath) => throw new NotImplementedException();
|
||||
|
||||
public void RegisterForUpdate(Action action) => throw new NotImplementedException();
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestSuccessfulStartup()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
{
|
||||
using (var webHost = WebHost.CreateDefaultBuilder(new string[] { "Database:DatabaseType=Sqlite", "Database:ConnectionString=Data Source=" + dbName }) //force it to use sqlite
|
||||
.UseStartup<Application>()
|
||||
.ConfigureServices((serviceCollection) => serviceCollection.AddSingleton<IServerUpdater>(this))
|
||||
.Build()
|
||||
)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user