using System; using System.Collections.Generic; using System.IO; using System.Text; 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 configDir; // This is the configuration directory. private Connection _connection; //This is the socket to the Host public List channels = new List(); //This is the list of channels to join public List whitelist = new List(); //This is the list of admin users. public List blacklist = new List(); // This is the list of blocked people. public String helpText = null; // This is the text to send when people ask for help -- this is configurable to allow for skinning public List searches = new List(); //These are the searches public String[] magic8 = null; //These are the strings to return like a Magic 8-ball to questions. public String[] crowFacts = null; //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. public static Verbosity verbosity { get; private set; } // This is the level to which this and all Raven instances will log. /// /// 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",configDir)); sb.Append(String.Format("Verbosity: {0}\n",Raven.verbosity)); return sb.ToString(); } /// /// Read from the files in the directory to configure this Raven /// // TODO: This and ParseArgs may get punted into their own static class to improve readability. private void ConfigureSelfFromFiles(String configDir) { if (configDir==null || configDir == "" || !Directory.Exists(configDir)) { ReportMessage.Log(Verbosity.Error,"Configuration directory does not exist!"); return; } ReportMessage.Log(Verbosity.Always,String.Format("Reading from files in {0}...",configDir)); //These are locals that will be used throughout ReportMessage.Log(Verbosity.Verbose,"Reading login defaults"); String[] loginDefaults = RavenConfigure.ReadLineDelimitedFile(Path.Combine(configDir,"loginDefaults.txt")).ToArray(); //We have to populate these properties fom the list explicitly if (loginDefaults.Length < 4) { ReportMessage.Log(Verbosity.Error,"Login defaults are incomplete. No changes made."); } else { Host = (Host==null) ? loginDefaults[0] : Host; try { Port = (Port == 0) ? Int32.Parse(loginDefaults[1]) : Port; } catch (Exception e) { ReportMessage.Log(Verbosity.Verbose,"Cannot parse Port."); e.ToString(); Port = 6667; } Nick = (Nick == null) ? loginDefaults[2] : Nick; _nickServPass = (_nickServPass == null) ? loginDefaults[3] : _nickServPass; } //Read all the channels to join. List tempChannels = RavenConfigure.ReadLineDelimitedFile(Path.Combine(configDir,"rooms.txt")); // Because the convention is to use # for comments, channels in the rooms.txt file do not start with a # foreach (String channel in tempChannels) { channels.Add(String.Format("#{0}",channel)); } //Read the whitelist of folks allowed to execute administrative commands whitelist = RavenConfigure.ReadLineDelimitedFile(Path.Combine(configDir,"whitelist.txt")); //Read the blacklist of folks not allowed to do anything. blacklist = RavenConfigure.ReadLineDelimitedFile(Path.Combine(configDir,"blacklist.txt")); //Read the helptext helpText = RavenConfigure.ReadFirstLineFromFile(Path.Combine(configDir,"helptext.txt")); //Read the searches to use searches = RavenConfigure.ReadLineDelimitedFile(Path.Combine(configDir,"searches.txt")); //Read the Magic8 options magic8 = RavenConfigure.ReadLineDelimitedFileToArr(Path.Combine(configDir,"magic8.txt")); //Read the CrowFacts crowFacts = RavenConfigure.ReadLineDelimitedFileToArr(Path.Combine(configDir,"crowfacts.txt")); //Read the notifications foreach (String combo in RavenConfigure.ReadLineDelimitedFileToArr(Path.Combine(configDir,"notifications.txt"))) { String[] byPipe = combo.Split('|'); notifications.Add(String.Format("#{0}",byPipe[0]),byPipe[1]); } } /// /// 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": Raven.verbosity = Verbosity.VeryVerbose; break; case "-q": Raven.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) configDir = 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(configDir); ReportMessage.Log(Verbosity.VeryVerbose,"Started with these values:"); ReportMessage.Log(Verbosity.VeryVerbose,this.ToString()); } /// /// Populate the name recognition /// /// /// 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 configDir = "/usr/local/etc/TheRaven-Local", Verbosity verbosity = Verbosity.Verbose) { this.Host = host; Port = port; Nick = nick; _nickServPass = nickServPass; _autoSend = autoSend; this.configDir = configDir; Raven.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 { RavenExecute.Command(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(); send.PrivMsg(RavenExecute.Command(String.Format("bash /usr/local/src/TheRaven/chatbot-support.bash {0} {1}",response.message.Replace("'","").Replace("\"","").Split('\n')[0].Trim(),Nick)).Trim(),(response.target.Equals(Nick))?response.user:response.target); _connection.Write(send); } /* CROWFACTS the deserving */ if (crowFactsSubscribers.Contains(response.user) && randomSeed.Next(10) < 8) { IRCClientMessage send = new IRCClientMessage(); int location = randomSeed.Next(crowFacts.Length); send.PrivMsg(crowFacts[location],response.user); _connection.Write(send); } } } } /// /// 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; configDir = 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; } } } } }