diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs
index f4a53b31f5..cf6ba561aa 100644
--- a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
namespace Tgstation.Server.Api.Models.Internal
{
@@ -28,6 +29,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// The time interval in minutes the chat bot attempts to reconnect if and disconnected.
///
[Required]
+ [NotMapped]
public uint? ReconnectionInterval { get; set; }
///
diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
index a59a8ae153..e82a07da8c 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
@@ -82,9 +82,14 @@ namespace Tgstation.Server.Host.Components.Chat
readonly CancellationTokenSource handlerCts;
///
- /// The initial for the
+ /// The active for the
///
- readonly List initialChatBots;
+ readonly List activeChatBots;
+
+ ///
+ /// Used for various lock statements throughout this .
+ ///
+ readonly object synchronizationLock;
///
/// The for the
@@ -121,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// The value of
/// The value of
/// The value of
- /// The used to populate
+ /// The used to populate
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger logger, IEnumerable initialChatBots)
{
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
@@ -132,10 +137,12 @@ namespace Tgstation.Server.Host.Components.Chat
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
- this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
+ activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
restartRegistration = serverControl.RegisterForRestart(this);
+ synchronizationLock = new object();
+
builtinCommands = new Dictionary();
providers = new Dictionary();
mappedChannels = new Dictionary();
@@ -160,7 +167,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// The of the to delete
/// If should be update
/// The for the operation
- /// A resulting in the being removed if it exists, otherwise
+ /// A resulting in the being removed if it exists, otherwise
async Task RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
{
IProvider provider;
@@ -188,11 +195,31 @@ namespace Tgstation.Server.Host.Components.Chat
/// Processes a
///
/// The who recevied
- /// The to process
+ /// The to process. If , this indicates the provider reconnected.
/// The for the operation
/// A representing the running operation
+ #pragma warning disable CA1502
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
+ #pragma warning restore CA1502
{
+ // provider reconnected, remap channels.
+ if (message == null)
+ {
+ IEnumerable channelsToMap;
+ lock (activeChatBots)
+ channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
+
+ if (channelsToMap?.Any() ?? false)
+ {
+ long providerId;
+ lock (providers)
+ providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
+ await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
+ }
+
+ return;
+ }
+
// map the channel if it's private and we haven't seen it
lock (providers)
{
@@ -347,7 +374,7 @@ namespace Tgstation.Server.Host.Components.Chat
// add new ones
Task updatedTask;
- lock (this)
+ lock (synchronizationLock)
updatedTask = connectionsUpdated.Task;
lock (providers)
foreach (var I in providers)
@@ -367,9 +394,7 @@ namespace Tgstation.Server.Host.Components.Chat
foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
{
var message = await I.Value.ConfigureAwait(false);
-
await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
-
messageTasks.Remove(I.Key);
}
}
@@ -393,8 +418,23 @@ namespace Tgstation.Server.Host.Components.Chat
if (provider == null)
return;
var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
- if (results == null) // aborted
- return;
+ lock (activeChatBots)
+ {
+ var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
+ if (botToUpdate != null)
+ botToUpdate.Channels = newChannels
+ .Select(apiModel => new Models.ChatChannel
+ {
+ DiscordChannelId = apiModel.DiscordChannelId,
+ IrcChannel = apiModel.IrcChannel,
+ IsAdminChannel = apiModel.IsAdminChannel,
+ IsUpdatesChannel = apiModel.IsUpdatesChannel,
+ IsWatchdogChannel = apiModel.IsWatchdogChannel,
+ Tag = apiModel.Tag
+ })
+ .ToList();
+ }
+
var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
{
IsWatchdogChannel = x.IsWatchdogChannel == true,
@@ -405,13 +445,13 @@ namespace Tgstation.Server.Host.Components.Chat
});
ulong baseId;
- lock (this)
+ lock (synchronizationLock)
{
baseId = channelIdCounter;
channelIdCounter += (ulong)results.Count;
}
- Task task;
+ Task trackingContextUpdateTask;
lock (mappedChannels)
{
lock (providers)
@@ -425,10 +465,10 @@ namespace Tgstation.Server.Host.Components.Chat
}
lock (trackingContexts)
- task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
+ trackingContextUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
}
- await task.ConfigureAwait(false);
+ await trackingContextUpdateTask.ConfigureAwait(false);
}
///
@@ -478,7 +518,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
if (newSettings.Enabled.Value)
await provider.Connect(cancellationToken).ConfigureAwait(false);
- lock (this)
+ lock (synchronizationLock)
{
// same thread shennanigans
var oldOne = connectionsUpdated;
@@ -486,6 +526,31 @@ namespace Tgstation.Server.Host.Components.Chat
oldOne.SetResult(null);
}
}
+
+ Task reconnectionUpdateTask = Task.CompletedTask;
+ lock (activeChatBots)
+ {
+ var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
+ if (originalChatBot != null)
+ {
+ if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
+ reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
+
+ activeChatBots.Remove(originalChatBot);
+ }
+
+ activeChatBots.Add(new Models.ChatBot
+ {
+ Id = newSettings.Id,
+ ConnectionString = newSettings.ConnectionString,
+ Enabled = newSettings.Enabled,
+ Name = newSettings.Name,
+ ReconnectionInterval = newSettings.ReconnectionInterval,
+ Provider = newSettings.Provider
+ });
+ }
+
+ await reconnectionUpdateTask.ConfigureAwait(false);
}
///
@@ -535,9 +600,9 @@ namespace Tgstation.Server.Host.Components.Chat
{
foreach (var I in commandFactory.GenerateCommands())
builtinCommands.Add(I.Name.ToUpperInvariant(), I);
- await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
+ await Task.WhenAll(activeChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
- await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
+ await Task.WhenAll(activeChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
chatHandler = MonitorMessages(handlerCts.Token);
started = true;
}
@@ -579,13 +644,6 @@ namespace Tgstation.Server.Host.Components.Chat
return context;
}
- ///
- public bool Connected(long connectionId)
- {
- lock (providers)
- return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
- }
-
///
public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
{
diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs
index 59ecc05587..ef5d0b9632 100644
--- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs
@@ -12,13 +12,6 @@ namespace Tgstation.Server.Host.Components.Chat
///
public interface IChat : IHostedService, IDisposable
{
- ///
- /// If a given set of is connected
- ///
- /// The of the connection
- /// if it is connected, otherwise
- bool Connected(long connectionId);
-
///
/// Registers a to use
///
@@ -54,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// Send a chat to a given set of
///
/// The message being sent
- /// The s of the s to send to
+ /// The s of the s to send to
/// The for the operation
/// A representing the running operation
Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs
index 04a4874cfe..14f558a593 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs
@@ -50,10 +50,10 @@ namespace Tgstation.Server.Host.Components.Chat
{
case ChatProvider.Irc:
var ircBuilder = (IrcConnectionStringBuilder)builder;
- return new IrcProvider(application, asyncDelayer, loggerFactory.CreateLogger(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, ircBuilder.UseSsl.Value);
+ return new IrcProvider(application, asyncDelayer, loggerFactory.CreateLogger(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value);
case ChatProvider.Discord:
var discordBuilder = (DiscordConnectionStringBuilder)builder;
- return new DiscordProvider(loggerFactory.CreateLogger(), discordBuilder.BotToken);
+ return new DiscordProvider(loggerFactory.CreateLogger(), discordBuilder.BotToken, settings.ReconnectionInterval.Value);
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
index 753462dca9..32fbaab397 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
@@ -29,11 +29,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
- ///
- /// The for the
- ///
- readonly ILogger logger;
-
///
/// The for the
///
@@ -59,23 +54,24 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
/// Construct a
///
- /// The value of
+ /// The value of
/// The value of
- public DiscordProvider(ILogger logger, string botToken)
+ /// The initial reconnect interval in minutes.
+ public DiscordProvider(ILogger logger, string botToken, uint reconnectInterval)
+ : base(logger, reconnectInterval)
{
- this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
client = new DiscordSocketClient();
client.MessageReceived += Client_MessageReceived;
mappedChannels = new List();
- logger.LogTrace("Created.");
}
///
public override void Dispose()
{
- logger.LogTrace("Disposed.");
client.Dispose();
+
+ base.Dispose();
}
///
@@ -95,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id);
if (mentionedUs)
{
- logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
+ Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false);
}
@@ -127,10 +123,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
public override async Task Connect(CancellationToken cancellationToken)
{
- logger.LogTrace("Connecting...");
+ Logger.LogTrace("Connecting...");
if (Connected)
{
- logger.LogTrace("Already connected not doing connection attempt!");
+ Logger.LogTrace("Already connected not doing connection attempt!");
return true;
}
@@ -138,12 +134,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
- logger.LogTrace("Logged in.");
+ Logger.LogTrace("Logged in.");
cancellationToken.ThrowIfCancellationRequested();
await client.StartAsync().ConfigureAwait(false);
- logger.LogTrace("Started.");
+ Logger.LogTrace("Started.");
var channelsAvailable = new TaskCompletionSource
/// The for the operation
- /// A resulting in the next available
+ /// A resulting in the next available or if the needed to reconnect.
/// Note that private messages will come in the form of s not returned in
Task NextMessage(CancellationToken cancellationToken);
@@ -47,8 +47,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
/// The s to map
/// The for the operation
- /// A resulting in a of the s representing
- Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken);
+ /// A resulting in a of the s representing
+ Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken);
///
/// Send a message to the
@@ -58,5 +58,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// The for the operation
/// A representing the running operation
Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
+
+ ///
+ /// Set the interval at which the provider tries to reconnect.
+ ///
+ /// The reconnection interval in minutes.
+ /// A representing the running operation.
+ Task SetReconnectInterval(uint reconnectInterval);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
index cce4b47d37..c200d55f39 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
@@ -30,11 +30,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
readonly IAsyncDelayer asyncDelayer;
- ///
- /// The for the
- ///
- readonly ILogger logger;
-
///
/// The client
///
@@ -101,13 +96,24 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// The value of
/// The value of
/// The value of
+ /// The initial reconnect interval in minutes.
/// If should be used
- public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
+ public IrcProvider(
+ IApplication application,
+ IAsyncDelayer asyncDelayer,
+ ILogger logger,
+ string address,
+ ushort port,
+ string nickname,
+ string password,
+ IrcPasswordType? passwordType,
+ uint reconnectInterval,
+ bool useSsl)
+ : base(logger, reconnectInterval)
{
if (application == null)
throw new ArgumentNullException(nameof(application));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
- this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.address = address ?? throw new ArgumentNullException(nameof(address));
this.port = port;
@@ -157,6 +163,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
disconnecting = true;
client.Disconnect(); // just closes the socket
}
+
+ base.Dispose();
}
///
@@ -328,7 +336,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
- logger.LogWarning("Unable to connect to IRC: {0}", e);
+ Logger.LogWarning("Unable to connect to IRC: {0}", e);
return false;
}
@@ -350,7 +358,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
- logger.LogWarning("Error quitting IRC: {0}", e);
+ Logger.LogWarning("Error quitting IRC: {0}", e);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
Dispose();
@@ -362,12 +370,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
- logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
+ Logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
}
}
///
- public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
+ public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (channels.Any(x => x.IrcChannel == null))
throw new InvalidOperationException("ChatChannel missing IrcChannel!");
@@ -386,7 +394,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
foreach (var I in hs)
client.RfcJoin(I);
- return (IReadOnlyList)channels.Select(x =>
+ return (IReadOnlyCollection)channels.Select(x =>
{
ulong? id = null;
if (!channelIdMap.Any(y =>
@@ -432,7 +440,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
- logger.LogWarning("Unable to send to channel: {0}", e);
+ Logger.LogWarning("Unable to send to channel: {0}", e);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
index f0fb73e42a..c64033985b 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -8,6 +10,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
abstract class Provider : IProvider
{
+ ///
+ /// The for the .
+ ///
+ protected ILogger Logger { get; }
+
///
/// of received s
///
@@ -18,13 +25,30 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
TaskCompletionSource