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