Give Techbot the ability to check for and ghost existing bots, then rename itself
[reactos.git] / irc / TechBot / TechBot.IRCLibrary / IrcClient.cs
1 using System;
2 using System.IO;
3 using System.Text;
4 using System.Collections;
5 using System.Net.Sockets;
6
7 namespace TechBot.IRCLibrary
8 {
9 /// <summary>
10 /// Delegate that delivers an IRC message.
11 /// </summary>
12 public delegate void MessageReceivedHandler(IrcMessage message);
13
14 /// <summary>
15 /// Delegate that notifies if the user database for a channel has changed.
16 /// </summary>
17 public delegate void ChannelUserDatabaseChangedHandler(IrcChannel channel);
18
19 public delegate void OnConnectHandler ();
20 public delegate void OnDisconnectHandler();
21 public delegate void OnConnectionLostHandler();
22
23 /// <summary>
24 /// An IRC client.
25 /// </summary>
26 public class IrcClient
27 {
28 /// <summary>
29 /// Monitor when an IRC command is received.
30 /// </summary>
31 private class IrcCommandEventRegistration
32 {
33 /// <summary>
34 /// IRC command to monitor.
35 /// </summary>
36 private string command;
37 public string Command
38 {
39 get
40 {
41 return command;
42 }
43 }
44
45 /// <summary>
46 /// Handler to call when command is received.
47 /// </summary>
48 private MessageReceivedHandler handler;
49 public MessageReceivedHandler Handler
50 {
51 get
52 {
53 return handler;
54 }
55 }
56
57 /// <summary>
58 /// Constructor.
59 /// </summary>
60 /// <param name="command">IRC command to monitor.</param>
61 /// <param name="handler">Handler to call when command is received.</param>
62 public IrcCommandEventRegistration(string command,
63 MessageReceivedHandler handler)
64 {
65 this.command = command;
66 this.handler = handler;
67 }
68 }
69
70
71
72 /// <summary>
73 /// A buffer to store lines of text.
74 /// </summary>
75 private class LineBuffer
76 {
77 /// <summary>
78 /// Full lines of text in buffer.
79 /// </summary>
80 private ArrayList strings;
81
82 /// <summary>
83 /// Part of the last line of text in buffer.
84 /// </summary>
85 private string left = "";
86
87 /// <summary>
88 /// Standard constructor.
89 /// </summary>
90 public LineBuffer()
91 {
92 strings = new ArrayList();
93 }
94
95 /// <summary>
96 /// Return true if there is a complete line in the buffer or false if there is not.
97 /// </summary>
98 public bool DataAvailable
99 {
100 get
101 {
102 return (strings.Count > 0);
103 }
104 }
105
106 /// <summary>
107 /// Return next complete line in buffer or null if none exists.
108 /// </summary>
109 /// <returns>Next complete line in buffer or null if none exists.</returns>
110 public string Read()
111 {
112 if (DataAvailable)
113 {
114 string line = strings[0] as string;
115 strings.RemoveAt(0);
116 return line;
117 }
118 else
119 {
120 return null;
121 }
122 }
123
124 /// <summary>
125 /// Write a string to buffer splitting it into lines.
126 /// </summary>
127 /// <param name="data"></param>
128 public void Write(string data)
129 {
130 data = left + data;
131 left = "";
132 string[] sa = data.Split(new char[] { '\n' });
133 if (sa.Length <= 0)
134 {
135 left = data;
136 return;
137 }
138 else
139 {
140 left = "";
141 }
142 for (int i = 0; i < sa.Length; i++)
143 {
144 if (i < sa.Length - 1)
145 {
146 /* This is a complete line. Remove any \r characters at the end of the line. */
147 string line = sa[i].TrimEnd(new char[] { '\r', '\n'});
148 /* Silently ignore empty lines */
149 if (!line.Equals(String.Empty))
150 {
151 strings.Add(line);
152 }
153 }
154 else
155 {
156 /* This may be a partial line. */
157 left = sa[i];
158 }
159 }
160 }
161 }
162
163
164 /// <summary>
165 /// State for asynchronous reads.
166 /// </summary>
167 private class StateObject
168 {
169 /// <summary>
170 /// Network stream where data is read from.
171 /// </summary>
172 public NetworkStream Stream;
173
174 /// <summary>
175 /// Buffer where data is put.
176 /// </summary>
177 public byte[] Buffer;
178
179 /// <summary>
180 /// Constructor.
181 /// </summary>
182 /// <param name="stream">Network stream where data is read from.</param>
183 /// <param name="buffer">Buffer where data is put.</param>
184 public StateObject(NetworkStream stream, byte[] buffer)
185 {
186 this.Stream = stream;
187 this.Buffer = buffer;
188 }
189 }
190
191
192 #region Private fields
193 private bool firstPingReceived = false;
194 private bool awaitingGhostDeath = false;
195 private System.Text.Encoding encoding = System.Text.Encoding.UTF8;
196 private TcpClient tcpClient;
197 private NetworkStream networkStream;
198 private bool connected = false;
199 private LineBuffer messageStream;
200 private ArrayList ircCommandEventRegistrations = new ArrayList();
201 private ArrayList channels = new ArrayList();
202 private string reqNickname;
203 private string curNickname;
204 private string password;
205 #endregion
206
207 #region Public events
208
209 public event MessageReceivedHandler MessageReceived;
210
211 public event ChannelUserDatabaseChangedHandler ChannelUserDatabaseChanged;
212
213 public event OnConnectHandler OnConnect;
214 public event OnConnectionLostHandler OnConnectionLost;
215 public event OnDisconnectHandler OnDisconnect;
216
217 #endregion
218
219 #region Public properties
220
221 /// <summary>
222 /// Encoding used.
223 /// </summary>
224 public System.Text.Encoding Encoding
225 {
226 get
227 {
228 return encoding;
229 }
230 set
231 {
232 encoding = value;
233 }
234 }
235
236 /// <summary>
237 /// List of joined channels.
238 /// </summary>
239 public ArrayList Channels
240 {
241 get
242 {
243 return channels;
244 }
245 }
246
247 /// <summary>
248 /// Nickname for the bot.
249 /// </summary>
250 public string Nickname
251 {
252 get
253 {
254 return curNickname;
255 }
256 }
257 #endregion
258
259 #region Private methods
260
261 /// <summary>
262 /// Signal MessageReceived event.
263 /// </summary>
264 /// <param name="message">Message that was received.</param>
265 private void OnMessageReceived(IrcMessage message)
266 {
267 foreach (IrcCommandEventRegistration icre in ircCommandEventRegistrations)
268 {
269 if (message.Command.ToLower().Equals(icre.Command.ToLower()))
270 {
271 icre.Handler(message);
272 }
273 }
274 if (MessageReceived != null)
275 {
276 MessageReceived(message);
277 }
278 }
279
280 /// <summary>
281 /// Signal ChannelUserDatabaseChanged event.
282 /// </summary>
283 /// <param name="channel">Message that was received.</param>
284 private void OnChannelUserDatabaseChanged(IrcChannel channel)
285 {
286 if (ChannelUserDatabaseChanged != null)
287 {
288 ChannelUserDatabaseChanged(channel);
289 }
290 }
291
292 /// <summary>
293 /// Start an asynchronous read.
294 /// </summary>
295 private void Receive()
296 {
297 if ((networkStream != null) && (networkStream.CanRead))
298 {
299 byte[] buffer = new byte[1024];
300 networkStream.BeginRead(buffer, 0, buffer.Length,
301 new AsyncCallback(ReadComplete),
302 new StateObject(networkStream, buffer));
303 }
304 else
305 {
306 throw new Exception("Socket is closed.");
307 }
308 }
309
310 /// <summary>
311 /// Asynchronous read has completed.
312 /// </summary>
313 /// <param name="ar">IAsyncResult object.</param>
314 private void ReadComplete(IAsyncResult ar)
315 {
316 try
317 {
318 StateObject stateObject = (StateObject)ar.AsyncState;
319 if (stateObject.Stream.CanRead)
320 {
321 int bytesReceived = stateObject.Stream.EndRead(ar);
322 if (bytesReceived > 0)
323 {
324 messageStream.Write(Encoding.GetString(stateObject.Buffer, 0, bytesReceived));
325 while (messageStream.DataAvailable)
326 {
327 OnMessageReceived(new IrcMessage(messageStream.Read()));
328 }
329 }
330 }
331
332 Receive();
333 }
334 catch (SocketException)
335 {
336 if (OnConnectionLost != null)
337 OnConnectionLost();
338 }
339 catch (IOException)
340 {
341 if (OnConnectionLost != null)
342 OnConnectionLost();
343 }
344 catch (Exception)
345 {
346 if (OnConnectionLost != null)
347 OnConnectionLost();
348 }
349 }
350
351 /// <summary>
352 /// Locate channel.
353 /// </summary>
354 /// <param name="name">Channel name.</param>
355 /// <returns>Channel or null if none was found.</returns>
356 private IrcChannel LocateChannel(string name)
357 {
358 foreach (IrcChannel channel in Channels)
359 {
360 if (name.ToLower().Equals(channel.Name.ToLower()))
361 {
362 return channel;
363 }
364 }
365 return null;
366 }
367
368 /// <summary>
369 /// Send a PONG message when a PING message is received.
370 /// </summary>
371 /// <param name="message">Received IRC message.</param>
372 private void PingMessageReceived(IrcMessage message)
373 {
374 SendMessage(new IrcMessage(IRC.PONG, message.Parameters));
375 firstPingReceived = true;
376 }
377
378 /// <summary>
379 /// Send a PONG message when a PING message is received.
380 /// </summary>
381 /// <param name="message">Received IRC message.</param>
382 private void NoticeMessageReceived(IrcMessage message)
383 {
384 if (awaitingGhostDeath)
385 {
386 string str = string.Format("\ 2{0}\ 2 has been ghosted", reqNickname);
387 if (message.Parameters.Contains(str))
388 {
389 ChangeNick(reqNickname);
390 SubmitPassword(password);
391 awaitingGhostDeath = false;
392 }
393 }
394 }
395
396 /// <summary>
397 /// Process RPL_NAMREPLY message.
398 /// </summary>
399 /// <param name="message">Received IRC message.</param>
400 private void RPL_NAMREPLYMessageReceived(IrcMessage message)
401 {
402 try
403 {
404 // :Oslo2.NO.EU.undernet.org 353 E101 = #E101 :E101 KongFu_uK @Exception
405 /* "( "=" / "*" / "@" ) <channel>
406 :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
407 - "@" is used for secret channels, "*" for private
408 channels, and "=" for others (public channels). */
409 if (message.Parameters == null)
410 {
411 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
412 return;
413 }
414 string[] parameters = message.Parameters.Split(new char[] { ' '});
415 if (parameters.Length < 5)
416 {
417 System.Diagnostics.Debug.WriteLine(String.Format("{0} is two few parameters.", parameters.Length));
418 return;
419 }
420 IrcChannelType type;
421 switch (parameters[1])
422 {
423 case "=":
424 type = IrcChannelType.Public;
425 break;
426 case "*":
427 type = IrcChannelType.Private;
428 break;
429 case "@":
430 type = IrcChannelType.Secret;
431 break;
432 default:
433 type = IrcChannelType.Public;
434 break;
435 }
436 IrcChannel channel = LocateChannel(parameters[2].Substring(1));
437 if (channel == null)
438 {
439 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
440 parameters[2].Substring(1)));
441 return;
442 }
443 string nickname = parameters[3];
444 if (nickname[0] != ':')
445 {
446 System.Diagnostics.Debug.WriteLine(String.Format("String should start with : and not {0}.", nickname[0]));
447 return;
448 }
449 /* Skip : */
450 IrcUser user = channel.LocateUser(nickname.Substring(1));
451 if (user == null)
452 {
453 user = new IrcUser(this,
454 nickname.Substring(1));
455 channel.Users.Add(user);
456 }
457 for (int i = 4; i < parameters.Length; i++)
458 {
459 nickname = parameters[i];
460 user = channel.LocateUser(nickname);
461 if (user == null)
462 {
463 user = new IrcUser(this,
464 nickname);
465 channel.Users.Add(user);
466 }
467 }
468 }
469 catch (Exception ex)
470 {
471 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
472 }
473 }
474
475 /// <summary>
476 /// Process RPL_ENDOFNAMES message.
477 /// </summary>
478 /// <param name="message">Received IRC message.</param>
479 private void RPL_ENDOFNAMESMessageReceived(IrcMessage message)
480 {
481 try
482 {
483 /* <channel> :End of NAMES list */
484 if (message.Parameters == null)
485 {
486 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
487 return;
488 }
489
490 string[] parameters = message.Parameters.Split(new char[] { ' ' });
491 IrcChannel channel = LocateChannel(parameters[1].Substring(1));
492 if (channel == null)
493 {
494 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
495 parameters[0].Substring(1)));
496 return;
497 }
498
499 OnChannelUserDatabaseChanged(channel);
500 }
501 catch (Exception ex)
502 {
503 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
504 }
505 }
506
507 /// <summary>
508 /// Process ERR_NICKNAMEINUSE message.
509 /// </summary>
510 /// <param name="message">Received IRC message.</param>
511 private void ERR_NICKNAMEINUSEMessageReceived(IrcMessage message)
512 {
513 try
514 {
515 if (message.Parameters == null)
516 {
517 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
518 return;
519 }
520
521 /* Connect with a different name */
522 string[] parameters = message.Parameters.Split(new char[] { ' ' });
523 string nickname = parameters[1];
524 ChangeNick(nickname + "__");
525 }
526 catch (Exception ex)
527 {
528 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
529 }
530 }
531
532 #endregion
533
534 /// <summary>
535 /// Connect to the specified IRC server on the specified port.
536 /// </summary>
537 /// <param name="server">Address of IRC server.</param>
538 /// <param name="port">Port of IRC server.</param>
539 public void Connect(string server, int port)
540 {
541 if (connected)
542 {
543 throw new AlreadyConnectedException();
544 }
545 else
546 {
547 messageStream = new LineBuffer();
548 tcpClient = new TcpClient();
549 tcpClient.Connect(server, port);
550 tcpClient.NoDelay = true;
551 tcpClient.LingerState = new LingerOption(false, 0);
552 networkStream = tcpClient.GetStream();
553 connected = networkStream.CanRead && networkStream.CanWrite;
554 if (!connected)
555 {
556 throw new Exception("Cannot read and write from socket.");
557 }
558 /* Install PING message handler */
559 MonitorCommand(IRC.PING, new MessageReceivedHandler(PingMessageReceived));
560 /* Install NOTICE message handler */
561 MonitorCommand(IRC.NOTICE, new MessageReceivedHandler(NoticeMessageReceived));
562 /* Install RPL_NAMREPLY message handler */
563 MonitorCommand(IRC.RPL_NAMREPLY, new MessageReceivedHandler(RPL_NAMREPLYMessageReceived));
564 /* Install RPL_ENDOFNAMES message handler */
565 MonitorCommand(IRC.RPL_ENDOFNAMES, new MessageReceivedHandler(RPL_ENDOFNAMESMessageReceived));
566 /* Install ERR_NICKNAMEINUSE message handler */
567 MonitorCommand(IRC.ERR_NICKNAMEINUSE, new MessageReceivedHandler(ERR_NICKNAMEINUSEMessageReceived));
568 /* Start receiving data */
569 Receive();
570 }
571 }
572
573 /// <summary>
574 /// Disconnect from IRC server.
575 /// </summary>
576 public void Diconnect()
577 {
578 if (!connected)
579 {
580 throw new NotConnectedException();
581 }
582 else
583 {
584 connected = false;
585 tcpClient.Close();
586 tcpClient = null;
587
588 if (OnDisconnect != null)
589 OnDisconnect();
590 }
591 }
592
593 /// <summary>
594 /// Send an IRC message.
595 /// </summary>
596 /// <param name="message">The message to be sent.</param>
597 public void SendMessage(IrcMessage message)
598 {
599 try
600 {
601 if (!connected)
602 {
603 throw new NotConnectedException();
604 }
605
606 /* Serialize sending messages */
607 lock (typeof(IrcClient))
608 {
609 NetworkStream networkStream = tcpClient.GetStream();
610 byte[] bytes = Encoding.GetBytes(message.Line);
611 networkStream.Write(bytes, 0, bytes.Length);
612 networkStream.Flush();
613 }
614 }
615 catch (SocketException)
616 {
617 if (OnConnectionLost != null)
618 OnConnectionLost();
619 }
620 catch (IOException)
621 {
622 if (OnConnectionLost != null)
623 OnConnectionLost();
624 }
625 catch (Exception)
626 {
627 if (OnConnectionLost != null)
628 OnConnectionLost();
629 }
630 }
631
632 /// <summary>
633 /// Monitor when a message with an IRC command is received.
634 /// </summary>
635 /// <param name="command">IRC command to monitor.</param>
636 /// <param name="handler">Handler to call when command is received.</param>
637 public void MonitorCommand(string command, MessageReceivedHandler handler)
638 {
639 if (command == null)
640 {
641 throw new ArgumentNullException("command", "Command cannot be null.");
642 }
643 if (handler == null)
644 {
645 throw new ArgumentNullException("handler", "Handler cannot be null.");
646 }
647 ircCommandEventRegistrations.Add(new IrcCommandEventRegistration(command, handler));
648 }
649
650 /// <summary>
651 /// Talk to the channel.
652 /// </summary>
653 /// <param name="nickname">Nickname of user to talk to.</param>
654 /// <param name="text">Text to send to the channel.</param>
655 public void TalkTo(string nickname, string text)
656 {
657 }
658
659 /// <summary>
660 /// Change nickname.
661 /// </summary>
662 /// <param name="nickname">New nickname.</param>
663 public void ChangeNick(string nickname)
664 {
665 if (nickname == null)
666 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
667
668 Console.WriteLine("Changing nick to {0}\n", nickname);
669 curNickname = nickname;
670
671 /* NICK <nickname> [ <hopcount> ] */
672 SendMessage(new IrcMessage(IRC.NICK, nickname));
673 }
674
675 /// <summary>
676 /// Ghost nickname.
677 /// </summary>
678 /// <param name="nickname">Nickname.</param>
679 public void GhostNick(string nickname,
680 string password)
681 {
682 if (nickname == null)
683 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
684
685 if (password == null)
686 throw new ArgumentNullException("password", "Password cannot be null.");
687
688 awaitingGhostDeath = true;
689
690 /* GHOST <nickname> <password> */
691 SendMessage(new IrcMessage(IRC.GHOST, nickname + " " + password));
692 }
693
694 /// <summary>
695 /// Submit password to identify user.
696 /// </summary>
697 /// <param name="password">Password of registered nick.</param>
698 private void SubmitPassword(string password)
699 {
700 if (password == null)
701 throw new ArgumentNullException("password", "Password cannot be null.");
702
703 this.password = password;
704
705 /* PASS <password> */
706 SendMessage(new IrcMessage(IRC.PASS, password));
707 }
708
709 /// <summary>
710 /// Register.
711 /// </summary>
712 /// <param name="nickname">New nickname.</param>
713 /// <param name="password">Password. Can be null.</param>
714 /// <param name="realname">Real name. Can be null.</param>
715 public void Register(string nickname,
716 string password,
717 string realname)
718 {
719 if (nickname == null)
720 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
721 reqNickname = nickname;
722 firstPingReceived = false;
723 if (password != null)
724 {
725 SubmitPassword(password);
726 }
727 ChangeNick(nickname);
728 /* OLD: USER <username> <hostname> <servername> <realname> */
729 /* NEW: USER <user> <mode> <unused> <realname> */
730 SendMessage(new IrcMessage(IRC.USER, String.Format("{0} 0 * :{1}",
731 nickname, realname != null ? realname : "Anonymous")));
732
733 /* Wait for PING for up til 10 seconds */
734 int timer = 0;
735 while (!firstPingReceived && timer < 200)
736 {
737 System.Threading.Thread.Sleep(50);
738 timer++;
739 }
740 }
741
742 /// <summary>
743 /// Join an IRC channel.
744 /// </summary>
745 /// <param name="name">Name of channel (without leading #).</param>
746 /// <returns>New channel.</returns>
747 public IrcChannel JoinChannel(string name)
748 {
749 IrcChannel channel = new IrcChannel(this, name);
750 channels.Add(channel);
751 /* JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) / "0" */
752 SendMessage(new IrcMessage(IRC.JOIN, String.Format("#{0}", name)));
753 return channel;
754 }
755
756 /// <summary>
757 /// Part an IRC channel.
758 /// </summary>
759 /// <param name="channel">IRC channel. If null, the user parts from all channels.</param>
760 /// <param name="message">Part message. Can be null.</param>
761 public void PartChannel(IrcChannel channel, string message)
762 {
763 /* PART <channel> *( "," <channel> ) [ <Part Message> ] */
764 if (channel != null)
765 {
766 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
767 channel.Name, message != null ? String.Format(" :{0}", message) : "")));
768 channels.Remove(channel);
769 }
770 else
771 {
772 string channelList = null;
773 foreach (IrcChannel myChannel in Channels)
774 {
775 if (channelList == null)
776 {
777 channelList = "";
778 }
779 else
780 {
781 channelList += ",";
782 }
783 channelList += myChannel.Name;
784 }
785 if (channelList != null)
786 {
787 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
788 channelList, message != null ? String.Format(" :{0}", message) : "")));
789 Channels.Clear();
790 }
791 }
792 }
793 }
794 }