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);
230 ArgumentNullException.ThrowIfNull(gitHubOwner);
231 ArgumentNullException.ThrowIfNull(gitHubRepo);
233 var commitInsert = revisionInformation.CommitSha![..7];
234 string remoteCommitInsert;
235 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
237 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ?
"^{0}" :
"{0}", commitInsert);
238 remoteCommitInsert = String.Empty;
241 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture,
". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
243 var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
246 CultureInfo.InvariantCulture,
247 " (Test Merges: {0})",
252 .Select(x => x.TestMerge)
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);
266 Text = String.Format(
267 CultureInfo.InvariantCulture,
268 $
"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
273 estimatedCompletionTime.HasValue
274 ? $
" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
280 return async (errorMessage, dreamMakerOutput) =>
286 Text = $
"{prefix}: Deployment {(errorMessage == null ? "complete
" : "failed
")}!",
291 return active => ValueTask.CompletedTask;
296 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>>
MapChannelsImpl(
297 IEnumerable<Models.ChatChannel> channels,
298 CancellationToken cancellationToken)
299 => await Task.Factory.StartNew(
302 if (channels.Any(x => x.IrcChannel ==
null))
303 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
306 var channelsWithKeys =
new Dictionary<string, string>();
307 var hs =
new HashSet<string>();
308 foreach (var channel
in channels)
310 var name = channel.GetIrcChannelName();
311 var key = channel.GetIrcChannelKey();
312 if (hs.Add(name) && key !=
null)
313 channelsWithKeys.Add(name, key);
316 var toPart =
new List<string>();
317 foreach (var activeChannel
in client.JoinedChannels)
318 if (!hs.Remove(activeChannel))
319 toPart.Add(activeChannel);
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);
327 client.RfcJoin(channelToJoin);
329 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
333 var channelName = dbChannel.GetIrcChannelName();
337 if (y.Value != channelName)
343 id = channelIdCounter++;
344 channelIdMap.Add(id.Value, channelName);
347 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
349 new List<ChannelRepresentation>
351 new(address, channelName, id!.Value)
354 IsAdminChannel = dbChannel.IsAdminChannel == true,
355 IsPrivateChannel = false,
356 EmbedsSupported = false,
364 TaskScheduler.Current);
367 protected override async ValueTask
Connect(CancellationToken cancellationToken)
369 cancellationToken.ThrowIfCancellationRequested();
372 await Task.Factory.StartNew(
375 client = InstantiateClient();
376 client.Connect(address, port);
380 TaskScheduler.Current)
381 .WaitAsync(cancellationToken);
383 cancellationToken.ThrowIfCancellationRequested();
385 listenTask = Task.Factory.StartNew(
388 Logger.LogTrace(
"Starting blocking listen...");
395 Logger.LogWarning(ex,
"IRC Main Listen Exception!");
398 Logger.LogTrace(
"Exiting listening task...");
402 TaskScheduler.Current);
404 Logger.LogTrace(
"Authenticating ({passwordType})...", passwordType);
405 switch (passwordType)
408 client.RfcPass(password);
409 await Login(client, nickname, cancellationToken);
412 await Login(client, nickname, cancellationToken);
413 cancellationToken.ThrowIfCancellationRequested();
414 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
417 await SaslAuthenticate(cancellationToken);
420 await Login(client, nickname, cancellationToken);
421 cancellationToken.ThrowIfCancellationRequested();
422 client.RfcOper(nickname, password, Priority.Critical);
425 await Login(client, nickname, cancellationToken);
428 throw new InvalidOperationException($
"Invalid IrcPasswordType: {passwordType.Value}");
431 cancellationToken.ThrowIfCancellationRequested();
433 Logger.LogTrace(
"Connection established!");
435 catch (
Exception e) when (e is not OperationCanceledException)
442 protected override async ValueTask
DisconnectImpl(CancellationToken cancellationToken)
446 await Task.Factory.StartNew(
451 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
455 Logger.LogWarning(e,
"Error quitting IRC!");
460 TaskScheduler.Current);
461 await HardDisconnect(cancellationToken);
463 catch (OperationCanceledException)
469 Logger.LogWarning(e,
"Error disconnecting from IRC!");
481 async ValueTask
Login(IrcFeatures client,
string nickname, CancellationToken cancellationToken)
483 var promise =
new TaskCompletionSource<object>();
485 void Callback(
object? sender, EventArgs e)
487 Logger.LogTrace(
"IRC Registered.");
488 promise.TrySetResult(e);
491 client.OnRegistered += Callback;
493 client.Login(nickname, nickname, 0, nickname);
495 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
496 cts.CancelAfter(TimeSpan.FromSeconds(30));
500 await promise.Task.WaitAsync(cts.Token);
501 client.OnRegistered -= Callback;
503 catch (OperationCanceledException)
505 if (client.IsConnected)
507 throw new JobException(
"Timed out waiting for IRC Registration");
518 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
521 var username = e.Data.Nick;
522 var channelName = isPrivate ? username : e.Data.Channel;
524 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
526 ulong? resultId =
null;
527 if (!dicToCheck.Any(x =>
529 if (x.Value != channelName)
535 resultId = channelIdCounter++;
536 dicToCheck.Add(resultId.Value, channelName);
537 if (dicToCheck == queryChannelIdMap)
538 channelIdMap.Add(resultId.Value,
null);
541 return resultId!.Value;
544 ulong userId, channelId;
547 userId = MapAndGetChannelId(
new Dictionary<ulong, string?>(queryChannelIdMap
548 .Cast<KeyValuePair<ulong, string?>>()));
549 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
552 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName;
557 IsPrivateChannel = isPrivate,
558 EmbedsSupported =
false,
567 EnqueueMessage(message);
594 client.Listen(
false);
598 Logger.LogWarning(ex,
"IRC Non-Blocking Listen Exception!");
602 TaskCreationOptions.None,
603 TaskScheduler.Current)
604 .WaitAsync(cancellationToken);
613 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
614 cancellationToken.ThrowIfCancellationRequested();
616 Logger.LogTrace(
"Logging in...");
617 client.Login(nickname, nickname, 0, nickname);
618 cancellationToken.ThrowIfCancellationRequested();
621 var receivedAck =
false;
622 var receivedPlus =
false;
624 void AuthenticationDelegate(
object sender, ReadLineEventArgs e)
626 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
628 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
632 Logger.LogTrace(
"Performing handshake...");
633 client.OnReadLine += AuthenticationDelegate;
636 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
637 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
638 var timeoutToken = timeoutCts.Token;
640 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
643 await NonBlockingListen(cancellationToken);
645 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
646 timeoutToken.ThrowIfCancellationRequested();
648 for (; !receivedPlus;
650 await NonBlockingListen(cancellationToken);
654 client.OnReadLine -= AuthenticationDelegate;
657 cancellationToken.ThrowIfCancellationRequested();
660 Logger.LogTrace(
"Sending credentials...");
661 var authString = String.Format(
662 CultureInfo.InvariantCulture,
667 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
668 var authLine = $
"AUTHENTICATE {b64}";
669 client.WriteLine(authLine, Priority.Critical);
670 cancellationToken.ThrowIfCancellationRequested();
672 Logger.LogTrace(
"Finishing authentication...");
673 client.WriteLine(
"CAP END", Priority.Critical);
685 Logger.LogTrace(
"Not hard disconnecting, already offline");
689 Logger.LogTrace(
"Hard disconnect");
693 var disconnectTask = Task.Factory.StartNew(
702 Logger.LogWarning(e,
"Error disconnecting IRC!");
707 TaskScheduler.Current);
712 listenTask ?? Task.CompletedTask),
723 var newClient =
new IrcFeatures
725 SupportNonRfc =
true,
726 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
728 AutoRejoinOnKick =
true,
731 AutoReconnect =
false,
732 ActiveChannelSyncing =
true,
733 AutoNickHandling =
true,
734 CtcpVersion = assemblyInfo.VersionString,
736 EnableUTF8Recode =
true,
739 newClient.ValidateServerCertificate =
true;
741 newClient.OnChannelMessage += Client_OnChannelMessage;
742 newClient.OnQueryMessage += Client_OnQueryMessage;
744 if (loggingConfiguration.ProviderNetworkDebug)
746 newClient.OnReadLine += (sender, e) => Logger.LogTrace(
"READ: {line}", e.Line);
747 newClient.OnWriteLine += (sender, e) => Logger.LogTrace(
"WRITE: {line}", e.Line);
750 newClient.OnError += (sender, e) =>
752 Logger.LogError(
"IRC ERROR: {error}", e.ErrorMessage);
753 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.