using ChatSharp.Events; using ChatSharp.Handlers; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Timers; namespace ChatSharp { /// /// An IRC client. /// public sealed partial class IrcClient { /// /// A raw IRC message handler. /// public delegate void MessageHandler(IrcClient client, IrcMessage message); private Dictionary Handlers { get; set; } /// /// Sets a custom handler for an IRC message. This applies to the low level IRC protocol, /// not for private messages. /// public void SetHandler(string message, MessageHandler handler) { #if DEBUG // This is the default behavior if 3rd parties want to handle certain messages themselves // However, if it happens from our own code, we probably did something wrong if (Handlers.ContainsKey(message.ToUpper())) Console.WriteLine("Warning: {0} handler has been overwritten", message); #endif message = message.ToUpper(); Handlers[message] = handler; } internal static DateTime DateTimeFromIrcTime(int time) { return new DateTime(1970, 1, 1).AddSeconds(time); } private const int ReadBufferLength = 1024; private byte[] ReadBuffer { get; set; } private int ReadBufferIndex { get; set; } private string ServerHostname { get; set; } private int ServerPort { get; set; } private Timer PingTimer { get; set; } private Socket Socket { get; set; } private ConcurrentQueue WriteQueue { get; set; } private bool IsWriting { get; set; } internal RequestManager RequestManager { get; set; } internal string ServerNameFromPing { get; set; } /// /// The address this client is connected to, or will connect to. Setting this /// after the client is connected will not cause a reconnect. /// public string ServerAddress { get { return ServerHostname + ":" + ServerPort; } internal set { string[] parts = value.Split(':'); if (parts.Length > 2 || parts.Length == 0) throw new FormatException("Server address is not in correct format ('hostname:port')"); ServerHostname = parts[0]; if (parts.Length > 1) ServerPort = int.Parse(parts[1]); else ServerPort = 6667; } } /// /// The low level TCP stream for this client. /// public Stream NetworkStream { get; set; } /// /// If true, SSL will be used to connect. /// public bool UseSSL { get; private set; } /// /// If true, invalid SSL certificates are ignored. /// public bool IgnoreInvalidSSL { get; set; } /// /// The character encoding to use for the connection. Defaults to UTF-8. /// /// The encoding. public Encoding Encoding { get; set; } /// /// The user this client is logged in as. /// /// The user. public IrcUser User { get; set; } /// /// The channels this user is joined to. /// public ChannelCollection Channels { get; private set; } /// /// Settings that control the behavior of ChatSharp. /// public ClientSettings Settings { get; set; } /// /// Information about the server we are connected to. Servers may not send us this information, /// but it's required for ChatSharp to function, so by default this is a guess. Handle /// IrcClient.ServerInfoRecieved if you'd like to know when it's populated with real information. /// public ServerInfo ServerInfo { get; set; } /// /// A string to prepend to all PRIVMSGs sent. Many IRC bots prefix their messages with \u200B, to /// indicate to other bots that you are a bot. /// public string PrivmsgPrefix { get; set; } /// /// A list of users on this network that we are aware of. /// public UserPool Users { get; set; } /// /// A list of capabilities supported by the library, along with enabled and disabled capabilities /// after negotiating with the server. /// public CapabilityPool Capabilities { get; set; } /// /// Set to true when the client is negotiating IRC capabilities with the server. /// If set to False, capability negotiation is finished. /// public bool IsNegotiatingCapabilities { get; internal set; } /// /// Creates a new IRC client, but will not connect until ConnectAsync is called. /// /// Server address including port in the form of "hostname:port". /// The IRC user to connect as. /// Connect with SSL if true. public IrcClient(string serverAddress, IrcUser user, bool useSSL = false) { if (serverAddress == null) throw new ArgumentNullException("serverAddress"); if (user == null) throw new ArgumentNullException("user"); User = user; ServerAddress = serverAddress; Encoding = Encoding.UTF8; Settings = new ClientSettings(); Handlers = new Dictionary(); MessageHandlers.RegisterDefaultHandlers(this); RequestManager = new RequestManager(); UseSSL = useSSL; WriteQueue = new ConcurrentQueue(); ServerInfo = new ServerInfo(); PrivmsgPrefix = ""; Channels = User.Channels = new ChannelCollection(this); Users = new UserPool(); Users.Add(User); // Add self to user pool Capabilities = new CapabilityPool(); // List of supported capabilities Capabilities.AddRange(new string[] { "server-time", "multi-prefix", "cap-notify" }); IsNegotiatingCapabilities = false; } /// /// Connects to the IRC server. /// public void ConnectAsync() { if (Socket != null && Socket.Connected) throw new InvalidOperationException("Socket is already connected to server."); Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); ReadBuffer = new byte[ReadBufferLength]; ReadBufferIndex = 0; PingTimer = new Timer(30000); PingTimer.Elapsed += (sender, e) => { if (!string.IsNullOrEmpty(ServerNameFromPing)) SendRawMessage("PING :{0}", ServerNameFromPing); }; var checkQueue = new Timer(1000); checkQueue.Elapsed += (sender, e) => { string nextMessage; if (WriteQueue.Count > 0) { while (!WriteQueue.TryDequeue(out nextMessage)); SendRawMessage(nextMessage); } }; checkQueue.Start(); Socket.BeginConnect(ServerHostname, ServerPort, ConnectComplete, null); } /// /// Send a QUIT message and disconnect. /// public void Quit() { Quit(null); } /// /// Send a QUIT message with a reason and disconnect. /// public void Quit(string reason) { if (reason == null) SendRawMessage("QUIT"); else SendRawMessage("QUIT :{0}", reason); Socket.BeginDisconnect(false, ar => { Socket.EndDisconnect(ar); NetworkStream.Dispose(); NetworkStream = null; }, null); PingTimer.Dispose(); } private void ConnectComplete(IAsyncResult result) { try { Socket.EndConnect(result); NetworkStream = new NetworkStream(Socket); if (UseSSL) { if (IgnoreInvalidSSL) NetworkStream = new SslStream(NetworkStream, false, (sender, certificate, chain, policyErrors) => true); else NetworkStream = new SslStream(NetworkStream); ((SslStream)NetworkStream).AuthenticateAsClient(ServerHostname); } NetworkStream.BeginRead(ReadBuffer, ReadBufferIndex, ReadBuffer.Length, DataRecieved, null); // Begin capability negotiation SendRawMessage("CAP LS 302"); // Write login info if (!string.IsNullOrEmpty(User.Password)) SendRawMessage("PASS {0}", User.Password); SendRawMessage("NICK {0}", User.Nick); // hostname, servername are ignored by most IRC servers SendRawMessage("USER {0} hostname servername :{1}", User.User, User.RealName); PingTimer.Start(); } catch (SocketException e) { OnNetworkError(new SocketErrorEventArgs(e.SocketErrorCode)); } catch (Exception e) { OnError(new Events.ErrorEventArgs(e)); } } private void DataRecieved(IAsyncResult result) { if (NetworkStream == null) { OnNetworkError(new SocketErrorEventArgs(SocketError.NotConnected)); return; } int length; try { length = NetworkStream.EndRead(result) + ReadBufferIndex; } catch (IOException e) { var socketException = e.InnerException as SocketException; if (socketException != null) OnNetworkError(new SocketErrorEventArgs(socketException.SocketErrorCode)); else throw; return; } ReadBufferIndex = 0; while (length > 0) { int messageLength = Array.IndexOf(ReadBuffer, (byte)'\n', 0, length); if (messageLength == -1) // Incomplete message { ReadBufferIndex = length; break; } messageLength++; var message = Encoding.GetString(ReadBuffer, 0, messageLength - 2); // -2 to remove \r\n HandleMessage(message); Array.Copy(ReadBuffer, messageLength, ReadBuffer, 0, length - messageLength); length -= messageLength; } NetworkStream.BeginRead(ReadBuffer, ReadBufferIndex, ReadBuffer.Length - ReadBufferIndex, DataRecieved, null); } private void HandleMessage(string rawMessage) { OnRawMessageRecieved(new RawMessageEventArgs(rawMessage, false)); var message = new IrcMessage(rawMessage); if (Handlers.ContainsKey(message.Command.ToUpper())) Handlers[message.Command.ToUpper()](this, message); else { // TODO: Fire an event or something } } /// /// Send a raw IRC message. Behaves like /quote in most IRC clients. /// public void SendRawMessage(string message, params object[] format) { if (NetworkStream == null) { OnNetworkError(new SocketErrorEventArgs(SocketError.NotConnected)); return; } message = string.Format(message, format); var data = Encoding.GetBytes(message + "\r\n"); if (!IsWriting) { IsWriting = true; NetworkStream.BeginWrite(data, 0, data.Length, MessageSent, message); } else { WriteQueue.Enqueue(message); } } /// /// Send a raw IRC message. Behaves like /quote in most IRC clients. /// public void SendIrcMessage(IrcMessage message) { SendRawMessage(message.RawMessage); } private void MessageSent(IAsyncResult result) { if (NetworkStream == null) { OnNetworkError(new SocketErrorEventArgs(SocketError.NotConnected)); IsWriting = false; return; } try { NetworkStream.EndWrite(result); } catch (IOException e) { var socketException = e.InnerException as SocketException; if (socketException != null) OnNetworkError(new SocketErrorEventArgs(socketException.SocketErrorCode)); else throw; return; } finally { IsWriting = false; } OnRawMessageSent(new RawMessageEventArgs((string)result.AsyncState, true)); string nextMessage; if (WriteQueue.Count > 0) { while (!WriteQueue.TryDequeue(out nextMessage)); SendRawMessage(nextMessage); } } /// /// IRC Error Replies. rfc1459 6.1. /// public event EventHandler ErrorReply; internal void OnErrorReply(Events.ErrorReplyEventArgs e) { if (ErrorReply != null) ErrorReply(this, e); } /// /// Raised for errors. /// public event EventHandler Error; internal void OnError(Events.ErrorEventArgs e) { if (Error != null) Error(this, e); } /// /// Raised for socket errors. ChatSharp does not automatically reconnect. /// public event EventHandler NetworkError; internal void OnNetworkError(SocketErrorEventArgs e) { if (NetworkError != null) NetworkError(this, e); } /// /// Occurs when a raw message is sent. /// public event EventHandler RawMessageSent; internal void OnRawMessageSent(RawMessageEventArgs e) { if (RawMessageSent != null) RawMessageSent(this, e); } /// /// Occurs when a raw message recieved. /// public event EventHandler RawMessageRecieved; internal void OnRawMessageRecieved(RawMessageEventArgs e) { if (RawMessageRecieved != null) RawMessageRecieved(this, e); } /// /// Occurs when a notice recieved. /// public event EventHandler NoticeRecieved; internal void OnNoticeRecieved(IrcNoticeEventArgs e) { if (NoticeRecieved != null) NoticeRecieved(this, e); } /// /// Occurs when the server has sent us part of the MOTD. /// public event EventHandler MOTDPartRecieved; internal void OnMOTDPartRecieved(ServerMOTDEventArgs e) { if (MOTDPartRecieved != null) MOTDPartRecieved(this, e); } /// /// Occurs when the entire server MOTD has been recieved. /// public event EventHandler MOTDRecieved; internal void OnMOTDRecieved(ServerMOTDEventArgs e) { if (MOTDRecieved != null) MOTDRecieved(this, e); } /// /// Occurs when a private message recieved. This can be a channel OR a user message. /// public event EventHandler PrivateMessageRecieved; internal void OnPrivateMessageRecieved(PrivateMessageEventArgs e) { if (PrivateMessageRecieved != null) PrivateMessageRecieved(this, e); } /// /// Occurs when a message is recieved in an IRC channel. /// public event EventHandler ChannelMessageRecieved; internal void OnChannelMessageRecieved(PrivateMessageEventArgs e) { if (ChannelMessageRecieved != null) ChannelMessageRecieved(this, e); } /// /// Occurs when a message is recieved from a user. /// public event EventHandler UserMessageRecieved; internal void OnUserMessageRecieved(PrivateMessageEventArgs e) { if (UserMessageRecieved != null) UserMessageRecieved(this, e); } /// /// Raised if the nick you've chosen is in use. By default, ChatSharp will pick a /// random nick to use instead. Set ErronousNickEventArgs.DoNotHandle to prevent this. /// public event EventHandler NickInUse; internal void OnNickInUse(ErronousNickEventArgs e) { if (NickInUse != null) NickInUse(this, e); } /// /// Occurs when a user or channel mode is changed. /// public event EventHandler ModeChanged; internal void OnModeChanged(ModeChangeEventArgs e) { if (ModeChanged != null) ModeChanged(this, e); } /// /// Occurs when a user joins a channel. /// public event EventHandler UserJoinedChannel; internal void OnUserJoinedChannel(ChannelUserEventArgs e) { if (UserJoinedChannel != null) UserJoinedChannel(this, e); } /// /// Occurs when a user parts a channel. /// public event EventHandler UserPartedChannel; internal void OnUserPartedChannel(ChannelUserEventArgs e) { if (UserPartedChannel != null) UserPartedChannel(this, e); } /// /// Occurs when we have received the list of users present in a channel. /// public event EventHandler ChannelListRecieved; internal void OnChannelListRecieved(ChannelEventArgs e) { if (ChannelListRecieved != null) ChannelListRecieved(this, e); } /// /// Occurs when we have received the topic of a channel. /// public event EventHandler ChannelTopicReceived; internal void OnChannelTopicReceived(ChannelTopicEventArgs e) { if (ChannelTopicReceived != null) ChannelTopicReceived(this, e); } /// /// Occurs when the IRC connection is established and it is safe to begin interacting with the server. /// public event EventHandler ConnectionComplete; internal void OnConnectionComplete(EventArgs e) { if (ConnectionComplete != null) ConnectionComplete(this, e); } /// /// Occurs when we receive server info (such as max nick length). /// public event EventHandler ServerInfoRecieved; internal void OnServerInfoRecieved(SupportsEventArgs e) { if (ServerInfoRecieved != null) ServerInfoRecieved(this, e); } /// /// Occurs when a user is kicked. /// public event EventHandler UserKicked; internal void OnUserKicked(KickEventArgs e) { if (UserKicked != null) UserKicked(this, e); } /// /// Occurs when a WHOIS response is received. /// public event EventHandler WhoIsReceived; internal void OnWhoIsReceived(WhoIsReceivedEventArgs e) { if (WhoIsReceived != null) WhoIsReceived(this, e); } /// /// Occurs when a user has changed their nick. /// public event EventHandler NickChanged; internal void OnNickChanged(NickChangedEventArgs e) { if (NickChanged != null) NickChanged(this, e); } /// /// Occurs when a user has quit. /// public event EventHandler UserQuit; internal void OnUserQuit(UserEventArgs e) { if (UserQuit != null) UserQuit(this, e); } } }