2using System.Collections.Generic;
3using System.Globalization;
7using System.Threading.Tasks;
9using Meebey.SmartIrc4net;
10using Microsoft.Extensions.Logging;
123 ILogger<IrcProvider> logger,
125 Models.ChatBot chatBot,
127 : base(
jobManager, asyncDelayer, logger, chatBot)
129 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
132 var builder = chatBot.CreateConnectionStringBuilder();
134 throw new InvalidOperationException(
"Invalid ChatConnectionStringBuilder!");
137 port = ircBuilder.Port!.Value;
138 ssl = ircBuilder.UseSsl!.Value;
144 assemblyInfo = assemblyInformationProvider ??
throw new ArgumentNullException(nameof(assemblyInformationProvider));
157 await base.DisposeAsync();
166 ArgumentNullException.ThrowIfNull(message);
168 await Task.Factory.StartNew(
173 var messageText = message.
Text;
174 messageText ??= $
"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
176 messageText = String.Concat(
178 .Where(x => x !=
'\r')
179 .Select(x => x ==
'\n' ?
'|' : x));
183 if (channelName ==
null)
186 sendType = SendType.Notice;
189 sendType = SendType.Message;
191 var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) +
PreambleMessageLength;
194 messageText = $
"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
198 client.SendMessage(sendType, channelName, messageText);
202 Logger.LogWarning(e,
"Unable to send to channel {channelName}!", channelName);
208 "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
214 TaskScheduler.Current);
218 public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>>
SendUpdateMessage(
219 Models.RevisionInformation revisionInformation,
221 DateTimeOffset? estimatedCompletionTime,
225 bool localCommitPushed,
226 CancellationToken cancellationToken)
228 ArgumentNullException.ThrowIfNull(revisionInformation);
229 ArgumentNullException.ThrowIfNull(engineVersion);
231 var commitInsert = revisionInformation.CommitSha![..7];
232 string remoteCommitInsert;
233 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
235 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ?
"^{0}" :
"{0}", commitInsert);
236 remoteCommitInsert = String.Empty;
239 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture,
". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
241 var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
244 CultureInfo.InvariantCulture,
245 " (Test Merges: {0})",
250 .Select(x => x.TestMerge)
253 var result = String.Format(CultureInfo.InvariantCulture,
"#{0} at {1}", x.Number, x.TargetCommitSha![..7]);
254 if (x.Comment != null)
255 result += String.Format(CultureInfo.InvariantCulture,
" ({0})", x.Comment);
264 Text = String.Format(
265 CultureInfo.InvariantCulture,
266 $
"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
271 estimatedCompletionTime.HasValue
272 ? $
" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
278 return async (errorMessage, dreamMakerOutput) =>
284 Text = $
"{prefix}: Deployment {(errorMessage == null ? "complete
" : "failed
")}!",
289 return active => ValueTask.CompletedTask;
294 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>>
MapChannelsImpl(
295 IEnumerable<Models.ChatChannel> channels,
296 CancellationToken cancellationToken)
297 => await Task.Factory.StartNew(
300 if (channels.Any(x => x.IrcChannel ==
null))
301 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
304 var channelsWithKeys =
new Dictionary<string, string>();
305 var hs =
new HashSet<string>();
306 foreach (var channel
in channels)
308 var name = channel.GetIrcChannelName();
309 var key = channel.GetIrcChannelKey();
310 if (hs.Add(name) && key !=
null)
311 channelsWithKeys.Add(name, key);
314 var toPart =
new List<string>();
315 foreach (var activeChannel
in client.JoinedChannels)
316 if (!hs.Remove(activeChannel))
317 toPart.Add(activeChannel);
319 foreach (var channelToLeave
in toPart)
320 client.RfcPart(channelToLeave,
"Pretty nice abscond!");
321 foreach (var channelToJoin
in hs)
322 if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
323 client.RfcJoin(channelToJoin, key);
325 client.RfcJoin(channelToJoin);
327 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
331 var channelName = dbChannel.GetIrcChannelName();
335 if (y.Value != channelName)
341 id = channelIdCounter++;
342 channelIdMap.Add(id.Value, channelName);
345 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
347 new List<ChannelRepresentation>
349 new(address, channelName, id!.Value)
352 IsAdminChannel = dbChannel.IsAdminChannel == true,
353 IsPrivateChannel = false,
354 EmbedsSupported = false,
362 TaskScheduler.Current);
365 protected override async ValueTask
Connect(CancellationToken cancellationToken)
367 cancellationToken.ThrowIfCancellationRequested();
370 await Task.Factory.StartNew(
373 client = InstantiateClient();
374 client.Connect(address, port);
378 TaskScheduler.Current)
379 .WaitAsync(cancellationToken);
381 cancellationToken.ThrowIfCancellationRequested();
383 listenTask = Task.Factory.StartNew(
386 Logger.LogTrace(
"Starting blocking listen...");
393 Logger.LogWarning(ex,
"IRC Main Listen Exception!");
396 Logger.LogTrace(
"Exiting listening task...");
400 TaskScheduler.Current);
402 Logger.LogTrace(
"Authenticating ({passwordType})...", passwordType);
403 switch (passwordType)
406 client.RfcPass(password);
407 await Login(client, nickname, cancellationToken);
410 await Login(client, nickname, cancellationToken);
411 cancellationToken.ThrowIfCancellationRequested();
412 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
415 await SaslAuthenticate(cancellationToken);
418 await Login(client, nickname, cancellationToken);
419 cancellationToken.ThrowIfCancellationRequested();
420 client.RfcOper(nickname, password, Priority.Critical);
423 await Login(client, nickname, cancellationToken);
426 throw new InvalidOperationException($
"Invalid IrcPasswordType: {passwordType.Value}");
429 cancellationToken.ThrowIfCancellationRequested();
431 Logger.LogTrace(
"Connection established!");
433 catch (
Exception e) when (e is not OperationCanceledException)
440 protected override async ValueTask
DisconnectImpl(CancellationToken cancellationToken)
444 await Task.Factory.StartNew(
449 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
453 Logger.LogWarning(e,
"Error quitting IRC!");
458 TaskScheduler.Current);
459 await HardDisconnect(cancellationToken);
461 catch (OperationCanceledException)
467 Logger.LogWarning(e,
"Error disconnecting from IRC!");
479 async ValueTask
Login(IrcFeatures client,
string nickname, CancellationToken cancellationToken)
481 var promise =
new TaskCompletionSource<object>();
483 void Callback(
object? sender, EventArgs e)
485 Logger.LogTrace(
"IRC Registered.");
486 promise.TrySetResult(e);
489 client.OnRegistered += Callback;
491 client.Login(nickname, nickname, 0, nickname);
493 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
494 cts.CancelAfter(TimeSpan.FromSeconds(30));
498 await promise.Task.WaitAsync(cts.Token);
499 client.OnRegistered -= Callback;
501 catch (OperationCanceledException)
503 if (client.IsConnected)
505 throw new JobException(
"Timed out waiting for IRC Registration");
516 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
519 var username = e.Data.Nick;
520 var channelName = isPrivate ? username : e.Data.Channel;
522 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
524 ulong? resultId =
null;
525 if (!dicToCheck.Any(x =>
527 if (x.Value != channelName)
533 resultId = channelIdCounter++;
534 dicToCheck.Add(resultId.Value, channelName);
535 if (dicToCheck == queryChannelIdMap)
536 channelIdMap.Add(resultId.Value,
null);
539 return resultId!.Value;
542 ulong userId, channelId;
545 userId = MapAndGetChannelId(
new Dictionary<ulong, string?>(queryChannelIdMap
546 .Cast<KeyValuePair<ulong, string?>>()));
547 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
550 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName;
555 IsPrivateChannel = isPrivate,
556 EmbedsSupported =
false,
565 EnqueueMessage(message);
592 client.Listen(
false);
596 Logger.LogWarning(ex,
"IRC Non-Blocking Listen Exception!");
600 TaskCreationOptions.None,
601 TaskScheduler.Current)
602 .WaitAsync(cancellationToken);
611 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
612 cancellationToken.ThrowIfCancellationRequested();
614 Logger.LogTrace(
"Logging in...");
615 client.Login(nickname, nickname, 0, nickname);
616 cancellationToken.ThrowIfCancellationRequested();
619 var receivedAck =
false;
620 var receivedPlus =
false;
622 void AuthenticationDelegate(
object sender, ReadLineEventArgs e)
624 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
626 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
630 Logger.LogTrace(
"Performing handshake...");
631 client.OnReadLine += AuthenticationDelegate;
634 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
635 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
636 var timeoutToken = timeoutCts.Token;
638 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
641 await NonBlockingListen(cancellationToken);
643 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
644 timeoutToken.ThrowIfCancellationRequested();
646 for (; !receivedPlus;
648 await NonBlockingListen(cancellationToken);
652 client.OnReadLine -= AuthenticationDelegate;
655 cancellationToken.ThrowIfCancellationRequested();
658 Logger.LogTrace(
"Sending credentials...");
659 var authString = String.Format(
660 CultureInfo.InvariantCulture,
665 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
666 var authLine = $
"AUTHENTICATE {b64}";
667 client.WriteLine(authLine, Priority.Critical);
668 cancellationToken.ThrowIfCancellationRequested();
670 Logger.LogTrace(
"Finishing authentication...");
671 client.WriteLine(
"CAP END", Priority.Critical);
683 Logger.LogTrace(
"Not hard disconnecting, already offline");
687 Logger.LogTrace(
"Hard disconnect");
691 var disconnectTask = Task.Factory.StartNew(
700 Logger.LogWarning(e,
"Error disconnecting IRC!");
705 TaskScheduler.Current);
710 listenTask ?? Task.CompletedTask),
721 var newClient =
new IrcFeatures
723 SupportNonRfc =
true,
724 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
726 AutoRejoinOnKick =
true,
729 AutoReconnect =
false,
730 ActiveChannelSyncing =
true,
731 AutoNickHandling =
true,
732 CtcpVersion = assemblyInfo.VersionString,
734 EnableUTF8Recode =
true,
737 newClient.ValidateServerCertificate =
true;
739 newClient.OnChannelMessage += Client_OnChannelMessage;
740 newClient.OnQueryMessage += Client_OnQueryMessage;
742 if (loggingConfiguration.ProviderNetworkDebug)
744 newClient.OnReadLine += (sender, e) => Logger.LogTrace(
"READ: {line}", e.Line);
745 newClient.OnWriteLine += (sender, e) => Logger.LogTrace(
"WRITE: {line}", e.Line);
748 newClient.OnError += (sender, e) =>
750 Logger.LogError(
"IRC ERROR: {error}", e.ErrorMessage);
751 newClient.Disconnect();
Information about an engine installation.
override string ToString()
EngineType? Engine
The EngineType.
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 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)
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 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.
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.