1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
4 using System.Collections.Generic;
9 using System.Threading.Tasks;
20 const int TimeoutSeconds = 5;
23 public override bool Connected => client.IsConnected;
26 public override string BotMention => client.Nickname;
36 readonly ILogger<IrcProvider>
logger;
103 if (application == null)
104 throw new ArgumentNullException(nameof(application));
105 this.asyncDelayer = asyncDelayer ??
throw new ArgumentNullException(nameof(asyncDelayer));
106 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
108 this.address = address ??
throw new ArgumentNullException(nameof(address));
110 this.nickname = nickname ??
throw new ArgumentNullException(nameof(nickname));
112 if (passwordType.HasValue && password == null)
113 throw new ArgumentNullException(nameof(password));
115 if (password != null && !passwordType.HasValue)
116 throw new ArgumentNullException(nameof(passwordType));
118 this.password = password;
119 this.passwordType = passwordType;
121 client =
new IrcFeatures
123 SupportNonRfc =
true,
124 CtcpUserInfo =
"You are going to play. And I am going to watch. And everything will be just fine...",
126 AutoRejoinOnKick =
true,
129 AutoRetryLimit = TimeoutSeconds,
130 AutoRetryDelay = TimeoutSeconds,
131 ActiveChannelSyncing =
true,
132 AutoNickHandling =
true,
137 client.ValidateServerCertificate =
true;
139 client.OnChannelMessage += Client_OnChannelMessage;
140 client.OnQueryMessage += Client_OnQueryMessage;
142 channelIdMap =
new Dictionary<ulong, string>();
143 queryChannelIdMap =
new Dictionary<ulong, string>();
144 channelIdCounter = 1;
145 disconnecting =
false;
153 disconnecting =
true;
165 if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
168 var username = e.Data.Nick;
169 var channelName = isPrivate ? username : e.Data.Channel;
171 ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
173 ulong? resultId = null;
174 if (!dicToCheck.Any(x =>
176 if (x.Value != channelName)
182 resultId = channelIdCounter++;
183 dicToCheck.Add(resultId.Value, channelName);
184 if (dicToCheck == queryChannelIdMap)
185 channelIdMap.Add(resultId.Value, null);
187 return resultId.Value;
190 ulong userId, channelId;
193 userId = MapAndGetChannelId(queryChannelIdMap);
194 channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
199 Content = e.Data.Message,
204 ConnectionName = address,
205 FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture,
"PM: {0}", channelName) : channelName,
207 IsPrivateChannel = isPrivate
210 FriendlyName = username,
216 EnqueueMessage(message);
224 void Client_OnQueryMessage(
object sender, IrcEventArgs e) => HandleMessage(e,
true);
231 void Client_OnChannelMessage(
object sender, IrcEventArgs e) => HandleMessage(e,
false);
234 public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
236 disconnecting =
false;
240 client.Connect(address, port);
242 cancellationToken.ThrowIfCancellationRequested();
245 client.Login(nickname, nickname, 0, nickname, password);
250 client.WriteLine(
"CAP REQ :sasl", Priority.Critical);
251 cancellationToken.ThrowIfCancellationRequested();
253 client.Login(nickname, nickname, 0, nickname);
258 cancellationToken.ThrowIfCancellationRequested();
259 client.SendMessage(SendType.Message,
"NickServ", String.Format(CultureInfo.InvariantCulture,
"IDENTIFY {0}", password));
264 var recievedAck =
false;
265 var recievedPlus =
false;
266 client.OnReadLine += (sender, e) =>
268 if (e.Line.Contains(
"ACK :sasl", StringComparison.Ordinal))
270 else if (e.Line.Contains(
"AUTHENTICATE +", StringComparison.Ordinal))
274 var startTime = DateTimeOffset.Now;
275 var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
276 cancellationToken.ThrowIfCancellationRequested();
278 var listenTimeSpan = TimeSpan.FromMilliseconds(10);
279 for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
280 client.Listen(
false);
282 client.WriteLine(
"AUTHENTICATE PLAIN", Priority.Critical);
283 cancellationToken.ThrowIfCancellationRequested();
285 for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
286 client.Listen(
false);
289 var authString = String.Format(CultureInfo.InvariantCulture,
"{0}{1}{0}{1}{2}", nickname,
'\0', password);
290 var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
291 var authLine = String.Format(CultureInfo.InvariantCulture,
"AUTHENTICATE {0}", b64);
292 var chars = authLine.ToCharArray();
293 client.WriteLine(authLine, Priority.Critical);
295 cancellationToken.ThrowIfCancellationRequested();
296 client.WriteLine(
"CAP END", Priority.Critical);
299 client.Listen(
false);
301 listenTask = Task.Factory.StartNew(() =>
303 while (!disconnecting && client.IsConnected && client.Nickname != nickname)
305 client.ListenOnce(true);
306 if (disconnecting || !client.IsConnected)
308 client.Listen(false);
310 if (client.GetIrcUser(nickname) == null)
311 client.RfcNick(nickname);
314 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
316 catch (OperationCanceledException)
322 logger.LogWarning(
"Unable to connect to IRC: {0}", e);
326 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
329 public override async Task
Disconnect(CancellationToken cancellationToken)
335 await Task.Factory.StartNew(() =>
339 client.RfcQuit(
"Mr. Stark, I don't feel so good...", Priority.Critical);
343 logger.LogWarning(
"Error quitting IRC: {0}", e);
345 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
347 await listenTask.ConfigureAwait(
false);
349 catch (OperationCanceledException)
355 logger.LogWarning(
"Error disconnecting from IRC! Exception: {0}", e);
360 public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
362 if (channels.Any(x => x.IrcChannel == null))
363 throw new InvalidOperationException(
"ChatChannel missing IrcChannel!");
366 var hs =
new HashSet<string>();
367 foreach (var I
in channels)
368 hs.Add(I.IrcChannel);
369 var toPart =
new List<string>();
370 foreach (var I
in client.JoinedChannels)
374 foreach (var I
in toPart)
375 client.RfcPart(I,
"Pretty nice abscond!");
376 foreach (var I
in hs)
379 return (IReadOnlyList<Channel>)channels.Select(x =>
382 if (!channelIdMap.Any(y =>
384 if (y.Value != x.IrcChannel)
390 id = channelIdCounter++;
391 channelIdMap.Add(
id.Value, x.IrcChannel);
397 ConnectionName = address,
398 FriendlyName = channelIdMap[
id.Value],
399 IsPrivateChannel =
false,
404 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
407 public override Task SendMessage(ulong channelId,
string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
409 var channelName = channelIdMap[channelId];
411 if (channelName == null)
413 channelName = queryChannelIdMap[channelId];
414 sendType = SendType.Notice;
417 sendType = SendType.Message;
420 client.SendMessage(sendType, channelName, message);
424 logger.LogWarning(
"Unable to send to channel: {0}", e);
426 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
ulong channelIdCounter
Id counter for channelIdMap
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
readonly IrcFeatures client
The IrcFeatures client
readonly ushort port
Port of the server to connect to
Configures the ASP.NET Core web application
Use server authentication
readonly ILogger< IrcProvider > logger
The ILogger for the IrcProvider
readonly string address
Address of the server to connect to
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
readonly Dictionary< ulong, string > channelIdMap
Map of Channel.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
Task listenTask
The Task used for IrcConnection.Listen(bool)
Represents a Providers.IProvider channel
readonly IrcPasswordType passwordType
The IrcPasswordType of password
Represents a tgs_chat_user datum
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
string VersionString
A more verbose version of Version
bool IsAdminChannel
If this is considered a channel for admin commands
readonly string password
Password which will used for authentication
IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, string address, ushort port, string nickname, string password, IrcPasswordType?passwordType, bool useSsl)
Construct an IrcProvider
readonly string nickname
IRC nickname