Add tcpsvcs to the services directory.
[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(this,
395 nickname.Substring(1));
396 channel.Users.Add(user);
397 }
398 for (int i = 4; i < parameters.Length; i++)
399 {
400 nickname = parameters[i];
401 user = channel.LocateUser(nickname);
402 if (user == null)
403 {
404 user = new IrcUser(this,
405 nickname);
406 channel.Users.Add(user);
407 }
408 }
409 }
410 catch (Exception ex)
411 {
412 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
413 }
414 }
415
416 /// <summary>
417 /// Process RPL_ENDOFNAMES message.
418 /// </summary>
419 /// <param name="message">Received IRC message.</param>
420 private void RPL_ENDOFNAMESMessageReceived(IrcMessage message)
421 {
422 try
423 {
424 /* <channel> :End of NAMES list */
425 if (message.Parameters == null)
426 {
427 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
428 return;
429 }
430
431 string[] parameters = message.Parameters.Split(new char[] { ' ' });
432 IrcChannel channel = LocateChannel(parameters[1].Substring(1));
433 if (channel == null)
434 {
435 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
436 parameters[0].Substring(1)));
437 return;
438 }
439
440 OnChannelUserDatabaseChanged(channel);
441 }
442 catch (Exception ex)
443 {
444 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
445 }
446 }
447
448 #endregion
449
450 /// <summary>
451 /// Connect to the specified IRC server on the specified port.
452 /// </summary>
453 /// <param name="server">Address of IRC server.</param>
454 /// <param name="port">Port of IRC server.</param>
455 public void Connect(string server, int port)
456 {
457 if (connected)
458 {
459 throw new AlreadyConnectedException();
460 }
461 else
462 {
463 messageStream = new LineBuffer();
464 tcpClient = new TcpClient();
465 tcpClient.Connect(server, port);
466 tcpClient.NoDelay = true;
467 tcpClient.LingerState = new LingerOption(false, 0);
468 networkStream = tcpClient.GetStream();
469 connected = networkStream.CanRead && networkStream.CanWrite;
470 if (!connected)
471 {
472 throw new Exception("Cannot read and write from socket.");
473 }
474 /* Install PING message handler */
475 MonitorCommand(IRC.PING, new MessageReceivedHandler(PingMessageReceived));
476 /* Install RPL_NAMREPLY message handler */
477 MonitorCommand(IRC.RPL_NAMREPLY, new MessageReceivedHandler(RPL_NAMREPLYMessageReceived));
478 /* Install RPL_ENDOFNAMES message handler */
479 MonitorCommand(IRC.RPL_ENDOFNAMES, new MessageReceivedHandler(RPL_ENDOFNAMESMessageReceived));
480 /* Start receiving data */
481 Receive();
482 }
483 }
484
485 /// <summary>
486 /// Disconnect from IRC server.
487 /// </summary>
488 public void Diconnect()
489 {
490 if (!connected)
491 {
492 throw new NotConnectedException();
493 }
494 else
495 {
496
497
498 connected = false;
499 tcpClient.Close();
500 tcpClient = null;
501 }
502 }
503
504 /// <summary>
505 /// Send an IRC message.
506 /// </summary>
507 /// <param name="message">The message to be sent.</param>
508 public void SendMessage(IrcMessage message)
509 {
510 if (!connected)
511 {
512 throw new NotConnectedException();
513 }
514
515 /* Serialize sending messages */
516 lock (typeof(IrcClient))
517 {
518 NetworkStream networkStream = tcpClient.GetStream();
519 byte[] bytes = Encoding.GetBytes(message.Line);
520 networkStream.Write(bytes, 0, bytes.Length);
521 networkStream.Flush();
522 }
523 }
524
525 /// <summary>
526 /// Monitor when a message with an IRC command is received.
527 /// </summary>
528 /// <param name="command">IRC command to monitor.</param>
529 /// <param name="handler">Handler to call when command is received.</param>
530 public void MonitorCommand(string command, MessageReceivedHandler handler)
531 {
532 if (command == null)
533 {
534 throw new ArgumentNullException("command", "Command cannot be null.");
535 }
536 if (handler == null)
537 {
538 throw new ArgumentNullException("handler", "Handler cannot be null.");
539 }
540 ircCommandEventRegistrations.Add(new IrcCommandEventRegistration(command, handler));
541 }
542
543 /// <summary>
544 /// Talk to the channel.
545 /// </summary>
546 /// <param name="nickname">Nickname of user to talk to.</param>
547 /// <param name="text">Text to send to the channel.</param>
548 public void TalkTo(string nickname, string text)
549 {
550 }
551
552 /// <summary>
553 /// Change nickname.
554 /// </summary>
555 /// <param name="nickname">New nickname.</param>
556 public void ChangeNick(string nickname)
557 {
558 if (nickname == null)
559 {
560 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
561 }
562
563 /* NICK <nickname> [ <hopcount> ] */
564 SendMessage(new IrcMessage(IRC.NICK, nickname));
565 }
566
567 /// <summary>
568 /// Register.
569 /// </summary>
570 /// <param name="nickname">New nickname.</param>
571 /// <param name="realname">Real name. Can be null.</param>
572 public void Register(string nickname, string realname)
573 {
574 if (nickname == null)
575 {
576 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
577 }
578 firstPingReceived = false;
579 ChangeNick(nickname);
580 /* OLD: USER <username> <hostname> <servername> <realname> */
581 /* NEW: USER <user> <mode> <unused> <realname> */
582 SendMessage(new IrcMessage(IRC.USER, String.Format("{0} 0 * :{1}",
583 nickname, realname != null ? realname : "Anonymous")));
584
585 /* Wait for PING for up til 10 seconds */
586 int timer = 0;
587 while (!firstPingReceived && timer < 200)
588 {
589 System.Threading.Thread.Sleep(50);
590 timer++;
591 }
592 }
593
594 /// <summary>
595 /// Join an IRC channel.
596 /// </summary>
597 /// <param name="name">Name of channel (without leading #).</param>
598 /// <returns>New channel.</returns>
599 public IrcChannel JoinChannel(string name)
600 {
601 IrcChannel channel = new IrcChannel(this, name);
602 channels.Add(channel);
603 /* JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) / "0" */
604 SendMessage(new IrcMessage(IRC.JOIN, String.Format("#{0}", name)));
605 return channel;
606 }
607
608 /// <summary>
609 /// Part an IRC channel.
610 /// </summary>
611 /// <param name="channel">IRC channel. If null, the user parts from all channels.</param>
612 /// <param name="message">Part message. Can be null.</param>
613 public void PartChannel(IrcChannel channel, string message)
614 {
615 /* PART <channel> *( "," <channel> ) [ <Part Message> ] */
616 if (channel != null)
617 {
618 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
619 channel.Name, message != null ? String.Format(" :{0}", message) : "")));
620 channels.Remove(channel);
621 }
622 else
623 {
624 string channelList = null;
625 foreach (IrcChannel myChannel in Channels)
626 {
627 if (channelList == null)
628 {
629 channelList = "";
630 }
631 else
632 {
633 channelList += ",";
634 }
635 channelList += myChannel.Name;
636 }
637 if (channelList != null)
638 {
639 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
640 channelList, message != null ? String.Format(" :{0}", message) : "")));
641 Channels.Clear();
642 }
643 }
644 }
645 }
646 }