tgstation-server  4.4.0
The /tg/station 13 server suite
ChatManager.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
14 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components.Chat
17 {
19  // TODO: Decomplexify
20  #pragma warning disable CA1506
22  {
23  const string CommonMention = "!tgs";
24 
29 
34 
39 
44 
49 
53  readonly ILoggerFactory loggerFactory;
54 
58  readonly ILogger<ChatManager> logger;
59 
63  readonly IDictionary<string, ICommand> builtinCommands;
64 
68  readonly IDictionary<long, IProvider> providers;
69 
73  readonly IDictionary<ulong, ChannelMapping> mappedChannels;
74 
78  readonly IList<IChatTrackingContext> trackingContexts;
79 
83  readonly CancellationTokenSource handlerCts;
84 
88  readonly List<Models.ChatBot> activeChatBots;
89 
93  readonly object synchronizationLock;
94 
99 
104 
108  TaskCompletionSource<object> connectionsUpdated;
109 
114 
119 
123  bool started;
124 
136  public ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<ChatManager> logger, IEnumerable<Models.ChatBot> initialChatBots)
137  {
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));
147 
148  restartRegistration = serverControl.RegisterForRestart(this);
149 
150  synchronizationLock = new object();
151 
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;
159  }
160 
162  public void Dispose()
163  {
164  logger.LogTrace("Disposing...");
165  restartRegistration.Dispose();
166  handlerCts.Dispose();
167  foreach (var I in providers)
168  I.Value.Dispose();
169  }
170 
178  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
179  {
180  logger.LogTrace("RemoveProvider {0}...", connectionId);
181  IProvider provider;
182  lock (providers)
183  if (!providers.TryGetValue(connectionId, out provider))
184  {
185  logger.LogTrace("Aborted, no such provider!");
186  return null;
187  }
188 
189  Task trackingContextsUpdateTask;
190  lock (mappedChannels)
191  {
192  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
193  mappedChannels.Remove(I);
194 
195  var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
196 
197  if (updateTrackings)
198  lock (trackingContexts)
199  trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
200  else
201  trackingContextsUpdateTask = Task.CompletedTask;
202  }
203 
204  await trackingContextsUpdateTask.ConfigureAwait(false);
205 
206  return provider;
207  }
208 
216  #pragma warning disable CA1502
217  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
218  #pragma warning restore CA1502
219  {
220  // provider reconnected, remap channels.
221  if (message == null)
222  {
223  logger.LogTrace("Remapping channels for provider reconnection...");
224  IEnumerable<Api.Models.ChatChannel> channelsToMap;
225  lock (activeChatBots)
226  channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
227 
228  if (channelsToMap?.Any() ?? false)
229  {
230  long providerId;
231  lock (providers)
232  providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
233  await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
234  }
235 
236  return;
237  }
238 
239  // map the channel if it's private and we haven't seen it
240  lock (providers)
241  {
242  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
243  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
244  if (message.User.Channel.IsPrivateChannel)
245  lock (mappedChannels)
246  {
247  if (!provider.Connected)
248  return;
249  if (!enumerable.Any())
250  {
251  ulong newId;
252  lock (synchronizationLock)
253  newId = channelIdCounter++;
254  logger.LogTrace(
255  "Mapping private channel {0}:{1} as {2}",
256  message.User.Channel.ConnectionName,
257  message.User.FriendlyName,
258  newId);
259  mappedChannels.Add(newId, new ChannelMapping
260  {
261  IsWatchdogChannel = false,
262  ProviderChannelId = message.User.Channel.RealId,
263  ProviderId = providerId,
264  Channel = message.User.Channel
265  });
266  message.User.Channel.RealId = newId;
267  }
268  else
269  message.User.Channel.RealId = enumerable.First().Key;
270  }
271  else
272  {
273  // need to add tag and isAdminChannel
274  var mapping = enumerable.First().Value;
275  message.User.Channel.Id = mapping.Channel.Id;
276  message.User.Channel.Tag = mapping.Channel.Tag;
277  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
278  }
279  }
280 
281  var splits = new List<string>(message.Content.Trim().Split(' '));
282  var address = splits[0];
283  if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
284  address = address[0..^1];
285 
286  address = address.ToUpperInvariant();
287 
288  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
289 
290  // no mention
291  if (!addressed && !message.User.Channel.IsPrivateChannel)
292  return;
293 
294  logger.LogTrace(
295  "Start processing command: {0}. User (True provider Id): {1}",
296  message.Content,
297  JsonConvert.SerializeObject(message.User));
298  try
299  {
300  if (addressed)
301  splits.RemoveAt(0);
302 
303  if (splits.Count == 0)
304  {
305  // just a mention
306  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
307  return;
308  }
309 
310  var command = splits[0].ToUpperInvariant();
311  splits.RemoveAt(0);
312  var arguments = String.Join(" ", splits);
313 
314  ICommand GetCommand(string commandName)
315  {
316  if (!builtinCommands.TryGetValue(commandName, out var handler))
317  {
318  handler = trackingContexts
319  .Where(x => x.CustomCommands != null)
320  .SelectMany(x => x.CustomCommands)
321  .Where(x => x.Name.ToUpperInvariant() == commandName)
322  .FirstOrDefault();
323  }
324 
325  return handler;
326  }
327 
328  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
329 
330  if (command == "HELP" || command == "?")
331  {
332  string helpText;
333  if (splits.Count == 0)
334  {
335  var allCommands = builtinCommands.Select(x => x.Value).ToList();
336  allCommands.AddRange(
337  trackingContexts
338  .Where(x => x.CustomCommands != null)
339  .SelectMany(
340  x => x.CustomCommands));
341  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)));
342  }
343  else
344  {
345  var helpHandler = GetCommand(splits[0].ToUpperInvariant());
346  if (helpHandler != default)
347  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
348  else
349  helpText = UnknownCommandMessage;
350  }
351 
352  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
353  return;
354  }
355 
356  var commandHandler = GetCommand(command);
357 
358  if (commandHandler == default)
359  {
360  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
361  return;
362  }
363 
364  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
365  {
366  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
367  return;
368  }
369 
370  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
371  if (result != null)
372  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
373  }
374  catch (OperationCanceledException)
375  {
376  logger.LogTrace("Command processing canceled!");
377  throw;
378  }
379  catch (Exception e)
380  {
381  // error bc custom commands should reply about why it failed
382  logger.LogError("Error processing chat command: {0}", e);
383  await SendMessage(
384  "TGS: Internal error processing command! Check server logs!",
385  new List<ulong> { message.User.Channel.RealId },
386  cancellationToken)
387  .ConfigureAwait(false);
388  }
389  finally
390  {
391  logger.LogTrace("Done processing command.");
392  }
393  }
394 
400  async Task MonitorMessages(CancellationToken cancellationToken)
401  {
402  logger.LogTrace("Starting processing loop...");
403  var messageTasks = new Dictionary<IProvider, Task<Message>>();
404  try
405  {
406  while (!cancellationToken.IsCancellationRequested)
407  {
408  // prune disconnected providers
409  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
410  messageTasks.Remove(I.Key);
411 
412  // add new ones
413  Task updatedTask;
414  lock (synchronizationLock)
415  updatedTask = connectionsUpdated.Task;
416  lock (providers)
417  foreach (var I in providers)
418  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
419  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
420 
421  if (messageTasks.Count == 0)
422  {
423  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
424  continue;
425  }
426 
427  // wait for a message
428  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
429 
430  // process completed ones
431  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
432  {
433  var message = await I.Value.ConfigureAwait(false);
434  var messageNumber = Interlocked.Increment(ref messagesProcessed);
435  using (LogContext.PushProperty("ChatMessage", messageNumber))
436  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
437  messageTasks.Remove(I.Key);
438  }
439  }
440  }
441  catch (OperationCanceledException)
442  {
443  logger.LogTrace("Message processing loop cancelled!");
444  }
445  catch (Exception e)
446  {
447  logger.LogError("Message loop crashed! Exception: {0}", e);
448  }
449 
450  logger.LogTrace("Leaving message processing loop");
451  }
452 
454  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
455  {
456  if (newChannels == null)
457  throw new ArgumentNullException(nameof(newChannels));
458 
459  logger.LogTrace("ChangeChannels {0}...", connectionId);
460  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
461  if (provider == null)
462  return;
463  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
464  lock (activeChatBots)
465  {
466  var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
467  if (botToUpdate != null)
468  botToUpdate.Channels = newChannels
469  .Select(apiModel => new Models.ChatChannel
470  {
471  DiscordChannelId = apiModel.DiscordChannelId,
472  IrcChannel = apiModel.IrcChannel,
473  IsAdminChannel = apiModel.IsAdminChannel,
474  IsUpdatesChannel = apiModel.IsUpdatesChannel,
475  IsWatchdogChannel = apiModel.IsWatchdogChannel,
476  Tag = apiModel.Tag
477  })
478  .ToList();
479  }
480 
481  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
482  {
483  IsWatchdogChannel = x.IsWatchdogChannel == true,
484  IsUpdatesChannel = x.IsUpdatesChannel == true,
485  IsAdminChannel = x.IsAdminChannel == true,
486  ProviderChannelId = y.RealId,
487  ProviderId = connectionId,
488  Channel = y
489  });
490 
491  ulong baseId;
492  lock (synchronizationLock)
493  {
494  baseId = channelIdCounter;
495  channelIdCounter += (ulong)results.Count;
496  }
497 
498  Task trackingContextUpdateTask;
499  lock (mappedChannels)
500  {
501  lock (providers)
502  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
503  return;
504  foreach (var I in mappings)
505  {
506  var newId = baseId++;
507  logger.LogTrace("Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
508  mappedChannels.Add(newId, I);
509  I.Channel.RealId = newId;
510  }
511 
512  lock (trackingContexts)
513  trackingContextUpdateTask = Task.WhenAll(
514  trackingContexts.Select(
515  x => x.UpdateChannels(
516  mappedChannels.Select(y => y.Value.Channel).ToList(),
517  cancellationToken)));
518  }
519 
520  await trackingContextUpdateTask.ConfigureAwait(false);
521  }
522 
524  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
525  {
526  if (newSettings == null)
527  throw new ArgumentNullException(nameof(newSettings));
528 
529  logger.LogTrace("ChangeSettings...");
530  IProvider provider;
531 
532  async Task DisconnectProvider(IProvider p)
533  {
534  try
535  {
536  await p.Disconnect(cancellationToken).ConfigureAwait(false);
537  }
538  finally
539  {
540  p.Dispose();
541  }
542  }
543 
544  Task disconnectTask;
545  lock (providers)
546  {
547  // raw settings changes forces a rebuild of the provider
548  if (providers.TryGetValue(newSettings.Id, out provider))
549  {
550  providers.Remove(newSettings.Id);
551  disconnectTask = DisconnectProvider(provider);
552  }
553  else
554  disconnectTask = Task.CompletedTask;
555  if (newSettings.Enabled.Value)
556  {
557  provider = providerFactory.CreateProvider(newSettings);
558  providers.Add(newSettings.Id, provider);
559  }
560  }
561 
562  lock (mappedChannels)
563  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
564  mappedChannels.Remove(I);
565 
566  await disconnectTask.ConfigureAwait(false);
567 
568  if (started)
569  {
570  if (newSettings.Enabled.Value)
571  await provider.Connect(cancellationToken).ConfigureAwait(false);
572  lock (synchronizationLock)
573  {
574  // same thread shennanigans
575  var oldOne = connectionsUpdated;
576  connectionsUpdated = new TaskCompletionSource<object>();
577  oldOne.SetResult(null);
578  }
579  }
580 
581  Task reconnectionUpdateTask = Task.CompletedTask;
582  lock (activeChatBots)
583  {
584  var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
585  if (originalChatBot != null)
586  {
587  if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
588  reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
589 
590  activeChatBots.Remove(originalChatBot);
591  }
592 
593  activeChatBots.Add(new Models.ChatBot
594  {
595  Id = newSettings.Id,
596  ConnectionString = newSettings.ConnectionString,
597  Enabled = newSettings.Enabled,
598  Name = newSettings.Name,
599  ReconnectionInterval = newSettings.ReconnectionInterval,
600  Provider = newSettings.Provider
601  });
602  }
603 
604  await reconnectionUpdateTask.ConfigureAwait(false);
605  }
606 
608  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
609  {
610  if (message == null)
611  throw new ArgumentNullException(nameof(message));
612  if (channelIds == null)
613  throw new ArgumentNullException(nameof(channelIds));
614 
615  logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds));
616 
617  return Task.WhenAll(channelIds.Select(x =>
618  {
619  ChannelMapping channelMapping;
620  lock (mappedChannels)
621  if (!mappedChannels.TryGetValue(x, out channelMapping))
622  return Task.CompletedTask;
623  IProvider provider;
624  lock (providers)
625  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
626  return Task.CompletedTask;
627  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
628  }));
629  }
630 
632  public Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
633  {
634  List<ulong> wdChannels = null;
635  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
636 
637  // so it doesn't change while we're using it
638  lock (mappedChannels)
639  {
640  if (adminOnly)
641  {
642  wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
643  if (wdChannels.Count == 0)
644  adminOnly = false;
645  }
646 
647  if (!adminOnly)
648  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
649  }
650 
651  return SendMessage(message, wdChannels, cancellationToken);
652  }
653 
655  public async Task<Func<string, string, Task>> SendDeploymentMessage(
656  Models.RevisionInformation revisionInformation,
657  Version byondVersion,
658  DateTimeOffset? estimatedCompletionTime,
659  string gitHubOwner,
660  string gitHubRepo,
661  bool localCommitPushed,
662  CancellationToken cancellationToken)
663  {
664  List<ulong> wdChannels;
665  lock (mappedChannels) // so it doesn't change while we're using it
666  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
667 
668  logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
669 
670  var callbacks = new List<Func<string, string, Task>>();
671 
672  await Task.WhenAll(
673  wdChannels.Select(
674  async x =>
675  {
676  ChannelMapping channelMapping;
677  lock (mappedChannels)
678  if (!mappedChannels.TryGetValue(x, out channelMapping))
679  return;
680  IProvider provider;
681  lock (providers)
682  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
683  return;
684  try
685  {
686  var callback = await provider.SendUpdateMessage(
687  revisionInformation,
688  byondVersion,
689  estimatedCompletionTime,
690  gitHubOwner,
691  gitHubRepo,
692  channelMapping.ProviderChannelId,
693  localCommitPushed,
694  cancellationToken)
695  .ConfigureAwait(false);
696 
697  callbacks.Add(callback);
698  }
699  catch (Exception ex)
700  {
701  logger.LogWarning(
702  "Error sending deploy message to provider {0}! Exception: {1}",
703  channelMapping.ProviderId,
704  ex);
705  }
706  }))
707  .ConfigureAwait(false);
708 
709  return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
710  }
711 
713  public async Task StartAsync(CancellationToken cancellationToken)
714  {
715  foreach (var I in commandFactory.GenerateCommands())
716  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
717  var initialChatBots = activeChatBots.ToList();
718  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
719  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
720  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
721  chatHandler = MonitorMessages(handlerCts.Token);
722  started = true;
723  }
724 
726  public async Task StopAsync(CancellationToken cancellationToken)
727  {
728  handlerCts.Cancel();
729  if (chatHandler != null)
730  await chatHandler.ConfigureAwait(false);
731  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
732  }
733 
736  {
737  if (customCommandHandler == null)
738  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
739 
740  IChatTrackingContext context = null;
741  lock (mappedChannels)
742  context = new ChatTrackingContext(
743  customCommandHandler,
744  mappedChannels.Select(y => y.Value.Channel),
745  loggerFactory.CreateLogger<ChatTrackingContext>(),
746  () =>
747  {
748  lock (trackingContexts)
749  trackingContexts.Remove(context);
750  });
751 
752  lock (trackingContexts)
753  trackingContexts.Add(context);
754 
755  return context;
756  }
757 
759  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
760  {
761  if (this.customCommandHandler != null)
762  throw new InvalidOperationException("RegisterCommandHandler() already called!");
763  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
764  }
765 
767  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
768  {
769  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
770  if (provider != null)
771  try
772  {
773  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
774  }
775  finally
776  {
777  provider.Dispose();
778  }
779  }
780 
782  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
783  {
784  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
785  List<ulong> wdChannels;
786  lock (mappedChannels) // so it doesn't change while we're using it
787  wdChannels = mappedChannels.Select(x => x.Key).ToList();
788  return SendMessage(message, wdChannels, cancellationToken);
789  }
790  }
791 }
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a ChannelRepresentation.RealId
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds
Definition: ChatManager.cs:113
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: ChatManager.cs:632
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...
Definition: ChatManager.cs:524
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager
Definition: ChatManager.cs:38
ChatUser User
The ChatUser who sent the Message
Definition: Message.cs:16
readonly IDictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name
Definition: ChatManager.cs:63
ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a ChatManager
Definition: ChatManager.cs:136
Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Permanently stops the reconnection timer.
readonly IDictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: ChatManager.cs:68
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...
Definition: ChatManager.cs:178
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: ChatManager.cs:782
Represents a message recieved by a IProvider
Definition: Message.cs:6
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: ChatManager.cs:767
For interacting with a chat service
Definition: IProvider.cs:12
Task chatHandler
The Task that monitors incoming chat messages
Definition: ChatManager.cs:103
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager
Definition: ChatManager.cs:43
Manage the server chat bots
Definition: ChatBot.cs:9
readonly IList< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager
Definition: ChatManager.cs:78
For managing connected chat services
Definition: IChatManager.cs:13
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.
Definition: ChatManager.cs:655
string FriendlyName
The friendly name of the user
Definition: ChatUser.cs:30
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Definition: ChatManager.cs:735
string ConnectionName
The name of the connection the ChannelRepresentation belongs to
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
Definition: ChatManager.cs:83
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Definition: ChatManager.cs:98
Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
readonly object synchronizationLock
Used for various lock statements throughout this .
Definition: ChatManager.cs:93
bool started
If StartAsync(CancellationToken) has been called
Definition: ChatManager.cs:123
readonly IIOManager ioManager
The IIOManager for the ChatManager
Definition: ChatManager.cs:33
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: ChatManager.cs:454
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:22
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:726
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: ChatManager.cs:759
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: ChatManager.cs:108
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:713
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager
Definition: ChatManager.cs:88
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
Definition: ChatManager.cs:608
Represents a tracking of dynamic chat json files
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager
Definition: ChatManager.cs:53
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Definition: ChatManager.cs:217
Interface for using filesystems
Definition: IIOManager.cs:11
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the ChatManager
Definition: ChatManager.cs:48
ChannelRepresentation Channel
The ChannelRepresentation the user spoke from
Definition: ChatUser.cs:40
readonly IDictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings
Definition: ChatManager.cs:73
Represents the lifetime of a IRestartHandler registration
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager
Definition: ChatManager.cs:28
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
Definition: ChatManager.cs:58
bool Enabled
If the connection is enabled
Definition: ChatBot.cs:26
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: ChatManager.cs:400
Represents a command that can be invoked by talking to chat bots
Definition: ICommand.cs:9
Task< IReadOnlyCollection< ChannelRepresentation > > MapChannels(IEnumerable< Api.Models.ChatChannel > channels, CancellationToken cancellationToken)
Get the ChannelRepresentations for given channels
long messagesProcessed
The number of Messages processed.
Definition: ChatManager.cs:118
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...