1 /**
2  * Common IRC code.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Stéphan Kochen <stephan@kochen.nl>
12  *   Vladimir Panteleev <ae@cy.md>
13  *   Vincent Povirk <madewokherd@gmail.com>
14  *   Simon Arlott
15  */
16 
17 module ae.net.irc.common;
18 
19 import core.time;
20 import std.algorithm.comparison;
21 import std.algorithm.iteration;
22 import std.array;
23 import std.exception;
24 import std.string;
25 import std.utf;
26 
27 import ae.net.asockets;
28 import ae.utils.array : asBytes;
29 
30 debug(IRC) import std.stdio : stderr;
31 
32 /// Types of a chat message.
33 enum IrcMessageType
34 {
35 	NORMAL,  /// PRIVMSG.
36 	ACTION,  /// PRIVMSG with an ACTION CTCP. (TODO remove this, as it is orthogonal to the IRC message type)
37 	NOTICE,  /// NOTICE message.
38 }
39 
40 static assert(toLower("[") == "[" && toUpper("[") == "[");
41 static assert(toLower("]") == "]" && toUpper("]") == "]");
42 static assert(toLower("{") == "{" && toUpper("{") == "{");
43 static assert(toLower("}") == "}" && toUpper("}") == "}");
44 static assert(toLower("|") == "|" && toUpper("|") == "|");
45 static assert(toLower("\\") == "\\" && toUpper("\\") == "\\");
46 
47 /// RFC1459 case mapping.
48 char rfc1459toLower(char c) pure
49 {
50 	if (c >= 'A' && c <= ']')
51 		c += ('a' - 'A');
52 	return c;
53 }
54 
55 /// ditto
56 char rfc1459toUpper(char c) pure
57 {
58 	if (c >= 'a' && c <= '}')
59 		c -= ('a' - 'A');
60 	return c;
61 }
62 
63 /// ditto
64 string rfc1459toLower(string name) pure
65 {
66 	return name.byChar.map!rfc1459toLower.array;
67 }
68 
69 
70 /// ditto
71 string rfc1459toUpper(string name) pure
72 {
73 	return name.byChar.map!rfc1459toUpper.array;
74 }
75 
76 unittest
77 {
78 	assert(rfc1459toLower("{}|[]\\") == "{}|{}|");
79 	assert(rfc1459toUpper("{}|[]\\") == "[]\\[]\\");
80 }
81 
82 /// Like `icmp`, but honoring RFC1459 case mapping rules.
83 int rfc1459cmp(in char[] a, in char[] b)
84 {
85 	return cmp(a.byChar.map!rfc1459toUpper, b.byChar.map!rfc1459toUpper);
86 }
87 
88 unittest
89 {
90 	assert(rfc1459cmp("{}|[]\\", "[]\\[]\\") == 0);
91 	assert(rfc1459cmp("a", "b") == -1);
92 }
93 
94 /// Base class for an IRC client-server connection.
95 final class IrcConnection
96 {
97 private:
98 	LineBufferedAdapter line;
99 	TimeoutAdapter timer;
100 
101 public:
102 	IConnection conn; /// Underlying transport.
103 
104 	this(IConnection c, size_t maxLineLength = 512)
105 	{
106 		c = line = new LineBufferedAdapter(c);
107 		line.delimiter = "\n";
108 		line.maxLength = maxLineLength;
109 
110 		c = timer = new TimeoutAdapter(c);
111 		timer.setIdleTimeout(90.seconds);
112 		timer.handleIdleTimeout = &onIdleTimeout;
113 		timer.handleNonIdle = &onNonIdle;
114 
115 		conn = c;
116 		conn.handleReadData = &onReadData;
117 	} ///
118 
119 	/// Send `line`, plus a newline.
120 	void send(string line)
121 	{
122 		debug(IRC) stderr.writeln("> ", line);
123 		// Send with \r\n, but support receiving with \n
124 		import ae.sys.data;
125 		conn.send(Data(line.asBytes ~ "\r\n".asBytes));
126 	}
127 
128 	/// Inactivity handler (for sending a `PING` request).
129 	void delegate() handleInactivity;
130 
131 	/// Timeout handler - called if `handleInactivity` was null or did not result in activity.
132 	void delegate() handleTimeout;
133 
134 	/// Data handler.
135 	void delegate(string line) handleReadLine;
136 
137 	/// Forwards to the underlying transport.
138 	@property void handleConnect(IConnection.ConnectHandler value) { conn.handleConnect = value; }
139 	@property void handleDisconnect(IConnection.DisconnectHandler value) { conn.handleDisconnect = value; } /// ditto
140 	void disconnect(string reason = IConnection.defaultDisconnectReason, DisconnectType type = DisconnectType.requested) { conn.disconnect(reason, type); } /// ditto
141 	@property ConnectionState state() { return conn.state; } /// ditto
142 
143 private:
144 	void onNonIdle()
145 	{
146 		if (pingSent)
147 			pingSent = false;
148 	}
149 
150 	void onReadData(Data data)
151 	{
152 		string line = data.asDataOf!char.toGC().chomp("\r");
153 		debug(IRC) stderr.writeln("< ", line);
154 
155 		if (handleReadLine)
156 			handleReadLine(line);
157 	}
158 
159 	void onIdleTimeout()
160 	{
161 		if (pingSent || handleInactivity is null || conn.state != ConnectionState.connected)
162 		{
163 			if (handleTimeout)
164 				handleTimeout();
165 			else
166 				conn.disconnect("Time-out", DisconnectType.error);
167 		}
168 		else
169 		{
170 			handleInactivity();
171 			pingSent = true;
172 		}
173 	}
174 
175 	bool pingSent;
176 }
177 
178 // TODO: this is server-specific
179 deprecated const string IRC_NICK_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-`";
180 deprecated const string IRC_USER_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
181 deprecated const string IRC_HOST_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.";
182 
183 /// Numeric IRC replies.
184 enum Reply
185 {
186 	RPL_WELCOME             = 001,  ///
187 	RPL_YOURHOST            = 002,  ///
188 	RPL_CREATED             = 003,  ///
189 	RPL_MYINFO              = 004,  ///
190 	RPL_BOUNCE              = 005,  ///
191 	RPL_TRACELINK           = 200,  ///
192 	RPL_TRACECONNECTING     = 201,  ///
193 	RPL_TRACEHANDSHAKE      = 202,  ///
194 	RPL_TRACEUNKNOWN        = 203,  ///
195 	RPL_TRACEOPERATOR       = 204,  ///
196 	RPL_TRACEUSER           = 205,  ///
197 	RPL_TRACESERVER         = 206,  ///
198 	RPL_TRACESERVICE        = 207,  ///
199 	RPL_TRACENEWTYPE        = 208,  ///
200 	RPL_TRACECLASS          = 209,  ///
201 	RPL_TRACERECONNECT      = 210,  ///
202 	RPL_STATSLINKINFO       = 211,  ///
203 	RPL_STATSCOMMANDS       = 212,  ///
204 	RPL_STATSCLINE          = 213,  ///
205 	RPL_STATSNLINE          = 214,  ///
206 	RPL_STATSILINE          = 215,  ///
207 	RPL_STATSKLINE          = 216,  ///
208 	RPL_STATSQLINE          = 217,  ///
209 	RPL_STATSYLINE          = 218,  ///
210 	RPL_ENDOFSTATS          = 219,  ///
211 	RPL_UMODEIS             = 221,  ///
212 	RPL_SERVICEINFO         = 231,  ///
213 	RPL_ENDOFSERVICES       = 232,  ///
214 	RPL_SERVICE             = 233,  ///
215 	RPL_SERVLIST            = 234,  ///
216 	RPL_SERVLISTEND         = 235,  ///
217 	RPL_STATSVLINE          = 240,  ///
218 	RPL_STATSLLINE          = 241,  ///
219 	RPL_STATSUPTIME         = 242,  ///
220 	RPL_STATSOLINE          = 243,  ///
221 	RPL_STATSHLINE          = 244,  ///
222 	RPL_STATSSLINE          = 244,  ///
223 	RPL_STATSPING           = 246,  ///
224 	RPL_STATSBLINE          = 247,  ///
225 	RPL_STATSDLINE          = 250,  ///
226 	RPL_LUSERCLIENT         = 251,  ///
227 	RPL_LUSEROP             = 252,  ///
228 	RPL_LUSERUNKNOWN        = 253,  ///
229 	RPL_LUSERCHANNELS       = 254,  ///
230 	RPL_LUSERME             = 255,  ///
231 	RPL_ADMINME             = 256,  ///
232 	RPL_ADMINLOC1           = 257,  ///
233 	RPL_ADMINLOC2           = 258,  ///
234 	RPL_ADMINEMAIL          = 259,  ///
235 	RPL_TRACELOG            = 261,  ///
236 	RPL_TRACEEND            = 262,  ///
237 	RPL_TRYAGAIN            = 263,  ///
238 	RPL_NONE                = 300,  ///
239 	RPL_AWAY                = 301,  ///
240 	RPL_USERHOST            = 302,  ///
241 	RPL_ISON                = 303,  ///
242 	RPL_UNAWAY              = 305,  ///
243 	RPL_NOWAWAY             = 306,  ///
244 	RPL_WHOISUSER           = 311,  ///
245 	RPL_WHOISSERVER         = 312,  ///
246 	RPL_WHOISOPERATOR       = 313,  ///
247 	RPL_WHOWASUSER          = 314,  ///
248 	RPL_ENDOFWHO            = 315,  ///
249 	RPL_WHOISCHANOP         = 316,  ///
250 	RPL_WHOISIDLE           = 317,  ///
251 	RPL_ENDOFWHOIS          = 318,  ///
252 	RPL_WHOISCHANNELS       = 319,  ///
253 	RPL_LISTSTART           = 321,  ///
254 	RPL_LIST                = 322,  ///
255 	RPL_LISTEND             = 323,  ///
256 	RPL_CHANNELMODEIS       = 324,  ///
257 	RPL_UNIQOPIS            = 325,  ///
258 	RPL_NOTOPIC             = 331,  ///
259 	RPL_TOPIC               = 332,  ///
260 	RPL_INVITING            = 341,  ///
261 	RPL_SUMMONING           = 342,  ///
262 	RPL_INVITELIST          = 346,  ///
263 	RPL_ENDOFINVITELIST     = 347,  ///
264 	RPL_EXCEPTLIST          = 348,  ///
265 	RPL_ENDOFEXCEPTLIST     = 349,  ///
266 	RPL_VERSION             = 351,  ///
267 	RPL_WHOREPLY            = 352,  ///
268 	RPL_NAMREPLY            = 353,  ///
269 	RPL_KILLDONE            = 361,  ///
270 	RPL_CLOSING             = 362,  ///
271 	RPL_CLOSEEND            = 363,  ///
272 	RPL_LINKS               = 364,  ///
273 	RPL_ENDOFLINKS          = 365,  ///
274 	RPL_ENDOFNAMES          = 366,  ///
275 	RPL_BANLIST             = 367,  ///
276 	RPL_ENDOFBANLIST        = 368,  ///
277 	RPL_ENDOFWHOWAS         = 369,  ///
278 	RPL_INFO                = 371,  ///
279 	RPL_MOTD                = 372,  ///
280 	RPL_INFOSTART           = 373,  ///
281 	RPL_ENDOFINFO           = 374,  ///
282 	RPL_MOTDSTART           = 375,  ///
283 	RPL_ENDOFMOTD           = 376,  ///
284 	RPL_YOUREOPER           = 381,  ///
285 	RPL_REHASHING           = 382,  ///
286 	RPL_YOURESERVICE        = 383,  ///
287 	RPL_MYPORTIS            = 384,  ///
288 	RPL_TIME                = 391,  ///
289 	RPL_USERSSTART          = 392,  ///
290 	RPL_USERS               = 393,  ///
291 	RPL_ENDOFUSERS          = 394,  ///
292 	RPL_NOUSERS             = 395,  ///
293 	ERR_NOSUCHNICK          = 401,  ///
294 	ERR_NOSUCHSERVER        = 402,  ///
295 	ERR_NOSUCHCHANNEL       = 403,  ///
296 	ERR_CANNOTSENDTOCHAN    = 404,  ///
297 	ERR_TOOMANYCHANNELS     = 405,  ///
298 	ERR_WASNOSUCHNICK       = 406,  ///
299 	ERR_TOOMANYTARGETS      = 407,  ///
300 	ERR_NOSUCHSERVICE       = 408,  ///
301 	ERR_NOORIGIN            = 409,  ///
302 	ERR_NORECIPIENT         = 411,  ///
303 	ERR_NOTEXTTOSEND        = 412,  ///
304 	ERR_NOTOPLEVEL          = 413,  ///
305 	ERR_WILDTOPLEVEL        = 414,  ///
306 	ERR_BADMASK             = 415,  ///
307 	ERR_UNKNOWNCOMMAND      = 421,  ///
308 	ERR_NOMOTD              = 422,  ///
309 	ERR_NOADMININFO         = 423,  ///
310 	ERR_FILEERROR           = 424,  ///
311 	ERR_NONICKNAMEGIVEN     = 431,  ///
312 	ERR_ERRONEUSNICKNAME    = 432,  ///
313 	ERR_NICKNAMEINUSE       = 433,  ///
314 	ERR_NICKCOLLISION       = 436,  ///
315 	ERR_UNAVAILRESOURCE     = 437,  ///
316 	ERR_USERNOTINCHANNEL    = 441,  ///
317 	ERR_NOTONCHANNEL        = 442,  ///
318 	ERR_USERONCHANNEL       = 443,  ///
319 	ERR_NOLOGIN             = 444,  ///
320 	ERR_SUMMONDISABLED      = 445,  ///
321 	ERR_USERSDISABLED       = 446,  ///
322 	ERR_NOTREGISTERED       = 451,  ///
323 	ERR_NEEDMOREPARAMS      = 461,  ///
324 	ERR_ALREADYREGISTRED    = 462,  ///
325 	ERR_NOPERMFORHOST       = 463,  ///
326 	ERR_PASSWDMISMATCH      = 464,  ///
327 	ERR_YOUREBANNEDCREEP    = 465,  ///
328 	ERR_YOUWILLBEBANNED     = 466,  ///
329 	ERR_KEYSET              = 467,  ///
330 	ERR_CHANNELISFULL       = 471,  ///
331 	ERR_UNKNOWNMODE         = 472,  ///
332 	ERR_INVITEONLYCHAN      = 473,  ///
333 	ERR_BANNEDFROMCHAN      = 474,  ///
334 	ERR_BADCHANNELKEY       = 475,  ///
335 	ERR_BADCHANMASK         = 476,  ///
336 	ERR_NOCHANMODES         = 477,  ///
337 	ERR_BANLISTFULL         = 478,  ///
338 	ERR_NOPRIVILEGES        = 481,  ///
339 	ERR_CHANOPRIVSNEEDED    = 482,  ///
340 	ERR_CANTKILLSERVER      = 483,  ///
341 	ERR_RESTRICTED          = 484,  ///
342 	ERR_UNIQOPPRIVSNEEDED   = 485,  ///
343 	ERR_NOOPERHOST          = 491,  ///
344 	ERR_NOSERVICEHOST       = 492,  ///
345 	ERR_UMODEUNKNOWNFLAG    = 501,  ///
346 	ERR_USERSDONTMATCH      = 502,  ///
347 }