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(); client.Ready += () => @@ -153,7 +149,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }; using (cancellationToken.Register(() => channelsAvailable.SetCanceled())) await channelsAvailable.Task.ConfigureAwait(false); - logger.LogDebug("Connection established!"); + Logger.LogDebug("Connection established!"); } catch (OperationCanceledException) { @@ -161,7 +157,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - logger.LogWarning("Error connecting to Discord: {0}", e); + Logger.LogWarning("Error connecting to Discord: {0}", e); return false; } @@ -171,20 +167,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public override async Task Disconnect(CancellationToken cancellationToken) { - logger.LogTrace("Disconnecting..."); + Logger.LogTrace("Disconnecting..."); if (!Connected) { - logger.LogTrace("Already disconnected not doing disconnection attempt!"); + Logger.LogTrace("Already disconnected not doing disconnection attempt!"); return; } try { await client.StopAsync().ConfigureAwait(false); - logger.LogTrace("Stopped."); + Logger.LogTrace("Stopped."); cancellationToken.ThrowIfCancellationRequested(); await client.LogoutAsync().ConfigureAwait(false); - logger.LogDebug("Disconnected!"); + Logger.LogDebug("Disconnected!"); } catch (OperationCanceledException) { @@ -192,18 +188,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - logger.LogWarning("Error disconnecting from discord: {0}", e); + Logger.LogWarning("Error disconnecting from discord: {0}", e); } } /// - public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) + public override Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken) { if (channels == null) throw new ArgumentNullException(nameof(channels)); if (!Connected) - throw new InvalidOperationException("Provider not connected!"); + { + Logger.LogWarning("Cannot map channels, provider disconnected!"); + return Task.FromResult>(Array.Empty()); + } Channel GetModelChannelFromDBChannel(ChatChannel channelFromDB) { @@ -223,11 +222,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers IsPrivateChannel = false, Tag = channelFromDB.Tag }; - logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName); + Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName); return channelModel; } - logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel.GetType()); + Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel.GetType()); return null; } @@ -239,7 +238,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers mappedChannels.AddRange(enumerator.Select(x => x.RealId)); } - return Task.FromResult>(enumerator); + return Task.FromResult>(enumerator); } /// @@ -259,7 +258,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - logger.LogWarning("Error sending discord message: {0}", e); + Logger.LogWarning("Error sending discord message: {0}", e); } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 99d13e1768..d0e5269920 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -24,7 +24,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Get a resulting in the next the recieves or on a disconnect /// /// 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 nextMessage; + /// + /// The auto reconnect + /// + Task reconnectTask; + + /// + /// for + /// + CancellationTokenSource reconnectCts; + /// /// Construct a /// - protected Provider() + /// The value of . + /// The initial reconnection interval. + protected Provider(ILogger logger, uint reconnectInterval) { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + messageQueue = new Queue(); nextMessage = new TaskCompletionSource(); + + SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult(); + logger.LogTrace("Created."); } /// @@ -47,7 +71,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public abstract void Dispose(); + public virtual void Dispose() + { + StopReconnectionTimer().GetAwaiter().GetResult(); + Logger.LogTrace("Disposed"); + } /// public abstract Task Connect(CancellationToken cancellationToken); @@ -56,7 +84,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers public abstract Task Disconnect(CancellationToken cancellationToken); /// - public abstract Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); + public abstract Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); /// public async Task NextMessage(CancellationToken cancellationToken) @@ -74,6 +102,74 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + /// + /// Stops and awaits the . + /// + /// A representing the running operation. + async Task StopReconnectionTimer() + { + reconnectCts?.Cancel(); + reconnectCts?.Dispose(); + if (reconnectTask != null) + { + await reconnectTask.ConfigureAwait(false); + reconnectTask = null; + } + } + + /// + public async Task SetReconnectInterval(uint reconnectInterval) + { + if (reconnectInterval == 0) + throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!"); + + await StopReconnectionTimer().ConfigureAwait(false); + reconnectCts = new CancellationTokenSource(); + try + { + reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token); + } + catch + { + reconnectCts.Dispose(); + reconnectCts = null; + throw; + } + } + + /// + /// Creates a that will attempt to reconnect the every minutes. + /// + /// The amount of minutes to wait between reconnection attempts. + /// The for the operation. + /// A representing the running operation. + async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken) + { + do + { + try + { + await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false); + if (!Connected) + { + Logger.LogInformation("Attempting to reconnect provider..."); + await Disconnect(cancellationToken).ConfigureAwait(false); + if (await Connect(cancellationToken).ConfigureAwait(false)) + EnqueueMessage(null); + } + } + catch (OperationCanceledException) + { + break; + } + catch(Exception e) + { + Logger.LogError(e, "Error reconnecting!"); + } + } + while (true); + } + /// public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 15fec66f51..8725718aa3 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -21,18 +21,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests [TestMethod] public void TestConstructionAndDisposal() { - Assert.ThrowsException(() => new DiscordProvider(null, null)); + Assert.ThrowsException(() => new DiscordProvider(null, null, 1)); var mockLogger = new Mock>(); - Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, null)); + Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, null, 1)); var mockToken = "asdf"; - new DiscordProvider(mockLogger.Object, mockToken).Dispose(); + Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, mockToken, 0)); + new DiscordProvider(mockLogger.Object, mockToken, 1).Dispose(); } [TestMethod] public async Task TestConnectWithFakeTokenFails() { var mockLogger = new Mock>(); - using (var provider = new DiscordProvider(mockLogger.Object, "asdf")) + using (var provider = new DiscordProvider(mockLogger.Object, "asdf", 1)) { Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false)); Assert.IsFalse(provider.Connected); @@ -48,7 +49,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests var mockLogger = new Mock>(); - using (var provider = new DiscordProvider(mockLogger.Object, testToken1)) + using (var provider = new DiscordProvider(mockLogger.Object, testToken1, 1)) { Assert.IsFalse(provider.Connected); await provider.Disconnect(default).ConfigureAwait(false);