1 /**
2  * A simple IRC client.
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.client;
18 
19 import std.conv;
20 import std.datetime;
21 import std.random;
22 import std.string;
23 
24 import ae.net.asockets;
25 import ae.sys.log;
26 import ae.utils.text;
27 
28 public import ae.net.irc.common;
29 
30 debug(IRC) import std.stdio;
31 
32 /// An IRC client class.
33 class IrcClient
34 {
35 private:
36 	/// The socket this class wraps.
37 	IrcSocket conn;
38 	/// Whether the socket is connected
39 	bool _connected;
40 	/// The password used when logging in.
41 	string password;
42 
43 	/// Helper function for sending a command.
44 	void command(string command, string[] params ...)
45 	{
46 		assert(command.length > 1);
47 		string message = toUpper(command);
48 
49 		while((params.length > 1) && (params[$-1]==null || params[$-1]==""))
50 			params.length = params.length-1;
51 
52 		assert(params.length <= 15);
53 
54 		// SA 2007.08.12: Can't send "PASS ELSILRACLIHP "
55 		//                If we NEED to then the ircd is broken
56 		//                - make "PASS" special if and when this happens.
57 		foreach(i,parameter; params)
58 		{
59 			message ~= " ";
60 			if(parameter.indexOf(" ")!=-1)
61 			{
62 				assert(i == params.length-1);
63 				message ~= ":";
64 			}
65 			message ~= parameter;
66 		}
67 
68 		sendRaw(message);
69 	}
70 
71 	/// Called when a connection has been established.
72 	void onConnect(ClientSocket sender)
73 	{
74 		if (log) log("* Connected.");
75 		if (password.length > 0)
76 			command("PASS", password);
77 		command("NICK", nickname);
78 		command("USER", nickname, "hostname", "servername", realname);
79 	}
80 
81 	/// Called when a connection was closed.
82 	void onDisconnect(ClientSocket sender, string reason, DisconnectType type)
83 	{
84 		if (log) log(format("* Disconnected (%s)", reason));
85 		nickname = realname = null;
86 		password = null;
87 		_connected = false;
88 		if (handleDisconnect)
89 			handleDisconnect(this, reason, type);
90 		channels = null;
91 		users = null;
92 		canonicalChannelNames = canonicalUserNames = null;
93 	}
94 
95 	/// Remove the @+ etc. prefix from a nickname
96 	string removePrefix(string nick)
97 	{
98 		// TODO: do this properly, maybe?
99 		if(nick[0]=='@' || nick[0]=='+')
100 			return nick[1..$];
101 		else
102 			return nick;
103 	}
104 
105 	/// Called when a line has been received.
106 	void onReadLine(LineBufferedSocket sender, string line)
107 	{
108 		line = decoder(line);
109 		if (handleRaw)
110 		{
111 			handleRaw(this, line);
112 			if (line is null)
113 				return;
114 		}
115 		if (log) log("< " ~ line);
116 		string nick, username, hostname;
117 		debug (IRC) std.stdio.writefln("< %s", line);
118 		auto colon = line.indexOf(':');
119 		if (colon == 0)
120 		{
121 			auto space = line.indexOf(' ');
122 			string target = line[1 .. space];
123 			parseTarget(target, nick, username, hostname);
124 			//std.stdio.writefln("%s => %s!%s@%s", target, nick, username, hostname);
125 			nick = canonicalUserName(nick);
126 			auto userptr = nick in users;
127 			if (userptr)
128 			{
129 				userptr.username = username;
130 				userptr.hostname = hostname;
131 			}
132 			line = line[space + 1 .. line.length];
133 			colon = line.indexOf(':');
134 		}
135 
136 		string[] params;
137 		if (colon == -1)
138 			params = split(line);
139 		else
140 		{
141 			params = split(line[0 .. colon]);
142 			params.length = params.length + 1;
143 			params[$-1] = line[colon + 1 .. line.length];
144 		}
145 
146 		string command = toUpper(params[0]);
147 		params = params[1 .. params.length];
148 
149 		// Whenever D supports this, turn this into a
150 		// constant associative array of anonymous functions.
151 		// VP 2006.12.16: for now, moving functions inside the switch, because the code is too cumbersome to read and maintain with each handler in a separate function
152 		switch (command)
153 		{
154 		case "001":     // login successful
155 			// VP 2006.12.13: changing 376 to 001, since 376 doesn't appear on all servers and it's safe to send commands after 001 anyway
156 			_connected = true;
157 			onEnter(nickname, username, hostname, realname); // add ourselves
158 
159 			if (handleConnect)
160 				handleConnect(this);
161 			if (autoWho)
162 				who();  // get info on all users
163 			break;
164 
165 		case "433":     // nickname in use
166 			if (exactNickname)
167 				disconnect("Nickname in use", DisconnectType.Error);
168 			else
169 			{
170 				while (nickname.length > 1 && nickname[$-1] >= '0' && nickname[$-1] <= '9')
171 					nickname = nickname[0..$-1];
172 				if (params[1] == nickname)
173 					nickname ~= format("%03d", uniform(0, 1000));
174 				this.command("NICK", nickname);
175 			}
176 			break;
177 
178 		case "321":     // LIST channel start
179 			channelList = null; // clear list
180 			break;
181 
182 		case "322":     // LIST channel line
183 			channelList ~= params[1];
184 			break;
185 
186 		case "323":     // LIST channel end
187 			if (handleChannelList)
188 				handleChannelList(this, channelList);
189 			break;
190 
191 		case "353":     // NAMES line
192 			string channel = canonicalChannelName(params[$-2]);
193 			assert(channel in channels);
194 			string[] nicks = params[$-1].split(" ");
195 			foreach (fullnick; nicks)
196 				if (fullnick.length>0)
197 				{
198 					auto nickname = removePrefix(fullnick);
199 					if (!(nickname in users))
200 						onEnter(nickname, null, null);
201 					channels[channel].users[nickname] = true;
202 				}
203 			break;
204 
205 		case "366":     // NAMES end
206 			// VP 2007.01.07: perhaps the onJoin handler code for when we join a channel ought to be moved here...
207 			break;
208 
209 		case "352":     // WHO line
210 			//                          0           1 2        3                                  4                   5           6 7
211 			// :wormnet1.team17.com 352 CyberShadow * Username host-86-106-217-211.moldtelecom.md wormnet1.team17.com CyberShadow H :0 40 0 RO
212 			//void delegate(string channel, string username, string host, string server, string name, string flags, string userinfo) handleWho;
213 			while (params.length<8)
214 				params ~= [null];
215 
216 			string[] gecos = params[7].split(" ");
217 			int hopcount = 0;
218 			try
219 				hopcount = to!int(gecos[0]);
220 			catch (Exception e)
221 				hopcount = 0;
222 			if(gecos.length > 1)
223 				gecos = gecos[1..$];
224 			else
225 				gecos = null;
226 
227 			string nickname = params[5];
228 			username = params[2];
229 			hostname = params[3];
230 			auto userptr = nickname in users;
231 			if (userptr)
232 			{
233 				if (userptr.username is null)
234 					userptr.username = username;
235 				else
236 					assert(userptr.username == username, userptr.username ~ " != " ~ username);
237 				if (userptr.hostname is null)
238 					userptr.hostname = hostname;
239 				//else
240 				//	assert(userptr.hostname == hostname, userptr.hostname ~ " != " ~ hostname);
241 				string realname = std..string.join(gecos, " ");
242 				if (userptr.realname is null)
243 					userptr.realname = realname;
244 				else
245 					assert(userptr.realname == realname);
246 			}
247 
248 			if (handleWho)
249 				handleWho(this, params[1],params[2],params[3],params[4],params[5],params[6],hopcount,std..string.join(gecos, " "));
250 			break;
251 
252 		case "315":     // WHO end
253 			if(handleWhoEnd)
254 				handleWhoEnd(this, params.length>=2 ? params[1] : null);
255 			break;
256 
257 		case "437":     // Nick/channel is temporarily unavailable
258 			if (handleUnavailable)
259 				handleUnavailable(this, params[1], params[2]);
260 			break;
261 
262 		case "471":     // Channel full
263 			if (handleChannelFull)
264 				handleChannelFull(this, params[1], params[2]);
265 			break;
266 
267 		case "473":     // Invite only
268 			if (handleInviteOnly)
269 				handleInviteOnly(this, params[1], params[2]);
270 			break;
271 
272 		case "474":     // Banned
273 			if (handleBanned)
274 				handleBanned(this, params[1], params[2]);
275 			break;
276 
277 		case "475":     // Wrong key
278 			if (handleChannelKey)
279 				handleChannelKey(this, params[1], params[2]);
280 			break;
281 
282 		case "PING":
283 			if (params.length == 1)
284 				this.command("PONG", params[0]);
285 			break;
286 
287 		case "PRIVMSG":
288 			if (params.length != 2)
289 				return;
290 
291 			string target = canonicalName(params[0]);
292 			IrcMessageType type = IrcMessageType.NORMAL;
293 			string text = params[1];
294 			if (text.startsWith("\x01ACTION"))
295 			{
296 				type = IrcMessageType.ACTION;
297 				text = text[7 .. $];
298 				if (text.startsWith(" "))
299 					text = text[1..$];
300 				if (text.endsWith("\x01"))
301 					text = text[0..$-1];
302 			}
303 			onMessage(nick, target, text, type);
304 			break;
305 
306 		case "NOTICE":
307 			if (params.length != 2)
308 				return;
309 
310 			string target = canonicalName(params[0]);
311 			onMessage(nick, target, params[1], IrcMessageType.NOTICE);
312 			break;
313 
314 		case "JOIN":
315 			if (params.length != 1)
316 				return;
317 
318 			string channel = canonicalChannelName(params[0]);
319 
320 			if (!(nick in users))
321 			{
322 				onEnter(nick, username, hostname);
323 				if (autoWho)
324 					who(nick);
325 			}
326 			else
327 				users[nick].channelsJoined++;
328 
329 			if (nick == nickname)
330 			{
331 				assert(!(channel in channels));
332 				channels[channel] = Channel();
333 			}
334 			else
335 			{
336 				assert(channel in channels);
337 				channels[channel].users[nick] = true;
338 			}
339 
340 			if (handleJoin)
341 				handleJoin(this, channel, nick);
342 
343 			break;
344 
345 		case "PART":
346 			if (params.length < 1 || params.length > 2)
347 				return;
348 
349 			string channel = canonicalChannelName(params[0]);
350 
351 			if (handlePart)
352 				handlePart(this, channel, nick, params.length == 2 ? params[1] : null);
353 
354 			onUserParted(nick, channel);
355 			break;
356 
357 		case "QUIT":
358 			string[] oldChannels;
359 			foreach (channelName,channel;channels)
360 				if (nick in channel.users)
361 					oldChannels ~= channelName;
362 
363 			if (handleQuit)
364 				handleQuit(this, nick, params.length == 1 ? params[0] : null, oldChannels);
365 
366 			foreach (channel;channels)
367 				if (nick in channel.users)
368 					channel.users.remove(nick);
369 
370 			onLeave(nick);
371 			break;
372 
373 		case "KICK":
374 			if (params.length < 2 || params.length > 3)
375 				return;
376 
377 			string channel = canonicalChannelName(params[0]);
378 
379 			string user = canonicalUserName(params[1]);
380 			if (handleKick)
381 			{
382 				if (params.length == 3)
383 					handleKick(this, channel, user, nick, params[2]);
384 				else
385 					handleKick(this, channel, user, nick, null);
386 			}
387 
388 			onUserParted(user, channel);
389 			break;
390 
391 		case "NICK":
392 			if (params.length != 1)
393 				return;
394 
395 			onNick(nick, params[0]);
396 			break;
397 
398 		default:
399 			break;
400 		}
401 	}
402 
403 	void onUserParted(string nick, string channel)
404 	{
405 		assert(channel in channels);
406 		if (nick == nickname)
407 		{
408 			foreach(user,b;channels[channel].users)
409 				users[user].channelsJoined--;
410 			purgeUsers();
411 			channels.remove(channel);
412 		}
413 		else
414 		{
415 			channels[channel].users.remove(nick);
416 			users[nick].channelsJoined--;
417 			if (users[nick].channelsJoined==0)
418 				onLeave(nick);
419 		}
420 	}
421 
422 	/// Remove users that aren't in any channels
423 	void purgeUsers()
424 	{
425 		throw new Exception("not implemented");
426 	}
427 
428 	void parseTarget(string target, out string nickname, out string username, out string hostname)
429 	{
430 		username = hostname = null;
431 		auto userdelimpos = target.indexOf('!');
432 		if (userdelimpos == -1)
433 			nickname = target;
434 		else
435 		{
436 			nickname = target[0 .. userdelimpos];
437 
438 			auto hostdelimpos = target.indexOf('@');
439 			if (hostdelimpos == -1)
440 				assert(0);
441 			else
442 			{
443 				//bool identified = target[userdelimpos + 1] != '~';
444 				//if (!identified)
445 				//	userdelimpos++;
446 
447 				username = target[userdelimpos + 1 .. hostdelimpos];
448 				hostname = target[hostdelimpos + 1 .. target.length];
449 
450 				//if (hostname == "no.address.for.you") // WormNET hack
451 				//	hostname = null;
452 			}
453 		}
454 	}
455 
456 	void onSocketInactivity(IrcSocket sender)
457 	{
458 		command("PING", to!string(Clock.currTime().toUnixTime()));
459 	}
460 
461 	void onSocketTimeout(IrcSocket sender)
462 	{
463 		disconnect("Time-out", DisconnectType.Error);
464 	}
465 
466 protected: // overridable methods
467 	void onEnter(string nick, string username, string hostname, string realname = null)
468 	{
469 		users[nick] = User(1, username, hostname, realname);
470 		canonicalUserNames[rfc1459toLower(nick)] = nick;
471 		if (handleEnter)
472 			handleEnter(this, nick);
473 	}
474 
475 	void onLeave(string nick)
476 	{
477 		users.remove(nick);
478 		canonicalUserNames.remove(rfc1459toLower(nick));
479 		if (handleLeave)
480 			handleLeave(this, nick);
481 	}
482 
483 	void onNick(string oldNick, string newNick)
484 	{
485 		users[newNick] = users[oldNick];
486 		users.remove(oldNick);
487 		canonicalUserNames.remove(rfc1459toLower(oldNick));
488 		canonicalUserNames[rfc1459toLower(newNick)] = newNick;
489 
490 		foreach (ref channel; channels)
491 			if (oldNick in channel.users)
492 			{
493 				channel.users[newNick] = channel.users[oldNick];
494 				channel.users.remove(oldNick);
495 			}
496 	}
497 
498 	void onMessage(string from, string to, string message, IrcMessageType type)
499 	{
500 		if (handleMessage)
501 			handleMessage(this, from, to, message, type);
502 	}
503 
504 public:
505 	/// The user's information.
506 	string nickname, realname;
507 	/// A list of joined channels.
508 	Channel[string] channels;
509 	/// Canonical names
510 	string[string] canonicalChannelNames, canonicalUserNames;
511 	/// Known user info
512 	User[string] users;
513 	/// Channel list for LIST command
514 	string[] channelList;
515 
516 	/// Whether to automatically send WHO requests
517 	bool autoWho;
518 	/// Log all input/output to this logger.
519 	Logger log;
520 	/// Fail to connect if the specified nickname is taken.
521 	bool exactNickname;
522 	/// How to convert the IRC 8-bit data to and from UTF-8 (D strings must be valid UTF-8).
523 	string function(in char[]) decoder = &rawToUTF8, encoder = &UTF8ToRaw;
524 
525 	struct Channel
526 	{
527 		bool[string] users;
528 	}
529 
530 	struct User
531 	{
532 		int channelsJoined; // acts as a reference count
533 		string username, hostname;
534 		string realname;
535 	}
536 
537 	string canonicalChannelName(string channel)
538 	{
539 		string channelLower = rfc1459toLower(channel);
540 		if (channelLower in canonicalChannelNames)
541 			return canonicalChannelNames[channelLower];
542 		else
543 		{
544 			canonicalChannelNames[channelLower] = channel; // for consistency!
545 			return channel;
546 		}
547 	}
548 
549 	string canonicalUserName(string user)
550 	{
551 		string userLower = rfc1459toLower(user);
552 		if (userLower in canonicalUserNames)
553 			return canonicalUserNames[userLower];
554 		else
555 			return user;
556 	}
557 
558 	string canonicalName(string name)
559 	{
560 		string nameLower = rfc1459toLower(name);
561 		if (name[0]=='#')
562 			if (nameLower in canonicalChannelNames)
563 				return canonicalChannelNames[nameLower];
564 			else
565 				return name;
566 		else
567 			if (nameLower in canonicalUserNames)
568 				return canonicalUserNames[nameLower];
569 			else
570 				return name;
571 	}
572 
573 	string[] getUserChannels(string name)
574 	{
575 		string[] result;
576 		foreach (channelName, ref channel; channels)
577 			if (name in channel.users)
578 				result ~= channelName;
579 		return result;
580 	}
581 
582 
583 	this()
584 	{
585 		conn = new IrcSocket();
586 		conn.delimiter = "\r\n";
587 		conn.handleConnect = &onConnect;
588 		conn.handleDisconnect = &onDisconnect;
589 		conn.handleReadLine = &onReadLine;
590 		conn.handleInactivity = &onSocketInactivity;
591 		conn.handleTimeout = &onSocketTimeout;
592 	}
593 
594 	/// Returns true if the connection was successfully established,
595 	/// and we have authorized ourselves to the server
596 	/// (and can thus join channels, send private messages, etc.)
597 	@property bool connected() { return _connected; }
598 
599 	/// Start establishing a connection to the IRC network.
600 	void connect(string nickname, string realname, string host, ushort port = 6667, string password = null)
601 	{
602 		assert(!connected);
603 		this.nickname = nickname;
604 		this.realname = realname;
605 		this.password = password;
606 		if (log) log(format("* Connecting to %s:%d...", host, port));
607 		conn.connect(host, port);
608 	}
609 
610 	/// Cancel a connection.
611 	void disconnect(string reason = null, DisconnectType type = DisconnectType.Requested)
612 	{
613 		assert(conn.connected);
614 
615 		if (reason)
616 			command("QUIT", reason);
617 		conn.disconnect(reason, type);
618 	}
619 
620 	/// Send raw string to server.
621 	void sendRaw(string message)
622 	{
623 		debug (IRC) std.stdio.writefln("> %s", message);
624 		assert(!message.contains("\n"), "Newline in outgoing IRC line: " ~ message);
625 		if (log) log("> " ~ message);
626 		conn.send(encoder(message));
627 	}
628 
629 	/// Join a channel on the network.
630 	void join(string channel, string password=null)
631 	{
632 		assert(connected);
633 
634 		canonicalChannelNames[rfc1459toLower(channel)] = channel;
635 		if (password.length)
636 			command("JOIN", channel, password);
637 		else
638 			command("JOIN", channel);
639 	}
640 
641 	/// Get a list of channels on the server
642 	void requestChannelList()
643 	{
644 		assert(connected);
645 
646 		channelList = null;  // clear channel list
647 		command("LIST");
648 	}
649 
650 	/// Get a list of logged on users
651 	void who(string mask=null)
652 	{
653 		command("WHO", mask);
654 	}
655 
656 	/// Send a regular message to the target.
657 	void message(string name, string text)
658 	{
659 		command("PRIVMSG", name, text);
660 	}
661 
662 	/// Perform an action for the target.
663 	void action(string name, string text)
664 	{
665 		command("PRIVMSG", name, "\x01" ~ "ACTION " ~ text ~ "\x01");
666 	}
667 
668 	/// Send a notice to the target.
669 	void notice(string name, string text)
670 	{
671 		command("NOTICE", name, text);
672 	}
673 
674 	/// Get/set IRC mode
675 	void mode(string[] params ...)
676 	{
677 		command("MODE", params);
678 	}
679 
680 	/// Callback for received data before it's processed.
681 	void delegate(IrcClient sender, ref string s) handleRaw;
682 
683 	/// Callback for when we have succesfully logged in.
684 	void delegate(IrcClient sender) handleConnect;
685 	/// Callback for when the socket was closed.
686 	void delegate(IrcClient sender, string reason, DisconnectType type) handleDisconnect;
687 	/// Callback for when a message has been received.
688 	void delegate(IrcClient sender, string from, string to, string message, IrcMessageType type) handleMessage;
689 	/// Callback for when someone has joined a channel.
690 	void delegate(IrcClient sender, string channel, string nick) handleJoin;
691 	/// Callback for when someone has left a channel.
692 	void delegate(IrcClient sender, string channel, string nick, string reason) handlePart;
693 	/// Callback for when someone was kicked from a channel.
694 	void delegate(IrcClient sender, string channel, string nick, string op, string reason) handleKick;
695 	/// Callback for when someone has quit from the network.
696 	void delegate(IrcClient sender, string nick, string reason, string[] channels) handleQuit;
697 	/// Callback for when the channel list was retreived
698 	void delegate(IrcClient sender, string[] channelList) handleChannelList;
699 	/// Callback for a WHO result line
700 	void delegate(IrcClient sender, string channel, string username, string host, string server, string name, string flags, int hopcount, string realname) handleWho;
701 	/// Callback for a WHO listing end
702 	void delegate(IrcClient sender, string mask) handleWhoEnd;
703 
704 	/// Callback for when we're banned from a channel.
705 	void delegate(IrcClient sender, string channel, string reason) handleBanned;
706 	/// Callback for when a channel is invite only.
707 	void delegate(IrcClient sender, string channel, string reason) handleInviteOnly;
708 	/// Callback for when a nick/channel is unavailable.
709 	void delegate(IrcClient sender, string what, string reason) handleUnavailable;
710 	/// Callback for when a channel is full.
711 	void delegate(IrcClient sender, string channel, string reason) handleChannelFull;
712 	/// Callback for when a channel needs a key.
713 	void delegate(IrcClient sender, string channel, string reason) handleChannelKey;
714 
715 	/// Callback for when a user enters our sight.
716 	void delegate(IrcClient sender, string nick) handleEnter;
717 	/// Callback for when a user leaves our sight.
718 	void delegate(IrcClient sender, string nick) handleLeave;
719 }