using System; using System.Collections.Generic; using System.IO; using System.Text; using AniNIX.Shared; #pragma warning disable 0168 #pragma warning disable 2002 namespace AniNIX.TheRaven { public sealed class Raven : IDisposable { /* These are the private globals for this instance * They should never be accessible outside this Raven */ //These are the basic configuration information to be overwritten public String Host { get; private set; } //This is the Host we are connecting to. public int Port { get; private set; } // This is Port to connect on public string Nick { get; private set; } // This is the Nickname for this Raven to use. private string _nickServPass; // This is the password we will send to NickServ to identify private string _autoSend; // This is the command we will automatically send to the Host private string _configFile; // This is the configuration directory. private Connection _connection; //This is the socket to the Host public List channels; //This is the list of channels to join public List whitelist; //This is the list of admin users. public List blacklist; // This is the list of blocked people. public String helpText = "Available commands are r.d , r.heartbeat, r.magic8, r.math , r.msg , r.raven, r.searches, r.tinyurl , r.wikidiff \"one\" \"other\", and r.uptime"; // This is the text to send when people ask for help -- this is configurable to allow for skinning public List searches; //These are the searches public String searchesIndex; //This is the helptext for the searches public List magic8; //These are the strings to return like a Magic 8-ball to questions. public List crowFacts; //These are the possible CrowFacts public List crowFactsSubscribers = new List(); //These are the subscribers to CrowFacts public Dictionary notifications = new Dictionary(); // This is the notifications list for TheRaven. public Random randomSeed = new Random(); //This is our random seed public Dictionary MailerCount = new Dictionary(); // Messages may only be sent up to a maximum to the admins. private RavenNetListener _netListener; /// /// Show the settings used by this Raven. /// /// A string representing this Raven public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append("### AniNIX::TheRaven -- Running Values ###\n"); sb.Append(String.Format("Host: {0}\n",Host)); sb.Append(String.Format("Port: {0}\n",Port)); sb.Append(String.Format("Nick: {0}\n",Nick)); sb.Append("NickServPass: ****\n"); sb.Append(String.Format("Auto: {0}\n",_autoSend)); sb.Append(String.Format("Conf: {0}\n",_configFile)); sb.Append(String.Format("Verbosity: {0}\n",ReportMessage.verbosity)); return sb.ToString(); } /// /// Read from the files in the current directory to configure this Raven /// // TODO: This and ParseArgs may get punted into their own static class to improve readability. private void ConfigureSelfFromFiles() { if (!File.Exists(_configFile)) { throw new Exception(String.Format("Configuration file {0} doesn't exist.",_configFile)); } ReportMessage.Log(Verbosity.Always,String.Format("Reading from config file in {0} and the global files in the same directory...",_configFile)); Configure conf = new Configure(_configFile); //These are locals that will be used throughout ReportMessage.Log(Verbosity.Verbose,"Reading login info"); try { Dictionary loginDefaults = conf.ReadSection("Login"); this.Host = loginDefaults["host"]; this.Port = Int32.Parse(loginDefaults["port"]); this.Nick = loginDefaults["username"]; this._nickServPass = loginDefaults["password"]; this._netListener = new RavenNetListener(loginDefaults["password"]); channels=new List(); foreach (String channel in conf.ReadSectionLines("Rooms")) { channels.Add(String.Format("#{0}",channel)); } } catch (Exception e) { throw new Exception("Failed to read default login info from Login section in configuration."); } //Parse the lists. ReportMessage.Log(Verbosity.Verbose,"Building lists."); try { notifications = conf.ReadSection("Notifications"); } catch (Exception e) { throw new Exception("Couldn't read Notifications section."); } try { whitelist = conf.ReadSectionLines("Whitelist"); } catch (Exception e) { throw new Exception("Couldn't read Whitelist section."); } try { blacklist = conf.ReadSectionLines("Blacklist"); } catch (Exception e) { throw new Exception("Couldn't read Blacklist section."); } try { searches = conf.ReadSectionLines("Searches"); } catch (Exception e) { throw new Exception("Couldn't read Searches section."); } StringBuilder searchIndexBuilder = new StringBuilder(); foreach (String searchLine in searches) { String[] byPipe = searchLine.Split('|'); if (byPipe.Length > 3) searchIndexBuilder.Append(String.Format("{0} <{1} search>, ",byPipe[0],byPipe[3])); } searchesIndex = searchIndexBuilder.ToString(); //Read the globals magic8 = (new Configure("magic8.txt")).GetLines(); crowFacts = (new Configure("crowfacts.txt")).GetLines(); } /// /// Print helptext and exit /// param retcode: what to return to the system. /// public void Usage() { ReportMessage.Log(Verbosity.Always,"Usage: mono ./raven.mono -c conf # start the Raven with the conf file"); ReportMessage.Log(Verbosity.Always,"Usage: mono ./raven.mono -h # Get help"); ReportMessage.Log(Verbosity.Always,""); ReportMessage.Log(Verbosity.Always,"The following flags are optional:"); ReportMessage.Log(Verbosity.Always,"-n Nickname"); ReportMessage.Log(Verbosity.Always,"-t Host"); ReportMessage.Log(Verbosity.Always,"-p Port"); ReportMessage.Log(Verbosity.Always,"-v Verbose"); ReportMessage.Log(Verbosity.Always,"-q Quiet"); ReportMessage.Log(Verbosity.Always,"-P NickServ passphrase"); ReportMessage.Log(Verbosity.Always,"-a Autosend command"); throw new RavenExitedException(); } /// /// Parse arguments from the command line. /// //TODO: Move this to RavenConfigure and add struct for these configurations public void ParseArguments(String[] args) { if (args != null) { for (int i = 0; i < args.Length; i++) { ReportMessage.Log(Verbosity.Verbose,String.Format("Handling Argument {0}: {1}",i,args[i])); switch (args[i]) { case "-n": if (i < args.Length-1) Nick = args[++i]; break; case "-t": if (i < args.Length-1) Host = args[++i]; break; case "-p": if (i < args.Length-1) try { Port = Int32.Parse(args[++i]); } catch (Exception e) { e.ToString(); Port = 6667; } break; case "-v": ReportMessage.verbosity = Verbosity.VeryVerbose; break; case "-q": ReportMessage.verbosity = Verbosity.Quiet; break; case "-P": if (i < args.Length-1) _nickServPass = args[++i]; break; case "-a": if (i < args.Length-1) _autoSend = args[++i]; break; case "-h": Usage(); return; case "-c": if (i < args.Length-1) _configFile = args[++i]; break; } } } } /// /// Create a new Raven instance /// /// /// The arguments for creating the bot /// public Raven(string[] args) { ReportMessage.Log(Verbosity.Always,"Reading arguments..."); // If we have arguments this.ParseArguments(args); this.ConfigureSelfFromFiles(); this._isDisposed = false; ReportMessage.Log(Verbosity.VeryVerbose,"Started with these values:"); ReportMessage.Log(Verbosity.VeryVerbose,this.ToString()); } /// /// Create a raven with default settings. /* public Raven(String host = "localhost", int port = 6667, String nick = "TheRaven-Guest", String nickServPass = "null", String autoSend = null, String configFile = "raven.conf", Verbosity verbosity = Verbosity.Verbose) { this.Host = host; Port = port; Nick = nick; _nickServPass = nickServPass; _autoSend = autoSend; this._configFile = String.Format("/usr/local/etc/TheRaven/{0}",configFile); ReportMessage.verbosity = verbosity; } */ /// /// Identify to the server and join the initial channels /// public void IdentifySelfToServer() { ReportMessage.Log(Verbosity.Always,"Identifying to the server"); //Read for the initial two NOTICE messages about Hostnames IRCServerMessage response = null; int countNotice = 0; while (countNotice < 2) { response = _connection.Read(); if (response.msgCode.Equals("NOTICE")) countNotice += 1; } ReportMessage.Log(Verbosity.VeryVerbose,"Past the notices."); //Send USER and NICK lines to identify. IRCClientMessage send = new IRCClientMessage(); send.CreateCustomMessage(String.Format("NICK {0}\nUSER {0} * * :{0}",Nick)); _connection.Write(send); ReportMessage.Log(Verbosity.VeryVerbose,"USER and NICK sent"); //thanks to cfrayne for the refactor do { response = _connection.Read(); if (response.msgCode != null && response.msgCode.Equals("433")) throw new AlreadyIdentifiedException(); } while (response.msgCode == null || !response.msgCode.Equals("266")); //Identify to NickServ send.NickServIdent(_nickServPass); _connection.Write(send); ReportMessage.Log(Verbosity.VeryVerbose,"Identified to NickServ"); //Send the autosend send.CreateCustomMessage(_autoSend); _connection.Write(send); ReportMessage.Log(Verbosity.VeryVerbose,"Sent autosend"); //Join the default channels foreach (String channel in channels) { if (channel != null && channel.Length > 2 && channel[0] == '#') { send.CreateJoinMessage(channel); _connection.Write(send); } } } /// /// Read from the connection, and for each message act appropriately. /// public void LoopOnTraffic() { ReportMessage.Log(Verbosity.Verbose,"Looping on trafffic now! We're useful!"); // Start a network listener to allow relaying traffic via ncat into IRCd. this._netListener.NetListener(this._connection); // Loop on main connect to ircd while (true) { IRCServerMessage response = _connection.Read(); if (response != null && response.message != null && response.message.Length > 3 && response.message.Substring(0,2).Equals("r.")) { RavenCommand.Respond(_connection,response,this); } else if (response != null) { // Integrate with the ALICE chatbot project. // TODO Create a local instance instead if (response.msgCode.Equals("PRIVMSG") && !String.IsNullOrWhiteSpace(response.message) && (response.target.Equals(Nick) || response.message.StartsWith(String.Format("{0}:",Nick)) || response.message.EndsWith(String.Format("{0}!",Nick)) || response.message.EndsWith(String.Format("{0}?",Nick)) || response.message.EndsWith(String.Format("{0}.",Nick)) || response.message.EndsWith(String.Format("{0}",Nick)))) { IRCClientMessage send = new IRCClientMessage(); try { String aliceResponse = ExecuteCommand.Run(String.Format("bash ./chatbot-support.bash \"{0}\" {1}",response.message.Replace("'","").Replace("\"","").Split('\n')[0].Trim(),Nick)).Trim(); if (String.IsNullOrWhiteSpace(aliceResponse)) throw new Exception("No response from ALICE chatbot service"); send.PrivMsg(aliceResponse,(response.target.Equals(Nick))?response.user:response.target); } catch (Exception e) { e.ToString(); send.PrivMsg("Cannot talk right now.",(response.target.Equals(Nick))?response.user:response.target); } _connection.Write(send); } /* CROWFACTS the deserving */ else if (crowFactsSubscribers.Contains(response.user) && randomSeed.Next(10) < 8) { IRCClientMessage send = new IRCClientMessage(); int location = randomSeed.Next(crowFacts.Count); send.PrivMsg(crowFacts[location],response.user); _connection.Write(send); } // If the WebPage if (WebPageAPI.URLRegEx.Match(response.message).Success) { try { String title = WebPageAPI.GetPageTitle(WebPageAPI.URLRegEx.Match(response.message).Value); if (!String.IsNullOrWhiteSpace(title)) { IRCClientMessage send = new IRCClientMessage(); send.PrivMsg(String.Format("Web page title: {0}",title),(response.target.Equals(Nick))?response.user:response.target); _connection.Write(send); } } catch (Exception e) { e.ToString(); } } } } } /// /// Close the _connection /// private void CloseConnection() { if (this._connection != null) { this._connection.Dispose(); } } /// /// Execute the work of connecting the IRCbot to the network and handle the traffic. /// /// The proper OS-level exit status -- if there are problems, return 1; else return 0 public int Run() { ReportMessage.Log(Verbosity.Verbose,"Beginning..."); //create a new _connection to the Host. try { _connection = new Connection(this.Host,this.Port); IdentifySelfToServer(); LoopOnTraffic(); // Allow the program to exit cleanly } catch (RavenExitedException e) { this.CloseConnection(); this.Dispose(); ReportMessage.Log(Verbosity.Always,String.Format("Exited cleanly.\n{0}",e.Message)); return 0; } //Cleanly exit return 0; } /* Make a Raven disposable */ /// /// Clean up this Connection, implementing IDisposable /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Force the GarbageCollector to Dispose if programmer does not /// ~Raven() { Dispose(false); ReportMessage.Log(Verbosity.Error,"Programmer forgot to dispose of Raven. Marking for Garbage Collector"); } // This bool indicates whether we have disposed of this Raven public bool _isDisposed = true; /// /// Dispose of this Raven's's resources responsibly. /// private void Dispose(bool disposing) { if (this == null) { return; } if (!_isDisposed) { if (disposing) { Host = null; Port = 0; _nickServPass = null; _autoSend = null; _configFile = null; whitelist = null; blacklist = null; magic8 = null; crowFacts = null; crowFactsSubscribers = null; channels = null; searches = null; } _connection.Dispose(); } this._isDisposed = true; } /// /// The default function /// static int Main(string[] args) { try { Raven theRaven = new Raven(args); ReportMessage.Log(Verbosity.Verbose,"### AniNIX::TheRaven ###"); //Continue until we cleanly exit. while (true) { try { return theRaven.Run(); //If we are already identified, we're done. } catch (AlreadyIdentifiedException e) { ReportMessage.Log(Verbosity.Error,"There is already a Raven on this Host."); ReportMessage.Log(Verbosity.Error,e.Message); return 0; // Timeouts should result in a respawn } catch (RavenTimedOutException e) { ReportMessage.Log(Verbosity.Always,"Connection timed out. Respawning"); ReportMessage.Log(Verbosity.Verbose,e.Message); continue; //If an exception gets here, something went wrong } catch (Exception e) { ReportMessage.Log(Verbosity.Error,"Unexpected exception caught!"); ReportMessage.Log(Verbosity.Error,e.Message); return 1; } } } catch (RavenExitedException e) { return 0; } } } }