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