4 using System.Collections;
5 using System.Net.Sockets;
7 namespace TechBot.IRCLibrary
10 /// Delegate that delivers an IRC message.
12 public delegate void MessageReceivedHandler(IrcMessage message);
15 /// Delegate that notifies if the user database for a channel has changed.
17 public delegate void ChannelUserDatabaseChangedHandler(IrcChannel channel);
22 public class IrcClient
25 /// Monitor when an IRC command is received.
27 private class IrcCommandEventRegistration
30 /// IRC command to monitor.
32 private string command;
42 /// Handler to call when command is received.
44 private MessageReceivedHandler handler;
45 public MessageReceivedHandler Handler
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)
61 this.command = command;
62 this.handler = handler;
69 /// A buffer to store lines of text.
71 private class LineBuffer
74 /// Full lines of text in buffer.
76 private ArrayList strings;
79 /// Part of the last line of text in buffer.
81 private string left = "";
84 /// Standard constructor.
88 strings = new ArrayList();
92 /// Return true if there is a complete line in the buffer or false if there is not.
94 public bool DataAvailable
98 return (strings.Count > 0);
103 /// Return next complete line in buffer or null if none exists.
105 /// <returns>Next complete line in buffer or null if none exists.</returns>
110 string line = strings[0] as string;
121 /// Write a string to buffer splitting it into lines.
123 /// <param name="data"></param>
124 public void Write(string data)
128 string[] sa = data.Split(new char[] { '\n' });
138 for (int i = 0; i < sa.Length; i++)
140 if (i < sa.Length - 1)
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))
152 /* This may be a partial line. */
161 /// State for asynchronous reads.
163 private class StateObject
166 /// Network stream where data is read from.
168 public NetworkStream Stream;
171 /// Buffer where data is put.
173 public byte[] Buffer;
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)
182 this.Stream = stream;
183 this.Buffer = buffer;
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();
199 #region Public events
201 public event MessageReceivedHandler MessageReceived;
203 public event ChannelUserDatabaseChangedHandler ChannelUserDatabaseChanged;
207 #region Public properties
212 public System.Text.Encoding Encoding
225 /// List of joined channels.
227 public ArrayList Channels
237 #region Private methods
240 /// Signal MessageReceived event.
242 /// <param name="message">Message that was received.</param>
243 private void OnMessageReceived(IrcMessage message)
245 foreach (IrcCommandEventRegistration icre in ircCommandEventRegistrations)
247 if (message.Command.ToLower().Equals(icre.Command.ToLower()))
249 icre.Handler(message);
252 if (MessageReceived != null)
254 MessageReceived(message);
259 /// Signal ChannelUserDatabaseChanged event.
261 /// <param name="channel">Message that was received.</param>
262 private void OnChannelUserDatabaseChanged(IrcChannel channel)
264 if (ChannelUserDatabaseChanged != null)
266 ChannelUserDatabaseChanged(channel);
271 /// Start an asynchronous read.
273 private void Receive()
275 if ((networkStream != null) && (networkStream.CanRead))
277 byte[] buffer = new byte[1024];
278 networkStream.BeginRead(buffer, 0, buffer.Length,
279 new AsyncCallback(ReadComplete),
280 new StateObject(networkStream, buffer));
284 throw new Exception("Socket is closed.");
289 /// Asynchronous read has completed.
291 /// <param name="ar">IAsyncResult object.</param>
292 private void ReadComplete(IAsyncResult ar)
294 StateObject stateObject = (StateObject) ar.AsyncState;
295 if (stateObject.Stream.CanRead)
297 int bytesReceived = stateObject.Stream.EndRead(ar);
298 if (bytesReceived > 0)
300 messageStream.Write(Encoding.GetString(stateObject.Buffer, 0, bytesReceived));
301 while (messageStream.DataAvailable)
303 OnMessageReceived(new IrcMessage(messageStream.Read()));
313 /// <param name="name">Channel name.</param>
314 /// <returns>Channel or null if none was found.</returns>
315 private IrcChannel LocateChannel(string name)
317 foreach (IrcChannel channel in Channels)
319 if (name.ToLower().Equals(channel.Name.ToLower()))
328 /// Send a PONG message when a PING message is received.
330 /// <param name="message">Received IRC message.</param>
331 private void PingMessageReceived(IrcMessage message)
333 SendMessage(new IrcMessage(IRC.PONG, message.Parameters));
334 firstPingReceived = true;
338 /// Process RPL_NAMREPLY message.
340 /// <param name="message">Received IRC message.</param>
341 private void RPL_NAMREPLYMessageReceived(IrcMessage message)
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)
352 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
355 string[] parameters = message.Parameters.Split(new char[] { ' '});
356 if (parameters.Length < 5)
358 System.Diagnostics.Debug.WriteLine(String.Format("{0} is two few parameters.", parameters.Length));
362 switch (parameters[1])
365 type = IrcChannelType.Public;
368 type = IrcChannelType.Private;
371 type = IrcChannelType.Secret;
374 type = IrcChannelType.Public;
377 IrcChannel channel = LocateChannel(parameters[2].Substring(1));
380 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
381 parameters[2].Substring(1)));
384 string nickname = parameters[3];
385 if (nickname[0] != ':')
387 System.Diagnostics.Debug.WriteLine(String.Format("String should start with : and not {0}.", nickname[0]));
391 IrcUser user = channel.LocateUser(nickname.Substring(1));
394 user = new IrcUser(this,
395 nickname.Substring(1));
396 channel.Users.Add(user);
398 for (int i = 4; i < parameters.Length; i++)
400 nickname = parameters[i];
401 user = channel.LocateUser(nickname);
404 user = new IrcUser(this,
406 channel.Users.Add(user);
412 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
417 /// Process RPL_ENDOFNAMES message.
419 /// <param name="message">Received IRC message.</param>
420 private void RPL_ENDOFNAMESMessageReceived(IrcMessage message)
424 /* <channel> :End of NAMES list */
425 if (message.Parameters == null)
427 System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
431 string[] parameters = message.Parameters.Split(new char[] { ' ' });
432 IrcChannel channel = LocateChannel(parameters[1].Substring(1));
435 System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
436 parameters[0].Substring(1)));
440 OnChannelUserDatabaseChanged(channel);
444 System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
451 /// Connect to the specified IRC server on the specified port.
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)
459 throw new AlreadyConnectedException();
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;
472 throw new Exception("Cannot read and write from socket.");
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 */
486 /// Disconnect from IRC server.
488 public void Diconnect()
492 throw new NotConnectedException();
505 /// Send an IRC message.
507 /// <param name="message">The message to be sent.</param>
508 public void SendMessage(IrcMessage message)
512 throw new NotConnectedException();
515 /* Serialize sending messages */
516 lock (typeof(IrcClient))
518 NetworkStream networkStream = tcpClient.GetStream();
519 byte[] bytes = Encoding.GetBytes(message.Line);
520 networkStream.Write(bytes, 0, bytes.Length);
521 networkStream.Flush();
526 /// Monitor when a message with an IRC command is received.
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)
534 throw new ArgumentNullException("command", "Command cannot be null.");
538 throw new ArgumentNullException("handler", "Handler cannot be null.");
540 ircCommandEventRegistrations.Add(new IrcCommandEventRegistration(command, handler));
544 /// Talk to the channel.
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)
555 /// <param name="nickname">New nickname.</param>
556 public void ChangeNick(string nickname)
558 if (nickname == null)
560 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
563 /* NICK <nickname> [ <hopcount> ] */
564 SendMessage(new IrcMessage(IRC.NICK, nickname));
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)
574 if (nickname == null)
576 throw new ArgumentNullException("nickname", "Nickname cannot be null.");
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")));
585 /* Wait for PING for up til 10 seconds */
587 while (!firstPingReceived && timer < 200)
589 System.Threading.Thread.Sleep(50);
595 /// Join an IRC channel.
597 /// <param name="name">Name of channel (without leading #).</param>
598 /// <returns>New channel.</returns>
599 public IrcChannel JoinChannel(string name)
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)));
609 /// Part an IRC channel.
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)
615 /* PART <channel> *( "," <channel> ) [ <Part Message> ] */
618 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
619 channel.Name, message != null ? String.Format(" :{0}", message) : "")));
620 channels.Remove(channel);
624 string channelList = null;
625 foreach (IrcChannel myChannel in Channels)
627 if (channelList == null)
635 channelList += myChannel.Name;
637 if (channelList != null)
639 SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
640 channelList, message != null ? String.Format(" :{0}", message) : "")));