using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Net.Sockets;
namespace TechBot.IRCLibrary
{
///
/// Delegate that delivers an IRC message.
///
public delegate void MessageReceivedHandler(IrcMessage message);
///
/// Delegate that notifies if the user database for a channel has changed.
///
public delegate void ChannelUserDatabaseChangedHandler(IrcChannel channel);
public delegate void OnConnectHandler ();
public delegate void OnDisconnectHandler();
public delegate void OnConnectionLostHandler();
///
/// An IRC client.
///
public class IrcClient
{
///
/// Monitor when an IRC command is received.
///
private class IrcCommandEventRegistration
{
///
/// IRC command to monitor.
///
private string command;
public string Command
{
get
{
return command;
}
}
///
/// Handler to call when command is received.
///
private MessageReceivedHandler handler;
public MessageReceivedHandler Handler
{
get
{
return handler;
}
}
///
/// Constructor.
///
/// IRC command to monitor.
/// Handler to call when command is received.
public IrcCommandEventRegistration(string command,
MessageReceivedHandler handler)
{
this.command = command;
this.handler = handler;
}
}
///
/// A buffer to store lines of text.
///
private class LineBuffer
{
///
/// Full lines of text in buffer.
///
private ArrayList strings;
///
/// Part of the last line of text in buffer.
///
private string left = "";
///
/// Standard constructor.
///
public LineBuffer()
{
strings = new ArrayList();
}
///
/// Return true if there is a complete line in the buffer or false if there is not.
///
public bool DataAvailable
{
get
{
return (strings.Count > 0);
}
}
///
/// Return next complete line in buffer or null if none exists.
///
/// Next complete line in buffer or null if none exists.
public string Read()
{
if (DataAvailable)
{
string line = strings[0] as string;
strings.RemoveAt(0);
return line;
}
else
{
return null;
}
}
///
/// Write a string to buffer splitting it into lines.
///
///
public void Write(string data)
{
data = left + data;
left = "";
string[] sa = data.Split(new char[] { '\n' });
if (sa.Length <= 0)
{
left = data;
return;
}
else
{
left = "";
}
for (int i = 0; i < sa.Length; i++)
{
if (i < sa.Length - 1)
{
/* This is a complete line. Remove any \r characters at the end of the line. */
string line = sa[i].TrimEnd(new char[] { '\r', '\n'});
/* Silently ignore empty lines */
if (!line.Equals(String.Empty))
{
strings.Add(line);
}
}
else
{
/* This may be a partial line. */
left = sa[i];
}
}
}
}
///
/// State for asynchronous reads.
///
private class StateObject
{
///
/// Network stream where data is read from.
///
public NetworkStream Stream;
///
/// Buffer where data is put.
///
public byte[] Buffer;
///
/// Constructor.
///
/// Network stream where data is read from.
/// Buffer where data is put.
public StateObject(NetworkStream stream, byte[] buffer)
{
this.Stream = stream;
this.Buffer = buffer;
}
}
#region Private fields
private bool firstPingReceived = false;
private System.Text.Encoding encoding = System.Text.Encoding.UTF8;
private TcpClient tcpClient;
private NetworkStream networkStream;
private bool connected = false;
private LineBuffer messageStream;
private ArrayList ircCommandEventRegistrations = new ArrayList();
private ArrayList channels = new ArrayList();
#endregion
#region Public events
public event MessageReceivedHandler MessageReceived;
public event ChannelUserDatabaseChangedHandler ChannelUserDatabaseChanged;
public event OnConnectHandler OnConnect;
public event OnConnectionLostHandler OnConnectionLost;
public event OnDisconnectHandler OnDisconnect;
#endregion
#region Public properties
///
/// Encoding used.
///
public System.Text.Encoding Encoding
{
get
{
return encoding;
}
set
{
encoding = value;
}
}
///
/// List of joined channels.
///
public ArrayList Channels
{
get
{
return channels;
}
}
#endregion
#region Private methods
///
/// Signal MessageReceived event.
///
/// Message that was received.
private void OnMessageReceived(IrcMessage message)
{
foreach (IrcCommandEventRegistration icre in ircCommandEventRegistrations)
{
if (message.Command.ToLower().Equals(icre.Command.ToLower()))
{
icre.Handler(message);
}
}
if (MessageReceived != null)
{
MessageReceived(message);
}
}
///
/// Signal ChannelUserDatabaseChanged event.
///
/// Message that was received.
private void OnChannelUserDatabaseChanged(IrcChannel channel)
{
if (ChannelUserDatabaseChanged != null)
{
ChannelUserDatabaseChanged(channel);
}
}
///
/// Start an asynchronous read.
///
private void Receive()
{
if ((networkStream != null) && (networkStream.CanRead))
{
byte[] buffer = new byte[1024];
networkStream.BeginRead(buffer, 0, buffer.Length,
new AsyncCallback(ReadComplete),
new StateObject(networkStream, buffer));
}
else
{
throw new Exception("Socket is closed.");
}
}
///
/// Asynchronous read has completed.
///
/// IAsyncResult object.
private void ReadComplete(IAsyncResult ar)
{
try
{
StateObject stateObject = (StateObject)ar.AsyncState;
if (stateObject.Stream.CanRead)
{
int bytesReceived = stateObject.Stream.EndRead(ar);
if (bytesReceived > 0)
{
messageStream.Write(Encoding.GetString(stateObject.Buffer, 0, bytesReceived));
while (messageStream.DataAvailable)
{
OnMessageReceived(new IrcMessage(messageStream.Read()));
}
}
}
Receive();
}
catch (SocketException)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
catch (IOException)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
catch (Exception)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
}
///
/// Locate channel.
///
/// Channel name.
/// Channel or null if none was found.
private IrcChannel LocateChannel(string name)
{
foreach (IrcChannel channel in Channels)
{
if (name.ToLower().Equals(channel.Name.ToLower()))
{
return channel;
}
}
return null;
}
///
/// Send a PONG message when a PING message is received.
///
/// Received IRC message.
private void PingMessageReceived(IrcMessage message)
{
SendMessage(new IrcMessage(IRC.PONG, message.Parameters));
firstPingReceived = true;
}
///
/// Process RPL_NAMREPLY message.
///
/// Received IRC message.
private void RPL_NAMREPLYMessageReceived(IrcMessage message)
{
try
{
// :Oslo2.NO.EU.undernet.org 353 E101 = #E101 :E101 KongFu_uK @Exception
/* "( "=" / "*" / "@" )
:[ "@" / "+" ] *( " " [ "@" / "+" ] )
- "@" is used for secret channels, "*" for private
channels, and "=" for others (public channels). */
if (message.Parameters == null)
{
System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
return;
}
string[] parameters = message.Parameters.Split(new char[] { ' '});
if (parameters.Length < 5)
{
System.Diagnostics.Debug.WriteLine(String.Format("{0} is two few parameters.", parameters.Length));
return;
}
IrcChannelType type;
switch (parameters[1])
{
case "=":
type = IrcChannelType.Public;
break;
case "*":
type = IrcChannelType.Private;
break;
case "@":
type = IrcChannelType.Secret;
break;
default:
type = IrcChannelType.Public;
break;
}
IrcChannel channel = LocateChannel(parameters[2].Substring(1));
if (channel == null)
{
System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
parameters[2].Substring(1)));
return;
}
string nickname = parameters[3];
if (nickname[0] != ':')
{
System.Diagnostics.Debug.WriteLine(String.Format("String should start with : and not {0}.", nickname[0]));
return;
}
/* Skip : */
IrcUser user = channel.LocateUser(nickname.Substring(1));
if (user == null)
{
user = new IrcUser(this,
nickname.Substring(1));
channel.Users.Add(user);
}
for (int i = 4; i < parameters.Length; i++)
{
nickname = parameters[i];
user = channel.LocateUser(nickname);
if (user == null)
{
user = new IrcUser(this,
nickname);
channel.Users.Add(user);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
}
}
///
/// Process RPL_ENDOFNAMES message.
///
/// Received IRC message.
private void RPL_ENDOFNAMESMessageReceived(IrcMessage message)
{
try
{
/* :End of NAMES list */
if (message.Parameters == null)
{
System.Diagnostics.Debug.WriteLine(String.Format("Message has no parameters."));
return;
}
string[] parameters = message.Parameters.Split(new char[] { ' ' });
IrcChannel channel = LocateChannel(parameters[1].Substring(1));
if (channel == null)
{
System.Diagnostics.Debug.WriteLine(String.Format("Channel not found '{0}'.",
parameters[0].Substring(1)));
return;
}
OnChannelUserDatabaseChanged(channel);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(String.Format("Ex. {0}", ex));
}
}
#endregion
///
/// Connect to the specified IRC server on the specified port.
///
/// Address of IRC server.
/// Port of IRC server.
public void Connect(string server, int port)
{
if (connected)
{
throw new AlreadyConnectedException();
}
else
{
messageStream = new LineBuffer();
tcpClient = new TcpClient();
tcpClient.Connect(server, port);
tcpClient.NoDelay = true;
tcpClient.LingerState = new LingerOption(false, 0);
networkStream = tcpClient.GetStream();
connected = networkStream.CanRead && networkStream.CanWrite;
if (!connected)
{
throw new Exception("Cannot read and write from socket.");
}
/* Install PING message handler */
MonitorCommand(IRC.PING, new MessageReceivedHandler(PingMessageReceived));
/* Install RPL_NAMREPLY message handler */
MonitorCommand(IRC.RPL_NAMREPLY, new MessageReceivedHandler(RPL_NAMREPLYMessageReceived));
/* Install RPL_ENDOFNAMES message handler */
MonitorCommand(IRC.RPL_ENDOFNAMES, new MessageReceivedHandler(RPL_ENDOFNAMESMessageReceived));
/* Start receiving data */
Receive();
}
}
///
/// Disconnect from IRC server.
///
public void Diconnect()
{
if (!connected)
{
throw new NotConnectedException();
}
else
{
connected = false;
tcpClient.Close();
tcpClient = null;
if (OnDisconnect != null)
OnDisconnect();
}
}
///
/// Send an IRC message.
///
/// The message to be sent.
public void SendMessage(IrcMessage message)
{
try
{
if (!connected)
{
throw new NotConnectedException();
}
/* Serialize sending messages */
lock (typeof(IrcClient))
{
NetworkStream networkStream = tcpClient.GetStream();
byte[] bytes = Encoding.GetBytes(message.Line);
networkStream.Write(bytes, 0, bytes.Length);
networkStream.Flush();
}
}
catch (SocketException)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
catch (IOException)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
catch (Exception)
{
if (OnConnectionLost != null)
OnConnectionLost();
}
}
///
/// Monitor when a message with an IRC command is received.
///
/// IRC command to monitor.
/// Handler to call when command is received.
public void MonitorCommand(string command, MessageReceivedHandler handler)
{
if (command == null)
{
throw new ArgumentNullException("command", "Command cannot be null.");
}
if (handler == null)
{
throw new ArgumentNullException("handler", "Handler cannot be null.");
}
ircCommandEventRegistrations.Add(new IrcCommandEventRegistration(command, handler));
}
///
/// Talk to the channel.
///
/// Nickname of user to talk to.
/// Text to send to the channel.
public void TalkTo(string nickname, string text)
{
}
///
/// Change nickname.
///
/// New nickname.
public void ChangeNick(string nickname)
{
if (nickname == null)
throw new ArgumentNullException("nickname", "Nickname cannot be null.");
/* NICK [ ] */
SendMessage(new IrcMessage(IRC.NICK, nickname));
}
///
/// Submit password to identify user.
///
/// Password of registered nick.
private void SubmitPassword(string password)
{
if (password == null)
throw new ArgumentNullException("password", "Password cannot be null.");
/* PASS */
SendMessage(new IrcMessage(IRC.PASS, password));
}
///
/// Register.
///
/// New nickname.
/// Password. Can be null.
/// Real name. Can be null.
public void Register(string nickname,
string password,
string realname)
{
if (nickname == null)
throw new ArgumentNullException("nickname", "Nickname cannot be null.");
firstPingReceived = false;
if (password != null)
{
/* First ghost ourself and then register */
if (nickname != null)
SendMessage(new IrcMessage(IRC.GHOST, nickname + " " + password));
SubmitPassword(password);
}
ChangeNick(nickname);
/* OLD: USER */
/* NEW: USER */
SendMessage(new IrcMessage(IRC.USER, String.Format("{0} 0 * :{1}",
nickname, realname != null ? realname : "Anonymous")));
/* Wait for PING for up til 10 seconds */
int timer = 0;
while (!firstPingReceived && timer < 200)
{
System.Threading.Thread.Sleep(50);
timer++;
}
}
///
/// Join an IRC channel.
///
/// Name of channel (without leading #).
/// New channel.
public IrcChannel JoinChannel(string name)
{
IrcChannel channel = new IrcChannel(this, name);
channels.Add(channel);
/* JOIN ( *( "," ) [ *( "," ) ] ) / "0" */
SendMessage(new IrcMessage(IRC.JOIN, String.Format("#{0}", name)));
return channel;
}
///
/// Part an IRC channel.
///
/// IRC channel. If null, the user parts from all channels.
/// Part message. Can be null.
public void PartChannel(IrcChannel channel, string message)
{
/* PART *( "," ) [ ] */
if (channel != null)
{
SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
channel.Name, message != null ? String.Format(" :{0}", message) : "")));
channels.Remove(channel);
}
else
{
string channelList = null;
foreach (IrcChannel myChannel in Channels)
{
if (channelList == null)
{
channelList = "";
}
else
{
channelList += ",";
}
channelList += myChannel.Name;
}
if (channelList != null)
{
SendMessage(new IrcMessage(IRC.PART, String.Format("#{0}{1}",
channelList, message != null ? String.Format(" :{0}", message) : "")));
Channels.Clear();
}
}
}
}
}