mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-21 03:57:19 +01:00
Hack automatic reconnection into the chat system
This commit is contained in:
@@ -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 <see cref="Enabled"/> and disconnected.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[NotMapped]
|
||||
public uint? ReconnectionInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -82,9 +82,14 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
readonly CancellationTokenSource handlerCts;
|
||||
|
||||
/// <summary>
|
||||
/// The initial <see cref="Models.ChatBot"/> for the <see cref="Chat"/>
|
||||
/// The active <see cref="Models.ChatBot"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
readonly List<Models.ChatBot> initialChatBots;
|
||||
readonly List<Models.ChatBot> activeChatBots;
|
||||
|
||||
/// <summary>
|
||||
/// Used for various lock statements throughout this <see langword="class"/>.
|
||||
/// </summary>
|
||||
readonly object synchronizationLock;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICustomCommandHandler"/> for the <see cref="ChangeChannels(long, IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/>
|
||||
@@ -121,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="initialChatBots">The <see cref="IEnumerable{T}"/> used to populate <see cref="initialChatBots"/></param>
|
||||
/// <param name="initialChatBots">The <see cref="IEnumerable{T}"/> used to populate <see cref="activeChatBots"/></param>
|
||||
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> 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<string, ICommand>();
|
||||
providers = new Dictionary<long, IProvider>();
|
||||
mappedChannels = new Dictionary<ulong, ChannelMapping>();
|
||||
@@ -160,7 +167,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <param name="connectionId">The <see cref="ChatBot.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>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IProvider"/> being removed if it exists, <see langword="null"/> otherwise</returns>
|
||||
async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
|
||||
{
|
||||
IProvider provider;
|
||||
@@ -188,11 +195,31 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// Processes a <paramref name="message"/>
|
||||
/// </summary>
|
||||
/// <param name="provider">The <see cref="IProvider"/> who recevied <paramref name="message"/></param>
|
||||
/// <param name="message">The <see cref="Message"/> to process</param>
|
||||
/// <param name="message">The <see cref="Message"/> to process. If <see langword="null"/>, this indicates the provider reconnected.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
#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<Api.Models.ChatChannel> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Connected(long connectionId)
|
||||
{
|
||||
lock (providers)
|
||||
return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
|
||||
{
|
||||
|
||||
@@ -12,13 +12,6 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// </summary>
|
||||
public interface IChat : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// If a given set of <see cref="ChatBot"/> is connected
|
||||
/// </summary>
|
||||
/// <param name="connectionId">The <see cref="ChatBot.Id"/> of the connection</param>
|
||||
/// <returns><see langword="true"/> if it is connected, <see langword="false"/> otherwise</returns>
|
||||
bool Connected(long connectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <paramref name="customCommandHandler"/> to use
|
||||
/// </summary>
|
||||
@@ -54,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// Send a chat <paramref name="message"/> to a given set of <paramref name="channelIds"/>
|
||||
/// </summary>
|
||||
/// <param name="message">The message being sent</param>
|
||||
/// <param name="channelIds">The <see cref="Models.ChatChannel.Id"/>s of the <see cref="Host.Models.ChatChannel"/>s to send to</param>
|
||||
/// <param name="channelIds">The <see cref="Models.ChatChannel.Id"/>s of the <see cref="Models.ChatChannel"/>s to send to</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken);
|
||||
|
||||
@@ -50,10 +50,10 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
case ChatProvider.Irc:
|
||||
var ircBuilder = (IrcConnectionStringBuilder)builder;
|
||||
return new IrcProvider(application, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, ircBuilder.UseSsl.Value);
|
||||
return new IrcProvider(application, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), 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<DiscordProvider>(), discordBuilder.BotToken);
|
||||
return new DiscordProvider(loggerFactory.CreateLogger<DiscordProvider>(), discordBuilder.BotToken, settings.ReconnectionInterval.Value);
|
||||
default:
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
|
||||
}
|
||||
|
||||
@@ -29,11 +29,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
readonly ILogger<DiscordProvider> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
@@ -59,23 +54,24 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <summary>
|
||||
/// Construct a <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/></param>
|
||||
/// <param name="botToken">The value of <see cref="botToken"/></param>
|
||||
public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
|
||||
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
|
||||
public DiscordProvider(ILogger<DiscordProvider> 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<ulong>();
|
||||
logger.LogTrace("Created.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose()
|
||||
{
|
||||
logger.LogTrace("Disposed.");
|
||||
client.Dispose();
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> 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<object>();
|
||||
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
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
|
||||
public override Task<IReadOnlyCollection<Channel>> MapChannels(IEnumerable<ChatChannel> 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<IReadOnlyCollection<Channel>>(Array.Empty<Channel>());
|
||||
}
|
||||
|
||||
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<IReadOnlyList<Channel>>(enumerator);
|
||||
return Task.FromResult<IReadOnlyCollection<Channel>>(enumerator);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// 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>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/></returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/> or <see langword="null"/> if the <see cref="IProvider"/> needed to reconnect.</returns>
|
||||
/// <remarks>Note that private messages will come in the form of <see cref="Channel"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/></remarks>
|
||||
Task<Message> NextMessage(CancellationToken cancellationToken);
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </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);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyCollection{T}"/> of the <see cref="Channel"/>s representing <paramref name="channels"/></returns>
|
||||
Task<IReadOnlyCollection<Channel>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send a message to the <see cref="IProvider"/>
|
||||
@@ -58,5 +58,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Set the interval at which the provider tries to reconnect.
|
||||
/// </summary>
|
||||
/// <param name="reconnectInterval">The reconnection interval in minutes.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task SetReconnectInterval(uint reconnectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </summary>
|
||||
readonly IAsyncDelayer asyncDelayer;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="IrcProvider"/>
|
||||
/// </summary>
|
||||
readonly ILogger<IrcProvider> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IrcFeatures"/> client
|
||||
/// </summary>
|
||||
@@ -101,13 +96,24 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="nickname">The value of <see cref="nickname"/></param>
|
||||
/// <param name="password">The value of <see cref="password"/></param>
|
||||
/// <param name="passwordType">The value of <see cref="passwordType"/></param>
|
||||
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
|
||||
/// <param name="useSsl">If <see cref="IrcConnection.UseSsl"/> should be used</param>
|
||||
public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
|
||||
public IrcProvider(
|
||||
IApplication application,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ILogger<IrcProvider> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
public override Task<IReadOnlyCollection<Channel>> MapChannels(IEnumerable<ChatChannel> 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<Channel>)channels.Select(x =>
|
||||
return (IReadOnlyCollection<Channel>)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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
abstract class Provider : IProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Provider"/>.
|
||||
/// </summary>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Queue{T}"/> of received <see cref="Message"/>s
|
||||
/// </summary>
|
||||
@@ -18,13 +25,30 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </summary>
|
||||
TaskCompletionSource<object> nextMessage;
|
||||
|
||||
/// <summary>
|
||||
/// The auto reconnect <see cref="Task"/>
|
||||
/// </summary>
|
||||
Task reconnectTask;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CancellationTokenSource"/> for <see cref="reconnectTask"/>
|
||||
/// </summary>
|
||||
CancellationTokenSource reconnectCts;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="Provider"/>
|
||||
/// </summary>
|
||||
protected Provider()
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
/// <param name="reconnectInterval">The initial reconnection interval.</param>
|
||||
protected Provider(ILogger logger, uint reconnectInterval)
|
||||
{
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
messageQueue = new Queue<Message>();
|
||||
nextMessage = new TaskCompletionSource<object>();
|
||||
|
||||
SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult();
|
||||
logger.LogTrace("Created.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -47,7 +71,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Dispose();
|
||||
public virtual void Dispose()
|
||||
{
|
||||
StopReconnectionTimer().GetAwaiter().GetResult();
|
||||
Logger.LogTrace("Disposed");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<bool> Connect(CancellationToken cancellationToken);
|
||||
@@ -56,7 +84,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
public abstract Task Disconnect(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken);
|
||||
public abstract Task<IReadOnlyCollection<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> NextMessage(CancellationToken cancellationToken)
|
||||
@@ -74,6 +102,74 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops and awaits the <see cref="reconnectTask"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task StopReconnectionTimer()
|
||||
{
|
||||
reconnectCts?.Cancel();
|
||||
reconnectCts?.Dispose();
|
||||
if (reconnectTask != null)
|
||||
{
|
||||
await reconnectTask.ConfigureAwait(false);
|
||||
reconnectTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Task"/> that will attempt to reconnect the <see cref="Provider"/> every <paramref name="reconnectInterval"/> minutes.
|
||||
/// </summary>
|
||||
/// <param name="reconnectInterval">The amount of minutes to wait between reconnection attempts.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -21,18 +21,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
|
||||
[TestMethod]
|
||||
public void TestConstructionAndDisposal()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, 1));
|
||||
var mockLogger = new Mock<ILogger<DiscordProvider>>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null, 1));
|
||||
var mockToken = "asdf";
|
||||
new DiscordProvider(mockLogger.Object, mockToken).Dispose();
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new DiscordProvider(mockLogger.Object, mockToken, 0));
|
||||
new DiscordProvider(mockLogger.Object, mockToken, 1).Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestConnectWithFakeTokenFails()
|
||||
{
|
||||
var mockLogger = new Mock<ILogger<DiscordProvider>>();
|
||||
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<ILogger<DiscordProvider>>();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user