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