7851d6fa366130e964c4d6161dcd3ccc265e6fb3
[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 System.Text.Encoding encoding = System.Text.Encoding.UTF8;
195 private TcpClient tcpClient;
196 private NetworkStream networkStream;
197 private bool connected = false;
198 private LineBuffer messageStream;
199 private ArrayList ircCommandEventRegistrations = new ArrayList();
200 private ArrayList channels = new ArrayList();
201 #endregion
202
203 #region Public events
204
205 public event MessageReceivedHandler MessageReceived;
206
207 public event ChannelUserDatabaseChangedHandler ChannelUserDatabaseChanged;
208
209 public event OnConnectHandler OnConnect;
210 public event OnConnectionLostHandler OnConnectionLost;
211 public event OnDisconnectHandler OnDisconnect;
212
213 #endregion
214
215 #region Public properties
216
217 /// <summary>
218 /// Encoding used.
219 /// </summary>
220 public System.Text.Encoding Encoding
221 {
222 get
223 {
224 return encoding;
225 }
226 set
227 {
228 encoding = value;
229 }
230 }
231
232 /// <summary>
233 /// List of joined channels.
234 /// </summary>
235 public ArrayList Channels
236 {
237 get
238 {
239 return channels;
240 }
241 }
242
243 #endregion
244
245 #region Private methods
246
247 /// <summary>
248 /// Signal MessageReceived event.
249 /// </summary>
250 /// <param name="message">Message that was received.</param>
251 private void OnMessageReceived(IrcMessage message)
252 {
253 foreach (IrcCommandEventRegistration icre in ircCommandEventRegistrations)
254 {
255 if (message.Command.ToLower().Equals(icre.Command.ToLower()))
256 {
257 icre.Handler(message);
258 }
259 }
260 if (MessageReceived != null)
261 {
262 MessageReceived(message);
263 }
264 }
265
266 /// <summary>
267 /// Signal ChannelUserDatabaseChanged event.
268 /// </summary>
269 /// <param name="channel">Message that was received.</param>
270 private void OnChannelUserDatabaseChanged(IrcChannel channel)
271 {
272 if (ChannelUserDatabaseChanged != null)
273 {
274 ChannelUserDatabaseChanged(channel);
275 }
276 }
277
278 /// <summary>
279 /// Start an asynchronous read.
280 /// </summary>
281 private void Receive()
282 {
283 if ((networkStream != null) && (networkStream.CanRead))
284 {
285 byte[] buffer = new byte[1024];
286 networkStream.BeginRead(buffer, 0, buffer.Length,
287 new AsyncCallback(ReadComplete),
288 new StateObject(networkStream, buffer));
289 }
290 else
291 {
292 throw new Exception("Socket is closed.");
293 }
294 }
295
296 /// <summary>
297 /// Asynchronous read has completed.
298 /// </summary>
299 /// <param name="ar">IAsyncResult object.</param>
300 private void ReadComplete(IAsyncResult ar)
301 {
302 try
303 {
304 StateObject stateObject = (StateObject)ar.AsyncState;
305 if (stateObject.Stream.CanRead)
306 {
307 int bytesReceived = stateObject.Stream.EndRead(ar);
308 if (bytesReceived > 0)
309 {
310 messageStream.Write(Encoding.GetString(stateObject.Buffer, 0, bytesReceived));
311 while (messageStream.DataAvailable)
312 {
313 OnMessageReceived(new IrcMessage(messageStream.Read()));
314 }
315 }
316 }
317
318 Receive();
319 }
320 catch (SocketException)
321 {
322 if (OnConnectionLost != null)
323 OnConnectionLost();
324 }
325 catch (IOException)
326 {
327 if (OnConnectionLost != null)
328 OnConnectionLost();
329 }
330 catch (Exception)
331 {
332 if (OnConnectionLost != null)
333 OnConnectionLost();
334 }
335 }
336
337 /// <summary>
338 /// Locate channel.
339 /// </summary>
340 /// <param name="name">Channel name.</param>
341 /// <returns>Channel or null if none was found.</returns>
342 private IrcChannel LocateChannel(string name)
343 {
344 foreach (IrcChannel channel in Channels)
345 {
346 if (name.ToLower().Equals(channel.Name.ToLower()))
347 {
348 return channel;
349 }
350 }
351 return null;
352 }
353
354 /// <summary>
355 /// Send a PONG message when a PING message is received.
356 /// </summary>
357 /// <param name="message">Received IRC message.</param>
358 private void PingMessageReceived(IrcMessage message)
359 {
360 SendMessage(new IrcMessage(IRC.PONG, message.Parameters));
361 firstPingReceived = true;
362 }
363
364 /// <summary>
365 /// Process RPL_NAMREPLY message.
366 /// </summary>
367 /// <param name="message">Received IRC message.</param>
368 private void RPL_NAMREPLYMessageReceived(IrcMessage message)
369 {
370 try
371 {
372 // :Oslo2.NO.EU.undernet.org 353 E101 = #E101 :E101 KongFu_uK @Exception
373 /* "( "=" / "*" / "@" ) <channel>
374 :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
375 - "@" is used for secret channels, "*" for private
376 channels, and "=" for others (public channels). */
377 if (message.Parameters == null)
378 {
379 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
380 return;
381 }
382 string[] parameters = message.Parameters.Split(new char[] { ' '});
383 if (parameters.Length < 5)
384 {
385 System.Diagnostics.Debug.WriteLine(String.Format("{0} is two few parameters.", parameters.Length));
386 return;
387 }
388 IrcChannelType type;
389 switch (parameters[1])
390 {
391 case "=":
392 type = IrcChannelType.Public;
393 break;
394 case "*":
395 type = IrcChannelType.Private;
396 break;
397 case "@":
398 type = IrcChannelType.Secret;
399 break;
400 default:
401 type = IrcChannelType.Public;
402 break;
403 }
404 IrcChannel channel = LocateChannel(parameters[2].Substring(1));
405 if (channel == null)
406 {
407 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
408 parameters[2].Substring(1)));
409 return;
410 }
411 string nickname = parameters[3];
412 if (nickname[0] != ':')
413 {
414 System.Diagnostics.Debug.WriteLine(String.Format("String should start with : and not {0}.", nickname[0]));
415 return;
416 }
417 /* Skip : */
418 IrcUser user = channel.LocateUser(nickname.Substring(1));
419 if (user == null)
420 {
421 user = new IrcUser(this,
422 nickname.Substring(1));
423 channel.Users.Add(user);
424 }
425 for (int i = 4; i < parameters.Length; i++)
426 {
427 nickname = parameters[i];
428 user = channel.LocateUser(nickname);
429 if (user == null)
430 {
431 user = new IrcUser(this,
432 nickname);
433 channel.Users.Add(user);
434 }
435 }
436 }
437 catch (Exception ex)
438 {
439 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
440 }
441 }
442
443 /// <summary>
444 /// Process RPL_ENDOFNAMES message.
445 /// </summary>
446 /// <param name="message">Received IRC message.</param>
447 private void RPL_ENDOFNAMESMessageReceived(IrcMessage message)
448 {
449 try
450 {
451 /* <channel> :End of NAMES list */
452 if (message.Parameters == null)
453 {
454 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
455 return;
456 }
457
458 string[] parameters = message.Parameters.Split(new char[] { ' ' });
459 IrcChannel channel = LocateChannel(parameters[1].Substring(1));
460 if (channel == null)
461 {
462 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
463 parameters[0].Substring(1)));
464 return;
465 }
466
467 OnChannelUserDatabaseChanged(channel);
468 }
469 catch (Exception ex)
470 {
471 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
472 }
473 }
474
475 #endregion
476
477 /// <summary>
478 /// Connect to the specified IRC server on the specified port.
479 /// </summary>
480 /// <param name="server">Address of IRC server.</param>
481 /// <param name="port">Port of IRC server.</param>
482 public void Connect(string server, int port)
483 {
484 if (connected)
485 {
486 throw new AlreadyConnectedException();
487 }
488 else
489 {
490 messageStream = new LineBuffer();
491 tcpClient = new TcpClient();
492 tcpClient.Connect(server, port);
493 tcpClient.NoDelay = true;
494 tcpClient.LingerState = new LingerOption(false, 0);
495 networkStream = tcpClient.GetStream();
496 connected = networkStream.CanRead && networkStream.CanWrite;
497 if (!connected)
498 {
499 throw new Exception("Cannot read and write from socket.");
500 }
501 /* Install PING message handler */
502 MonitorCommand(IRC.PING, new MessageReceivedHandler(PingMessageReceived));
503 /* Install RPL_NAMREPLY message handler */
504 MonitorCommand(IRC.RPL_NAMREPLY, new MessageReceivedHandler(RPL_NAMREPLYMessageReceived));
505 /* Install RPL_ENDOFNAMES message handler */
506 MonitorCommand(IRC.RPL_ENDOFNAMES, new MessageReceivedHandler(RPL_ENDOFNAMESMessageReceived));
507 /* Start receiving data */
508 Receive();
509 }
510 }
511
512 /// <summary>
513 /// Disconnect from IRC server.
514 /// </summary>
515 public void Diconnect()
516 {
517 if (!connected)
518 {
519 throw new NotConnectedException();
520 }
521 else
522 {
523
524
525 connected = false;
526 tcpClient.Close();
527 tcpClient = null;
528
529 if (OnDisconnect != null)
530 OnDisconnect();
531 }
532 }
533
534 /// <summary>
535 /// Send an IRC message.
536 /// </summary>
537 /// <param name="message">The message to be sent.</param>
538 public void SendMessage(IrcMessage message)
539 {
540 try
541 {
542 if (!connected)
543 {
544 throw new NotConnectedException();
545 }
546
547 /* Serialize sending messages */
548 lock (typeof(IrcClient))
549 {
550 NetworkStream networkStream = tcpClient.GetStream();
551 byte[] bytes = Encoding.GetBytes(message.Line);
552 networkStream.Write(bytes, 0, bytes.Length);
553 networkStream.Flush();
554 }
555 }
556 catch (SocketException)
557 {
558 if (OnConnectionLost != null)
559 OnConnectionLost();
560 }
561 catch (IOException)
562 {
563 if (OnConnectionLost != null)
564 OnConnectionLost();
565 }
566 catch (Exception)
567 {
568 if (OnConnectionLost != null)
569 OnConnectionLost();
570 }
571 }
572
573 /// <summary>
574 /// Monitor when a message with an IRC command is received.
575 /// </summary>
576 /// <param name="command">IRC command to monitor.</param>
577 /// <param name="handler">Handler to call when command is received.</param>
578 public void MonitorCommand(string command, MessageReceivedHandler handler)
579 {
580 if (command == null)
581 {
582 throw new ArgumentNullException("command", "Command cannot be null.");
583 }
584 if (handler == null)
585 {
586 throw new ArgumentNullException("handler", "Handler cannot be null.");
587 }
588 ircCommandEventRegistrations.Add(new IrcCommandEventRegistration(command, handler));
589 }
590
591 /// <summary>
592 /// Talk to the channel.
593 /// </summary>
594 /// <param name="nickname">Nickname of user to talk to.</param>
595 /// <param name="text">Text to send to the channel.</param>
596 public void TalkTo(string nickname, string text)
597 {
598 }
599
600 /// <summary>
601 /// Change nickname.
602 /// </summary>
603 /// <param name="nickname">New nickname.</param>
604 public void ChangeNick(string nickname)
605 {
606 if (nickname == null)
607 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
608
609 /* NICK <nickname> [ <hopcount> ] */
610 SendMessage(new IrcMessage(IRC.NICK, nickname));
611 }
612
613 /// <summary>
614 /// Submit password to identify user.
615 /// </summary>
616 /// <param name="password">Password of registered nick.</param>
617 private void SubmitPassword(string password)
618 {
619 if (password == null)
620 throw new ArgumentNullException("password", "Password cannot be null.");
621
622 /* PASS <password> */
623 SendMessage(new IrcMessage(IRC.PASS, password));
624 }
625
626 /// <summary>
627 /// Register.
628 /// </summary>
629 /// <param name="nickname">New nickname.</param>
630 /// <param name="password">Password. Can be null.</param>
631 /// <param name="realname">Real name. Can be null.</param>
632 public void Register(string nickname,
633 string password,
634 string realname)
635 {
636 if (nickname == null)
637 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
638 firstPingReceived = false;
639 if (password != null)
640 SubmitPassword(password);
641 ChangeNick(nickname);
642 /* Before we send our nickname, just ghost it */
643 SendMessage(new IrcMessage(IRC.GHOST, nickname));
644 /* OLD: USER <username> <hostname> <servername> <realname> */
645 /* NEW: USER <user> <mode> <unused> <realname> */
646 SendMessage(new IrcMessage(IRC.USER, String.Format("{0} 0 * :{1}",
647 nickname, realname != null ? realname : "Anonymous")));
648
649 /* Wait for PING for up til 10 seconds */
650 int timer = 0;
651 while (!firstPingReceived && timer < 200)
652 {
653 System.Threading.Thread.Sleep(50);
654 timer++;
655 }
656 }
657
658 /// <summary>
659 /// Join an IRC channel.
660 /// </summary>
661 /// <param name="name">Name of channel (without leading #).</param>
662 /// <returns>New channel.</returns>
663 public IrcChannel JoinChannel(string name)
664 {
665 IrcChannel channel = new IrcChannel(this, name);
666 channels.Add(channel);
667 /* JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) / "0" */
668 SendMessage(new IrcMessage(IRC.JOIN, String.Format("#{0}", name)));
669 return channel;
670 }
671
672 /// <summary>
673 /// Part an IRC channel.
674 /// </summary>
675 /// <param name="channel">IRC channel. If null, the user parts from all channels.</param>
676 /// <param name="message">Part message. Can be null.</param>
677 public void PartChannel(IrcChannel channel, string message)
678 {
679 /* PART <channel> *( "," <channel> ) [ <Part Message> ] */
680 if (channel != null)
681 {
682 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
683 channel.Name, message != null ? String.Format(" :{0}", message) : "")));
684 channels.Remove(channel);
685 }
686 else
687 {
688 string channelList = null;
689 foreach (IrcChannel myChannel in Channels)
690 {
691 if (channelList == null)
692 {
693 channelList = "";
694 }
695 else
696 {
697 channelList += ",";
698 }
699 channelList += myChannel.Name;
700 }
701 if (channelList != null)
702 {
703 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
704 channelList, message != null ? String.Format(" :{0}", message) : "")));
705 Channels.Clear();
706 }
707 }
708 }
709 }
710 }