tgstation-server 5.12.7
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ChatManager.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.Extensions.Logging;
9using Newtonsoft.Json;
10using Serilog.Context;
11
19
21{
23 // TODO: Decomplexify
24#pragma warning disable CA1506
26 {
30 public const string CommonMention = "!tgs";
31
36
41
46
50 readonly ILoggerFactory loggerFactory;
51
55 readonly ILogger<ChatManager> logger;
56
60 readonly IDictionary<string, ICommand> builtinCommands;
61
65 readonly IDictionary<long, IProvider> providers;
66
70 readonly IDictionary<ulong, ChannelMapping> mappedChannels;
71
75 readonly IList<IChatTrackingContext> trackingContexts;
76
80 readonly CancellationTokenSource handlerCts;
81
85 readonly List<Models.ChatBot> activeChatBots;
86
90 readonly object synchronizationLock;
91
96
101
106
111
115 TaskCompletionSource connectionsUpdated;
116
121
126
139 IServerControl serverControl,
140 ILoggerFactory loggerFactory,
141 ILogger<ChatManager> logger,
142 IEnumerable<Models.ChatBot> initialChatBots)
143 {
144 this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
145 this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
146 ArgumentNullException.ThrowIfNull(serverControl);
147 this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
148 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
149 activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
150
151 restartRegistration = serverControl.RegisterForRestart(this);
152
153 synchronizationLock = new object();
154
155 builtinCommands = new Dictionary<string, ICommand>();
156 providers = new Dictionary<long, IProvider>();
157 mappedChannels = new Dictionary<ulong, ChannelMapping>();
158 trackingContexts = new List<IChatTrackingContext>();
159 handlerCts = new CancellationTokenSource();
160 connectionsUpdated = new TaskCompletionSource();
161
162 messageSendTask = Task.CompletedTask;
164 }
165
167 public async ValueTask DisposeAsync()
168 {
169 logger.LogTrace("Disposing...");
170 restartRegistration.Dispose();
171 handlerCts.Dispose();
172 foreach (var providerKvp in providers)
173 await providerKvp.Value.DisposeAsync();
174
175 await messageSendTask;
176 }
177
179 public async Task ChangeChannels(long connectionId, IEnumerable<Models.ChatChannel> newChannels, CancellationToken cancellationToken)
180 {
181 ArgumentNullException.ThrowIfNull(newChannels);
182
183 logger.LogTrace("ChangeChannels {connectionId}...", connectionId);
184 var provider = await RemoveProviderChannels(connectionId, false, cancellationToken);
185 if (provider == null)
186 return;
187
188 if (!provider.Connected)
189 {
190 logger.LogDebug("Cannot map channels, provider {providerId} disconnected!", connectionId);
191 return;
192 }
193
194 var results = await provider.MapChannels(newChannels, cancellationToken);
195 try
196 {
197 lock (activeChatBots)
198 {
199 var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
200 if (botToUpdate != null)
201 botToUpdate.Channels = newChannels
202 .Select(apiModel => new Models.ChatChannel
203 {
204 DiscordChannelId = apiModel.DiscordChannelId,
205 IrcChannel = apiModel.IrcChannel,
206 IsAdminChannel = apiModel.IsAdminChannel,
207 IsUpdatesChannel = apiModel.IsUpdatesChannel,
208 IsSystemChannel = apiModel.IsSystemChannel,
209 IsWatchdogChannel = apiModel.IsWatchdogChannel,
210 Tag = apiModel.Tag,
211 })
212 .ToList();
213 }
214
215 var newMappings = results.SelectMany(
216 kvp => kvp.Value.Select(
217 channelRepresentation => new ChannelMapping
218 {
219 IsWatchdogChannel = kvp.Key.IsWatchdogChannel == true,
220 IsUpdatesChannel = kvp.Key.IsUpdatesChannel == true,
221 IsAdminChannel = kvp.Key.IsAdminChannel == true,
222 IsSystemChannel = kvp.Key.IsSystemChannel == true,
223 ProviderChannelId = channelRepresentation.RealId,
224 ProviderId = connectionId,
225 Channel = channelRepresentation,
226 }));
227
228 ulong baseId;
230 {
231 baseId = channelIdCounter;
232 channelIdCounter += (ulong)results.Count;
233 }
234
235 lock (mappedChannels)
236 {
237 lock (providers)
238 if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
239 return;
240 foreach (var newMapping in newMappings)
241 {
242 var newId = baseId++;
243 logger.LogTrace("Mapping channel {connectionName}:{channelFriendlyName} as {newId}", newMapping.Channel.ConnectionName, newMapping.Channel.FriendlyName, newId);
244 mappedChannels.Add(newId, newMapping);
245 newMapping.Channel.RealId = newId;
246 }
247 }
248
249 // we only want to update contexts if everything at startup has connected once already
250 // otherwise we could send an incomplete channel set to the DMAPI, which will then spout all its queued messages into it instead of all relevant chatbots
251 // The watchdog can call this if it needs to after starting up
252 if (initialProviderConnectionsTask.IsCompleted)
253 await UpdateTrackingContexts(cancellationToken);
254 }
255 finally
256 {
257 provider.InitialMappingComplete();
258 }
259 }
260
262 public async Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken)
263 {
264 ArgumentNullException.ThrowIfNull(newSettings);
265
266 logger.LogTrace("ChangeSettings...");
267
268 Task disconnectTask;
269 IProvider provider = null;
270 lock (providers)
271 {
272 // raw settings changes forces a rebuild of the provider
273 if (providers.ContainsKey(newSettings.Id.Value))
274 disconnectTask = DeleteConnection(newSettings.Id.Value, cancellationToken);
275 else
276 disconnectTask = Task.CompletedTask;
277 if (newSettings.Enabled.Value)
278 {
279 provider = providerFactory.CreateProvider(newSettings);
280 providers.Add(newSettings.Id.Value, provider);
281 }
282 }
283
284 lock (mappedChannels)
285 foreach (var oldMappedChannelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
286 mappedChannels.Remove(oldMappedChannelId);
287
288 await disconnectTask;
289
291 {
292 // same thread shennanigans
293 var oldOne = connectionsUpdated;
294 connectionsUpdated = new TaskCompletionSource();
295 oldOne.SetResult();
296 }
297
298 var reconnectionUpdateTask = provider?.SetReconnectInterval(
299 newSettings.ReconnectionInterval.Value,
300 newSettings.Enabled.Value)
301 ?? Task.CompletedTask;
302 lock (activeChatBots)
303 {
304 var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
305 if (originalChatBot != null)
306 activeChatBots.Remove(originalChatBot);
307
308 activeChatBots.Add(new Models.ChatBot
309 {
310 Id = newSettings.Id,
311 ConnectionString = newSettings.ConnectionString,
312 Enabled = newSettings.Enabled,
313 Name = newSettings.Name,
314 ReconnectionInterval = newSettings.ReconnectionInterval,
315 Provider = newSettings.Provider,
316 Channels = newSettings.Channels,
317 });
318 }
319
320 await reconnectionUpdateTask;
321 }
322
324 public void QueueMessage(MessageContent message, IEnumerable<ulong> channelIds)
325 {
326 ArgumentNullException.ThrowIfNull(message);
327 ArgumentNullException.ThrowIfNull(channelIds);
328
329 QueueMessageInternal(message, () => channelIds, false);
330 }
331
333 public void QueueWatchdogMessage(string message)
334 {
335 ArgumentNullException.ThrowIfNull(message);
336
337 message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
338
339 if (!initialProviderConnectionsTask.IsCompleted)
340 logger.LogTrace("Waiting for initial provider connections before sending watchdog message...");
341
342 // Reimplementing QueueMessage
345 {
346 Text = message,
347 },
348 () =>
349 {
350 // so it doesn't change while we're using it
351 lock (mappedChannels)
352 return mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
353 },
354 true);
355 }
356
358 public Action<string, string> QueueDeploymentMessage(
359 Models.RevisionInformation revisionInformation,
360 Version byondVersion,
361 DateTimeOffset? estimatedCompletionTime,
362 string gitHubOwner,
363 string gitHubRepo,
364 bool localCommitPushed)
365 {
366 List<ulong> wdChannels;
367 lock (mappedChannels) // so it doesn't change while we're using it
368 wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
369
370 logger.LogTrace("Sending deployment message for RevisionInformation: {revisionInfoId}", revisionInformation.Id);
371
372 var callbacks = new List<Func<string, string, Task>>();
373
374 var task = Task.WhenAll(
375 wdChannels.Select(
376 async x =>
377 {
378 ChannelMapping channelMapping;
379 lock (mappedChannels)
380 if (!mappedChannels.TryGetValue(x, out channelMapping))
381 return;
382 IProvider provider;
383 lock (providers)
384 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
385 return;
386 try
387 {
388 var callback = await provider.SendUpdateMessage(
389 revisionInformation,
390 byondVersion,
391 estimatedCompletionTime,
392 gitHubOwner,
393 gitHubRepo,
394 channelMapping.ProviderChannelId,
395 localCommitPushed,
396 handlerCts.Token);
397
398 lock (callbacks)
399 callbacks.Add(callback);
400 }
401 catch (Exception ex)
402 {
403 logger.LogWarning(
404 ex,
405 "Error sending deploy message to provider {providerId}!",
406 channelMapping.ProviderId);
407 }
408 }));
409
410 AddMessageTask(task);
411
412 async Task CollateTasks(string errorMessage, string dreamMakerOutput)
413 {
414 await task;
415 await Task.WhenAll(
416 callbacks.Select(
417 x => x(
418 errorMessage,
419 dreamMakerOutput)));
420 }
421
422 return (errorMessage, dreamMakerOutput) => AddMessageTask(CollateTasks(errorMessage, dreamMakerOutput));
423 }
424
426 public async Task StartAsync(CancellationToken cancellationToken)
427 {
428 foreach (var tgsCommand in commandFactory.GenerateCommands())
429 builtinCommands.Add(tgsCommand.Name.ToUpperInvariant(), tgsCommand);
430 var initialChatBots = activeChatBots.ToList();
431 await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken)));
432 initialProviderConnectionsTask = InitialConnection();
433 chatHandler = MonitorMessages(handlerCts.Token);
434 }
435
437 public async Task StopAsync(CancellationToken cancellationToken)
438 {
439 handlerCts.Cancel();
440 if (chatHandler != null)
441 await chatHandler;
442 await Task.WhenAll(providers.Select(x => x.Key).Select(x => DeleteConnection(x, cancellationToken)));
443 await messageSendTask;
444 }
445
448 {
449 if (customCommandHandler == null)
450 throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
451
452 IChatTrackingContext context = null;
453 lock (mappedChannels)
454 context = new ChatTrackingContext(
455 customCommandHandler,
456 mappedChannels.Select(y => y.Value.Channel),
457 loggerFactory.CreateLogger<ChatTrackingContext>(),
458 () =>
459 {
460 lock (trackingContexts)
461 trackingContexts.Remove(context);
462 });
463
464 lock (trackingContexts)
465 trackingContexts.Add(context);
466
467 return context;
468 }
469
471 public async Task UpdateTrackingContexts(CancellationToken cancellationToken)
472 {
473 var logMessageSent = 0;
474 async Task UpdateTrackingContext(IChatTrackingContext channelSink, IEnumerable<ChannelRepresentation> channels)
475 {
476 if (Interlocked.Exchange(ref logMessageSent, 1) == 0)
477
478 await channelSink.UpdateChannels(channels, cancellationToken);
479 }
480
481 var waitingForInitialConnection = !initialProviderConnectionsTask.IsCompleted;
482 if (waitingForInitialConnection)
483 {
484 logger.LogTrace("Waiting for initial chat bot connections before updating tracking contexts...");
485 await initialProviderConnectionsTask.WithToken(cancellationToken);
486 }
487
488 List<Task> tasks;
489 lock (mappedChannels)
490 lock (trackingContexts)
491 tasks = trackingContexts.Select(x => UpdateTrackingContext(x, mappedChannels.Select(y => y.Value.Channel))).ToList();
492
493 if (waitingForInitialConnection)
494 if (tasks.Count > 0)
495 logger.LogTrace("Updating chat tracking contexts...");
496 else
497 logger.LogTrace("No chat tracking contexts to update");
498
499 await Task.WhenAll(tasks);
500 }
501
503 public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
504 {
505 if (this.customCommandHandler != null)
506 throw new InvalidOperationException("RegisterCommandHandler() already called!");
507 this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
508 }
509
511 public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
512 {
513 logger.LogTrace("DeleteConnection {connectionId}", connectionId);
514 var provider = await RemoveProviderChannels(connectionId, true, cancellationToken);
515 if (provider != null)
516 {
517 var startTime = DateTimeOffset.UtcNow;
518 try
519 {
520 await provider.Disconnect(cancellationToken);
521 }
522 catch (Exception ex)
523 {
524 logger.LogError(ex, "Error disconnecting connection {connectionId}!", connectionId);
525 }
526
527 await provider.DisposeAsync();
528 var duration = DateTimeOffset.UtcNow - startTime;
529 if (duration.TotalSeconds > 3)
530 logger.LogWarning("Disconnecting a {providerType} took {totalSeconds}s!", provider.GetType().Name, duration.TotalSeconds);
531 }
532 else
533 logger.LogTrace("DeleteConnection: ID {connectionId} doesn't exist!", connectionId);
534 }
535
537 public Task HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken)
538 {
539 var message = updateVersion == null
540 ? $"TGS: {(handlerMayDelayShutdownWithExtremelyLongRunningTasks ? "Graceful shutdown" : "Going down")}..."
541 : $"TGS: Updating to version {updateVersion}...";
542 List<ulong> wdChannels;
543 lock (mappedChannels) // so it doesn't change while we're using it
544 wdChannels = mappedChannels
545 .Where(x => !x.Value.IsSystemChannel)
546 .Select(x => x.Key)
547 .ToList();
548
549 return SendMessage(
550 wdChannels,
551 null,
553 {
554 Text = message,
555 },
556 cancellationToken);
557 }
558
566 async Task<IProvider> RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken)
567 {
568 logger.LogTrace("RemoveProviderChannels {connectionId}...", connectionId);
569 IProvider provider;
570 lock (providers)
571 {
572 if (!providers.TryGetValue(connectionId, out provider))
573 {
574 logger.LogTrace("Aborted, no such provider!");
575 return null;
576 }
577
578 if (removeProvider)
579 providers.Remove(connectionId);
580 }
581
582 Task trackingContextsUpdateTask;
583 lock (mappedChannels)
584 {
585 foreach (var mappedConnectionChannel in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
586 mappedChannels.Remove(mappedConnectionChannel);
587
588 var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
589
590 if (removeProvider)
591 lock (trackingContexts)
592 trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
593 else
594 trackingContextsUpdateTask = Task.CompletedTask;
595 }
596
597 await trackingContextsUpdateTask;
598
599 return provider;
600 }
601
608 async Task RemapProvider(IProvider provider, CancellationToken cancellationToken)
609 {
610 logger.LogTrace("Remapping channels for provider reconnection...");
611 IEnumerable<Models.ChatChannel> channelsToMap;
612 long providerId;
613 lock (providers)
614 providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
615
616 lock (activeChatBots)
617 channelsToMap = activeChatBots.FirstOrDefault(x => x.Id == providerId)?.Channels;
618
619 if (channelsToMap?.Any() ?? false)
620 await ChangeChannels(providerId, channelsToMap, cancellationToken);
621 }
622
631#pragma warning disable CA1502
632 async Task ProcessMessage(IProvider provider, Message message, bool recursed, CancellationToken cancellationToken)
633#pragma warning restore CA1502
634 {
635 if (!provider.Connected)
636 {
637 logger.LogTrace("Abort message processing because provider is disconnected!");
638 return;
639 }
640
641 // provider reconnected, remap channels.
642 if (message == null)
643 {
644 await RemapProvider(provider, cancellationToken);
645 return;
646 }
647
648 // map the channel if it's private and we haven't seen it
649 var providerChannelId = message.User.Channel.RealId;
650 KeyValuePair<ulong, ChannelMapping>? mappedChannel;
651 long providerId;
652 bool hasChannelZero;
653 lock (providers)
654 {
655 // important, otherwise we could end up processing during shutdown
656 cancellationToken.ThrowIfCancellationRequested();
657
658 var providerIdNullable = providers
659 .Where(x => x.Value == provider)
660 .Select(x => (long?)x.Key)
661 .FirstOrDefault();
662
663 if (!providerIdNullable.HasValue)
664 {
665 // possible to have a message queued and then the provider immediately disconnects
666 logger.LogDebug("Unable to process command \"{command}\" due to provider disconnecting", message.Content);
667 return;
668 }
669
670 providerId = providerIdNullable.Value;
671 mappedChannel = mappedChannels
672 .Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == providerChannelId)
673 .Select(x => (KeyValuePair<ulong, ChannelMapping>?)x)
674 .FirstOrDefault();
675 hasChannelZero = mappedChannels
676 .Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == 0)
677 .Any();
678 }
679
680 if (!recursed && !mappedChannel.HasValue && !message.User.Channel.IsPrivateChannel && hasChannelZero)
681 {
682 logger.LogInformation("Receieved message from unmapped channel whose provider contains ID 0. Remapping...");
683 await RemapProvider(provider, cancellationToken);
684 logger.LogTrace("Resume processing original message...");
685 await ProcessMessage(provider, message, true, cancellationToken);
686 return;
687 }
688
689 if (message.User.Channel.IsPrivateChannel)
690 lock (mappedChannels)
691 if (!mappedChannel.HasValue)
692 {
693 ulong newId;
694 lock (synchronizationLock)
695 newId = channelIdCounter++;
696 logger.LogTrace(
697 "Mapping private channel {connectionName}:{channelFriendlyName} as {newId}",
699 message.User.FriendlyName,
700 newId);
701 mappedChannels.Add(newId, new ChannelMapping
702 {
703 ProviderChannelId = message.User.Channel.RealId,
704 ProviderId = providerId,
705 Channel = message.User.Channel,
706 });
707
708 logger.LogTrace(
709 "Mapping DM {connectionName}:{userId} ({userFriendlyName}) as {newId}",
711 message.User.RealId,
712 message.User.FriendlyName,
713 newId);
714 message.User.Channel.RealId = newId;
715 }
716 else
717 message.User.Channel.RealId = mappedChannel.Value.Key;
718 else
719 {
720 if (!mappedChannel.HasValue)
721 {
722 logger.LogError(
723 "Error mapping message: Provider ID: {providerId}, Channel Real ID: {realId}",
724 providerId,
725 message.User.Channel.RealId);
726 logger.LogTrace("message: {messageJson}", JsonConvert.SerializeObject(message));
727 lock (mappedChannels)
728 logger.LogTrace("mappedChannels: {mappedChannelsJson}", JsonConvert.SerializeObject(mappedChannels));
729 await SendMessage(
730 new List<ulong>
731 {
732 message.User.Channel.RealId,
733 },
734 message,
736 {
737 Text = "TGS: Processing error, check logs!",
738 },
739 cancellationToken);
740 return;
741 }
742
743 var mappingChannelRepresentation = mappedChannel.Value.Value.Channel;
744
745 message.User.Channel.Id = mappingChannelRepresentation.Id;
746 message.User.Channel.Tag = mappingChannelRepresentation.Tag;
747 message.User.Channel.IsAdminChannel = mappingChannelRepresentation.IsAdminChannel;
748 }
749
750 var trimmedMessage = message.Content.Trim();
751 if (trimmedMessage.Length == 0)
752 return;
753
754 var splits = new List<string>(trimmedMessage.Split(' ', StringSplitOptions.RemoveEmptyEntries));
755 var address = splits[0];
756 if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
757 address = address[0..^1];
758
759 address = address.ToUpperInvariant();
760
761 var addressed =
762 address == CommonMention.ToUpperInvariant()
763 || address == provider.BotMention.ToUpperInvariant();
764
765 // no mention
766 if (!addressed && !message.User.Channel.IsPrivateChannel)
767 return;
768
769 logger.LogTrace(
770 "Start processing command: {message}. User (True provider Id): {profiderId}",
771 message.Content,
772 JsonConvert.SerializeObject(message.User));
773 try
774 {
775 if (addressed)
776 splits.RemoveAt(0);
777
778 if (splits.Count == 0)
779 {
780 // just a mention
781 await SendMessage(
782 new List<ulong>
783 {
784 message.User.Channel.RealId,
785 },
786 message,
788 {
789 Text = "Hi!",
790 },
791 cancellationToken);
792 return;
793 }
794
795 var command = splits[0].ToUpperInvariant();
796 splits.RemoveAt(0);
797 var arguments = String.Join(" ", splits);
798
799 ICommand GetCommand(string commandName)
800 {
801 if (!builtinCommands.TryGetValue(commandName, out var handler))
802 {
803 handler = trackingContexts
804 .Where(x => x.CustomCommands != null)
805 .SelectMany(x => x.CustomCommands)
806 .Where(x => x.Name.ToUpperInvariant() == commandName)
807 .FirstOrDefault();
808 }
809
810 return handler;
811 }
812
813 const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
814
815 if (command == "HELP" || command == "?")
816 {
817 string helpText;
818 if (splits.Count == 0)
819 {
820 var allCommands = builtinCommands.Select(x => x.Value).ToList();
821 allCommands.AddRange(
822 trackingContexts
823 .Where(x => x.CustomCommands != null)
824 .SelectMany(
825 x => x.CustomCommands));
826 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)));
827 }
828 else
829 {
830 var helpHandler = GetCommand(splits[0].ToUpperInvariant());
831 if (helpHandler != default)
832 helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
833 else
834 helpText = UnknownCommandMessage;
835 }
836
837 await SendMessage(
838 new List<ulong> { message.User.Channel.RealId },
839 message,
841 {
842 Text = helpText,
843 },
844 cancellationToken);
845 return;
846 }
847
848 var commandHandler = GetCommand(command);
849
850 if (commandHandler == default)
851 {
852 await SendMessage(
853 new List<ulong> { message.User.Channel.RealId },
854 message,
856 {
857 Text = UnknownCommandMessage,
858 },
859 cancellationToken);
860 return;
861 }
862
863 if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
864 {
865 await SendMessage(
866 new List<ulong> { message.User.Channel.RealId },
867 message,
869 {
870 Text = "Use this command in an admin channel!",
871 },
872 cancellationToken);
873 return;
874 }
875
876 var result = await commandHandler.Invoke(arguments, message.User, cancellationToken);
877 if (result != null)
878 await SendMessage(new List<ulong> { message.User.Channel.RealId }, message, result, cancellationToken);
879 }
880 catch (OperationCanceledException ex)
881 {
882 logger.LogTrace(ex, "Command processing canceled!");
883 }
884 catch (Exception e)
885 {
886 // error bc custom commands should reply about why it failed
887 logger.LogError(e, "Error processing chat command");
888 await SendMessage(
889 new List<ulong> { message.User.Channel.RealId },
890 message,
892 {
893 Text = "TGS: Internal error processing command! Check server logs!",
894 },
895 cancellationToken);
896 }
897 finally
898 {
899 logger.LogTrace("Done processing command.");
900 }
901 }
902
908 async Task MonitorMessages(CancellationToken cancellationToken)
909 {
910 logger.LogTrace("Starting processing loop...");
911 var messageTasks = new Dictionary<IProvider, Task<Message>>();
912 Task activeProcessingTask = Task.CompletedTask;
913 try
914 {
915 Task updatedTask = null;
916 while (!cancellationToken.IsCancellationRequested)
917 {
918 if (updatedTask?.IsCompleted != false)
919 lock (synchronizationLock)
920 updatedTask = connectionsUpdated.Task;
921
922 // prune disconnected providers
923 foreach (var disposedProviderMessageTaskKvp in messageTasks.Where(x => x.Key.Disposed).ToList())
924 messageTasks.Remove(disposedProviderMessageTaskKvp.Key);
925
926 // add new ones
927 lock (providers)
928 foreach (var providerKvp in providers)
929 if (!messageTasks.ContainsKey(providerKvp.Value))
930 messageTasks.Add(providerKvp.Value, providerKvp.Value.NextMessage(cancellationToken));
931
932 if (messageTasks.Count == 0)
933 {
934 logger.LogTrace("No providers active, pausing messsage monitoring...");
935 await updatedTask.WithToken(cancellationToken);
936 logger.LogTrace("Resuming message monitoring...");
937 continue;
938 }
939
940 // wait for a message
941 await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value)));
942
943 // process completed ones
944 foreach (var completedMessageTaskKvp in messageTasks.Where(x => x.Value.IsCompleted).ToList())
945 {
946 var provider = completedMessageTaskKvp.Key;
947 messageTasks.Remove(provider);
948
949 if (provider.Disposed) // valid to receive one, but don't process it
950 continue;
951
952 var message = await completedMessageTaskKvp.Value;
953 var messageNumber = Interlocked.Increment(ref messagesProcessed);
954
955 async Task WrapProcessMessage()
956 {
957 var localActiveProcessingTask = activeProcessingTask;
958 using (LogContext.PushProperty(SerilogContextHelper.ChatMessageIterationContextProperty, messageNumber))
959 try
960 {
961 await ProcessMessage(provider, message, false, cancellationToken);
962 }
963 catch (Exception ex)
964 {
965 logger.LogError(ex, "Error processing message {messageNumber}!", messageNumber);
966 }
967
968 await localActiveProcessingTask;
969 }
970
971 activeProcessingTask = WrapProcessMessage();
972 }
973 }
974 }
975 catch (OperationCanceledException ex)
976 {
977 logger.LogTrace(ex, "Message processing loop cancelled!");
978 }
979 catch (Exception e)
980 {
981 logger.LogError(e, "Message loop crashed!");
982 }
983 finally
984 {
985 await activeProcessingTask;
986 }
987
988 logger.LogTrace("Leaving message processing loop");
989 }
990
999 Task SendMessage(IEnumerable<ulong> channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken)
1000 {
1001 channelIds = channelIds.ToList();
1002
1003 logger.LogTrace(
1004 "Chat send \"{message}\"{embed} to channels: [{channelIdsCommaSeperated}]",
1005 message.Text,
1006 message.Embed != null ? " (with embed)" : String.Empty,
1007 String.Join(", ", channelIds));
1008
1009 if (!channelIds.Any())
1010 return Task.CompletedTask;
1011
1012 return Task.WhenAll(
1013 channelIds.Select(x =>
1014 {
1015 ChannelMapping channelMapping;
1016 lock (mappedChannels)
1017 if (!mappedChannels.TryGetValue(x, out channelMapping))
1018 return Task.CompletedTask;
1019 IProvider provider;
1020 lock (providers)
1021 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
1022 return Task.CompletedTask;
1023 return provider.SendMessage(replyTo, message, channelMapping.ProviderChannelId, cancellationToken);
1024 }));
1025 }
1026
1032 {
1033 await Task.WhenAll(providers.Select(x => x.Value.InitialConnectionJob));
1034 logger.LogTrace("Initial provider connection task completed");
1035 }
1036
1041 void AddMessageTask(Task task)
1042 {
1043 async Task Wrap(Task originalTask)
1044 {
1045 await originalTask;
1046 try
1047 {
1048 await task;
1049 }
1050 catch (OperationCanceledException ex)
1051 {
1052 logger.LogDebug(ex, "Async chat message cancelled!");
1053 }
1054 catch (Exception ex)
1055 {
1056 logger.LogError(ex, "Error in asynchronous chat message!");
1057 }
1058 }
1059
1060 lock (handlerCts)
1061 messageSendTask = Wrap(messageSendTask);
1062 }
1063
1070 void QueueMessageInternal(MessageContent message, Func<IEnumerable<ulong>> channelIdsFactory, bool waitForConnections)
1071 {
1072 async Task SendMessageTask()
1073 {
1074 var cancellationToken = handlerCts.Token;
1075 if (waitForConnections)
1076 await initialProviderConnectionsTask.WithToken(cancellationToken);
1077
1078 await SendMessage(
1079 channelIdsFactory(),
1080 null,
1081 message,
1082 cancellationToken);
1083 }
1084
1085 AddMessageTask(SendMessageTask());
1086 }
1087 }
1088}
Represents a mapping of a ChannelRepresentation.RealId.
string ConnectionName
The name of the connection the ChannelRepresentation belongs to.
bool IsAdminChannel
If this is considered a channel for admin commands.
async Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken)
Change chat settings. If the Api.Models.EntityId.Id is not currently in use, a new connection will be...
Definition: ChatManager.cs:262
async Task ProcessMessage(IProvider provider, Message message, bool recursed, CancellationToken cancellationToken)
Processes a message .
Definition: ChatManager.cs:632
const string CommonMention
The common bot mention.
Definition: ChatManager.cs:30
long messagesProcessed
The number of Messages processed.
Definition: ChatManager.cs:125
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager.
Definition: ChatManager.cs:35
readonly object synchronizationLock
Used for various lock statements throughout this class.
Definition: ChatManager.cs:90
ChatManager(IProviderFactory providerFactory, ICommandFactory commandFactory, IServerControl serverControl, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Initializes a new instance of the ChatManager class.
Definition: ChatManager.cs:136
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Models.ChatChannel>,...
Definition: ChatManager.cs:95
void QueueMessageInternal(MessageContent message, Func< IEnumerable< ulong > > channelIdsFactory, bool waitForConnections)
Adds a given message to the send queue.
readonly IDictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBotSettings Api.Models.EntityId.Id.
Definition: ChatManager.cs:65
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager.
Definition: ChatManager.cs:85
async Task InitialConnection()
Aggregate all IProvider.InitialConnectionJobs into one <sse cref="Task">.
Task initialProviderConnectionsTask
A Task that represents the IProviders initial connection.
Definition: ChatManager.cs:105
Task SendMessage(IEnumerable< ulong > channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken)
Asynchronously send a given message to a set of channelIds .
Definition: ChatManager.cs:999
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages.
Definition: ChatManager.cs:908
readonly IDictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings.
Definition: ChatManager.cs:70
Task HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken)
Handle a restart of the server. A Task representing the running operation.
Definition: ChatManager.cs:537
void QueueMessage(MessageContent message, IEnumerable< ulong > channelIds)
Queue a chat message to a given set of channelIds .
Definition: ChatManager.cs:324
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager.
Definition: ChatManager.cs:50
Task messageSendTask
A Task that represents all sent messages.
Definition: ChatManager.cs:110
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds.
Definition: ChatManager.cs:120
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager.
Definition: ChatManager.cs:40
async Task RemapProvider(IProvider provider, CancellationToken cancellationToken)
Remap the channels for a given provider .
Definition: ChatManager.cs:608
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations. A new IChatTrackingContext.
Definition: ChatManager.cs:447
void AddMessageTask(Task task)
Adds a given task to messageSendTask.
readonly ILogger< ChatManager > logger
The ILogger for the ChatManager.
Definition: ChatManager.cs:55
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager.
Definition: ChatManager.cs:45
void QueueWatchdogMessage(string message)
Queue a chat message to configured watchdog channels.
Definition: ChatManager.cs:333
Task chatHandler
The Task that monitors incoming chat messages.
Definition: ChatManager.cs:100
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler.
Definition: ChatManager.cs:80
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:426
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use.
Definition: ChatManager.cs:503
TaskCompletionSource connectionsUpdated
The TaskCompletionSource that completes when ChatBotSettingss change.
Definition: ChatManager.cs:115
readonly IDictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name.
Definition: ChatManager.cs:60
readonly IList< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager.
Definition: ChatManager.cs:75
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection. A Task representing the running operation.
Definition: ChatManager.cs:511
async Task ChangeChannels(long connectionId, IEnumerable< Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels. A Task representing the running operation.
Definition: ChatManager.cs:179
async Task< IProvider > RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken)
Remove a IProvider from mappedChannels optionally removing the provider itself from providers and upd...
Definition: ChatManager.cs:566
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:437
async Task UpdateTrackingContexts(CancellationToken cancellationToken)
Force an update with the active channels on all active IChatTrackingContexts. A Task representing the...
Definition: ChatManager.cs:471
Action< string, string > QueueDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed)
Send the message for a deployment to configured deployment channels. An Action<T1,...
Definition: ChatManager.cs:358
string FriendlyName
The friendly name of the user.
Definition: ChatUser.cs:31
ulong RealId
The internal user id.
Definition: ChatUser.cs:23
ChannelRepresentation Channel
The ChannelRepresentation the user spoke from.
Definition: ChatUser.cs:41
Represents a message recieved by a IProvider.
Definition: Message.cs:7
ChatUser User
The ChatUser who sent the Message.
Definition: Message.cs:16
Represents a message to send to a chat provider.
Helpers for manipulating the Serilog.Context.LogContext.
const string ChatMessageIterationContextProperty
The Serilog.Context.LogContext property name for the ID of the chat message currently being processed...
Represents a command that can be invoked by talking to chat bots.
Definition: ICommand.cs:12
Task UpdateChannels(IEnumerable< ChannelRepresentation > newChannels, CancellationToken cancellationToken)
Called when newChannels are set.
For managing connected chat services.
Definition: IChatManager.cs:15
Represents a tracking of dynamic chat json files.
Handles Commands.ICommands that map to those defined in a IChatTrackingContext.
IProvider CreateProvider(ChatBot settings)
Create a IProvider.
For interacting with a chat service.
Definition: IProvider.cs:15
string BotMention
The string that indicates the IProvider was mentioned.
Definition: IProvider.cs:29
bool Connected
If the IProvider is currently connected.
Definition: IProvider.cs:19
Represents the lifetime of a IRestartHandler registration.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart.