using System; using System.Collections.Generic; using System.IO; using System.Text; using AniNIX.Shared; 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; // 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. /// /// 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 /usr/local/etc/TheRaven directory to configure this Raven /// // TODO: This and ParseArgs may get punted into their own static class to improve readability. private void ConfigureSelfFromFiles() { String confFilePath = String.Format("/usr/local/etc/TheRaven/{0}",_configFile); if (!File.Exists(confFilePath)) { ReportMessage.Log(Verbosity.Error,"Configuration file doesn't exist."); return; } ReportMessage.Log(Verbosity.Always,String.Format("Reading from config file in /usr/local/etc/{0} and the global files in the same directory...",_configFile)); Configure conf = new Configure(confFilePath); //These are locals that will be used throughout Dictionary loginDefaults = conf.ReadSection("Login"); this.Host = loginDefaults["host"]; this.Port = Int32.Parse(loginDefaults["port"]); this.Nick = loginDefaults["username"]; this._nickServPass = loginDefaults["password"]; channels=new List(); foreach (String channel in conf.ReadSectionLines("Rooms")) { channels.Add(String.Format("#{0}",channel)); } //Parse the lists. notifications = conf.ReadSection("Notifications"); whitelist = conf.ReadSectionLines("Whitelist"); blacklist = conf.ReadSectionLines("Blacklist"); helpText = "Available commands are r.raven, r.magic8, r.uptime, r.heartbeat, r.msg , r.d , r.tinyurl , and r.searches"; searches = conf.ReadSectionLines("Searches"); 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("/usr/local/etc/TheRaven/magic8.txt")).GetLines(); crowFacts = (new Configure("/usr/local/etc/TheRaven/crowfacts.txt")).GetLines(); } /// /// 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 "-h": 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; //TODO: Add daemonizing? case "-a": if (i < args.Length-1) _autoSend = args[++i]; break; case "--help": //TODO Add helptext break; case "-c": if (i < args.Length-1) _configFile = args[++i]; break; case "--version": ReportMessage.Log(Verbosity.Always,"AniNIX::TheRaven version 0.2"); 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(); 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 = _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!"); 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) { //Try to notify the admins when a given string is found in a given channel String result; if (notifications.TryGetValue(response.target,out result)) { if (response.message.Contains(result)) { try { ExecuteCommand.Run(String.Format("/usr/local/bin/djinni admin \"Found {1} in {0}\"",response.target,result)); } catch (Exception e) { ReportMessage.Log(Verbosity.Error,e.ToString()); } } } // 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 /usr/local/src/TheRaven/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() { 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 = false; /// /// Dispose of this Raven's's resources responsibly. /// private void Dispose(bool disposing) { 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) { 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.ToString()); return 1; } } } } }