1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
4 using System.Collections.Generic;
9 using System.Threading.Tasks;
22 const int TimeoutSeconds = 5;
25 public override bool Connected => client.IsConnected;
28 public override string BotMention => client.Nickname;
106 ILogger<IrcProvider> logger,
112 uint reconnectInterval,
114 : base(logger, reconnectInterval)
116 if (assemblyInformationProvider == null)
117 throw new ArgumentNullException(nameof(assemblyInformationProvider));
118 this.asyncDelayer = asyncDelayer ??
throw new ArgumentNullException(nameof(asyncDelayer));
120 this.address = address ??
throw new ArgumentNullException(nameof(address));
122 this.nickname = nickname ??
throw new ArgumentNullException(nameof(nickname));
124 if (passwordType.HasValue && password == null)
125 throw new ArgumentNullException(nameof(password));
127 if (password != null && !passwordType.HasValue)
128 throw new ArgumentNullException(nameof(passwordType));
130 this.password = password;
131 this.passwordType = passwordType;
133 client =
new IrcFeatures
135 SupportNonRfc =
true,
136 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
138 AutoRejoinOnKick =
true,
141 AutoRetryLimit = TimeoutSeconds,
142 AutoRetryDelay = TimeoutSeconds,
143 ActiveChannelSyncing =
true,
144 AutoNickHandling =
true,
149 client.ValidateServerCertificate =
true;
151 client.OnChannelMessage += Client_OnChannelMessage;
152 client.OnQueryMessage += Client_OnQueryMessage;
154 channelIdMap =
new Dictionary<ulong, string>();
155 queryChannelIdMap =
new Dictionary<ulong, string>();
156 channelIdCounter = 1;
157 disconnecting =
false;
165 disconnecting =
true;
179 if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
182 var username = e.Data.Nick;
183 var channelName = isPrivate ? username : e.Data.Channel;
185 ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
187 ulong? resultId = null;
188 if (!dicToCheck.Any(x =>
190 if (x.Value != channelName)
196 resultId = channelIdCounter++;
197 dicToCheck.Add(resultId.Value, channelName);
198 if (dicToCheck == queryChannelIdMap)
199 channelIdMap.Add(resultId.Value, null);
202 return resultId.Value;
205 ulong userId, channelId;
208 userId = MapAndGetChannelId(queryChannelIdMap);
209 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
214 Content = e.Data.Message,
219 ConnectionName = address,
220 FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName,
222 IsPrivateChannel = isPrivate
226 FriendlyName = username,
232 EnqueueMessage(message);
240 void Client_OnQueryMessage(
object sender, IrcEventArgs e) => HandleMessage(e,
true);
247 void Client_OnChannelMessage(
object sender, IrcEventArgs e) => HandleMessage(e,
false);
250 public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
252 disconnecting =
false;
256 client.Connect(address, port);
258 cancellationToken.ThrowIfCancellationRequested();
261 client.Login(nickname, nickname, 0, nickname, password);
266 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
267 cancellationToken.ThrowIfCancellationRequested();
270 client.Login(nickname, nickname, 0, nickname);
275 cancellationToken.ThrowIfCancellationRequested();
276 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
281 var recievedAck =
false;
282 var recievedPlus =
false;
283 client.OnReadLine += (sender, e) =>
285 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
287 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
291 var startTime = DateTimeOffset.Now;
292 var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
293 cancellationToken.ThrowIfCancellationRequested();
295 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
296 for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
297 client.Listen(
false);
299 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
300 cancellationToken.ThrowIfCancellationRequested();
302 for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
303 client.Listen(
false);
306 var authString = String.Format(CultureInfo.InvariantCulture,
"{0}{1}{0}{1}{2}", nickname,
'\0', password);
307 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
308 var authLine = String.Format(CultureInfo.InvariantCulture,
"AUTHENTICATE {0}", b64);
309 var chars = authLine.ToCharArray();
310 client.WriteLine(authLine, Priority.Critical);
312 cancellationToken.ThrowIfCancellationRequested();
313 client.WriteLine(
"CAP END", Priority.Critical);
316 client.Listen(
false);
318 listenTask = Task.Factory.StartNew(() =>
320 while (!disconnecting && client.IsConnected && client.Nickname != nickname)
322 client.ListenOnce(true);
323 if (disconnecting || !client.IsConnected)
325 client.Listen(false);
328 if (client.GetIrcUser(nickname) == null)
329 client.RfcNick(nickname);
333 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
335 catch (OperationCanceledException)
341 Logger.LogWarning(
"Unable to connect to IRC: {0}", e);
346 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
349 protected override async Task
DisconnectImpl(CancellationToken cancellationToken)
355 await Task.Factory.StartNew(() =>
359 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
363 Logger.LogWarning(
"Error quitting IRC: {0}", e);
365 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
367 await listenTask.ConfigureAwait(
false);
369 catch (OperationCanceledException)
375 Logger.LogWarning(
"Error disconnecting from IRC! Exception: {0}", e);
380 public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(
381 IEnumerable<ChatChannel> channels,
382 CancellationToken cancellationToken)
383 => Task.Factory.StartNew(() =>
385 if (channels.Any(x => x.IrcChannel == null))
386 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
389 var channelsWithKeys =
new Dictionary<string, string>();
390 var hs =
new HashSet<string>();
391 foreach (var channel
in channels)
393 var name = channel.GetIrcChannelName();
394 var key = channel.GetIrcChannelKey();
395 if (hs.Add(name) && key != null)
396 channelsWithKeys.Add(name, key);
399 var toPart =
new List<string>();
400 foreach (var activeChannel
in client.JoinedChannels)
401 if (!hs.Remove(activeChannel))
402 toPart.Add(activeChannel);
404 foreach (var channelToLeave
in toPart)
405 client.RfcPart(channelToLeave,
"Pretty nice abscond!");
406 foreach (var channelToJoin
in hs)
407 if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
408 client.RfcJoin(channelToJoin, key);
410 client.RfcJoin(channelToJoin);
412 return (IReadOnlyCollection<ChannelRepresentation>)channels
415 var channelName = x.GetIrcChannelName();
417 if (!channelIdMap.Any(y =>
419 if (y.Value != channelName)
425 id = channelIdCounter++;
426 channelIdMap.Add(
id.Value, channelName);
433 ConnectionName = address,
434 FriendlyName = channelIdMap[
id.Value],
435 IsPrivateChannel =
false,
441 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
444 public override Task SendMessage(ulong channelId,
string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
447 message = String.Concat(
449 .Where(x => x !=
'\r')
450 .Select(x => x ==
'\n' ?
'|' : x));
452 var channelName = channelIdMap[channelId];
454 if (channelName == null)
456 channelName = queryChannelIdMap[channelId];
457 sendType = SendType.Notice;
460 sendType = SendType.Message;
463 client.SendMessage(sendType, channelName, message);
467 Logger.LogWarning(
"Unable to send to channel: {0}", e);
469 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
473 Models.RevisionInformation revisionInformation,
474 Version byondVersion,
475 DateTimeOffset? estimatedCompletionTime,
479 bool localCommitPushed,
480 CancellationToken cancellationToken)
482 var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
483 string remoteCommitInsert;
484 if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
486 commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ?
"^{0}" :
"{0}", commitInsert);
487 remoteCommitInsert = String.Empty;
490 remoteCommitInsert = String.Format(CultureInfo.InvariantCulture,
". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
492 var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture,
" (Test Merges: {0})",
493 String.Join(
", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
495 var result = String.Format(CultureInfo.InvariantCulture,
"#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
496 if (x.Comment != null)
497 result += String.Format(CultureInfo.InvariantCulture,
" ({0})", x.Comment);
504 CultureInfo.InvariantCulture,
505 "DM: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
509 byondVersion.Build > 0
510 ? byondVersion.ToString()
511 : $
"{byondVersion.Major}.{byondVersion.Minor}",
512 estimatedCompletionTime.HasValue
513 ? $
" ETA: {estimatedCompletionTime - DateTimeOffset.Now}" 515 cancellationToken).ConfigureAwait(
false);
517 return (errorMessage, dreamMakerOutput) => SendMessage(
519 $
"DM: Deployment {(errorMessage == null ? "complete
" : "failed
")}!",
ulong channelIdCounter
Id counter for channelIdMap
IrcProvider(IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, string address, ushort port, string nickname, string password, IrcPasswordType?passwordType, uint reconnectInterval, bool useSsl)
Construct an IrcProvider
readonly Dictionary< ulong, string > queryChannelIdMap
Map of ChannelRepresentation.RealIds to query users
readonly IrcFeatures client
The IrcFeatures client
readonly ushort port
Port of the server to connect to
Use server authentication
bool IsAdminChannel
If this is considered a channel for admin commands
readonly string address
Address of the server to connect to
readonly Dictionary< ulong, string > channelIdMap
Map of ChannelRepresentation.RealIds to channel names
Represents a message recieved by a IProvider
For waiting asynchronously
void HandleMessage(IrcEventArgs e, bool isPrivate)
Handle an IRC message
bool disconnecting
If we are disconnecting
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the IrcProvider
IProvider for internet relay chat
override async Task DisconnectImpl(CancellationToken cancellationToken)
Gracefully disconnects the provider.
Task listenTask
The Task used for IrcConnection.Listen(bool)
Represents a tgs_chat_user datum
Represents a Providers.IProvider channel.
override async Task< Func< string, string, Task > > SendUpdateMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
readonly IrcPasswordType passwordType
The IrcPasswordType of password
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
readonly string password
Password which will used for authentication
readonly string nickname
IRC nickname