2using System.Collections.Generic;
3using System.Globalization;
7using System.Threading.Tasks;
9using Meebey.SmartIrc4net;
10using Microsoft.Extensions.Logging;
124 ILogger<IrcProvider> logger,
126 Models.ChatBot chatBot,
128 : base(
jobManager, asyncDelayer, logger, chatBot)
130 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
133 var builder = chatBot.CreateConnectionStringBuilder();
135 throw new InvalidOperationException(
"Invalid ChatConnectionStringBuilder!");
138 port = ircBuilder.Port!.Value;
139 ssl = ircBuilder.UseSsl!.Value;
145 assemblyInfo = assemblyInformationProvider ??
throw new ArgumentNullException(nameof(assemblyInformationProvider));
158 await base.DisposeAsync();
167 ArgumentNullException.ThrowIfNull(message);
169 await Task.Factory.StartNew(
174 var messageText = message.
Text;
175 messageText ??= $
"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
177 messageText = String.Concat(
179 .Where(x => x !=
'\r')
180 .Select(x => x ==
'\n' ?
'|' : x));
184 if (channelName ==
null)
187 sendType = SendType.Notice;
190 sendType = SendType.Message;
192 var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) +
PreambleMessageLength;
195 messageText = $
"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
199 client.SendMessage(sendType, channelName, messageText);
203 Logger.LogWarning(e,
"Unable to send to channel {channelName}!", channelName);
209 "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
215 TaskScheduler.Current);
219 public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>>
SendUpdateMessage(
220 Models.RevisionInformation revisionInformation,
221 Models.RevisionInformation? previousRevisionInformation,
223 DateTimeOffset? estimatedCompletionTime,
227 bool localCommitPushed,
228 CancellationToken cancellationToken)
230 ArgumentNullException.ThrowIfNull(revisionInformation);
231 ArgumentNullException.ThrowIfNull(engineVersion);
233 var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<
RevInfoTestMerge>();
234 var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<
RevInfoTestMerge>();
236 var commitInsert = revisionInformation.CommitSha![..7];
237 string remoteCommitInsert;
238 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
240 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ?
"^{0}" :
"{0}", commitInsert);
241 remoteCommitInsert = String.Empty;
244 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture,
". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
246 var testmergeInsert = !currentTestMerges.Any()
249 CultureInfo.InvariantCulture,
250 " (Test Merges: {0})",
254 .Select(x => x.TestMerge)
257 var status = string.Empty;
258 if (!previousTestMerges.Any(y => y.TestMerge.Number == x.Number))
260 else if (previousTestMerges.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
263 var result = $
"#{x.Number} at {x.TargetCommitSha![..7]}";
265 if (!string.IsNullOrEmpty(x.Comment))
267 if (!string.IsNullOrEmpty(status))
268 result += $
" ({status} - {x.Comment})";
270 result += $
" ({x.Comment})";
272 else if (!
string.IsNullOrEmpty(status))
273 result += $
" ({status})";
283 Text = String.Format(
284 CultureInfo.InvariantCulture,
285 $
"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
289 engineVersion.ToString(),
290 estimatedCompletionTime.HasValue
291 ? $
" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
297 return async (errorMessage, dreamMakerOutput) =>
303 Text = $
"{prefix}: Deployment {(errorMessage == null ? "complete
" : "failed
")}!",
308 return active => ValueTask.CompletedTask;
313 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>>
MapChannelsImpl(
314 IEnumerable<Models.ChatChannel> channels,
315 CancellationToken cancellationToken)
316 => await Task.Factory.StartNew(
319 if (channels.Any(x => x.IrcChannel ==
null))
320 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
323 var channelsWithKeys =
new Dictionary<string, string>();
324 var hs =
new HashSet<string>();
325 foreach (var channel
in channels)
327 var name = channel.GetIrcChannelName();
328 var key = channel.GetIrcChannelKey();
329 if (hs.Add(name) && key !=
null)
330 channelsWithKeys.Add(name, key);
333 var toPart =
new List<string>();
334 foreach (var activeChannel
in client.JoinedChannels)
335 if (!hs.Remove(activeChannel))
336 toPart.Add(activeChannel);
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);
344 client.RfcJoin(channelToJoin);
346 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
350 var channelName = dbChannel.GetIrcChannelName();
354 if (y.Value != channelName)
360 id = channelIdCounter++;
361 channelIdMap.Add(id.Value, channelName);
364 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
366 new List<ChannelRepresentation>
368 new(address, channelName, id!.Value)
371 IsAdminChannel = dbChannel.IsAdminChannel == true,
372 IsPrivateChannel = false,
373 EmbedsSupported = false,
381 TaskScheduler.Current);
384 protected override async ValueTask
Connect(CancellationToken cancellationToken)
386 cancellationToken.ThrowIfCancellationRequested();
389 await Task.Factory.StartNew(
392 client = InstantiateClient();
393 client.Connect(address, port);
397 TaskScheduler.Current)
398 .WaitAsync(cancellationToken);
400 cancellationToken.ThrowIfCancellationRequested();
402 listenTask = Task.Factory.StartNew(
405 Logger.LogTrace(
"Starting blocking listen...");
412 Logger.LogWarning(ex,
"IRC Main Listen Exception!");
415 Logger.LogTrace(
"Exiting listening task...");
419 TaskScheduler.Current);
421 Logger.LogTrace(
"Authenticating ({passwordType})...", passwordType);
422 switch (passwordType)
425 client.RfcPass(password);
426 await Login(client, nickname, cancellationToken);
429 await Login(client, nickname, cancellationToken);
430 cancellationToken.ThrowIfCancellationRequested();
431 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
434 await SaslAuthenticate(cancellationToken);
437 await Login(client, nickname, cancellationToken);
438 cancellationToken.ThrowIfCancellationRequested();
439 client.RfcOper(nickname, password, Priority.Critical);
442 await Login(client, nickname, cancellationToken);
445 throw new InvalidOperationException($
"Invalid IrcPasswordType: {passwordType.Value}");
448 cancellationToken.ThrowIfCancellationRequested();
450 Logger.LogTrace(
"Connection established!");
452 catch (
Exception e) when (e is not OperationCanceledException)
459 protected override async ValueTask
DisconnectImpl(CancellationToken cancellationToken)
463 await Task.Factory.StartNew(
468 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
472 Logger.LogWarning(e,
"Error quitting IRC!");
477 TaskScheduler.Current);
478 await HardDisconnect(cancellationToken);
480 catch (OperationCanceledException)
486 Logger.LogWarning(e,
"Error disconnecting from IRC!");
498 async ValueTask
Login(IrcFeatures client,
string nickname, CancellationToken cancellationToken)
500 var promise =
new TaskCompletionSource<object>();
502 void Callback(
object? sender, EventArgs e)
504 Logger.LogTrace(
"IRC Registered.");
505 promise.TrySetResult(e);
508 client.OnRegistered += Callback;
510 client.Login(nickname, nickname, 0, nickname);
512 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
513 cts.CancelAfter(TimeSpan.FromSeconds(30));
517 await promise.Task.WaitAsync(cts.Token);
518 client.OnRegistered -= Callback;
520 catch (OperationCanceledException)
522 if (client.IsConnected)
524 throw new JobException(
"Timed out waiting for IRC Registration");
535 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
538 var username = e.Data.Nick;
539 var channelName = isPrivate ? username : e.Data.Channel;
541 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
543 ulong? resultId =
null;
544 if (!dicToCheck.Any(x =>
546 if (x.Value != channelName)
552 resultId = channelIdCounter++;
553 dicToCheck.Add(resultId.Value, channelName);
554 if (dicToCheck == queryChannelIdMap)
555 channelIdMap.Add(resultId.Value,
null);
558 return resultId!.Value;
561 ulong userId, channelId;
564 userId = MapAndGetChannelId(
new Dictionary<ulong, string?>(queryChannelIdMap
565 .Cast<KeyValuePair<ulong, string?>>()));
566 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
569 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName;
574 IsPrivateChannel = isPrivate,
575 EmbedsSupported =
false,
584 EnqueueMessage(message);
611 client.Listen(
false);
615 Logger.LogWarning(ex,
"IRC Non-Blocking Listen Exception!");
619 TaskCreationOptions.None,
620 TaskScheduler.Current)
621 .WaitAsync(cancellationToken);
630 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
631 cancellationToken.ThrowIfCancellationRequested();
633 Logger.LogTrace(
"Logging in...");
634 client.Login(nickname, nickname, 0, nickname);
635 cancellationToken.ThrowIfCancellationRequested();
638 var receivedAck =
false;
639 var receivedPlus =
false;
641 void AuthenticationDelegate(
object sender, ReadLineEventArgs e)
643 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
645 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
649 Logger.LogTrace(
"Performing handshake...");
650 client.OnReadLine += AuthenticationDelegate;
653 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
654 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
655 var timeoutToken = timeoutCts.Token;
657 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
660 await NonBlockingListen(cancellationToken);
662 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
663 timeoutToken.ThrowIfCancellationRequested();
665 for (; !receivedPlus;
667 await NonBlockingListen(cancellationToken);
671 client.OnReadLine -= AuthenticationDelegate;
674 cancellationToken.ThrowIfCancellationRequested();
677 Logger.LogTrace(
"Sending credentials...");
678 var authString = String.Format(
679 CultureInfo.InvariantCulture,
684 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
685 var authLine = $
"AUTHENTICATE {b64}";
686 client.WriteLine(authLine, Priority.Critical);
687 cancellationToken.ThrowIfCancellationRequested();
689 Logger.LogTrace(
"Finishing authentication...");
690 client.WriteLine(
"CAP END", Priority.Critical);
702 Logger.LogTrace(
"Not hard disconnecting, already offline");
706 Logger.LogTrace(
"Hard disconnect");
710 var disconnectTask = Task.Factory.StartNew(
719 Logger.LogWarning(e,
"Error disconnecting IRC!");
724 TaskScheduler.Current);
729 listenTask ?? Task.CompletedTask),
740 var newClient =
new IrcFeatures
742 SupportNonRfc =
true,
743 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
745 AutoRejoinOnKick =
true,
748 AutoReconnect =
false,
749 ActiveChannelSyncing =
true,
750 AutoNickHandling =
true,
751 CtcpVersion = assemblyInfo.VersionString,
753 EnableUTF8Recode =
true,
756 newClient.ValidateServerCertificate =
true;
758 newClient.OnChannelMessage += Client_OnChannelMessage;
759 newClient.OnQueryMessage += Client_OnQueryMessage;
761 if (loggingConfiguration.ProviderNetworkDebug)
763 newClient.OnReadLine += (sender, e) => Logger.LogTrace(
"READ: {line}", e.Line);
764 newClient.OnWriteLine += (sender, e) => Logger.LogTrace(
"WRITE: {line}", e.Line);
767 newClient.OnError += (sender, e) =>
769 Logger.LogError(
"IRC ERROR: {error}", e.ErrorMessage);
770 newClient.Disconnect();
Information about an engine installation.
ChatConnectionStringBuilder for ChatProvider.Irc.
Represents a Providers.IProvider channel.
Represents a tgs_chat_user datum.
IProvider for internet relay chat.
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)
ulong channelIdCounter
Id counter for channelIdMap.
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 ...
override async ValueTask DisposeAsync()
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.
readonly string nickname
IRC nickname.
void HandleMessage(IrcEventArgs e, bool isPrivate)
Handle an IRC message.
IrcFeatures client
The IrcFeatures client.
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.
static string GetEngineCompilerPrefix(Api.Models.EngineType engineType)
Get the prefix for messages about deployments.
readonly IJobManager jobManager
The IJobManager for the Provider.
ILogger< Provider > Logger
The ILogger for the Provider.
Represents a message to send to a chat provider.
string? Text
The message string.
File logging configuration options.
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.
For waiting asynchronously.
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
if(removedTestMerges.Count !=0) fields.Add(new EmbedField("Removed