2using System.Collections.Generic;
3using System.Globalization;
7using System.Threading.Tasks;
9using Meebey.SmartIrc4net;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
125 ILogger<IrcProvider> logger,
126 Models.ChatBot chatBot,
129 : base(
jobManager, asyncDelayer, logger, chatBot)
131 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
134 var builder = chatBot.CreateConnectionStringBuilder();
136 throw new InvalidOperationException(
"Invalid ChatConnectionStringBuilder!");
139 port = ircBuilder.Port!.Value;
140 ssl = ircBuilder.UseSsl!.Value;
146 assemblyInfo = assemblyInformationProvider ??
throw new ArgumentNullException(nameof(assemblyInformationProvider));
159 await base.DisposeAsync();
168 ArgumentNullException.ThrowIfNull(message);
170 await Task.Factory.StartNew(
175 var messageText = message.
Text;
176 messageText ??= $
"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
178 messageText = String.Concat(
180 .Where(x => x !=
'\r')
181 .Select(x => x ==
'\n' ?
'|' : x));
185 if (channelName ==
null)
188 sendType = SendType.Notice;
191 sendType = SendType.Message;
193 var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) +
PreambleMessageLength;
196 messageText = $
"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
200 client.SendMessage(sendType, channelName, messageText);
204 Logger.LogWarning(e,
"Unable to send to channel {channelName}!", channelName);
210 "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
216 TaskScheduler.Current);
220 public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>>
SendUpdateMessage(
221 Models.RevisionInformation revisionInformation,
222 Models.RevisionInformation? previousRevisionInformation,
224 DateTimeOffset? estimatedCompletionTime,
228 bool localCommitPushed,
229 CancellationToken cancellationToken)
231 ArgumentNullException.ThrowIfNull(revisionInformation);
232 ArgumentNullException.ThrowIfNull(engineVersion);
234 var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<
RevInfoTestMerge>();
235 var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<
RevInfoTestMerge>();
237 var commitInsert = revisionInformation.CommitSha![..7];
238 string remoteCommitInsert;
239 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
241 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ?
"^{0}" :
"{0}", commitInsert);
242 remoteCommitInsert = String.Empty;
245 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture,
". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
247 var testmergeInsert = !currentTestMerges.Any()
250 CultureInfo.InvariantCulture,
251 " (Test Merges: {0})",
255 .Select(x => x.TestMerge)
258 var status = string.Empty;
259 if (!previousTestMerges.Any(y => y.TestMerge.Number == x.Number))
261 else if (previousTestMerges.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
264 var result = $
"#{x.Number} at {x.TargetCommitSha![..7]}";
266 if (!string.IsNullOrEmpty(x.Comment))
268 if (!string.IsNullOrEmpty(status))
269 result += $
" ({status} - {x.Comment})";
271 result += $
" ({x.Comment})";
273 else if (!
string.IsNullOrEmpty(status))
274 result += $
" ({status})";
284 Text = String.Format(
285 CultureInfo.InvariantCulture,
286 $
"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
290 engineVersion.ToString(),
291 estimatedCompletionTime.HasValue
292 ? $
" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
298 return async (errorMessage, dreamMakerOutput) =>
304 Text = $
"{prefix}: Deployment {(errorMessage == null ? "complete
" : "failed
")}!",
309 return active => ValueTask.CompletedTask;
314 protected override async ValueTask<Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>>
MapChannelsImpl(
315 IEnumerable<Models.ChatChannel> channels,
316 CancellationToken cancellationToken)
317 => await Task.Factory.StartNew(
320 if (channels.Any(x => x.IrcChannel ==
null))
321 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
324 var channelsWithKeys =
new Dictionary<string, string>();
325 var hs =
new HashSet<string>();
326 foreach (var channel
in channels)
328 var name = channel.GetIrcChannelName();
329 var key = channel.GetIrcChannelKey();
330 if (hs.Add(name) && key !=
null)
331 channelsWithKeys.Add(name, key);
334 var toPart =
new List<string>();
335 foreach (var activeChannel
in client.JoinedChannels)
336 if (!hs.Remove(activeChannel))
337 toPart.Add(activeChannel);
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);
345 client.RfcJoin(channelToJoin);
347 return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
351 var channelName = dbChannel.GetIrcChannelName();
355 if (y.Value != channelName)
361 id = channelIdCounter++;
362 channelIdMap.Add(id.Value, channelName);
365 return new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
367 new List<ChannelRepresentation>
369 new(address, channelName, id!.Value)
372 IsAdminChannel = dbChannel.IsAdminChannel == true,
373 IsPrivateChannel = false,
374 EmbedsSupported = false,
382 TaskScheduler.Current);
385 protected override async ValueTask
Connect(CancellationToken cancellationToken)
387 cancellationToken.ThrowIfCancellationRequested();
390 await Task.Factory.StartNew(
393 client = InstantiateClient();
394 client.Connect(address, port);
398 TaskScheduler.Current)
399 .WaitAsync(cancellationToken);
401 cancellationToken.ThrowIfCancellationRequested();
403 listenTask = Task.Factory.StartNew(
406 Logger.LogTrace(
"Starting blocking listen...");
413 Logger.LogWarning(ex,
"IRC Main Listen Exception!");
416 Logger.LogTrace(
"Exiting listening task...");
420 TaskScheduler.Current);
422 Logger.LogTrace(
"Authenticating ({passwordType})...", passwordType);
423 switch (passwordType)
426 client.RfcPass(password);
427 await Login(client, nickname, cancellationToken);
430 await Login(client, nickname, cancellationToken);
431 cancellationToken.ThrowIfCancellationRequested();
432 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
435 await SaslAuthenticate(cancellationToken);
438 await Login(client, nickname, cancellationToken);
439 cancellationToken.ThrowIfCancellationRequested();
440 client.RfcOper(nickname, password, Priority.Critical);
443 await Login(client, nickname, cancellationToken);
446 throw new InvalidOperationException($
"Invalid IrcPasswordType: {passwordType.Value}");
449 cancellationToken.ThrowIfCancellationRequested();
451 Logger.LogTrace(
"Connection established!");
453 catch (
Exception e) when (e is not OperationCanceledException)
460 protected override async ValueTask
DisconnectImpl(CancellationToken cancellationToken)
464 await Task.Factory.StartNew(
469 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
473 Logger.LogWarning(e,
"Error quitting IRC!");
478 TaskScheduler.Current);
479 await HardDisconnect(cancellationToken);
481 catch (OperationCanceledException)
487 Logger.LogWarning(e,
"Error disconnecting from IRC!");
499 async ValueTask
Login(IrcFeatures client,
string nickname, CancellationToken cancellationToken)
501 var promise =
new TaskCompletionSource<object>();
503 void Callback(
object? sender, EventArgs e)
505 Logger.LogTrace(
"IRC Registered.");
506 promise.TrySetResult(e);
509 client.OnRegistered += Callback;
511 client.Login(nickname, nickname, 0, nickname);
513 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
514 cts.CancelAfter(TimeSpan.FromSeconds(30));
518 await promise.Task.WaitAsync(cts.Token);
519 client.OnRegistered -= Callback;
521 catch (OperationCanceledException)
523 if (client.IsConnected)
525 throw new JobException(
"Timed out waiting for IRC Registration");
536 if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase))
539 var username = e.Data.Nick;
540 var channelName = isPrivate ? username : e.Data.Channel;
542 ulong MapAndGetChannelId(Dictionary<ulong, string?> dicToCheck)
544 ulong? resultId =
null;
545 if (!dicToCheck.Any(x =>
547 if (x.Value != channelName)
553 resultId = channelIdCounter++;
554 dicToCheck.Add(resultId.Value, channelName);
555 if (dicToCheck == queryChannelIdMap)
556 channelIdMap.Add(resultId.Value,
null);
559 return resultId!.Value;
562 ulong userId, channelId;
565 userId = MapAndGetChannelId(
new Dictionary<ulong, string?>(queryChannelIdMap
566 .Cast<KeyValuePair<ulong, string?>>()));
567 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
570 var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName;
575 IsPrivateChannel = isPrivate,
576 EmbedsSupported =
false,
585 EnqueueMessage(message);
612 client.Listen(
false);
616 Logger.LogWarning(ex,
"IRC Non-Blocking Listen Exception!");
620 TaskCreationOptions.None,
621 TaskScheduler.Current)
622 .WaitAsync(cancellationToken);
631 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
632 cancellationToken.ThrowIfCancellationRequested();
634 Logger.LogTrace(
"Logging in...");
635 client.Login(nickname, nickname, 0, nickname);
636 cancellationToken.ThrowIfCancellationRequested();
639 var receivedAck =
false;
640 var receivedPlus =
false;
642 void AuthenticationDelegate(
object sender, ReadLineEventArgs e)
644 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
646 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
650 Logger.LogTrace(
"Performing handshake...");
651 client.OnReadLine += AuthenticationDelegate;
654 using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
655 timeoutCts.CancelAfter(TimeSpan.FromSeconds(25));
656 var timeoutToken = timeoutCts.Token;
658 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
661 await NonBlockingListen(cancellationToken);
663 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
664 timeoutToken.ThrowIfCancellationRequested();
666 for (; !receivedPlus;
668 await NonBlockingListen(cancellationToken);
672 client.OnReadLine -= AuthenticationDelegate;
675 cancellationToken.ThrowIfCancellationRequested();
678 Logger.LogTrace(
"Sending credentials...");
679 var authString = String.Format(
680 CultureInfo.InvariantCulture,
685 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
686 var authLine = $
"AUTHENTICATE {b64}";
687 client.WriteLine(authLine, Priority.Critical);
688 cancellationToken.ThrowIfCancellationRequested();
690 Logger.LogTrace(
"Finishing authentication...");
691 client.WriteLine(
"CAP END", Priority.Critical);
703 Logger.LogTrace(
"Not hard disconnecting, already offline");
707 Logger.LogTrace(
"Hard disconnect");
711 var disconnectTask = Task.Factory.StartNew(
720 Logger.LogWarning(e,
"Error disconnecting IRC!");
725 TaskScheduler.Current);
730 listenTask ?? Task.CompletedTask),
741 var newClient =
new IrcFeatures
743 SupportNonRfc =
true,
744 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
746 AutoRejoinOnKick =
true,
749 AutoReconnect =
false,
750 ActiveChannelSyncing =
true,
751 AutoNickHandling =
true,
752 CtcpVersion = assemblyInfo.VersionString,
754 EnableUTF8Recode =
true,
757 newClient.ValidateServerCertificate =
true;
759 newClient.OnChannelMessage += Client_OnChannelMessage;
760 newClient.OnQueryMessage += Client_OnQueryMessage;
762 if (loggingConfigurationOptions.CurrentValue.ProviderNetworkDebug)
764 newClient.OnReadLine += (sender, e) => Logger.LogTrace(
"READ: {line}", e.Line);
765 newClient.OnWriteLine += (sender, e) => Logger.LogTrace(
"WRITE: {line}", e.Line);
768 newClient.OnError += (sender, e) =>
770 Logger.LogError(
"IRC ERROR: {error}", e.ErrorMessage);
771 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.
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)
ulong channelIdCounter
Id counter for channelIdMap.
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.
readonly IOptionsMonitor< FileLoggingConfiguration > loggingConfigurationOptions
The FileLoggingConfiguration for the IrcProvider.
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.
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.
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