2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Globalization;
7using System.Threading.Tasks;
9using Microsoft.Extensions.Logging;
27#pragma warning disable CA1506
63 readonly ILogger<ChatManager>
logger;
154 ILogger<ChatManager>
logger,
155 IEnumerable<Models.ChatBot> initialChatBots)
159 ArgumentNullException.ThrowIfNull(serverControl);
161 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
162 activeChatBots = initialChatBots?.ToList() ??
throw new ArgumentNullException(nameof(initialChatBots));
168 builtinCommands =
new Dictionary<string, ICommand>(StringComparer.OrdinalIgnoreCase);
169 providers =
new Dictionary<long, IProvider>();
183 logger.LogTrace(
"Disposing...");
187 await providerKvp.Value.DisposeAsync();
190 providerKvp.Value.Dispose();
196 public async ValueTask
ChangeChannels(
long connectionId, IEnumerable<Models.ChatChannel> newChannels, CancellationToken cancellationToken)
198 ArgumentNullException.ThrowIfNull(newChannels);
200 logger.LogTrace(
"ChangeChannels {connectionId}...", connectionId);
203 logger.LogTrace(
"Creating ChangeChannels semaphore for connection ID {connectionId}...", connectionId);
204 return new SemaphoreSlim(1);
209 if (provider ==
null)
212 if (!provider.Connected)
214 logger.LogDebug(
"Cannot map channels, provider {providerId} disconnected!", connectionId);
218 var results = await provider.MapChannels(newChannels, cancellationToken);
223 var botToUpdate =
activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
224 if (botToUpdate !=
null)
225 botToUpdate.Channels = newChannels
226 .Select(apiModel =>
new Models.ChatChannel
228 DiscordChannelId = apiModel.DiscordChannelId,
229 IrcChannel = apiModel.IrcChannel,
230 IsAdminChannel = apiModel.IsAdminChannel,
231 IsUpdatesChannel = apiModel.IsUpdatesChannel,
232 IsSystemChannel = apiModel.IsSystemChannel,
233 IsWatchdogChannel = apiModel.IsWatchdogChannel,
239 var newMappings = results.SelectMany(
240 kvp => kvp.Value.Select(
241 channelRepresentation =>
new ChannelMapping(channelRepresentation)
243 IsWatchdogChannel = kvp.Key.IsWatchdogChannel ==
true,
244 IsUpdatesChannel = kvp.Key.IsUpdatesChannel ==
true,
245 IsAdminChannel = kvp.Key.IsAdminChannel ==
true,
246 IsSystemChannel = kvp.Key.IsSystemChannel ==
true,
247 ProviderChannelId = channelRepresentation.RealId,
248 ProviderId = connectionId,
261 if (!
providers.TryGetValue(connectionId, out var verify) || verify != provider)
263 foreach (var newMapping
in newMappings)
265 var newId = baseId++;
266 logger.LogTrace(
"Mapping channel {connectionName}:{channelFriendlyName} as {newId}", newMapping.Channel.ConnectionName, newMapping.Channel.FriendlyName, newId);
268 newMapping.Channel.RealId = newId;
280 provider.InitialMappingComplete();
286 public async ValueTask
ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken)
288 ArgumentNullException.ThrowIfNull(newSettings);
290 logger.LogTrace(
"ChangeSettings...");
294 var newSettingsId = Models.ModelExtensions.Require(newSettings, x => x.Id);
295 var newSettingsEnabled = Models.ModelExtensions.Require(newSettings, x => x.Enabled);
299 if (
providers.ContainsKey(newSettingsId))
302 disconnectTask = Task.CompletedTask;
303 if (newSettingsEnabled)
311 foreach (var oldMappedChannelId
in mappedChannels.Where(x => x.Value.ProviderId == newSettingsId).Select(x => x.Key).ToList())
314 await disconnectTask;
324 var reconnectionUpdateTask = provider?.SetReconnectInterval(
325 Models.ModelExtensions.Require(newSettings, x => x.ReconnectionInterval),
327 ?? Task.CompletedTask;
330 var originalChatBot =
activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
331 if (originalChatBot !=
null)
337 ConnectionString = newSettings.ConnectionString,
338 Enabled = newSettings.Enabled,
339 Name = newSettings.Name,
340 ReconnectionInterval = newSettings.ReconnectionInterval,
341 Provider = newSettings.Provider,
345 await reconnectionUpdateTask;
351 ArgumentNullException.ThrowIfNull(message);
352 ArgumentNullException.ThrowIfNull(channelIds);
367 Models.RevisionInformation revisionInformation,
368 Models.RevisionInformation? previousRevisionInformation,
370 DateTimeOffset? estimatedCompletionTime,
373 bool localCommitPushed)
375 List<ulong> wdChannels;
377 wdChannels =
mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
379 logger.LogTrace(
"Sending deployment message for RevisionInformation: {revisionInfoId}", revisionInformation.Id);
381 var callbacks =
new List<Func<string?, string, ValueTask<Func<bool, ValueTask>>>>();
383 var task = Task.WhenAll(
387 ChannelMapping? channelMapping;
388 lock (mappedChannels)
389 if (!mappedChannels.TryGetValue(x, out channelMapping))
393 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
397 var callback = await provider.SendUpdateMessage(
399 previousRevisionInformation,
401 estimatedCompletionTime,
404 channelMapping.ProviderChannelId,
409 callbacks.Add(callback);
415 "Error sending deploy message to provider {providerId}!",
416 channelMapping.ProviderId);
423 Func<bool, Task>? finalUpdateAction =
null;
424 async Task CallbackTask(
string? errorMessage,
string dreamMakerOutput)
434 finalUpdateAction = active =>
ValueTaskExtensions.
WhenAll(callbackResults.Select(finalizerCallback => finalizerCallback(active))).AsTask();
437 async Task CompletionTask(
bool active)
452 return (errorMessage, dreamMakerOutput) =>
454 callbackTask = CallbackTask(errorMessage, dreamMakerOutput);
461 public async Task
StartAsync(CancellationToken cancellationToken)
463 foreach (var tgsCommand
in commandFactory.GenerateCommands())
464 builtinCommands.Add(tgsCommand.Name.ToUpperInvariant(), tgsCommand);
465 var initialChatBots = activeChatBots.ToList();
467 initialProviderConnectionsTask = InitialConnection();
468 chatHandler = MonitorMessages(handlerCts.Token);
472 public async Task
StopAsync(CancellationToken cancellationToken)
475 if (chatHandler !=
null)
477 await Task.WhenAll(providers.Select(x => x.Key).Select(x => DeleteConnection(x, cancellationToken)));
478 await messageSendTask;
484 if (customCommandHandler ==
null)
485 throw new InvalidOperationException(
"RegisterCommandHandler() hasn't been called!");
488 lock (mappedChannels)
490 customCommandHandler,
491 mappedChannels.Select(y => y.Value.Channel),
495 lock (trackingContexts)
496 trackingContexts.Remove(context);
499 lock (trackingContexts)
500 trackingContexts.Add(context);
508 var logMessageSent = 0;
509 async Task UpdateTrackingContext(
IChatTrackingContext channelSink, IEnumerable<ChannelRepresentation> channels)
511 if (Interlocked.Exchange(ref logMessageSent, 1) == 0)
516 var waitingForInitialConnection = !initialProviderConnectionsTask!.IsCompleted;
517 if (waitingForInitialConnection)
519 logger.LogTrace(
"Waiting for initial chat bot connections before updating tracking contexts...");
520 await initialProviderConnectionsTask.WaitAsync(cancellationToken);
524 lock (mappedChannels)
525 lock (trackingContexts)
526 tasks = trackingContexts.Select(x => UpdateTrackingContext(x, mappedChannels.Select(y => y.Value.Channel))).ToList();
528 if (waitingForInitialConnection)
530 logger.LogTrace(
"Updating chat tracking contexts...");
532 logger.LogTrace(
"No chat tracking contexts to update");
534 await Task.WhenAll(tasks);
540 if (this.customCommandHandler !=
null)
541 throw new InvalidOperationException(
"RegisterCommandHandler() already called!");
542 this.customCommandHandler = customCommandHandler ??
throw new ArgumentNullException(nameof(customCommandHandler));
548 logger.LogTrace(
"DeleteConnection {connectionId}", connectionId);
549 var hasSemaphore = changeChannelSemaphores.TryRemove(connectionId, out var semaphore);
557 var provider = await RemoveProviderChannels(connectionId,
true, cancellationToken);
558 if (provider !=
null)
560 var startTime = DateTimeOffset.UtcNow;
563 await provider.Disconnect(cancellationToken);
567 logger.LogError(ex,
"Error disconnecting connection {connectionId}!", connectionId);
570 await provider.DisposeAsync();
571 var duration = DateTimeOffset.UtcNow - startTime;
572 if (duration.TotalSeconds > 3)
573 logger.LogWarning(
"Disconnecting a {providerType} took {totalSeconds}s!", provider.GetType().Name, duration.TotalSeconds);
576 logger.LogTrace(
"DeleteConnection: ID {connectionId} doesn't exist!", connectionId);
581 public ValueTask
HandleRestart(Version? updateVersion,
bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken)
583 var message = updateVersion ==
null
584 ? $
"TGS: {(handlerMayDelayShutdownWithExtremelyLongRunningTasks ? "Graceful shutdown
" : "Going down
")}..."
585 : $
"TGS: Updating to version {updateVersion}...";
586 List<ulong> systemChannels;
587 lock (mappedChannels)
588 systemChannels = mappedChannels
589 .Where(x => x.Value.IsSystemChannel)
609 var commands =
new List<string> { HelpCommandName };
610 commands.AddRange(builtinCommands.Values.Select(command => command.Name));
611 lock (trackingContexts)
614 .Where(trackingContext => trackingContext.Active)
615 .SelectMany(trackingContext => trackingContext.CustomCommands.Select(command => command.Name)));
618 .Distinct(StringComparer.OrdinalIgnoreCase)
619 .OrderBy(command => command, StringComparer.OrdinalIgnoreCase)
630 async ValueTask<IProvider?>
RemoveProviderChannels(
long connectionId,
bool removeProvider, CancellationToken cancellationToken)
632 logger.LogTrace(
"RemoveProviderChannels {connectionId}...", connectionId);
636 if (!providers.TryGetValue(connectionId, out provider))
638 logger.LogTrace(
"Aborted, no such provider!");
643 providers.Remove(connectionId);
646 ValueTask trackingContextsUpdateTask;
647 lock (mappedChannels)
649 foreach (var mappedConnectionChannel
in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
650 mappedChannels.Remove(mappedConnectionChannel);
652 var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
655 lock (trackingContexts)
656 trackingContextsUpdateTask =
ValueTaskExtensions.
WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
658 trackingContextsUpdateTask = ValueTask.CompletedTask;
661 await trackingContextsUpdateTask;
674 logger.LogTrace(
"Remapping channels for provider reconnection...");
675 IEnumerable<Models.ChatChannel>? channelsToMap;
678 providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
680 lock (activeChatBots)
681 channelsToMap = activeChatBots.FirstOrDefault(x => x.Id == providerId)?.Channels;
683 if (channelsToMap?.Any() ??
false)
684 await ChangeChannels(providerId, channelsToMap, cancellationToken);
695#pragma warning disable CA1502
697#pragma warning restore CA1502
701 logger.LogTrace(
"Abort message processing because provider is disconnected!");
708 await RemapProvider(provider, cancellationToken);
714 KeyValuePair<ulong, ChannelMapping>? mappedChannel;
720 cancellationToken.ThrowIfCancellationRequested();
722 var providerIdNullable = providers
723 .Where(x => x.Value == provider)
724 .Select(x => (
long?)x.Key)
727 if (!providerIdNullable.HasValue)
730 logger.LogDebug(
"Unable to process command \"{command}\" due to provider disconnecting", message.
Content);
734 providerId = providerIdNullable.Value;
735 mappedChannel = mappedChannels
736 .Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == providerChannelId)
737 .Select(x => (KeyValuePair<ulong, ChannelMapping>?)x)
739 hasChannelZero = mappedChannels
740 .Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == 0)
746 logger.LogInformation(
"Receieved message from unmapped channel whose provider contains ID 0. Remapping...");
747 await RemapProvider(provider, cancellationToken);
748 logger.LogTrace(
"Resume processing original message...");
749 await ProcessMessage(provider, message,
true, cancellationToken);
753 ValueTask TextReply(
string reply) => SendMessage(
766 lock (mappedChannels)
767 if (!mappedChannel.HasValue)
770 lock (synchronizationLock)
771 newId = channelIdCounter++;
773 "Mapping private channel {connectionName}:{channelFriendlyName} as {newId}",
779 ProviderChannelId = message.User.Channel.RealId,
780 ProviderId = providerId,
784 "Mapping DM {connectionName}:{userId} ({userFriendlyName}) as {newId}",
789 message.User.Channel.RealId = newId;
792 message.User.Channel.RealId = mappedChannel.Value.Key;
795 if (!mappedChannel.HasValue)
798 "Error mapping message: Provider ID: {providerId}, Channel Real ID: {realId}",
801 logger.LogTrace(
"message: {messageJson}", JsonConvert.SerializeObject(message));
802 lock (mappedChannels)
803 logger.LogTrace(
"mappedChannels: {mappedChannelsJson}", JsonConvert.SerializeObject(mappedChannels));
804 await TextReply(
"TGS: Processing error, check logs!");
808 var mappingChannelRepresentation = mappedChannel.Value.Value.Channel;
810 message.User.Channel.RealId = mappingChannelRepresentation.RealId;
811 message.User.Channel.Tag = mappingChannelRepresentation.Tag;
812 message.User.Channel.IsAdminChannel = mappingChannelRepresentation.IsAdminChannel;
815 var trimmedMessage = message.
Content.Trim();
816 if (trimmedMessage.Length == 0)
819 var splits =
new List<string>(trimmedMessage.Split(
' ', StringSplitOptions.RemoveEmptyEntries));
820 var address = splits[0];
821 if (address.Length > 1 && (address.Last() ==
':' || address.Last() ==
','))
822 address = address[0..^1];
825 address.Equals(CommonMention, StringComparison.OrdinalIgnoreCase)
826 || address.Equals(provider.
BotMention, StringComparison.OrdinalIgnoreCase);
833 "Start processing command: {message}. User (True provider Id): {profiderId}",
835 JsonConvert.SerializeObject(message.
User));
841 if (splits.Count == 0)
844 await TextReply(
"Hi!");
848 var command = splits[0];
850 var arguments = String.Join(
" ", splits);
852 Tuple<ICommand, IChatTrackingContext?>? GetCommand(
string command)
854 if (!builtinCommands.TryGetValue(command, out var handler))
855 return trackingContexts
856 .Where(trackingContext => trackingContext.Active)
857 .SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create<
ICommand,
IChatTrackingContext?>(customCommand, trackingContext)))
858 .Where(tuple => tuple.Item1.Name.Equals(command, StringComparison.OrdinalIgnoreCase))
864 const string UnknownCommandMessage =
"TGS: Unknown command! Type '?' or 'help' for available commands.";
866 if (command.Equals(HelpCommandName, StringComparison.OrdinalIgnoreCase) || command ==
"?")
869 if (splits.Count == 0)
871 var allCommands = builtinCommands.Select(x => x.Value).ToList();
872 allCommands.AddRange(
875 x => x.CustomCommands));
876 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)));
880 var helpTuple = GetCommand(splits[0]);
881 if (helpTuple !=
default)
883 var (helpHandler, _) = helpTuple;
884 helpText = String.Format(CultureInfo.InvariantCulture,
"{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ?
" - May only be used in admin channels" : String.Empty);
887 helpText = UnknownCommandMessage;
890 await TextReply(helpText);
894 var tuple = GetCommand(command);
896 if (tuple ==
default)
898 await TextReply(UnknownCommandMessage);
902 var (commandHandler, trackingContext) = tuple;
904 if (trackingContext?.Active ==
false)
906 await TextReply(
"TGS: The server is rebooting, please try again later");
912 await TextReply(
"TGS: Use this command in an admin channel!");
916 var result = await commandHandler.Invoke(arguments, message.
User, cancellationToken);
918 await SendMessage(
new List<ulong> { message.User.Channel.RealId }, message, result, cancellationToken);
920 catch (OperationCanceledException ex)
922 logger.LogTrace(ex,
"Command processing canceled!");
927 logger.LogError(e,
"Error processing chat command");
928 await TextReply(
"TGS: Internal error processing command! Check server logs!");
932 logger.LogTrace(
"Done processing command.");
943 logger.LogTrace(
"Starting processing loop...");
944 var messageTasks =
new Dictionary<IProvider, Task<Message?>>();
945 ValueTask activeProcessingTask = ValueTask.CompletedTask;
948 Task? updatedTask =
null;
949 while (!cancellationToken.IsCancellationRequested)
951 if (updatedTask?.IsCompleted !=
false)
952 lock (synchronizationLock)
953 updatedTask = connectionsUpdated.Task;
956 foreach (var disposedProviderMessageTaskKvp
in messageTasks.Where(x => x.Key.Disposed).ToList())
957 messageTasks.Remove(disposedProviderMessageTaskKvp.Key);
961 foreach (var providerKvp
in providers)
962 if (!messageTasks.ContainsKey(providerKvp.Value))
965 providerKvp.Value.NextMessage(cancellationToken));
967 if (messageTasks.Count == 0)
969 logger.LogTrace(
"No providers active, pausing messsage monitoring...");
970 await updatedTask.WaitAsync(cancellationToken);
971 logger.LogTrace(
"Resuming message monitoring...");
976 await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value)));
979 foreach (var completedMessageTaskKvp
in messageTasks.Where(x => x.Value.IsCompleted).ToList())
981 var provider = completedMessageTaskKvp.Key;
982 messageTasks.Remove(provider);
984 if (provider.Disposed)
987 var message = await completedMessageTaskKvp.Value;
988 var messageNumber = Interlocked.Increment(ref messagesProcessed);
990 async ValueTask WrapProcessMessage()
992 var localActiveProcessingTask = activeProcessingTask;
996 await ProcessMessage(provider, message,
false, cancellationToken);
1000 logger.LogError(ex,
"Error processing message {messageNumber}!", messageNumber);
1003 await localActiveProcessingTask;
1006 activeProcessingTask = WrapProcessMessage();
1010 catch (OperationCanceledException ex)
1012 logger.LogTrace(ex,
"Message processing loop cancelled!");
1016 logger.LogError(e,
"Message loop crashed!");
1020 await activeProcessingTask;
1023 logger.LogTrace(
"Leaving message processing loop");
1036 var channelIdsList = channelIds.ToList();
1039 "Chat send \"{message}\"{embed} to channels: [{channelIdsCommaSeperated}]",
1041 message.
Embed !=
null ?
" (with embed)" : String.Empty,
1042 String.Join(
", ", channelIdsList));
1044 if (channelIdsList.Count == 0)
1045 return ValueTask.CompletedTask;
1048 channelIdsList.Select(x =>
1050 ChannelMapping? channelMapping;
1051 lock (mappedChannels)
1052 if (!mappedChannels.TryGetValue(x, out channelMapping))
1053 return ValueTask.CompletedTask;
1054 IProvider? provider;
1056 if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
1057 return ValueTask.CompletedTask;
1058 return provider.SendMessage(replyTo, message, channelMapping.ProviderChannelId, cancellationToken);
1068 await Task.WhenAll(providers.Select(x => x.Value.InitialConnectionJob));
1069 logger.LogTrace(
"Initial provider connection task completed");
1078 async Task Wrap(Task originalTask)
1085 catch (OperationCanceledException ex)
1087 logger.LogDebug(ex,
"Async chat message cancelled!");
1091 logger.LogError(ex,
"Error in asynchronous chat message!");
1096 messageSendTask = Wrap(messageSendTask);
1107 async Task SendMessageTask()
1109 var cancellationToken = handlerCts.Token;
1110 if (waitForConnections)
1111 await initialProviderConnectionsTask!.WaitAsync(cancellationToken);
1114 channelIdsFactory(),
1120 AddMessageTask(SendMessageTask());
1131 ArgumentNullException.ThrowIfNull(message);
1135 message = $
"{prefix}: {message}";
1138 if (!initialProviderConnectionsTask!.IsCompleted)
1139 logger.LogTrace(
"Waiting for initial provider connections before sending chat message...");
1142 QueueMessageInternal(
1150 lock (mappedChannels)
1151 return mappedChannels.Where(x => channelSelector(x.Value)).Select(x => x.Key).ToList();
Information about an engine installation.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
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.
ulong RealId
The Providers.IProvider channel Id.
bool IsPrivateChannel
If this is a 1-to-1 chat channel.
const string CommonMention
The common bot mention.
long messagesProcessed
The number of Messages processed.
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager.
ValueTask SendMessage(IEnumerable< ulong > channelIds, Message? replyTo, MessageContent message, CancellationToken cancellationToken)
Asynchronously send a given message to a set of channelIds .
IReadOnlyList< string > GetCommandNames()
Gets the currently available chat command names.
readonly object synchronizationLock
Used for various lock statements throughout this class.
ChatManager(IProviderFactory providerFactory, ICommandFactory commandFactory, IServerControl serverControl, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Initializes a new instance of the ChatManager class.
const string HelpCommandName
The name of the built-in help command.
Task? initialProviderConnectionsTask
A Task that represents the IProviders initial connection.
ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken)
Handle a restart of the server.A ValueTask representing the running operation.
void QueueMessageInternal(MessageContent message, Func< IEnumerable< ulong > > channelIdsFactory, bool waitForConnections)
Adds a given message to the send queue.
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager.
async Task InitialConnection()
Aggregate all IProvider.InitialConnectionJobs into one <sse cref="Task">.
async ValueTask RemapProvider(IProvider provider, CancellationToken cancellationToken)
Remap the channels for a given provider .
async ValueTask ProcessMessage(IProvider provider, Message? message, bool recursed, CancellationToken cancellationToken)
Processes a message .
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBotSettings EntityId.Id.
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages.
ICustomCommandHandler? customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Models.ChatChannel>,...
async ValueTask ChangeChannels(long connectionId, IEnumerable< Models.ChatChannel > newChannels, CancellationToken cancellationToken)
readonly Dictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name.
void QueueMessage(MessageContent message, IEnumerable< ulong > channelIds)
Queue a chat message to a given set of channelIds .
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager.
void QueueMessageGeneric(Predicate< ChannelMapping > channelSelector, string message, string? prefix)
Queues a message to a selected set of ChannelMappings.
Task messageSendTask
A Task that represents all sent messages.
Task? chatHandler
The Task that monitors incoming chat messages.
async ValueTask 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...
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds.
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager.
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.A new IChatTrackingContext.
void AddMessageTask(Task task)
Adds a given task to messageSendTask.
readonly ILogger< ChatManager > logger
The ILogger for the ChatManager.
readonly ConcurrentDictionary< long, SemaphoreSlim > changeChannelSemaphores
Map of SemaphoreSlims used to guard concurrent access to ChangeChannels(long, IEnumerable<Models....
void QueueRawDeploymentMessage(string message)
Queue a chat message to configured deployment channels.
async ValueTask DisposeAsync()
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager.
void QueueWatchdogMessage(string message)
Queue a chat message to configured watchdog channels.
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler.
async Task StartAsync(CancellationToken cancellationToken)
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use.
TaskCompletionSource connectionsUpdated
The TaskCompletionSource that completes when ChatBotSettingss change.
async ValueTask UpdateTrackingContexts(CancellationToken cancellationToken)
Force an update with the active channels on all active IChatTrackingContexts.A ValueTask representing...
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection.A Task representing the running operation.
Func< string?, string, Action< bool > > QueueDeploymentMessage(Models.RevisionInformation revisionInformation, Models.RevisionInformation? previousRevisionInformation, EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, string? gitHubOwner, string? gitHubRepo, bool localCommitPushed)
Send the message for a deployment to configured deployment channels.A Func<T1, T2,...
async ValueTask< IProvider?> RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken)
Remove a IProvider from mappedChannels optionally removing the provider itself from providers and upd...
async Task StopAsync(CancellationToken cancellationToken)
readonly List< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager.
readonly Dictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings.
string FriendlyName
The friendly name of the user.
ulong RealId
The internal user id.
ChannelRepresentation Channel
The ChannelRepresentation the user spoke from.
Represents a message received by a IProvider.
string Content
The text of the message.
ChatUser User
The ChatUser who sent the Message.
Represents a message to send to a chat provider.
ChatEmbed? Embed
The ChatEmbed.
string? Text
The message string.
Async lock context helper.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
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...
Factory for built in ICommands.
Represents a command that can be invoked by talking to chat bots.
ValueTask UpdateChannels(IEnumerable< ChannelRepresentation > newChannels, CancellationToken cancellationToken)
Called when newChannels are set.
For managing connected chat services.
Represents a tracking of dynamic chat json files.
Handles Commands.ICommands that map to those defined in a IChatTrackingContext.
IProvider CreateProvider(ChatBot settings, Func< IEnumerable< string > > commandNamesFactory)
Create a IProvider.
For interacting with a chat service.
string BotMention
The string that indicates the IProvider was mentioned.
bool Connected
If the IProvider is currently connected.
Handler for server restarts.
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.