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 
29 debug(IRC) import std.stdio : stderr;
30 
31 /// Types of a chat message.
32 enum IrcMessageType
33 {
34 	NORMAL,  /// PRIVMSG.
35 	ACTION,  /// PRIVMSG with an ACTION CTCP. (TODO remove this, as it is orthogonal to the IRC message type)
36 	NOTICE,  /// NOTICE message.
37 }
38 
39 static assert(toLower("[") == "[" && toUpper("[") == "[");
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 
46 /// RFC1459 case mapping.
47 char rfc1459toLower(char c) pure
48 {
49 	if (c >= 'A' && c <= ']')
50 		c += ('a' - 'A');
51 	return c;
52 }
53 
54 /// ditto
55 char rfc1459toUpper(char c) pure
56 {
57 	if (c >= 'a' && c <= '}')
58 		c -= ('a' - 'A');
59 	return c;
60 }
61 
62 /// ditto
63 string rfc1459toLower(string name) pure
64 {
65 	return name.byChar.map!rfc1459toLower.array;
66 }
67 
68 
69 /// ditto
70 string rfc1459toUpper(string name) pure
71 {
72 	return name.byChar.map!rfc1459toUpper.array;
73 }
74 
75 unittest
76 {
77 	assert(rfc1459toLower("{}|[]\\") == "{}|{}|");
78 	assert(rfc1459toUpper("{}|[]\\") == "[]\\[]\\");
79 }
80 
81 /// Like `icmp`, but honoring RFC1459 case mapping rules.
82 int rfc1459cmp(in char[] a, in char[] b)
83 {
84 	return cmp(a.byChar.map!rfc1459toUpper, b.byChar.map!rfc1459toUpper);
85 }
86 
87 unittest
88 {
89 	assert(rfc1459cmp("{}|[]\\", "[]\\[]\\") == 0);
90 	assert(rfc1459cmp("a", "b") == -1);
91 }
92 
93 /// Base class for an IRC client-server connection.
94 final class IrcConnection
95 {
96 private:
97 	LineBufferedAdapter line;
98 	TimeoutAdapter timer;
99 
100 public:
101 	IConnection conn; /// Underlying transport.
102 	alias conn this;
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 ~ "\r\n"));
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 private:
138 	void onNonIdle()
139 	{
140 		if (pingSent)
141 			pingSent = false;
142 	}
143 
144 	void onReadData(Data data)
145 	{
146 		string line = (cast(string)data.toHeap()).chomp("\r");
147 		debug(IRC) stderr.writeln("< ", line);
148 
149 		if (handleReadLine)
150 			handleReadLine(line);
151 	}
152 
153 	void onIdleTimeout()
154 	{
155 		if (pingSent || handleInactivity is null || conn.state != ConnectionState.connected)
156 		{
157 			if (handleTimeout)
158 				handleTimeout();
159 			else
160 				conn.disconnect("Time-out", DisconnectType.error);
161 		}
162 		else
163 		{
164 			handleInactivity();
165 			pingSent = true;
166 		}
167 	}
168 
169 	bool pingSent;
170 }
171 
172 // TODO: this is server-specific
173 deprecated const string IRC_NICK_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-`";
174 deprecated const string IRC_USER_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
175 deprecated const string IRC_HOST_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.";
176 
177 /// Numeric IRC replies.
178 enum Reply
179 {
180 	RPL_WELCOME             = 001,  ///
181 	RPL_YOURHOST            = 002,  ///
182 	RPL_CREATED             = 003,  ///
183 	RPL_MYINFO              = 004,  ///
184 	RPL_BOUNCE              = 005,  ///
185 	RPL_TRACELINK           = 200,  ///
186 	RPL_TRACECONNECTING     = 201,  ///
187 	RPL_TRACEHANDSHAKE      = 202,  ///
188 	RPL_TRACEUNKNOWN        = 203,  ///
189 	RPL_TRACEOPERATOR       = 204,  ///
190 	RPL_TRACEUSER           = 205,  ///
191 	RPL_TRACESERVER         = 206,  ///
192 	RPL_TRACESERVICE        = 207,  ///
193 	RPL_TRACENEWTYPE        = 208,  ///
194 	RPL_TRACECLASS          = 209,  ///
195 	RPL_TRACERECONNECT      = 210,  ///
196 	RPL_STATSLINKINFO       = 211,  ///
197 	RPL_STATSCOMMANDS       = 212,  ///
198 	RPL_STATSCLINE          = 213,  ///
199 	RPL_STATSNLINE          = 214,  ///
200 	RPL_STATSILINE          = 215,  ///
201 	RPL_STATSKLINE          = 216,  ///
202 	RPL_STATSQLINE          = 217,  ///
203 	RPL_STATSYLINE          = 218,  ///
204 	RPL_ENDOFSTATS          = 219,  ///
205 	RPL_UMODEIS             = 221,  ///
206 	RPL_SERVICEINFO         = 231,  ///
207 	RPL_ENDOFSERVICES       = 232,  ///
208 	RPL_SERVICE             = 233,  ///
209 	RPL_SERVLIST            = 234,  ///
210 	RPL_SERVLISTEND         = 235,  ///
211 	RPL_STATSVLINE          = 240,  ///
212 	RPL_STATSLLINE          = 241,  ///
213 	RPL_STATSUPTIME         = 242,  ///
214 	RPL_STATSOLINE          = 243,  ///
215 	RPL_STATSHLINE          = 244,  ///
216 	RPL_STATSSLINE          = 244,  ///
217 	RPL_STATSPING           = 246,  ///
218 	RPL_STATSBLINE          = 247,  ///
219 	RPL_STATSDLINE          = 250,  ///
220 	RPL_LUSERCLIENT         = 251,  ///
221 	RPL_LUSEROP             = 252,  ///
222 	RPL_LUSERUNKNOWN        = 253,  ///
223 	RPL_LUSERCHANNELS       = 254,  ///
224 	RPL_LUSERME             = 255,  ///
225 	RPL_ADMINME             = 256,  ///
226 	RPL_ADMINLOC1           = 257,  ///
227 	RPL_ADMINLOC2           = 258,  ///
228 	RPL_ADMINEMAIL          = 259,  ///
229 	RPL_TRACELOG            = 261,  ///
230 	RPL_TRACEEND            = 262,  ///
231 	RPL_TRYAGAIN            = 263,  ///
232 	RPL_NONE                = 300,  ///
233 	RPL_AWAY                = 301,  ///
234 	RPL_USERHOST            = 302,  ///
235 	RPL_ISON                = 303,  ///
236 	RPL_UNAWAY              = 305,  ///
237 	RPL_NOWAWAY             = 306,  ///
238 	RPL_WHOISUSER           = 311,  ///
239 	RPL_WHOISSERVER         = 312,  ///
240 	RPL_WHOISOPERATOR       = 313,  ///
241 	RPL_WHOWASUSER          = 314,  ///
242 	RPL_ENDOFWHO            = 315,  ///
243 	RPL_WHOISCHANOP         = 316,  ///
244 	RPL_WHOISIDLE           = 317,  ///
245 	RPL_ENDOFWHOIS          = 318,  ///
246 	RPL_WHOISCHANNELS       = 319,  ///
247 	RPL_LISTSTART           = 321,  ///
248 	RPL_LIST                = 322,  ///
249 	RPL_LISTEND             = 323,  ///
250 	RPL_CHANNELMODEIS       = 324,  ///
251 	RPL_UNIQOPIS            = 325,  ///
252 	RPL_NOTOPIC             = 331,  ///
253 	RPL_TOPIC               = 332,  ///
254 	RPL_INVITING            = 341,  ///
255 	RPL_SUMMONING           = 342,  ///
256 	RPL_INVITELIST          = 346,  ///
257 	RPL_ENDOFINVITELIST     = 347,  ///
258 	RPL_EXCEPTLIST          = 348,  ///
259 	RPL_ENDOFEXCEPTLIST     = 349,  ///
260 	RPL_VERSION             = 351,  ///
261 	RPL_WHOREPLY            = 352,  ///
262 	RPL_NAMREPLY            = 353,  ///
263 	RPL_KILLDONE            = 361,  ///
264 	RPL_CLOSING             = 362,  ///
265 	RPL_CLOSEEND            = 363,  ///
266 	RPL_LINKS               = 364,  ///
267 	RPL_ENDOFLINKS          = 365,  ///
268 	RPL_ENDOFNAMES          = 366,  ///
269 	RPL_BANLIST             = 367,  ///
270 	RPL_ENDOFBANLIST        = 368,  ///
271 	RPL_ENDOFWHOWAS         = 369,  ///
272 	RPL_INFO                = 371,  ///
273 	RPL_MOTD                = 372,  ///
274 	RPL_INFOSTART           = 373,  ///
275 	RPL_ENDOFINFO           = 374,  ///
276 	RPL_MOTDSTART           = 375,  ///
277 	RPL_ENDOFMOTD           = 376,  ///
278 	RPL_YOUREOPER           = 381,  ///
279 	RPL_REHASHING           = 382,  ///
280 	RPL_YOURESERVICE        = 383,  ///
281 	RPL_MYPORTIS            = 384,  ///
282 	RPL_TIME                = 391,  ///
283 	RPL_USERSSTART          = 392,  ///
284 	RPL_USERS               = 393,  ///
285 	RPL_ENDOFUSERS          = 394,  ///
286 	RPL_NOUSERS             = 395,  ///
287 	ERR_NOSUCHNICK          = 401,  ///
288 	ERR_NOSUCHSERVER        = 402,  ///
289 	ERR_NOSUCHCHANNEL       = 403,  ///
290 	ERR_CANNOTSENDTOCHAN    = 404,  ///
291 	ERR_TOOMANYCHANNELS     = 405,  ///
292 	ERR_WASNOSUCHNICK       = 406,  ///
293 	ERR_TOOMANYTARGETS      = 407,  ///
294 	ERR_NOSUCHSERVICE       = 408,  ///
295 	ERR_NOORIGIN            = 409,  ///
296 	ERR_NORECIPIENT         = 411,  ///
297 	ERR_NOTEXTTOSEND        = 412,  ///
298 	ERR_NOTOPLEVEL          = 413,  ///
299 	ERR_WILDTOPLEVEL        = 414,  ///
300 	ERR_BADMASK             = 415,  ///
301 	ERR_UNKNOWNCOMMAND      = 421,  ///
302 	ERR_NOMOTD              = 422,  ///
303 	ERR_NOADMININFO         = 423,  ///
304 	ERR_FILEERROR           = 424,  ///
305 	ERR_NONICKNAMEGIVEN     = 431,  ///
306 	ERR_ERRONEUSNICKNAME    = 432,  ///
307 	ERR_NICKNAMEINUSE       = 433,  ///
308 	ERR_NICKCOLLISION       = 436,  ///
309 	ERR_UNAVAILRESOURCE     = 437,  ///
310 	ERR_USERNOTINCHANNEL    = 441,  ///
311 	ERR_NOTONCHANNEL        = 442,  ///
312 	ERR_USERONCHANNEL       = 443,  ///
313 	ERR_NOLOGIN             = 444,  ///
314 	ERR_SUMMONDISABLED      = 445,  ///
315 	ERR_USERSDISABLED       = 446,  ///
316 	ERR_NOTREGISTERED       = 451,  ///
317 	ERR_NEEDMOREPARAMS      = 461,  ///
318 	ERR_ALREADYREGISTRED    = 462,  ///
319 	ERR_NOPERMFORHOST       = 463,  ///
320 	ERR_PASSWDMISMATCH      = 464,  ///
321 	ERR_YOUREBANNEDCREEP    = 465,  ///
322 	ERR_YOUWILLBEBANNED     = 466,  ///
323 	ERR_KEYSET              = 467,  ///
324 	ERR_CHANNELISFULL       = 471,  ///
325 	ERR_UNKNOWNMODE         = 472,  ///
326 	ERR_INVITEONLYCHAN      = 473,  ///
327 	ERR_BANNEDFROMCHAN      = 474,  ///
328 	ERR_BADCHANNELKEY       = 475,  ///
329 	ERR_BADCHANMASK         = 476,  ///
330 	ERR_NOCHANMODES         = 477,  ///
331 	ERR_BANLISTFULL         = 478,  ///
332 	ERR_NOPRIVILEGES        = 481,  ///
333 	ERR_CHANOPRIVSNEEDED    = 482,  ///
334 	ERR_CANTKILLSERVER      = 483,  ///
335 	ERR_RESTRICTED          = 484,  ///
336 	ERR_UNIQOPPRIVSNEEDED   = 485,  ///
337 	ERR_NOOPERHOST          = 491,  ///
338 	ERR_NOSERVICEHOST       = 492,  ///
339 	ERR_UMODEUNKNOWNFLAG    = 501,  ///
340 	ERR_USERSDONTMATCH      = 502,  ///
341 }