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