tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
IrcProvider.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Text;
6using System.Threading;
7using System.Threading.Tasks;
8
9using Meebey.SmartIrc4net;
10using Microsoft.Extensions.Logging;
11
12using Newtonsoft.Json;
13
22
24{
28 sealed class IrcProvider : Provider
29 {
33 const int PreambleMessageLength = 12;
34
38 const int MessageBytesLimit = 512;
39
41 public override bool Connected => client.IsConnected;
42
44 public override string BotMention => client.Nickname;
45
49 readonly string address;
50
54 readonly ushort port;
55
59 readonly bool ssl;
60
64 readonly string nickname;
65
69 readonly string password;
70
75
79 readonly Dictionary<ulong, string?> channelIdMap;
80
84 readonly Dictionary<ulong, string> queryChannelIdMap;
85
90
95
99 IrcFeatures client;
100
105
110
122 IAsyncDelayer asyncDelayer,
123 ILogger<IrcProvider> logger,
124 IAssemblyInformationProvider assemblyInformationProvider,
125 Models.ChatBot chatBot,
127 : base(jobManager, asyncDelayer, logger, chatBot)
128 {
129 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
130 ArgumentNullException.ThrowIfNull(loggingConfiguration);
131
132 var builder = chatBot.CreateConnectionStringBuilder();
133 if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder)
134 throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
135
136 address = ircBuilder.Address!;
137 port = ircBuilder.Port!.Value;
138 ssl = ircBuilder.UseSsl!.Value;
139 nickname = ircBuilder.Nickname!;
140
141 password = ircBuilder.Password!;
142 passwordType = ircBuilder.PasswordType;
143
144 assemblyInfo = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
145 this.loggingConfiguration = loggingConfiguration ?? throw new ArgumentNullException(nameof(loggingConfiguration));
146
148
149 channelIdMap = new Dictionary<ulong, string?>();
150 queryChannelIdMap = new Dictionary<ulong, string>();
152 }
153
155 public override async ValueTask DisposeAsync()
156 {
157 await base.DisposeAsync();
158
159 // DCT: None available
160 await HardDisconnect(CancellationToken.None);
161 }
162
164 public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
165 {
166 ArgumentNullException.ThrowIfNull(message);
167
168 await Task.Factory.StartNew(
169 () =>
170 {
171 // IRC doesn't allow newlines
172 // Explicitly ignore embeds
173 var messageText = message.Text;
174 messageText ??= $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
175
176 messageText = String.Concat(
177 messageText
178 .Where(x => x != '\r')
179 .Select(x => x == '\n' ? '|' : x));
180
181 var channelName = channelIdMap[channelId];
182 SendType sendType;
183 if (channelName == null)
184 {
185 channelName = queryChannelIdMap[channelId];
186 sendType = SendType.Notice;
187 }
188 else
189 sendType = SendType.Message;
190
191 var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) + PreambleMessageLength;
192 var messageTooLong = messageSize > MessageBytesLimit;
193 if (messageTooLong)
194 messageText = $"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
195
196 try
197 {
198 client.SendMessage(sendType, channelName, messageText);
199 }
200 catch (Exception e)
201 {
202 Logger.LogWarning(e, "Unable to send to channel {channelName}!", channelName);
203 return;
204 }
205
206 if (messageTooLong)
207 Logger.LogWarning(
208 "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
209 channelId,
210 messageSize);
211 },
212 cancellationToken,
214 TaskScheduler.Current);
215 }
216
218 public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
219 Models.RevisionInformation revisionInformation,
220 EngineVersion engineVersion,
221 DateTimeOffset? estimatedCompletionTime,
222 string? gitHubOwner,
223 string? gitHubRepo,
224 ulong channelId,
225 bool localCommitPushed,
226 CancellationToken cancellationToken)
227 {
228 ArgumentNullException.ThrowIfNull(revisionInformation);
229 ArgumentNullException.ThrowIfNull(engineVersion);
230 ArgumentNullException.ThrowIfNull(gitHubOwner);
231 ArgumentNullException.ThrowIfNull(gitHubRepo);
232
233 var commitInsert = revisionInformation.CommitSha![..7];
234 string remoteCommitInsert;
235 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
236 {
237 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ? "^{0}" : "{0}", commitInsert);
238 remoteCommitInsert = String.Empty;
239 }
240 else
241 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
242
243 var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
244 ? String.Empty
245 : String.Format(
246 CultureInfo.InvariantCulture,
247 " (Test Merges: {0})",
248 String.Join(
249 ", ",
250 revisionInformation
251 .ActiveTestMerges!
252 .Select(x => x.TestMerge)
253 .Select(x =>
254 {
255 var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha![..7]);
256 if (x.Comment != null)
257 result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
258 return result;
259 })));
260
261 var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value);
262 await SendMessage(
263 null,
265 {
266 Text = String.Format(
267 CultureInfo.InvariantCulture,
268 $"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
269 commitInsert,
270 testmergeInsert,
271 remoteCommitInsert,
272 engineVersion.ToString(),
273 estimatedCompletionTime.HasValue
274 ? $" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
275 : String.Empty),
276 },
277 channelId,
278 cancellationToken);
279
280 return async (errorMessage, dreamMakerOutput) =>
281 {
282 await SendMessage(
283 null,
285 {
286 Text = $"{prefix}: Deployment {(errorMessage == null ? "complete" : "failed")}!",
287 },
288 channelId,
289 cancellationToken);
290
291 return active => ValueTask.CompletedTask;
292 };
293 }
294
296 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannelsImpl(
297 IEnumerable<Models.ChatChannel> channels,
298 CancellationToken cancellationToken)
299 => await Task.Factory.StartNew(
300 () =>
301 {
302 if (channels.Any(x => x.IrcChannel == null))
303 throw new InvalidOperationException("ChatChannel missing IrcChannel!");
304 lock (client)
305 {
306 var channelsWithKeys = new Dictionary<string, string>();
307 var hs = new HashSet<string>(); // for unique inserts
308 foreach (var channel in channels)
309 {
310 var name = channel.GetIrcChannelName();
311 var key = channel.GetIrcChannelKey();
312 if (hs.Add(name) && key != null)
313 channelsWithKeys.Add(name, key);
314 }
315
316 var toPart = new List<string>();
317 foreach (var activeChannel in client.JoinedChannels)
318 if (!hs.Remove(activeChannel))
319 toPart.Add(activeChannel);
320
321 foreach (var channelToLeave in toPart)
322 client.RfcPart(channelToLeave, "Pretty nice abscond!");
323 foreach (var channelToJoin in hs)
324 if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
325 client.RfcJoin(channelToJoin, key);
326 else
327 client.RfcJoin(channelToJoin);
328
329 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
330 channels
331 .Select(dbChannel =>
332 {
333 var channelName = dbChannel.GetIrcChannelName();
334 ulong? id = null;
335 if (!channelIdMap.Any(y =>
336 {
337 if (y.Value != channelName)
338 return false;
339 id = y.Key;
340 return true;
341 }))
342 {
343 id = channelIdCounter++;
344 channelIdMap.Add(id.Value, channelName);
345 }
346
347 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
348 dbChannel,
349 new List<ChannelRepresentation>
350 {
351 new(address, channelName, id!.Value)
352 {
353 Tag = dbChannel.Tag,
354 IsAdminChannel = dbChannel.IsAdminChannel == true,
355 IsPrivateChannel = false,
356 EmbedsSupported = false,
357 },
358 });
359 }));
360 }
361 },
362 cancellationToken,
364 TaskScheduler.Current);
365
367 protected override async ValueTask Connect(CancellationToken cancellationToken)
368 {
369 cancellationToken.ThrowIfCancellationRequested();
370 try
371 {
372 await Task.Factory.StartNew(
373 () =>
374 {
375 client = InstantiateClient();
376 client.Connect(address, port);
377 },
378 cancellationToken,
380 TaskScheduler.Current)
381 .WaitAsync(cancellationToken);
382
383 cancellationToken.ThrowIfCancellationRequested();
384
385 listenTask = Task.Factory.StartNew(
386 () =>
387 {
388 Logger.LogTrace("Starting blocking listen...");
389 try
390 {
391 client.Listen();
392 }
393 catch (Exception ex)
394 {
395 Logger.LogWarning(ex, "IRC Main Listen Exception!");
396 }
397
398 Logger.LogTrace("Exiting listening task...");
399 },
400 cancellationToken,
402 TaskScheduler.Current);
403
404 Logger.LogTrace("Authenticating ({passwordType})...", passwordType);
405 switch (passwordType)
406 {
407 case IrcPasswordType.Server:
408 client.RfcPass(password);
409 await Login(client, nickname, cancellationToken);
410 break;
411 case IrcPasswordType.NickServ:
412 await Login(client, nickname, cancellationToken);
413 cancellationToken.ThrowIfCancellationRequested();
414 client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
415 break;
416 case IrcPasswordType.Sasl:
417 await SaslAuthenticate(cancellationToken);
418 break;
419 case IrcPasswordType.Oper:
420 await Login(client, nickname, cancellationToken);
421 cancellationToken.ThrowIfCancellationRequested();
422 client.RfcOper(nickname, password, Priority.Critical);
423 break;
424 case null:
425 await Login(client, nickname, cancellationToken);
426 break;
427 default:
428 throw new InvalidOperationException($"Invalid IrcPasswordType: {passwordType.Value}");
429 }
430
431 cancellationToken.ThrowIfCancellationRequested();
432
433 Logger.LogTrace("Connection established!");
434 }
435 catch (Exception e) when (e is not OperationCanceledException)
436 {
437 throw new JobException(ErrorCode.ChatCannotConnectProvider, e);
438 }
439 }
440
442 protected override async ValueTask DisconnectImpl(CancellationToken cancellationToken)
443 {
444 try
445 {
446 await Task.Factory.StartNew(
447 () =>
448 {
449 try
450 {
451 client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
452 }
453 catch (Exception e)
454 {
455 Logger.LogWarning(e, "Error quitting IRC!");
456 }
457 },
458 cancellationToken,
460 TaskScheduler.Current);
461 await HardDisconnect(cancellationToken);
462 }
463 catch (OperationCanceledException)
464 {
465 throw;
466 }
467 catch (Exception e)
468 {
469 Logger.LogWarning(e, "Error disconnecting from IRC!");
470 }
471 }
472
481 async ValueTask Login(IrcFeatures client, string nickname, CancellationToken cancellationToken)
482 {
483 var promise = new TaskCompletionSource<object>();
484
485 void Callback(object? sender, EventArgs e)
486 {
487 Logger.LogTrace("IRC Registered.");
488 promise.TrySetResult(e);
489 }
490
491 client.OnRegistered += Callback;
492
493 client.Login(nickname, nickname, 0, nickname);
494
495 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
496 cts.CancelAfter(TimeSpan.FromSeconds(30));
497
498 try
499 {
500 await promise.Task.WaitAsync(cts.Token);
501 client.OnRegistered -= Callback;
502 }
503 catch (OperationCanceledException)
504 {
505 if (client.IsConnected)
506 client.Disconnect();
507 throw new JobException("Timed out waiting for IRC Registration");
508 }
509 }
510
516 void HandleMessage(IrcEventArgs e, bool isPrivate)
517 {
518 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
519 return;
520
521 var username = e.Data.Nick;
522 var channelName = isPrivate ? username : e.Data.Channel;
523
524 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
525 {
526 ulong? resultId = null;
527 if (!dicToCheck.Any(x =>
528 {
529 if (x.Value != channelName)
530 return false;
531 resultId = x.Key;
532 return true;
533 }))
534 {
535 resultId = channelIdCounter++;
536 dicToCheck.Add(resultId.Value, channelName);
537 if (dicToCheck == queryChannelIdMap)
538 channelIdMap.Add(resultId.Value, null);
539 }
540
541 return resultId!.Value;
542 }
543
544 ulong userId, channelId;
545 lock (client)
546 {
547 userId = MapAndGetChannelId(new Dictionary<ulong, string?>(queryChannelIdMap
548 .Cast<KeyValuePair<ulong, string?>>())); // NRT my beloathed
549 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
550 }
551
552 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName;
553 var message = new Message(
554 new ChatUser(
555 new ChannelRepresentation(address, channelFriendlyName, channelId)
556 {
557 IsPrivateChannel = isPrivate,
558 EmbedsSupported = false,
559
560 // isAdmin and Tag populated by manager
561 },
562 username,
563 username,
564 userId),
565 e.Data.Message);
566
567 EnqueueMessage(message);
568 }
569
575 void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
576
582 void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
583
589 Task NonBlockingListen(CancellationToken cancellationToken) => Task.Factory.StartNew(
590 () =>
591 {
592 try
593 {
594 client.Listen(false);
595 }
596 catch (Exception ex)
597 {
598 Logger.LogWarning(ex, "IRC Non-Blocking Listen Exception!");
599 }
600 },
601 cancellationToken,
602 TaskCreationOptions.None,
603 TaskScheduler.Current)
604 .WaitAsync(cancellationToken);
605
611 async ValueTask SaslAuthenticate(CancellationToken cancellationToken)
612 {
613 client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
614 cancellationToken.ThrowIfCancellationRequested();
615
616 Logger.LogTrace("Logging in...");
617 client.Login(nickname, nickname, 0, nickname);
618 cancellationToken.ThrowIfCancellationRequested();
619
620 // wait for the SASL ack or timeout
621 var receivedAck = false;
622 var receivedPlus = false;
623
624 void AuthenticationDelegate(object sender, ReadLineEventArgs e)
625 {
626 if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
627 receivedAck = true;
628 else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
629 receivedPlus = true;
630 }
631
632 Logger.LogTrace("Performing handshake...");
633 client.OnReadLine += AuthenticationDelegate;
634 try
635 {
636 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
637 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
638 var timeoutToken = timeoutCts.Token;
639
640 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
641 for (; !receivedAck;
642 await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
643 await NonBlockingListen(cancellationToken);
644
645 client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
646 timeoutToken.ThrowIfCancellationRequested();
647
648 for (; !receivedPlus;
649 await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
650 await NonBlockingListen(cancellationToken);
651 }
652 finally
653 {
654 client.OnReadLine -= AuthenticationDelegate;
655 }
656
657 cancellationToken.ThrowIfCancellationRequested();
658
659 // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
660 Logger.LogTrace("Sending credentials...");
661 var authString = String.Format(
662 CultureInfo.InvariantCulture,
663 "{0}{1}{0}{1}{2}",
664 nickname,
665 '\0',
666 password);
667 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
668 var authLine = $"AUTHENTICATE {b64}";
669 client.WriteLine(authLine, Priority.Critical);
670 cancellationToken.ThrowIfCancellationRequested();
671
672 Logger.LogTrace("Finishing authentication...");
673 client.WriteLine("CAP END", Priority.Critical);
674 }
675
681 async ValueTask HardDisconnect(CancellationToken cancellationToken)
682 {
683 if (!Connected)
684 {
685 Logger.LogTrace("Not hard disconnecting, already offline");
686 return;
687 }
688
689 Logger.LogTrace("Hard disconnect");
690
691 // This call blocks permanently randomly sometimes
692 // Frankly I don't give a shit
693 var disconnectTask = Task.Factory.StartNew(
694 () =>
695 {
696 try
697 {
698 client.Disconnect();
699 }
700 catch (Exception e)
701 {
702 Logger.LogWarning(e, "Error disconnecting IRC!");
703 }
704 },
705 cancellationToken,
707 TaskScheduler.Current);
708
709 await Task.WhenAny(
710 Task.WhenAll(
711 disconnectTask,
712 listenTask ?? Task.CompletedTask),
713 AsyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).AsTask());
714 }
715
721 IrcFeatures InstantiateClient()
722 {
723 var newClient = new IrcFeatures
724 {
725 SupportNonRfc = true,
726 CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
727 AutoRejoin = true,
728 AutoRejoinOnKick = true,
729 AutoRelogin = false,
730 AutoRetry = false,
731 AutoReconnect = false,
732 ActiveChannelSyncing = true,
733 AutoNickHandling = true,
734 CtcpVersion = assemblyInfo.VersionString,
735 UseSsl = ssl,
736 EnableUTF8Recode = true,
737 };
738 if (ssl)
739 newClient.ValidateServerCertificate = true; // dunno if it defaults to that or what
740
741 newClient.OnChannelMessage += Client_OnChannelMessage;
742 newClient.OnQueryMessage += Client_OnQueryMessage;
743
744 if (loggingConfiguration.ProviderNetworkDebug)
745 {
746 newClient.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line);
747 newClient.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);
748 }
749
750 newClient.OnError += (sender, e) =>
751 {
752 Logger.LogError("IRC ERROR: {error}", e.ErrorMessage);
753 newClient.Disconnect();
754 };
755
756 return newClient;
757 }
758 }
759}
Information about an engine installation.
ChatConnectionStringBuilder for ChatProvider.Irc.
Represents a tgs_chat_user datum.
Definition ChatUser.cs:12
IrcFeatures InstantiateClient()
Creates a new instance of the IRC client. Reusing the same client after a disconnection seems to caus...
readonly IAssemblyInformationProvider assemblyInfo
The IAssemblyInformationProvider obtained from constructor, used for the CTCP version string.
readonly ushort port
Port of the server to connect to.
readonly string password
Password which will used for authentication.
override async ValueTask< Dictionary< Models.ChatChannel, IEnumerable< ChannelRepresentation > > > MapChannelsImpl(IEnumerable< Models.ChatChannel > channels, CancellationToken cancellationToken)
override async ValueTask< Func< string?, string, ValueTask< Func< bool, ValueTask > > > > SendUpdateMessage(Models.RevisionInformation revisionInformation, EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, string? gitHubOwner, string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment.A ValueTask<TResult> resulting in a Func<T1, T2,...
override string BotMention
The string that indicates the IProvider was mentioned.
override async ValueTask DisconnectImpl(CancellationToken cancellationToken)
IrcProvider(IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, IAssemblyInformationProvider assemblyInformationProvider, Models.ChatBot chatBot, FileLoggingConfiguration loggingConfiguration)
Initializes a new instance of the IrcProvider class.
async ValueTask HardDisconnect(CancellationToken cancellationToken)
Attempt to disconnect from IRC immediately.
const int PreambleMessageLength
Length of the preamble when writing a message to the server. Must be summed with the channel name to ...
async ValueTask SaslAuthenticate(CancellationToken cancellationToken)
Run SASL authentication on client.
readonly string address
Address of the server to connect to.
void Client_OnQueryMessage(object sender, IrcEventArgs e)
When a query message is received in IRC.
override async ValueTask Connect(CancellationToken cancellationToken)
readonly? IrcPasswordType passwordType
The IrcPasswordType of password.
async ValueTask Login(IrcFeatures client, string nickname, CancellationToken cancellationToken)
Register the client on the network.
void HandleMessage(IrcEventArgs e, bool isPrivate)
Handle an IRC message.
readonly Dictionary< ulong, string > queryChannelIdMap
Map of ChannelRepresentation.RealIds to query users.
override bool Connected
If the IProvider is currently connected.
readonly bool ssl
Wether or not this IRC client is to use ssl.
override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
Send a message to the IProvider.A ValueTask representing the running operation.
readonly FileLoggingConfiguration loggingConfiguration
The FileLoggingConfiguration for the IrcProvider.
void Client_OnChannelMessage(object sender, IrcEventArgs e)
When a channel message is received in IRC.
const int MessageBytesLimit
Hard limit to sendable message size in bytes.
Task? listenTask
The ValueTask used for IrcConnection.Listen(bool).
Task NonBlockingListen(CancellationToken cancellationToken)
Perform a non-blocking IrcConnection.Listen(bool).
readonly Dictionary< ulong, string?> channelIdMap
Map of ChannelRepresentation.RealIds to channel names.
Represents a message received by a IProvider.
Definition Message.cs:9
static string GetEngineCompilerPrefix(Api.Models.EngineType engineType)
Get the prefix for messages about deployments.
readonly IJobManager jobManager
The IJobManager for the Provider.
Definition Provider.cs:40
ILogger< Provider > Logger
The ILogger for the Provider.
Definition Provider.cs:35
Represents a message to send to a chat provider.
IIOManager that resolves paths to Environment.CurrentDirectory.
const TaskCreationOptions BlockingTaskCreationOptions
The TaskCreationOptions used to spawn Tasks for potentially long running, blocking operations.
Operation exceptions thrown from the context of a Models.Job.
async ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .A ValueTask representing the running operation.
Manages the runtime of Jobs.
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12