1 using Microsoft.Extensions.Logging;
5 using System.Collections.Generic;
9 using System.Threading.Tasks;
20 #pragma warning disable CA1506 23 const string CommonMention =
"!tgs";
58 readonly ILogger<ChatManager>
logger;
138 this.providerFactory = providerFactory ??
throw new ArgumentNullException(nameof(providerFactory));
139 this.ioManager = ioManager ??
throw new ArgumentNullException(nameof(ioManager));
140 this.commandFactory = commandFactory ??
throw new ArgumentNullException(nameof(commandFactory));
141 if (serverControl == null)
142 throw new ArgumentNullException(nameof(serverControl));
143 this.asyncDelayer = asyncDelayer ??
throw new ArgumentNullException(nameof(asyncDelayer));
144 this.loggerFactory = loggerFactory ??
throw new ArgumentNullException(nameof(loggerFactory));
145 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
146 activeChatBots = initialChatBots?.ToList() ??
throw new ArgumentNullException(nameof(initialChatBots));
150 synchronizationLock =
new object();
152 builtinCommands =
new Dictionary<string, ICommand>();
153 providers =
new Dictionary<long, IProvider>();
154 mappedChannels =
new Dictionary<ulong, ChannelMapping>();
155 trackingContexts =
new List<IChatTrackingContext>();
156 handlerCts =
new CancellationTokenSource();
157 connectionsUpdated =
new TaskCompletionSource<object>();
158 channelIdCounter = 1;
164 restartRegistration.Dispose();
165 handlerCts.Dispose();
166 foreach (var I
in providers)
177 async Task<IProvider>
RemoveProvider(
long connectionId,
bool updateTrackings, CancellationToken cancellationToken)
181 if (!providers.TryGetValue(connectionId, out provider))
184 Task trackingContextsUpdateTask;
185 lock (mappedChannels)
187 foreach (var I
in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
188 mappedChannels.Remove(I);
190 var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
193 lock (trackingContexts)
194 trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
196 trackingContextsUpdateTask = Task.CompletedTask;
199 await trackingContextsUpdateTask.ConfigureAwait(
false);
211 #pragma warning disable CA1502 213 #pragma warning restore CA1502 218 IEnumerable<Api.Models.ChatChannel> channelsToMap;
219 lock (activeChatBots)
220 channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
222 if (channelsToMap?.Any() ??
false)
226 providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
227 await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(
false);
236 var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
237 var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.
User.
Channel.
RealId);
239 lock (mappedChannels)
243 if (!enumerable.Any())
246 lock (synchronizationLock)
247 newId = channelIdCounter++;
249 "Mapping private channel {0}:{1} as {2}",
255 IsWatchdogChannel =
false,
257 ProviderId = providerId,
268 var mapping = enumerable.First().Value;
275 var splits =
new List<string>(message.
Content.Trim().Split(
' '));
276 var address = splits[0];
277 if (address.Length > 1 && (address.Last() ==
':' || address.Last() ==
','))
278 address = address[0..^1];
280 address = address.ToUpperInvariant();
282 var addressed = address == CommonMention.ToUpperInvariant() || address == provider.
BotMention.ToUpperInvariant();
288 logger.LogTrace(
"Chat command: {0}. User (True provider Id): {1}", message.
Content, JsonConvert.SerializeObject(message.
User));
293 if (splits.Count == 0)
296 await SendMessage(
"Hi!",
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
300 var command = splits[0].ToUpperInvariant();
302 var arguments = String.Join(
" ", splits);
306 ICommand GetCommand(
string commandName)
308 if (!builtinCommands.TryGetValue(commandName, out var handler))
310 handler = trackingContexts
311 .Where(x => x.CustomCommands != null)
312 .SelectMany(x => x.CustomCommands)
313 .Where(x => x.Name.ToUpperInvariant() == commandName)
320 const string UnknownCommandMessage =
"Unknown command! Type '?' or 'help' for available commands.";
322 if (command ==
"HELP" || command ==
"?")
325 if (splits.Count == 0)
327 var allCommands = builtinCommands.Select(x => x.Value).ToList();
328 allCommands.AddRange(
330 .Where(x => x.CustomCommands != null)
332 x => x.CustomCommands));
333 helpText = String.Format(CultureInfo.InvariantCulture,
"Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(
", ", allCommands.Select(x => x.Name)));
337 var helpHandler = GetCommand(splits[0].ToUpperInvariant());
338 if (helpHandler !=
default)
339 helpText = String.Format(CultureInfo.InvariantCulture,
"{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ?
" - May only be used in admin channels" : String.Empty);
341 helpText = UnknownCommandMessage;
344 await SendMessage(helpText,
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
348 var commandHandler = GetCommand(command);
350 if (commandHandler ==
default)
352 await SendMessage(UnknownCommandMessage,
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
358 await SendMessage(
"Use this command in an admin channel!",
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
362 var result = await commandHandler.Invoke(arguments, message.
User, cancellationToken).ConfigureAwait(
false);
364 await SendMessage(result,
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
366 catch (OperationCanceledException)
373 logger.LogError(
"Error processing chat command: {0}", e);
374 await SendMessage(
"Internal error processing command!",
new List<ulong> { message.
User.
Channel.
RealId }, cancellationToken).ConfigureAwait(
false);
385 var messageTasks =
new Dictionary<IProvider, Task<Message>>();
388 while (!cancellationToken.IsCancellationRequested)
391 foreach (var I
in messageTasks.Where(x => !x.Key.Connected).ToList())
392 messageTasks.Remove(I.Key);
396 lock (synchronizationLock)
397 updatedTask = connectionsUpdated.Task;
399 foreach (var I
in providers)
400 if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
401 messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
403 if (messageTasks.Count == 0)
405 await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(
false);
410 await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(
false);
413 foreach (var I
in messageTasks.Where(x => x.Value.IsCompleted).ToList())
415 var message = await I.Value.ConfigureAwait(
false);
416 var messageNumber = Interlocked.Increment(ref messagesProcessed);
417 using (LogContext.PushProperty(
"ChatMessage", messageNumber))
418 await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(
false);
419 messageTasks.Remove(I.Key);
423 catch (OperationCanceledException)
425 logger.LogTrace(
"Message processing loop cancelled!");
429 logger.LogError(
"Message monitor crashed! Exception: {0}", e);
434 public async Task
ChangeChannels(
long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
436 if (newChannels == null)
437 throw new ArgumentNullException(nameof(newChannels));
438 var provider = await RemoveProvider(connectionId,
false, cancellationToken).ConfigureAwait(
false);
439 if (provider == null)
441 var results = await provider.
MapChannels(newChannels, cancellationToken).ConfigureAwait(
false);
442 lock (activeChatBots)
444 var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
445 if (botToUpdate != null)
446 botToUpdate.Channels = newChannels
447 .Select(apiModel =>
new Models.ChatChannel
449 DiscordChannelId = apiModel.DiscordChannelId,
450 IrcChannel = apiModel.IrcChannel,
451 IsAdminChannel = apiModel.IsAdminChannel,
452 IsUpdatesChannel = apiModel.IsUpdatesChannel,
453 IsWatchdogChannel = apiModel.IsWatchdogChannel,
459 var mappings = Enumerable.Zip(newChannels, results, (x, y) =>
new ChannelMapping 462 IsUpdatesChannel = x.IsUpdatesChannel ==
true,
463 IsAdminChannel = x.IsAdminChannel ==
true,
464 ProviderChannelId = y.RealId,
465 ProviderId = connectionId,
470 lock (synchronizationLock)
472 baseId = channelIdCounter;
473 channelIdCounter += (ulong)results.Count;
476 Task trackingContextUpdateTask;
477 lock (mappedChannels)
480 if (!providers.TryGetValue(connectionId, out
IProvider verify) || verify != provider)
482 foreach (var I
in mappings)
484 var newId = baseId++;
485 logger.LogTrace(
"Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
486 mappedChannels.Add(newId, I);
487 I.Channel.RealId = newId;
490 lock (trackingContexts)
491 trackingContextUpdateTask = Task.WhenAll(
492 trackingContexts.Select(
493 x => x.UpdateChannels(
494 mappedChannels.Select(y => y.Value.Channel).ToList(),
495 cancellationToken)));
498 await trackingContextUpdateTask.ConfigureAwait(
false);
504 if (newSettings == null)
505 throw new ArgumentNullException(nameof(newSettings));
508 async Task DisconnectProvider(
IProvider p)
512 await p.
Disconnect(cancellationToken).ConfigureAwait(
false);
524 if (providers.TryGetValue(newSettings.
Id, out provider))
526 providers.Remove(newSettings.
Id);
527 disconnectTask = DisconnectProvider(provider);
530 disconnectTask = Task.CompletedTask;
533 provider = providerFactory.CreateProvider(newSettings);
534 providers.Add(newSettings.
Id, provider);
538 lock (mappedChannels)
539 foreach (var I
in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
540 mappedChannels.Remove(I);
542 await disconnectTask.ConfigureAwait(
false);
546 if (newSettings.Enabled.Value)
547 await provider.
Connect(cancellationToken).ConfigureAwait(
false);
548 lock (synchronizationLock)
551 var oldOne = connectionsUpdated;
552 connectionsUpdated =
new TaskCompletionSource<object>();
553 oldOne.SetResult(null);
557 Task reconnectionUpdateTask = Task.CompletedTask;
558 lock (activeChatBots)
560 var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
561 if (originalChatBot != null)
563 if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
566 activeChatBots.Remove(originalChatBot);
569 activeChatBots.Add(
new Models.ChatBot
572 ConnectionString = newSettings.ConnectionString,
573 Enabled = newSettings.Enabled,
574 Name = newSettings.Name,
575 ReconnectionInterval = newSettings.ReconnectionInterval,
576 Provider = newSettings.Provider
580 await reconnectionUpdateTask.ConfigureAwait(
false);
584 public Task
SendMessage(
string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
587 throw new ArgumentNullException(nameof(message));
588 if (channelIds == null)
589 throw new ArgumentNullException(nameof(channelIds));
591 logger.LogTrace(
"Chat send \"{0}\" to channels: {1}", message, String.Join(
", ", channelIds));
593 return Task.WhenAll(channelIds.Select(x =>
595 ChannelMapping channelMapping;
596 lock (mappedChannels)
597 if (!mappedChannels.TryGetValue(x, out channelMapping))
598 return Task.CompletedTask;
601 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
602 return Task.CompletedTask;
603 return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
610 List<ulong> wdChannels = null;
611 message = String.Format(CultureInfo.InvariantCulture,
"WD: {0}", message);
614 lock (mappedChannels)
618 wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
619 if (wdChannels.Count == 0)
624 wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
627 return SendMessage(message, wdChannels, cancellationToken);
632 Models.RevisionInformation revisionInformation,
633 Version byondVersion,
634 DateTimeOffset? estimatedCompletionTime,
637 bool localCommitPushed,
638 CancellationToken cancellationToken)
640 List<ulong> wdChannels;
641 lock (mappedChannels)
642 wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
644 logger.LogTrace(
"Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
646 var callbacks =
new List<Func<string, string, Task>>();
652 ChannelMapping channelMapping;
653 lock (mappedChannels)
654 if (!mappedChannels.TryGetValue(x, out channelMapping))
658 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
662 var callback = await provider.SendUpdateMessage(
665 estimatedCompletionTime,
668 channelMapping.ProviderChannelId,
671 .ConfigureAwait(false);
673 callbacks.Add(callback);
678 "Error sending deploy message to provider {0}! Exception: {1}",
679 channelMapping.ProviderId,
683 .ConfigureAwait(
false);
685 return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
689 public async Task
StartAsync(CancellationToken cancellationToken)
691 foreach (var I
in commandFactory.GenerateCommands())
692 builtinCommands.Add(I.Name.ToUpperInvariant(), I);
693 var initialChatBots = activeChatBots.ToList();
694 await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(
false);
695 await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(
false);
696 await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(
false);
697 chatHandler = MonitorMessages(handlerCts.Token);
702 public async Task
StopAsync(CancellationToken cancellationToken)
705 if (chatHandler != null)
706 await chatHandler.ConfigureAwait(
false);
707 await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(
false);
713 if (customCommandHandler == null)
714 throw new InvalidOperationException(
"RegisterCommandHandler() hasn't been called!");
717 lock (mappedChannels)
719 customCommandHandler,
720 mappedChannels.Select(y => y.Value.Channel),
724 lock (trackingContexts)
725 trackingContexts.Remove(context);
728 lock (trackingContexts)
729 trackingContexts.Add(context);
737 if (this.customCommandHandler != null)
738 throw new InvalidOperationException(
"RegisterCommandHandler() already called!");
739 this.customCommandHandler = customCommandHandler ??
throw new ArgumentNullException(nameof(customCommandHandler));
745 var provider = await RemoveProvider(connectionId,
true, cancellationToken).ConfigureAwait(
false);
746 if (provider != null)
749 await provider.Disconnect(cancellationToken).ConfigureAwait(
false);
758 public Task
HandleRestart(Version updateVersion, CancellationToken cancellationToken)
760 var message = updateVersion == null ?
"TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture,
"TGS: Updating to version {0}...", updateVersion);
761 List<ulong> wdChannels;
762 lock (mappedChannels)
763 wdChannels = mappedChannels.Select(x => x.Key).ToList();
764 return SendMessage(message, wdChannels, cancellationToken);
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a ChannelRepresentation.RealId
bool IsPrivateChannel
If this is a 1-to-1 chat channel
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
Change chat settings. If the ChatBot.Id is not currently in use, a new connection will be made instea...
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager
ChatUser User
The ChatUser who sent the Message
Use server authentication
readonly IDictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name
ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a ChatManager
Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
readonly IDictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
ulong RealId
The Providers.IProvider channel Id.
bool IsAdminChannel
If this is considered a channel for admin commands
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Represents a message recieved by a IProvider
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
For interacting with a chat service
Task chatHandler
The Task that monitors incoming chat messages
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager
For waiting asynchronously
Manage the server chat bots
readonly IList< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager
string Content
The text of the message
For managing connected chat services
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
async Task< Func< string, string, Task > > SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment to configured deployment channels.
string FriendlyName
The friendly name of the user
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Factory for built in ICommands
Handler for server restarts
string ConnectionName
The name of the connection the ChannelRepresentation belongs to
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
readonly object synchronizationLock
Used for various lock statements throughout this .
bool started
If StartAsync(CancellationToken) has been called
readonly IIOManager ioManager
The IIOManager for the ChatManager
bool Connected
If the IProvider
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
string BotMention
The string that indicates the IProvider was mentioned
async Task StopAsync(CancellationToken cancellationToken)
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
async Task StartAsync(CancellationToken cancellationToken)
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Represents a tracking of dynamic chat json files
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Interface for using filesystems
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the ChatManager
ChannelRepresentation Channel
The ChannelRepresentation the user spoke from
readonly IDictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings
Represents the lifetime of a IRestartHandler registration
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
readonly ILogger< ChatManager > logger
The ILogger for the ChatManager
bool Enabled
If the connection is enabled
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Represents a command that can be invoked by talking to chat bots
Task< IReadOnlyCollection< ChannelRepresentation > > MapChannels(IEnumerable< Api.Models.ChatChannel > channels, CancellationToken cancellationToken)
Get the ChannelRepresentations for given channels
long messagesProcessed
The number of Messages processed.
Task SetReconnectInterval(uint reconnectInterval)
Set the interval at which the provider tries to reconnect.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...