mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 15:07:03 +01:00
Merge pull request #539 from Cyberboss/ContinuedV4Work
Continued v4 work
This commit is contained in:
Binary file not shown.
+3
-4
@@ -44,8 +44,8 @@
|
||||
|
||||
//EVENT CODES
|
||||
|
||||
#define TGS_EVENT_PORT_SWAP 1 //before a port change is about to happen, extra parameter is new port
|
||||
#define TGS_EVENT_REBOOT_MODE_CHANGE 2 //before a reboot mode change, extras parameters are the current and new reboot mode enums
|
||||
#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port
|
||||
#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums
|
||||
|
||||
//OTHER ENUMS
|
||||
|
||||
@@ -97,8 +97,7 @@
|
||||
/datum/tgs_chat_channel
|
||||
var/id //internal channel representation
|
||||
var/friendly_name //user friendly channel name
|
||||
var/server_name //server name the channel resides on
|
||||
var/provider_name //chat provider for the channel
|
||||
var/connection_name //the name of the configured chat connection
|
||||
var/is_admin_channel //if the server operator has marked this channel for game admins only
|
||||
var/is_private_channel //if this is a private chat channel
|
||||
|
||||
|
||||
+10
-8
@@ -203,14 +203,16 @@
|
||||
//no caching cause tgs may change this
|
||||
var/list/json = json_decode(file2text(chat_channels_json_path))
|
||||
for(var/I in json)
|
||||
var/datum/tgs_chat_channel/channel = new
|
||||
channel.id = I["id"]
|
||||
channel.friendly_name = I["friendly_name"]
|
||||
channel.server_name = I["server_name"]
|
||||
channel.provider_name = I["provider_name"]
|
||||
channel.is_admin_channel = I["is_admin_channel"]
|
||||
channel.is_private_channel = FALSE //tgs will never send us pm channels
|
||||
. += channel
|
||||
. += DecodeChannel(I)
|
||||
|
||||
/datum/tgs_api/v4/proc/DecodeChannel(channel_json)
|
||||
var/datum/tgs_chat_channel/channel = new
|
||||
channel.id = channel_json["id"]
|
||||
channel.friendly_name = channel_json["friendly_name"]
|
||||
channel.connection_name = channel_json["connection_name"]
|
||||
channel.is_admin_channel = channel_json["is_admin_channel"]
|
||||
channel.is_admin_channel = channel_json["is_private_channel"] || FALSE
|
||||
return channel
|
||||
|
||||
#undef TGS4_TOPIC_COMMAND
|
||||
#undef TGS4_TOPIC_TOKEN
|
||||
|
||||
@@ -30,14 +30,7 @@
|
||||
u.id = user["id"]
|
||||
u.friendly_name = user["friendly_name"]
|
||||
u.mention = user["mention"]
|
||||
var/datum/tgs_chat_channel/channel = new
|
||||
u.channel = channel
|
||||
var/channel_json = user["channel"]
|
||||
channel.id = channel_json["id"]
|
||||
channel.friendly_name = channel_json["friendly_name"]
|
||||
channel.server_name = channel_json["server_name"]
|
||||
channel.is_admin_channel = channel_json["is_admin_channel"]
|
||||
channel.is_private_channel = channel_json["is_private_channel"]
|
||||
u.channel = DecodeChannel(user["channel"])
|
||||
|
||||
var/datum/tgs_chat_command/sc = custom_commands[command]
|
||||
var/result = sc.Run(u, params)
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
/// <summary>
|
||||
/// The Discord channel ID
|
||||
/// </summary>
|
||||
public long DiscordChannelId { get; set; }
|
||||
public long? DiscordChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="ChatChannel"/> is an admin channel
|
||||
/// </summary>
|
||||
public bool IsAdminChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="ChatChannel"/> is a watchdog channel
|
||||
/// </summary>
|
||||
public bool IsWatchdogChannel { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat service provider
|
||||
/// </summary>
|
||||
public enum ChatProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Internet relay chat
|
||||
/// </summary>
|
||||
Irc,
|
||||
/// <summary>
|
||||
/// Superior chat service
|
||||
/// </summary>
|
||||
Discord
|
||||
}
|
||||
}
|
||||
@@ -6,22 +6,10 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <inheritdoc />
|
||||
public sealed class ChatSettings : Internal.ChatSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// If the IRC connection is established
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
bool IrcConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the Discord connection is established
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
bool DiscordConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Channels the Discord bot should listen/announce in
|
||||
/// </summary>
|
||||
[Permissions(WriteRight = ChatSettingsRights.SetChannels)]
|
||||
[Permissions(WriteRight = ChatSettingsRights.WriteChannels)]
|
||||
public List<ChatChannel> Channels { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
@@ -8,44 +6,39 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// Manage the server chat bots
|
||||
/// </summary>
|
||||
[Model(RightsType.ChatSettings, RequiresInstance = true)]
|
||||
[Model(RightsType.ChatSettings, RequiresInstance = true, CanList = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)]
|
||||
public class ChatSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// If the IRC client is enabled
|
||||
/// The settings id
|
||||
/// </summary>
|
||||
[Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
|
||||
public bool IrcEnabled { get; set; }
|
||||
[Permissions(DenyWrite = true)]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IRC server name
|
||||
/// The name of the connection
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
|
||||
[Permissions(WriteRight = ChatSettingsRights.WriteName)]
|
||||
[Required]
|
||||
public string IrcHost { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IRC server port
|
||||
/// If the connection is enabled
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
|
||||
public ushort IrcPort { get; set; }
|
||||
[Permissions(WriteRight = ChatSettingsRights.WriteEnabled)]
|
||||
public bool? Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IRC server NickServ password
|
||||
/// The <see cref="ChatProvider"/> used for the connection
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
|
||||
public string IrcNickServPassword { get; set; }
|
||||
[Permissions(WriteRight = ChatSettingsRights.WriteProvider)]
|
||||
public ChatProvider? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the Discord bot is enabled
|
||||
/// The information used to connect to the <see cref="Provider"/>
|
||||
/// </summary>
|
||||
[Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
|
||||
public bool DiscordEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Discord bot token
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
|
||||
public string DiscordBotToken { get; set; }
|
||||
[Permissions(ReadRight = ChatSettingsRights.ReadConnectionString, WriteRight = ChatSettingsRights.ReadConnectionString)]
|
||||
[Required]
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,40 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// User can enable/disable the IRC client
|
||||
/// User can change <see cref="Models.Internal.ChatSettings.Enabled"/>
|
||||
/// </summary>
|
||||
SetIrcEnabled = 1,
|
||||
WriteEnabled = 1,
|
||||
/// <summary>
|
||||
/// User can change the IRC settings
|
||||
/// User can change <see cref="Models.Internal.ChatSettings.Provider"/>
|
||||
/// </summary>
|
||||
SetIrcSettings = 2,
|
||||
WriteProvider = 2,
|
||||
/// <summary>
|
||||
/// User can change the chat channels
|
||||
/// User can change <see cref="Models.ChatSettings.Channels"/>
|
||||
/// </summary>
|
||||
SetChannels = 4,
|
||||
WriteChannels = 4,
|
||||
/// <summary>
|
||||
/// User can enable/disable the Discord bot
|
||||
/// User can change <see cref="Models.Internal.ChatSettings.ConnectionString"/>
|
||||
/// </summary>
|
||||
SetDiscordEnabled = 8,
|
||||
WriteConnectionString = 8,
|
||||
/// <summary>
|
||||
/// User can change the Discord settings
|
||||
/// User can read <see cref="Models.Internal.ChatSettings.ConnectionString"/>
|
||||
/// </summary>
|
||||
SetDiscordSettings = 16,
|
||||
ReadConnectionString = 16,
|
||||
/// <summary>
|
||||
/// User can read all chat settings except <see cref="Models.Internal.ChatSettings.ConnectionString"/>
|
||||
/// </summary>
|
||||
Read = 32,
|
||||
/// <summary>
|
||||
/// User can change <see cref="Models.Internal.ChatSettings.Name"/>
|
||||
/// </summary>
|
||||
WriteName = 32,
|
||||
/// <summary>
|
||||
/// User can create new <see cref="Models.ChatSettings"/>
|
||||
/// </summary>
|
||||
Create = 64,
|
||||
/// <summary>
|
||||
/// User can delete <see cref="Models.ChatSettings"/>
|
||||
/// </summary>
|
||||
Delete = 128
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
|
||||
<PackageReference Include="Octokit" Version="0.29.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a <see cref="Providers.IProvider"/> channel
|
||||
/// </summary>
|
||||
public sealed class Channel
|
||||
{
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// The user friendly name of the <see cref="Channel"/>
|
||||
/// </summary>
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the connection the <see cref="Channel"/> belongs to
|
||||
/// </summary>
|
||||
public string ConnectionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this is considered a channel for admin commands
|
||||
/// </summary>
|
||||
public bool IsAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this is a 1-to-1 chat channel
|
||||
/// </summary>
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
sealed class ChannelMapping
|
||||
{
|
||||
public long ProviderId { get; set; }
|
||||
public long ProviderChannelId { get; set; }
|
||||
public bool IsWatchdogChannel { get; set; }
|
||||
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
using Tgstation.Server.Host.Components.Chat.Providers;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Chat : IChat
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IProviderFactory"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
readonly IProviderFactory providerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Command"/>s that never change
|
||||
/// </summary>
|
||||
readonly IReadOnlyList<Command> builtinCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="IProvider"/>s in use, keyed by <see cref="ChatSettings.Id"/>
|
||||
/// </summary>
|
||||
readonly Dictionary<long, IProvider> providers;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="Channel.Id"/>s to <see cref="ChannelMapping"/>s
|
||||
/// </summary>
|
||||
readonly Dictionary<long, ChannelMapping> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// The active <see cref="IJsonTrackingContext"/>s for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
readonly List<IJsonTrackingContext> trackingContexts;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
ICustomCommandHandler customCommandHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Used for remapping <see cref="Channel.Id"/>s
|
||||
/// </summary>
|
||||
long channelIdCounter;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="StartAsync(CancellationToken)"/> has been called
|
||||
/// </summary>
|
||||
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="commandFactory">The <see cref="ICommandFactory"/> used to populate <see cref="builtinCommands"/></param>
|
||||
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory)
|
||||
{
|
||||
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory));
|
||||
|
||||
providers = new Dictionary<long, IProvider>();
|
||||
mappedChannels = new Dictionary<long, ChannelMapping>();
|
||||
trackingContexts = new List<IJsonTrackingContext>();
|
||||
channelIdCounter = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var I in providers)
|
||||
I.Value.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a <see cref="IProvider"/> from <see cref="providers"/> and <see cref="mappedChannels"/> optionally updating the <see cref="trackingContexts"/> as well
|
||||
/// </summary>
|
||||
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the <see cref="IProvider"/> to delete</param>
|
||||
/// <param name="updateTrackings">If <see cref="trackingContexts"/> should be update</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IProvider"/> being removed if it exists, <see langword="false"/> otherwise</returns>
|
||||
async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
|
||||
{
|
||||
IProvider provider;
|
||||
lock (providers)
|
||||
if (!providers.TryGetValue(connectionId, out provider))
|
||||
return null;
|
||||
Task task;
|
||||
lock (mappedChannels)
|
||||
{
|
||||
foreach (var kvp in mappedChannels.Where(x => x.Value.ProviderId == connectionId))
|
||||
mappedChannels.Remove(kvp.Key);
|
||||
|
||||
if (updateTrackings)
|
||||
lock (trackingContexts)
|
||||
task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
|
||||
else
|
||||
task = Task.CompletedTask;
|
||||
}
|
||||
await task.ConfigureAwait(false);
|
||||
return provider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
|
||||
{
|
||||
if (newChannels == null)
|
||||
throw new ArgumentNullException(nameof(newChannels));
|
||||
var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
|
||||
if (provider == null)
|
||||
return;
|
||||
var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
|
||||
if (results == null) //aborted
|
||||
return;
|
||||
var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
|
||||
{
|
||||
IsWatchdogChannel = x.IsWatchdogChannel,
|
||||
ProviderChannelId = y.Id,
|
||||
ProviderId = connectionId,
|
||||
Channel = y
|
||||
});
|
||||
|
||||
long baseId;
|
||||
lock (this)
|
||||
{
|
||||
baseId = channelIdCounter;
|
||||
channelIdCounter += results.Count;
|
||||
}
|
||||
|
||||
Task task;
|
||||
lock (mappedChannels)
|
||||
{
|
||||
lock (providers)
|
||||
if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
|
||||
return;
|
||||
foreach (var I in mappings)
|
||||
{
|
||||
var newId = baseId++;
|
||||
mappedChannels.Add(newId, I);
|
||||
I.Channel.Id = newId;
|
||||
}
|
||||
|
||||
lock (trackingContexts)
|
||||
task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
|
||||
}
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken)
|
||||
{
|
||||
if (newSettings == null)
|
||||
throw new ArgumentNullException(nameof(newSettings));
|
||||
IProvider provider;
|
||||
lock (providers)
|
||||
{
|
||||
//raw settings changes forces a rebuild of the provider
|
||||
if (providers.TryGetValue(newSettings.Id, out provider))
|
||||
{
|
||||
providers.Remove(newSettings.Id);
|
||||
provider.Dispose();
|
||||
}
|
||||
if (newSettings.Enabled.Value)
|
||||
{
|
||||
provider = providerFactory.CreateProvider(newSettings);
|
||||
providers.Add(newSettings.Id, provider);
|
||||
}
|
||||
}
|
||||
lock (mappedChannels)
|
||||
foreach (var channelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key))
|
||||
mappedChannels.Remove(channelId);
|
||||
if (newSettings.Enabled.Value && started)
|
||||
await provider.Connect(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
if (channelIds == null)
|
||||
throw new ArgumentNullException(nameof(channelIds));
|
||||
|
||||
return Task.WhenAll(channelIds.Select(x =>
|
||||
{
|
||||
ChannelMapping channelMapping;
|
||||
lock(mappedChannels)
|
||||
if (!mappedChannels.TryGetValue(x, out channelMapping))
|
||||
return Task.CompletedTask;
|
||||
IProvider provider;
|
||||
lock (providers)
|
||||
if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
|
||||
return Task.CompletedTask;
|
||||
return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
|
||||
{
|
||||
List<long> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
|
||||
started = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (customCommandHandler == null)
|
||||
throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
|
||||
JsonTrackingContext context = null;
|
||||
context = new JsonTrackingContext(ioManager, customCommandHandler, () =>
|
||||
{
|
||||
lock (trackingContexts)
|
||||
trackingContexts.Remove(context);
|
||||
}, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
|
||||
Task task;
|
||||
lock (trackingContexts)
|
||||
{
|
||||
trackingContexts.Add(context);
|
||||
lock (mappedChannels)
|
||||
task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
|
||||
}
|
||||
await task.ConfigureAwait(false);
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Connected(long connectionId)
|
||||
{
|
||||
lock (providers)
|
||||
return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
|
||||
{
|
||||
if (this.customCommandHandler != null)
|
||||
throw new InvalidOperationException("RegisterCommandHandler() already called!");
|
||||
this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteConnection(long connectionId, CancellationToken cancellationToken) => RemoveProvider(connectionId, true, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
sealed class ChatResponse
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public List<long> ChannelIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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,33 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command made from DM code
|
||||
/// </summary>
|
||||
public sealed class CustomCommand : Command
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="CustomCommand"/>
|
||||
/// </summary>
|
||||
ICustomCommandHandler handler;
|
||||
|
||||
/// <summary>
|
||||
/// Set a new <paramref name="handler"/>
|
||||
/// </summary>
|
||||
/// <param name="handler">The value of <see cref="handler"/></param>
|
||||
public void SetHandler(ICustomCommandHandler handler)
|
||||
{
|
||||
if (this.handler != null)
|
||||
throw new InvalidOperationException("SetHandler() already called!");
|
||||
this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Invoke(string arguments)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new InvalidOperationException("SetHandler() has not been called!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,33 +10,45 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// For managing connected chat services
|
||||
/// </summary>
|
||||
public interface IChat : IHostedService
|
||||
public interface IChat : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// If the IRC client is connected
|
||||
/// If a given set of <see cref="ChatSettings"/> is connected
|
||||
/// </summary>
|
||||
bool IrcConnected { get; }
|
||||
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
|
||||
/// <returns><see langword="true"/> if it is connected, <see langword="false"/> otherwise</returns>
|
||||
bool Connected(long connectionId);
|
||||
|
||||
/// <summary>
|
||||
/// If the Discord client is connected
|
||||
/// Registers a <paramref name="customCommandHandler"/> to use
|
||||
/// </summary>
|
||||
bool DiscordConnected { get; }
|
||||
/// <param name="customCommandHandler">A <see cref="ICustomCommandHandler"/></param>
|
||||
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Change chat settings
|
||||
/// Change chat settings. If the <see cref="ChatSettings.Id"/> is not currently in use, a new connection will be made instead
|
||||
/// </summary>
|
||||
/// <param name="newSettings">The new <see cref="ChatSettings"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation. Will complete immediately if the <see cref="ChatSettings.Enabled"/> property of <paramref name="newSettings"/> is <see langword="false"/></returns>
|
||||
Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects and deletes a given connection
|
||||
/// </summary>
|
||||
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task DeleteConnection(long connectionId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Change chat channels
|
||||
/// </summary>
|
||||
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
|
||||
/// <param name="newChannels">An <see cref="IEnumerable{T}"/> of the new list of <see cref="Api.Models.ChatChannel"/>s</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ChangeChannels(IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
|
||||
Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat <paramref name="message"/> to a given set of <paramref name="channelIds"/>
|
||||
@@ -63,6 +75,6 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <param name="commandsJsonName">The name of the chat commands json</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IDisposable"/> tied to the lifetime of the json trackings</returns>
|
||||
Task<IChatJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken);
|
||||
Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a tracking of dynamic chat json files
|
||||
/// </summary>
|
||||
public interface IChatJsonTrackingContext : IDisposable
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
interface ICommandFactory
|
||||
{
|
||||
IReadOnlyList<Command> GenerateCommands();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
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"/>
|
||||
/// </summary>
|
||||
public interface ICustomCommandHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle a chat command
|
||||
/// </summary>
|
||||
/// <param name="commandName">The command name</param>
|
||||
/// <param name="arguments">Everything typed after <paramref name="commandName"/> minus leading spaces</param>
|
||||
/// <param name="sender">The sending <see cref="User"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the response text to send back</returns>
|
||||
Task<string> HandleChatCommand(string commandName, string arguments, User sender, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a tracking of dynamic chat json files
|
||||
/// </summary>
|
||||
public interface IJsonTrackingContext : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Read <see cref="CustomCommand"/>s from the <see cref="IJsonTrackingContext"/>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of <see cref="CustomCommand"/>s in the <see cref="IJsonTrackingContext"/></returns>
|
||||
Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes information about connected <paramref name="channels"/> to the <see cref="IJsonTrackingContext"/>
|
||||
/// </summary>
|
||||
/// <param name="channels">The <see cref="Channel"/>s to write out</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Components.Chat.Providers;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for <see cref="IProvider"/>s
|
||||
/// </summary>
|
||||
interface IProviderFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="settings">The <see cref="ChatSettings"/> for the new provider</param>
|
||||
/// <returns>A new <see cref="IProvider"/></returns>
|
||||
IProvider CreateProvider(ChatSettings settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Chat.Commands;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class JsonTrackingContext : IJsonTrackingContext
|
||||
{
|
||||
readonly IIOManager ioManager;
|
||||
readonly ICustomCommandHandler customCommandHandler;
|
||||
readonly Action onDispose;
|
||||
|
||||
readonly string commandsPath;
|
||||
readonly string channelsPath;
|
||||
|
||||
readonly SemaphoreSlim channelsSemaphore;
|
||||
|
||||
public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, Action onDispose, string commandsPath, string channelsPath)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
|
||||
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
|
||||
this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath));
|
||||
this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath));
|
||||
|
||||
channelsSemaphore = new SemaphoreSlim(1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => onDispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
|
||||
var resultJson = Encoding.UTF8.GetString(resultBytes);
|
||||
var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson);
|
||||
foreach (var I in result)
|
||||
I.SetHandler(customCommandHandler);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<CustomCommand>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
|
||||
{
|
||||
using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
|
||||
await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels)), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
sealed class Message
|
||||
{
|
||||
string Content { get; set; }
|
||||
User User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Chat.Providers;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ProviderFactory : IProviderFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IProvider CreateProvider(Api.Models.Internal.ChatSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
switch (settings.Provider)
|
||||
{
|
||||
case ChatProvider.Irc:
|
||||
throw new NotImplementedException();
|
||||
case ChatProvider.Discord:
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// For interacting with a chat service
|
||||
/// </summary>
|
||||
interface IProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// If the <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
bool Connected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="string"/> that indicates the <see cref="IProvider"/> was mentioned
|
||||
/// </summary>
|
||||
string BotMention { get; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to connect the <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> on success, <see langword="false"/> otherwise</returns>
|
||||
Task<bool> Connect(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gracefully disconnects the provider. Implies a call to <see cref="IDisposable.Dispose"/>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Disconnect(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="Channel"/>s for given <paramref name="channels"/>
|
||||
/// </summary>
|
||||
/// <param name="channels">The <see cref="Api.Models.ChatChannel"/>s to map</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Channel"/>s representing <paramref name="channels"/></returns>
|
||||
Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send a message to the <see cref="IProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="channelId">The <see cref="Channel.Id"/> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat message requested by DD
|
||||
/// </summary>
|
||||
sealed class Response
|
||||
{
|
||||
/// <summary>
|
||||
/// The message string
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of internal channel ids to send <see cref="Message"/> to
|
||||
/// </summary>
|
||||
public List<long> ChannelIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed class User
|
||||
{
|
||||
long Id { get; set; }
|
||||
string FriendlyName { get; set; }
|
||||
string Mention { get; set; }
|
||||
Channel Channel { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
@@ -60,6 +61,14 @@ namespace Tgstation.Server.Host.Components
|
||||
/// The <see cref="IApplication"/> for <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
/// <summary>
|
||||
/// The <see cref="IEventConsumer"/> for <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
readonly IEventConsumer eventConsumer;
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
readonly ILogger<DreamMaker> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct <see cref="DreamMaker"/>
|
||||
@@ -70,8 +79,9 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="sessionControllerFactory">The value of <see cref="sessionControllerFactory"/></param>
|
||||
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/></param>
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
///
|
||||
public DreamMaker(IByond byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application)
|
||||
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public DreamMaker(IByond byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, ILogger<DreamMaker> logger)
|
||||
{
|
||||
this.byond = byond;
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
@@ -79,6 +89,8 @@ namespace Tgstation.Server.Host.Components
|
||||
this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
|
||||
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,9 +98,10 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout in seconds for validation</param>
|
||||
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
|
||||
async Task<bool> VerifyApi(int timeout, Models.CompileJob job, CancellationToken cancellationToken)
|
||||
async Task<bool> VerifyApi(int timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
|
||||
{
|
||||
var launchParameters = new DreamDaemonLaunchParameters
|
||||
{
|
||||
@@ -102,7 +115,7 @@ namespace Tgstation.Server.Host.Components
|
||||
var provider = new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension))));
|
||||
|
||||
var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
|
||||
using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, true, true, true, cancellationToken).ConfigureAwait(false))
|
||||
using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken);
|
||||
|
||||
@@ -119,10 +132,10 @@ namespace Tgstation.Server.Host.Components
|
||||
/// Compiles a .dme with DreamMaker
|
||||
/// </summary>
|
||||
/// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
|
||||
/// <param name="job">The <see cref="Host.Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task RunDreamMaker(string dreamMakerPath, Host.Models.CompileJob job, CancellationToken cancellationToken)
|
||||
async Task RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var dm = new Process())
|
||||
{
|
||||
@@ -169,7 +182,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// Adds server side includes to the .dme being compiled
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="Host.Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
|
||||
@@ -211,6 +224,8 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
public async Task<Models.CompileJob> Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogTrace("Begin Compile");
|
||||
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string>{ repository.Origin }, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
Status = CompilerStatus.Copying;
|
||||
@@ -266,18 +281,18 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
Status = CompilerStatus.Verifying;
|
||||
|
||||
ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, cancellationToken).ConfigureAwait(false);
|
||||
ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, byondLock, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!ddVerified)
|
||||
//server never validated or compile failed
|
||||
await CleanupFailedCompile().ConfigureAwait(false);
|
||||
await Task.WhenAll(CleanupFailedCompile(), eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { job.ExitCode == 0 ? "1" : "0" }, cancellationToken)).ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
job.DMApiValidated = true;
|
||||
|
||||
Status = CompilerStatus.Duplicating;
|
||||
|
||||
|
||||
//duplicate the dmb et al
|
||||
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -285,8 +300,10 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
//symlink in the static data
|
||||
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
|
||||
await configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken).ConfigureAwait(false);
|
||||
await symATask.ConfigureAwait(false);
|
||||
var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
|
||||
|
||||
await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
|
||||
await eventConsumer.HandleEvent(EventType.CompileComplete, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await compileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false);
|
||||
return job;
|
||||
@@ -297,6 +314,11 @@ namespace Tgstation.Server.Host.Components
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Status = CompilerStatus.Idle;
|
||||
|
||||
@@ -8,97 +8,65 @@
|
||||
/// <summary>
|
||||
/// Parameters: Reference name, commit sha
|
||||
/// </summary>
|
||||
RepoResetOrigin,
|
||||
RepoResetOrigin = 0,
|
||||
/// <summary>
|
||||
/// Parameters: Reference name, commit sha
|
||||
/// </summary>
|
||||
RepoCheckout,
|
||||
RepoCheckout = 1,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
RepoFetch,
|
||||
RepoFetch = 2,
|
||||
/// <summary>
|
||||
/// Parameters: Comma separated list in form of "#{Pull Request Number} @ {7 character SHA}
|
||||
/// </summary>
|
||||
RepoMergePullRequests,
|
||||
RepoMergePullRequests = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Current version, new version
|
||||
/// </summary>
|
||||
ByondChangeStart,
|
||||
ByondChangeStart = 4,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
ByondChangeCancelled,
|
||||
ByondChangeCancelled = 5,
|
||||
/// <summary>
|
||||
/// Parameters: Error string
|
||||
/// </summary>
|
||||
ByondFail,
|
||||
ByondFail = 6,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
ByondStageComplete,
|
||||
ByondStageComplete = 7,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
ByondChangeComplete,
|
||||
ByondChangeComplete = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Commit sha, parameter of <see cref="RepoMergePullRequests"/>
|
||||
/// Parameters: Origin commit sha
|
||||
/// </summary>
|
||||
CompileStart,
|
||||
CompileStart = 9,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
CompileCancelled,
|
||||
CompileCancelled = 10,
|
||||
/// <summary>
|
||||
/// Parameters: Error string
|
||||
/// Parameters: "1" if compile succeeded and api validation failed, "0" otherwise
|
||||
/// </summary>
|
||||
CompileFailure,
|
||||
CompileFailure = 11,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
CompileComplete,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Access token
|
||||
/// </summary>
|
||||
DDLaunched,
|
||||
CompileComplete = 12,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Exit code
|
||||
/// </summary>
|
||||
DDCrash,
|
||||
DDOtherCrash = 13,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDExit,
|
||||
/// <summary>
|
||||
/// Parameters: Exit code
|
||||
/// </summary>
|
||||
DDOtherCrash,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDOtherExit,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDRestart,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDBeginGracefulRestart,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDBeginGracefulShutdown,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDCancelGraceful,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
DDTerminated,
|
||||
DDOtherExit = 14,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// Sends a command to DreamDaemon through /world/Topic()
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send</param>
|
||||
/// <param name="command">The sanitized command to send</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the result of /world/Topic()</returns>
|
||||
Task<string> SendCommand(string command, CancellationToken cancellationToken);
|
||||
|
||||
@@ -14,12 +14,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use</param>
|
||||
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> to use</param>
|
||||
/// <param name="currentByondLock">The current <see cref="IByondExecutableLock"/> if any</param>
|
||||
/// <param name="primaryPort">If the <see cref="DreamDaemonLaunchParameters.PrimaryPort"/> of <paramref name="launchParameters"/> should be used</param>
|
||||
/// <param name="primaryDirectory">If the <see cref="IDmbProvider.PrimaryDirectory"/> of <paramref name="dmbProvider"/> should be used</param>
|
||||
/// <param name="apiValidate">If the <see cref="ISessionController"/> should only validate the DMAPI then exit</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="ISessionController"/></returns>
|
||||
Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken);
|
||||
Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="ISessionController"/> from an existing DreamDaemon instance
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -101,15 +102,20 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
readonly ISession session;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IChatJsonTrackingContext"/> for the <see cref="SessionController"/>
|
||||
/// The <see cref="IJsonTrackingContext"/> for the <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
readonly IChatJsonTrackingContext chatJsonTrackingContext;
|
||||
readonly IJsonTrackingContext chatJsonTrackingContext;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IChat"/> for the <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
readonly IChat chat;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
readonly ILogger<SessionController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TaskCompletionSource{TResult}"/> <see cref="SetPortImpl(ushort, CancellationToken)"/> waits on when DreamDaemon currently has it's ports closed
|
||||
/// </summary>
|
||||
@@ -147,15 +153,17 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="interopRegistrar">The <see cref="IInteropRegistrar"/> used to construct <see cref="interopContext"/></param>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param>
|
||||
public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IChatJsonTrackingContext chatJsonTrackingContext, IChat chat)
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger<SessionController> logger)
|
||||
{
|
||||
this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
|
||||
this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
|
||||
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
|
||||
this.session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
if (interopRegistrar == null)
|
||||
throw new ArgumentNullException(nameof(interopRegistrar));
|
||||
this.session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
interopContext = interopRegistrar.Register(reattachInformation.AccessIdentifier, this);
|
||||
|
||||
@@ -254,9 +262,29 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendCommand(string command, CancellationToken cancellationToken) => byondTopicSender.SendTopic(new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), String.Format(CultureInfo.InvariantCulture, "?{0}={1}&{2}={3}", InteropConstants.DMInteropAccessIdentifier, reattachInformation.AccessIdentifier, InteropConstants.DMParameterCommand, command), cancellationToken);
|
||||
public async Task<string> SendCommand(string command, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await byondTopicSender.SendTopic(
|
||||
new IPEndPoint(IPAddress.Loopback, reattachInformation.Port),
|
||||
String.Format(CultureInfo.InvariantCulture,
|
||||
"?{0}={1}&{2}={3}",
|
||||
byondTopicSender.SanitizeString(InteropConstants.DMInteropAccessIdentifier),
|
||||
byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
|
||||
byondTopicSender.SanitizeString(InteropConstants.DMParameterCommand),
|
||||
//intentionally don't sanitize command, that's up to the caller
|
||||
command),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> SetPortImpl(ushort port, CancellationToken cancellationToken) => await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", InteropConstants.DMTopicChangePort, InteropConstants.DMParameterNewPort, port), cancellationToken).ConfigureAwait(false) == InteropConstants.DMResponseSuccess;
|
||||
async Task<bool> SetPortImpl(ushort port, CancellationToken cancellationToken) => await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChangePort), byondTopicSender.SanitizeString(InteropConstants.DMParameterNewPort), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false) == InteropConstants.DMResponseSuccess;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ClosePort(CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
@@ -61,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
readonly IChat chat;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILoggerFactory"/> for the <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
@@ -73,7 +79,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="instance">The value of <see cref="instance"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IInstance instance, IIOManager ioManager, IChat chat)
|
||||
/// <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)
|
||||
{
|
||||
this.executor = executor ?? throw new ArgumentNullException(nameof(executor));
|
||||
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
|
||||
@@ -84,10 +91,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
|
||||
public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
|
||||
{
|
||||
var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
|
||||
if (!portToUse.HasValue)
|
||||
@@ -133,11 +141,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var byondLock = byond.UseExecutables(dmbProvider.CompileJob.ByondVersion);
|
||||
var byondLock = currentByondLock ?? byond.UseExecutables(dmbProvider.CompileJob.ByondVersion);
|
||||
try
|
||||
{
|
||||
var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", application.Version, interopJsonFile, InteropConstants.DMParamHostVersion, InteropConstants.DMParamInfoJson);
|
||||
|
||||
//more sanitization here cause it uses the same scheme
|
||||
var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(InteropConstants.DMParamHostVersion), byondTopicSender.SanitizeString(InteropConstants.DMParamInfoJson));
|
||||
|
||||
var session = executor.RunDreamDaemon(launchParameters, byondLock, dmbProvider, parameters, !primaryPort, !primaryDirectory);
|
||||
try
|
||||
@@ -149,7 +157,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
IsPrimary = primaryDirectory,
|
||||
Port = portToUse.Value,
|
||||
ProcessId = session.ProcessId
|
||||
}, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat);
|
||||
}, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat, loggerFactory.CreateLogger<SessionController>());
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -159,7 +167,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
catch
|
||||
{
|
||||
byondLock.Dispose();
|
||||
if (currentByondLock == null)
|
||||
byondLock.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -186,7 +195,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
var session = executor.AttachToDreamDaemon(reattachInformation.ProcessId, byondLock);
|
||||
try
|
||||
{
|
||||
return new SessionController(reattachInformation, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat);
|
||||
return new SessionController(reattachInformation, session, byondTopicSender, interopRegistrar, chatJsonTrackingContext, chat, loggerFactory.CreateLogger<SessionController>());
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,7 +15,7 @@ using Tgstation.Server.Host.Core;
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Watchdog : IWatchdog, IEventConsumer
|
||||
sealed class Watchdog : IWatchdog, IEventConsumer, ICustomCommandHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The time in milliseconds to wait from starting <see cref="alphaServer"/> to start <see cref="bravoServer"/>. Does not take responsiveness into account
|
||||
@@ -36,7 +38,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RebootState? RebootState => Running ? (RebootState?)(AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
|
||||
public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IChat"/> for the <see cref="Watchdog"/>
|
||||
@@ -68,6 +70,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IByondTopicSender"/> for the <see cref="Watchdog"/>
|
||||
/// </summary>
|
||||
readonly IByondTopicSender byondTopicSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SemaphoreSlim"/> for the <see cref="Watchdog"/>
|
||||
/// </summary>
|
||||
@@ -104,10 +111,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="reattachInfoHandler">The value of <see cref="reattachInfoHandler"/></param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
|
||||
/// <param name="initialLaunchParameters">The initial value of <see cref="ActiveLaunchParameters"/></param>
|
||||
/// <param name="instance">The <see cref="Models.Instance"/> containing the value of <see cref="instanceId"/></param>
|
||||
/// <param name="autoStart">The value of <see cref="autoStart"/></param>
|
||||
public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, DreamDaemonLaunchParameters initialLaunchParameters, Models.Instance instance, bool autoStart)
|
||||
public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, DreamDaemonLaunchParameters initialLaunchParameters, Models.Instance instance, bool autoStart)
|
||||
{
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
|
||||
@@ -115,6 +123,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
|
||||
instanceId = instance?.Id ?? throw new ArgumentNullException(nameof(instance));
|
||||
this.autoStart = autoStart;
|
||||
|
||||
@@ -123,6 +132,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
|
||||
serverUpdater.RegisterForUpdate(() => releaseServers = true);
|
||||
|
||||
chat.RegisterCommandHandler(this);
|
||||
|
||||
AlphaIsActive = true;
|
||||
ActiveLaunchParameters = initialLaunchParameters;
|
||||
releaseServers = false;
|
||||
@@ -354,7 +365,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Task chatTask;
|
||||
//this is necessary, the monitor could be in it's sleep loop trying to restart
|
||||
if (startMonitor && await StopMonitor().ConfigureAwait(false))
|
||||
@@ -371,46 +381,26 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
if (alphaServer != null || bravoServer != null)
|
||||
throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
|
||||
|
||||
WatchdogReattachInformation reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null;
|
||||
var reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null;
|
||||
var doesntNeedNewDmb = doReattach && reattachInfo.Alpha != null && reattachInfo.Bravo != null;
|
||||
var dmbToUse = doesntNeedNewDmb ? null : await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Task<ISessionController> alphaServerTask = null;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!doReattach || reattachInfo.Alpha == null)
|
||||
alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, true, true, false, alphaStartCts.Token);
|
||||
else
|
||||
alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
|
||||
//do a few seconds of delay so that any backends the servers use know that alpha came first
|
||||
await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false);
|
||||
Task<ISessionController> bravoServerTask;
|
||||
if (!doReattach || reattachInfo.Bravo == null)
|
||||
bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, false, false, false, cancellationToken);
|
||||
else
|
||||
bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken);
|
||||
if (!doReattach || reattachInfo.Alpha == null)
|
||||
alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token);
|
||||
else
|
||||
alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
|
||||
//do a few seconds of delay so that any backends the servers use know that alpha came first
|
||||
await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false);
|
||||
Task<ISessionController> bravoServerTask;
|
||||
if (!doReattach || reattachInfo.Bravo == null)
|
||||
bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken);
|
||||
else
|
||||
bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken);
|
||||
|
||||
bravoServer = await bravoServerTask.ConfigureAwait(false);
|
||||
alphaServer = await alphaServerTask.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (alphaServerTask != null)
|
||||
if (alphaServerTask.Status == TaskStatus.RanToCompletion)
|
||||
alphaServer = await alphaServerTask.ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
alphaStartCts.Cancel();
|
||||
try
|
||||
{
|
||||
alphaServer = await alphaServerTask.ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
throw;
|
||||
}
|
||||
await Task.WhenAll(alphaServerTask, bravoServerTask).ConfigureAwait(false);
|
||||
|
||||
async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
|
||||
{
|
||||
@@ -466,7 +456,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
catch
|
||||
{
|
||||
if (alphaServer == null && bravoServer == null)
|
||||
dmbToUse.Dispose(); //guaranteed to not be null here
|
||||
dmbToUse.Dispose(); //guaranteed to not be null here
|
||||
DisposeAndNullControllers();
|
||||
throw;
|
||||
}
|
||||
@@ -531,6 +521,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <inheritdoc />
|
||||
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
string results;
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!Running)
|
||||
@@ -540,11 +531,42 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
foreach (var I in parameters)
|
||||
{
|
||||
builder.Append("&");
|
||||
builder.Append(I);
|
||||
builder.Append(byondTopicSender.SanitizeString(I));
|
||||
}
|
||||
|
||||
var activeServer = AlphaIsActive ? alphaServer : bravoServer;
|
||||
await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (results == null)
|
||||
return;
|
||||
|
||||
List<Response> responses;
|
||||
try
|
||||
{
|
||||
responses = JsonConvert.DeserializeObject<List<Response>>(results);
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
|
||||
{
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!Running)
|
||||
return "ERROR: Server offline!";
|
||||
|
||||
var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChatCommand), byondTopicSender.SanitizeString(InteropConstants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments)));
|
||||
|
||||
var activeServer = AlphaIsActive ? alphaServer : bravoServer;
|
||||
return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
@@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// The <see cref="ISessionControllerFactory"/> for the <see cref="WatchdogFactory"/>
|
||||
/// </summary>
|
||||
readonly ISessionControllerFactory sessionManagerFactory;
|
||||
readonly ISessionControllerFactory sessionControllerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IServerUpdater"/> for the <see cref="WatchdogFactory"/>
|
||||
@@ -39,6 +40,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IByondTopicSender"/> for the <see cref="WatchdogFactory"/>
|
||||
/// </summary>
|
||||
readonly IByondTopicSender byondTopicSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance"/> for the <see cref="WatchdogFactory"/>
|
||||
/// </summary>
|
||||
@@ -49,24 +55,26 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// Construct a <see cref="WatchdogFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="sessionManagerFactory">The value of <see cref="sessionManagerFactory"/></param>
|
||||
/// <param name="sessionControllerFactory">The value of <see cref="sessionControllerFactory"/></param>
|
||||
/// <param name="serverUpdater">The value of <see cref="serverUpdater"/></param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
/// <param name="reattachInfoHandler">The value of <see cref="reattachInfoHandler"/></param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
|
||||
/// <param name="instance">The value of <see cref="instance"/></param>
|
||||
public WatchdogFactory(IChat chat, ISessionControllerFactory sessionManagerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
|
||||
public WatchdogFactory(IChat chat, ISessionControllerFactory sessionControllerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, Models.Instance instance)
|
||||
{
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
this.sessionManagerFactory = sessionManagerFactory ?? throw new ArgumentNullException(nameof(sessionManagerFactory));
|
||||
this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
|
||||
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
|
||||
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionManagerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger<Watchdog>(), reattachInfoHandler, databaseContextFactory, settings, instance, settings.AutoStart.Value);
|
||||
public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger<Watchdog>(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Z.EntityFramework.Plus;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for managing <see cref="Api.Models.ChatSettings"/>
|
||||
/// </summary>
|
||||
[TgsAuthorize]
|
||||
public sealed class ChatController : ModelController<Api.Models.ChatSettings>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ChatController"/>
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ChatController"/>
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
|
||||
{
|
||||
DiscordChannelId = api.DiscordChannelId,
|
||||
IrcChannel = api.IrcChannel,
|
||||
IsAdminChannel = api.IsAdminChannel,
|
||||
IsWatchdogChannel = api.IsWatchdogChannel
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ChatSettingsRights.Create)]
|
||||
public override async Task<IActionResult> Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (String.IsNullOrWhiteSpace(model.Name))
|
||||
return BadRequest(new { message = "name cannot be null or whitespace!" });
|
||||
|
||||
if (String.IsNullOrWhiteSpace(model.ConnectionString))
|
||||
return BadRequest(new { message = "connection_string cannot be null or whitespace!" });
|
||||
|
||||
if (!model.Provider.HasValue)
|
||||
return BadRequest(new { message = "provider cannot be null!" });
|
||||
|
||||
if (!model.Enabled.HasValue)
|
||||
return BadRequest(new { message = "enabled cannot be null!" });
|
||||
|
||||
//try to update das db first
|
||||
var dbModel = new Models.ChatSettings
|
||||
{
|
||||
Name = model.Name,
|
||||
ConnectionString = model.ConnectionString,
|
||||
Enabled = model.Enabled,
|
||||
Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(),
|
||||
InstanceId = Instance.Id,
|
||||
Provider = model.Provider,
|
||||
};
|
||||
DatabaseContext.ChatSettings.Add(dbModel);
|
||||
DatabaseContext.ChatChannels.AddRange(dbModel.Channels);
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
//try to create it
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (dbModel.Channels.Count > 0)
|
||||
await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//undo the add
|
||||
DatabaseContext.ChatSettings.Remove(dbModel);
|
||||
await DatabaseContext.Save(default).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
return BadRequest(new { message = e.Message });
|
||||
}
|
||||
return Json(dbModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ChatSettingsRights.Delete)]
|
||||
public override async Task<IActionResult> Delete([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
await Task.WhenAll(instance.Chat.DeleteConnection(model.Id, cancellationToken), DatabaseContext.ChatSettings.Where(x => x.Id == model.Id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ChatSettingsRights.Read)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
|
||||
|
||||
var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0;
|
||||
|
||||
if (!connectionStrings)
|
||||
foreach (var I in results)
|
||||
I.ConnectionString = null;
|
||||
|
||||
return Json(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ChatSettingsRights.WriteChannels | ChatSettingsRights.WriteConnectionString | ChatSettingsRights.WriteEnabled | ChatSettingsRights.WriteName | ChatSettingsRights.WriteProvider)]
|
||||
public override async Task<IActionResult> Update([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
|
||||
|
||||
var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (current == default)
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
|
||||
var userRights = (ChatSettingsRights)AuthenticationContext.GetRight(RightsType.ChatSettings);
|
||||
|
||||
bool anySettingsModified = false;
|
||||
|
||||
bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatSettings, T>> expression, ChatSettingsRights requiredRight)
|
||||
{
|
||||
var memberSelectorExpression = (MemberExpression)expression.Body;
|
||||
var property = (PropertyInfo)memberSelectorExpression.Member;
|
||||
|
||||
var newVal = property.GetValue(model);
|
||||
if (newVal == null)
|
||||
return false;
|
||||
if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
|
||||
return true;
|
||||
|
||||
property.SetValue(current, newVal);
|
||||
anySettingsModified = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!CheckModified(x => x.ConnectionString, ChatSettingsRights.WriteConnectionString)
|
||||
|| !CheckModified(x => x.Enabled, ChatSettingsRights.WriteEnabled)
|
||||
|| !CheckModified(x => x.Name, ChatSettingsRights.WriteName)
|
||||
|| !CheckModified(x => x.Provider, ChatSettingsRights.WriteProvider)
|
||||
|| (model.Channels != null && !userRights.HasFlag(ChatSettingsRights.WriteChannels)))
|
||||
return Forbid();
|
||||
|
||||
if (model.Channels != null)
|
||||
{
|
||||
DatabaseContext.ChatChannels.RemoveRange(current.Channels);
|
||||
var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
|
||||
DatabaseContext.ChatChannels.AddRange(dbChannels);
|
||||
current.Channels = dbChannels;
|
||||
}
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var chat = instanceManager.GetInstance(Instance).Chat;
|
||||
|
||||
if (anySettingsModified)
|
||||
//have to rebuild the thing first
|
||||
await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (model.Channels != null)
|
||||
await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if(userRights.HasFlag(ChatSettingsRights.Read))
|
||||
return Json(current);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for managing <see cref="Api.Models.DreamDaemon"/>
|
||||
/// <see cref="ModelController{TModel}"/> for managing the <see cref="DreamDaemon"/>
|
||||
/// </summary>
|
||||
[Route("/" + nameof(DreamDaemon))]
|
||||
public sealed class DreamDaemonController : ModelController<Api.Models.DreamDaemon>
|
||||
public sealed class DreamDaemonController : ModelController<DreamDaemon>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IJobManager"/> for the <see cref="DreamMakerController"/>
|
||||
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
|
||||
if (instance.Watchdog.Running)
|
||||
return StatusCode(HttpStatusCode.Gone);
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
|
||||
await jobManager.RegisterOperation(new Models.Job
|
||||
{
|
||||
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
|
||||
if (!instance.Watchdog.Running)
|
||||
return StatusCode(HttpStatusCode.Gone);
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
|
||||
await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
|
||||
return Ok();
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
//alias for cancelling the latest job
|
||||
var job = await DatabaseContext.CompileJobs.OrderByDescending(x => x.Job.StartedAt).Select(x => new Job { Id = x.Job.Id, StoppedAt = x.Job.StoppedAt }).FirstAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (job.StoppedAt != null)
|
||||
return StatusCode(HttpStatusCode.Gone);
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
var originalModel = await DatabaseContext.Instances.Where(x => x.Id == model.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (originalModel == default(Models.Instance))
|
||||
return StatusCode(HttpStatusCode.Gone);
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Forbid();
|
||||
|
||||
if(job.StoppedAt != null)
|
||||
return StatusCode(HttpStatusCode.Gone);
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
|
||||
await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
|
||||
return Ok();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChatSettings.Id"/>
|
||||
/// The <see cref="Api.Models.Internal.ChatSettings.Id"/>
|
||||
/// </summary>
|
||||
public long ChatSettingsId { get; set; }
|
||||
|
||||
|
||||
@@ -5,12 +5,7 @@ namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class ChatSettings : Api.Models.Internal.ChatSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance.Id"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -52,9 +52,7 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
public DbSet<InstanceUser> InstanceUsers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChatChannel"/>s in the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public DbSet<ChatChannel> ChatChannels { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
DbSet<ChatSettings> ChatSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChatChannel"/> in the <see cref="IDatabaseContext"/>
|
||||
/// </summary>
|
||||
DbSet<ChatChannel> ChatChannels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.RepositorySettings"/> in the <see cref="IDatabaseContext"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -34,19 +34,21 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Byond.TopicSender" Version="1.1.0.1" />
|
||||
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.5" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="1.0.2" />
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.26.0-preview-0017" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.0-preview2-final" />
|
||||
<PackageReference Include="MySql.Data.EntityFrameworkCore" Version="8.0.10-rc" />
|
||||
<PackageReference Include="Octokit" Version="0.29.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.2.1" />
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="1.7.17" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.1" />
|
||||
<PackageReference Include="MySql.Data.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageReference Include="Octokit" Version="0.30.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.2.4" />
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="1.8.4" />
|
||||
<PackageReference Include="ZNetCS.AspNetCore.Logging.EntityFrameworkCore" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user