tgstation-server 6.16.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
23
25{
29 sealed class IrcProvider : Provider
30 {
34 const int PreambleMessageLength = 12;
35
39 const int MessageBytesLimit = 512;
40
42 public override bool Connected => client.IsConnected;
43
45 public override string BotMention => client.Nickname;
46
50 readonly string address;
51
55 readonly ushort port;
56
60 readonly bool ssl;
61
65 readonly string nickname;
66
70 readonly string password;
71
76
80 readonly Dictionary<ulong, string?> channelIdMap;
81
85 readonly Dictionary<ulong, string> queryChannelIdMap;
86
91
96
100 IrcFeatures client;
101
106
111
123 IAsyncDelayer asyncDelayer,
124 ILogger<IrcProvider> logger,
125 IAssemblyInformationProvider assemblyInformationProvider,
126 Models.ChatBot chatBot,
128 : base(jobManager, asyncDelayer, logger, chatBot)
129 {
130 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
131 ArgumentNullException.ThrowIfNull(loggingConfiguration);
132
133 var builder = chatBot.CreateConnectionStringBuilder();
134 if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder)
135 throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
136
137 address = ircBuilder.Address!;
138 port = ircBuilder.Port!.Value;
139 ssl = ircBuilder.UseSsl!.Value;
140 nickname = ircBuilder.Nickname!;
141
142 password = ircBuilder.Password!;
143 passwordType = ircBuilder.PasswordType;
144
145 assemblyInfo = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
146 this.loggingConfiguration = loggingConfiguration ?? throw new ArgumentNullException(nameof(loggingConfiguration));
147
149
150 channelIdMap = new Dictionary<ulong, string?>();
151 queryChannelIdMap = new Dictionary<ulong, string>();
153 }
154
156 public override async ValueTask DisposeAsync()
157 {
158 await base.DisposeAsync();
159
160 // DCT: None available
161 await HardDisconnect(CancellationToken.None);
162 }
163
165 public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
166 {
167 ArgumentNullException.ThrowIfNull(message);
168
169 await Task.Factory.StartNew(
170 () =>
171 {
172 // IRC doesn't allow newlines
173 // Explicitly ignore embeds
174 var messageText = message.Text;
175 messageText ??= $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
176
177 messageText = String.Concat(
178 messageText
179 .Where(x => x != '\r')
180 .Select(x => x == '\n' ? '|' : x));
181
182 var channelName = channelIdMap[channelId];
183 SendType sendType;
184 if (channelName == null)
185 {
186 channelName = queryChannelIdMap[channelId];
187 sendType = SendType.Notice;
188 }
189 else
190 sendType = SendType.Message;
191
192 var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) + PreambleMessageLength;
193 var messageTooLong = messageSize > MessageBytesLimit;
194 if (messageTooLong)
195 messageText = $"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
196
197 try
198 {
199 client.SendMessage(sendType, channelName, messageText);
200 }
201 catch (Exception e)
202 {
203 Logger.LogWarning(e, "Unable to send to channel {channelName}!", channelName);
204 return;
205 }
206
207 if (messageTooLong)
208 Logger.LogWarning(
209 "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
210 channelId,
211 messageSize);
212 },
213 cancellationToken,
215 TaskScheduler.Current);
216 }
217
219 public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
220 Models.RevisionInformation revisionInformation,
221 Models.RevisionInformation? previousRevisionInformation,
222 EngineVersion engineVersion,
223 DateTimeOffset? estimatedCompletionTime,
224 string? gitHubOwner,
225 string? gitHubRepo,
226 ulong channelId,
227 bool localCommitPushed,
228 CancellationToken cancellationToken)
229 {
230 ArgumentNullException.ThrowIfNull(revisionInformation);
231 ArgumentNullException.ThrowIfNull(engineVersion);
232
233 var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
234 var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
235
236 var commitInsert = revisionInformation.CommitSha![..7];
237 string remoteCommitInsert;
238 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
239 {
240 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ? "^{0}" : "{0}", commitInsert);
241 remoteCommitInsert = String.Empty;
242 }
243 else
244 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
245
246 var testmergeInsert = !currentTestMerges.Any()
247 ? String.Empty
248 : String.Format(
249 CultureInfo.InvariantCulture,
250 " (Test Merges: {0})",
251 String.Join(
252 ", ",
253 currentTestMerges
254 .Select(x => x.TestMerge)
255 .Select(x =>
256 {
257 var status = string.Empty;
258 if (!previousTestMerges.Any(y => y.TestMerge.Number == x.Number))
259 status = "Added";
260 else if (previousTestMerges.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
261 status = "Updated";
262
263 var result = $"#{x.Number} at {x.TargetCommitSha![..7]}";
264
265 if (!string.IsNullOrEmpty(x.Comment))
266 {
267 if (!string.IsNullOrEmpty(status))
268 result += $" ({status} - {x.Comment})";
269 else
270 result += $" ({x.Comment})";
271 }
272 else if (!string.IsNullOrEmpty(status))
273 result += $" ({status})";
274
275 return result;
276 })));
277
278 var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value);
279 await SendMessage(
280 null,
282 {
283 Text = String.Format(
284 CultureInfo.InvariantCulture,
285 $"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
286 commitInsert,
287 testmergeInsert,
288 remoteCommitInsert,
289 engineVersion.ToString(),
290 estimatedCompletionTime.HasValue
291 ? $" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
292 : String.Empty),
293 },
294 channelId,
295 cancellationToken);
296
297 return async (errorMessage, dreamMakerOutput) =>
298 {
299 await SendMessage(
300 null,
302 {
303 Text = $"{prefix}: Deployment {(errorMessage == null ? "complete" : "failed")}!",
304 },
305 channelId,
306 cancellationToken);
307
308 return active => ValueTask.CompletedTask;
309 };
310 }
311
313 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> MapChannelsImpl(
314 IEnumerable<Models.ChatChannel> channels,
315 CancellationToken cancellationToken)
316 => await Task.Factory.StartNew(
317 () =>
318 {
319 if (channels.Any(x => x.IrcChannel == null))
320 throw new InvalidOperationException("ChatChannel missing IrcChannel!");
321 lock (client)
322 {
323 var channelsWithKeys = new Dictionary<string, string>();
324 var hs = new HashSet<string>(); // for unique inserts
325 foreach (var channel in channels)
326 {
327 var name = channel.GetIrcChannelName();
328 var key = channel.GetIrcChannelKey();
329 if (hs.Add(name) && key != null)
330 channelsWithKeys.Add(name, key);
331 }
332
333 var toPart = new List<string>();
334 foreach (var activeChannel in client.JoinedChannels)
335 if (!hs.Remove(activeChannel))
336 toPart.Add(activeChannel);
337
338 foreach (var channelToLeave in toPart)
339 client.RfcPart(channelToLeave, "Pretty nice abscond!");
340 foreach (var channelToJoin in hs)
341 if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
342 client.RfcJoin(channelToJoin, key);
343 else
344 client.RfcJoin(channelToJoin);
345
346 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
347 channels
348 .Select(dbChannel =>
349 {
350 var channelName = dbChannel.GetIrcChannelName();
351 ulong? id = null;
352 if (!channelIdMap.Any(y =>
353 {
354 if (y.Value != channelName)
355 return false;
356 id = y.Key;
357 return true;
358 }))
359 {
360 id = channelIdCounter++;
361 channelIdMap.Add(id.Value, channelName);
362 }
363
364 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
365 dbChannel,
366 new List<ChannelRepresentation>
367 {
368 new(address, channelName, id!.Value)
369 {
370 Tag = dbChannel.Tag,
371 IsAdminChannel = dbChannel.IsAdminChannel == true,
372 IsPrivateChannel = false,
373 EmbedsSupported = false,
374 },
375 });
376 }));
377 }
378 },
379 cancellationToken,
381 TaskScheduler.Current);
382
384 protected override async ValueTask Connect(CancellationToken cancellationToken)
385 {
386 cancellationToken.ThrowIfCancellationRequested();
387 try
388 {
389 await Task.Factory.StartNew(
390 () =>
391 {
392 client = InstantiateClient();
393 client.Connect(address, port);
394 },
395 cancellationToken,
397 TaskScheduler.Current)
398 .WaitAsync(cancellationToken);
399
400 cancellationToken.ThrowIfCancellationRequested();
401
402 listenTask = Task.Factory.StartNew(
403 () =>
404 {
405 Logger.LogTrace("Starting blocking listen...");
406 try
407 {
408 client.Listen();
409 }
410 catch (Exception ex)
411 {
412 Logger.LogWarning(ex, "IRC Main Listen Exception!");
413 }
414
415 Logger.LogTrace("Exiting listening task...");
416 },
417 cancellationToken,
419 TaskScheduler.Current);
420
421 Logger.LogTrace("Authenticating ({passwordType})...", passwordType);
422 switch (passwordType)
423 {
424 case IrcPasswordType.Server:
425 client.RfcPass(password);
426 await Login(client, nickname, cancellationToken);
427 break;
428 case IrcPasswordType.NickServ:
429 await Login(client, nickname, cancellationToken);
430 cancellationToken.ThrowIfCancellationRequested();
431 client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
432 break;
433 case IrcPasswordType.Sasl:
434 await SaslAuthenticate(cancellationToken);
435 break;
436 case IrcPasswordType.Oper:
437 await Login(client, nickname, cancellationToken);
438 cancellationToken.ThrowIfCancellationRequested();
439 client.RfcOper(nickname, password, Priority.Critical);
440 break;
441 case null:
442 await Login(client, nickname, cancellationToken);
443 break;
444 default:
445 throw new InvalidOperationException($"Invalid IrcPasswordType: {passwordType.Value}");
446 }
447
448 cancellationToken.ThrowIfCancellationRequested();
449
450 Logger.LogTrace("Connection established!");
451 }
452 catch (Exception e) when (e is not OperationCanceledException)
453 {
454 throw new JobException(ErrorCode.ChatCannotConnectProvider, e);
455 }
456 }
457
459 protected override async ValueTask DisconnectImpl(CancellationToken cancellationToken)
460 {
461 try
462 {
463 await Task.Factory.StartNew(
464 () =>
465 {
466 try
467 {
468 client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
469 }
470 catch (Exception e)
471 {
472 Logger.LogWarning(e, "Error quitting IRC!");
473 }
474 },
475 cancellationToken,
477 TaskScheduler.Current);
478 await HardDisconnect(cancellationToken);
479 }
480 catch (OperationCanceledException)
481 {
482 throw;
483 }
484 catch (Exception e)
485 {
486 Logger.LogWarning(e, "Error disconnecting from IRC!");
487 }
488 }
489
498 async ValueTask Login(IrcFeatures client, string nickname, CancellationToken cancellationToken)
499 {
500 var promise = new TaskCompletionSource<object>();
501
502 void Callback(object? sender, EventArgs e)
503 {
504 Logger.LogTrace("IRC Registered.");
505 promise.TrySetResult(e);
506 }
507
508 client.OnRegistered += Callback;
509
510 client.Login(nickname, nickname, 0, nickname);
511
512 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
513 cts.CancelAfter(TimeSpan.FromSeconds(30));
514
515 try
516 {
517 await promise.Task.WaitAsync(cts.Token);
518 client.OnRegistered -= Callback;
519 }
520 catch (OperationCanceledException)
521 {
522 if (client.IsConnected)
523 client.Disconnect();
524 throw new JobException("Timed out waiting for IRC Registration");
525 }
526 }
527
533 void HandleMessage(IrcEventArgs e, bool isPrivate)
534 {
535 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
536 return;
537
538 var username = e.Data.Nick;
539 var channelName = isPrivate ? username : e.Data.Channel;
540
541 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
542 {
543 ulong? resultId = null;
544 if (!dicToCheck.Any(x =>
545 {
546 if (x.Value != channelName)
547 return false;
548 resultId = x.Key;
549 return true;
550 }))
551 {
552 resultId = channelIdCounter++;
553 dicToCheck.Add(resultId.Value, channelName);
554 if (dicToCheck == queryChannelIdMap)
555 channelIdMap.Add(resultId.Value, null);
556 }
557
558 return resultId!.Value;
559 }
560
561 ulong userId, channelId;
562 lock (client)
563 {
564 userId = MapAndGetChannelId(new Dictionary<ulong, string?>(queryChannelIdMap
565 .Cast<KeyValuePair<ulong, string?>>())); // NRT my beloathed
566 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
567 }
568
569 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName;
570 var message = new Message(
571 new ChatUser(
572 new ChannelRepresentation(address, channelFriendlyName, channelId)
573 {
574 IsPrivateChannel = isPrivate,
575 EmbedsSupported = false,
576
577 // isAdmin and Tag populated by manager
578 },
579 username,
580 username,
581 userId),
582 e.Data.Message);
583
584 EnqueueMessage(message);
585 }
586
592 void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
593
599 void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
600
606 Task NonBlockingListen(CancellationToken cancellationToken) => Task.Factory.StartNew(
607 () =>
608 {
609 try
610 {
611 client.Listen(false);
612 }
613 catch (Exception ex)
614 {
615 Logger.LogWarning(ex, "IRC Non-Blocking Listen Exception!");
616 }
617 },
618 cancellationToken,
619 TaskCreationOptions.None,
620 TaskScheduler.Current)
621 .WaitAsync(cancellationToken);
622
628 async ValueTask SaslAuthenticate(CancellationToken cancellationToken)
629 {
630 client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
631 cancellationToken.ThrowIfCancellationRequested();
632
633 Logger.LogTrace("Logging in...");
634 client.Login(nickname, nickname, 0, nickname);
635 cancellationToken.ThrowIfCancellationRequested();
636
637 // wait for the SASL ack or timeout
638 var receivedAck = false;
639 var receivedPlus = false;
640
641 void AuthenticationDelegate(object sender, ReadLineEventArgs e)
642 {
643 if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
644 receivedAck = true;
645 else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
646 receivedPlus = true;
647 }
648
649 Logger.LogTrace("Performing handshake...");
650 client.OnReadLine += AuthenticationDelegate;
651 try
652 {
653 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
654 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
655 var timeoutToken = timeoutCts.Token;
656
657 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
658 for (; !receivedAck;
659 await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
660 await NonBlockingListen(cancellationToken);
661
662 client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
663 timeoutToken.ThrowIfCancellationRequested();
664
665 for (; !receivedPlus;
666 await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
667 await NonBlockingListen(cancellationToken);
668 }
669 finally
670 {
671 client.OnReadLine -= AuthenticationDelegate;
672 }
673
674 cancellationToken.ThrowIfCancellationRequested();
675
676 // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
677 Logger.LogTrace("Sending credentials...");
678 var authString = String.Format(
679 CultureInfo.InvariantCulture,
680 "{0}{1}{0}{1}{2}",
681 nickname,
682 '\0',
683 password);
684 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
685 var authLine = $"AUTHENTICATE {b64}";
686 client.WriteLine(authLine, Priority.Critical);
687 cancellationToken.ThrowIfCancellationRequested();
688
689 Logger.LogTrace("Finishing authentication...");
690 client.WriteLine("CAP END", Priority.Critical);
691 }
692
698 async ValueTask HardDisconnect(CancellationToken cancellationToken)
699 {
700 if (!Connected)
701 {
702 Logger.LogTrace("Not hard disconnecting, already offline");
703 return;
704 }
705
706 Logger.LogTrace("Hard disconnect");
707
708 // This call blocks permanently randomly sometimes
709 // Frankly I don't give a shit
710 var disconnectTask = Task.Factory.StartNew(
711 () =>
712 {
713 try
714 {
715 client.Disconnect();
716 }
717 catch (Exception e)
718 {
719 Logger.LogWarning(e, "Error disconnecting IRC!");
720 }
721 },
722 cancellationToken,
724 TaskScheduler.Current);
725
726 await Task.WhenAny(
727 Task.WhenAll(
728 disconnectTask,
729 listenTask ?? Task.CompletedTask),
730 AsyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).AsTask());
731 }
732
738 IrcFeatures InstantiateClient()
739 {
740 var newClient = new IrcFeatures
741 {
742 SupportNonRfc = true,
743 CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
744 AutoRejoin = true,
745 AutoRejoinOnKick = true,
746 AutoRelogin = false,
747 AutoRetry = false,
748 AutoReconnect = false,
749 ActiveChannelSyncing = true,
750 AutoNickHandling = true,
751 CtcpVersion = assemblyInfo.VersionString,
752 UseSsl = ssl,
753 EnableUTF8Recode = true,
754 };
755 if (ssl)
756 newClient.ValidateServerCertificate = true; // dunno if it defaults to that or what
757
758 newClient.OnChannelMessage += Client_OnChannelMessage;
759 newClient.OnQueryMessage += Client_OnQueryMessage;
760
761 if (loggingConfiguration.ProviderNetworkDebug)
762 {
763 newClient.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line);
764 newClient.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);
765 }
766
767 newClient.OnError += (sender, e) =>
768 {
769 Logger.LogError("IRC ERROR: {error}", e.ErrorMessage);
770 newClient.Disconnect();
771 };
772
773 return newClient;
774 }
775 }
776}
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 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< Func< string?, string, ValueTask< Func< bool, ValueTask > > > > SendUpdateMessage(Models.RevisionInformation revisionInformation, Models.RevisionInformation? previousRevisionInformation, 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 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.
Many to many relationship for Models.RevisionInformation and Models.TestMerge.
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
if(removedTestMerges.Count !=0) fields.Add(new EmbedField("Removed