diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fc206c2 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +raven.mono: /usr/bin/mcs /usr/bin/mono clean ./raven.csharp ./connection.csharp ./ravencommand.csharp /bin/bash /usr/bin/mail /usr/bin/wget + mkdir -p /usr/local/etc/TheRaven + mcs -out:raven.mono reportmessage.csharp *exception.csharp irc*message.csharp connection.csharp raven*.csharp + id raven || useradd -M -G bzr,ircd,api raven + id raven || usermod -d /usr/local/etc/TheRaven raven + chown raven:raven /usr/local/etc/TheRaven + +clean: + if [ "$$(ls ./*~ 2>/dev/null | wc -l)" -gt 0 ]; then rm -Rf *~; fi + if [ "$$(ls ./*.mono 2>/dev/null | wc -l)" -gt 0 ]; then rm -Rf *.mono; fi + if [ "$$(ls ./\#* 2>/dev/null | wc -l)" -gt 0 ]; then rm -Rf \#*; fi + if [ -f raven.mono ]; then rm raven.mono; fi + +edit: + emacs -nw raven.csharp + +test: raven.mono + su raven -c 'script -c "mono ./raven.mono -c /usr/local/etc/TheRaven-Test -v" /tmp/raven-test.log' + +check-for-verbosity: + grep Console.WriteLine *.csharp | egrep -v 'verbosity|raven.csharp'; echo + +install: raven.mono + cp raven.mono /opt/raven.mono + [ ! -d /usr/local/etc/TheRaven ] || mkdir -p /usr/local/etc/TheRaven + chown -R raven:raven /opt/raven.mono /usr/local/etc/TheRaven* + chmod 0600 /opt/raven.mono /usr/local/etc/TheRaven*/* + chmod 0700 /usr/local/etc/TheRaven* + cp ./raven.service /usr/lib/systemd/system/raven.service + /usr/bin/bash make-conf-dir.bash /usr/local/etc/TheRaven + systemctl daemon-reload + systemctl enable raven + +commit: /usr/bin/bzr + bzr commit + chown -R raven:bzr .bzr diff --git a/alreadyidentifiedexception.csharp b/alreadyidentifiedexception.csharp new file mode 100644 index 0000000..346764a --- /dev/null +++ b/alreadyidentifiedexception.csharp @@ -0,0 +1,13 @@ +using System; + +namespace AniNIX.TheRaven { + public class AlreadyIdentifiedException : System.Exception { + + /// + /// Create a new AlreadyIdentifiedException to identify this event + /// + public AlreadyIdentifiedException(String message) : base(message) { } + public AlreadyIdentifiedException() : base(null) { } + + } +} diff --git a/connection.csharp b/connection.csharp new file mode 100644 index 0000000..a80e6ab --- /dev/null +++ b/connection.csharp @@ -0,0 +1,177 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.IO; +using System.Text; +using System.Threading; + +namespace AniNIX.TheRaven { + + public class Connection : IDisposable { + + private const int _ircReadTimeout = 200000; // We set this to the IRC mimimum of two minutes in microseconds + + //These privates will be the socket we use. + private NetworkStream _networkStream = null; // This is the stream to use. + private TcpClient _tcpClient = null; // This is the TCP socket for the stream. + private StreamWriter _streamWriter = null; // This is the stream to write to + private StreamReader _streamReader = null; // This is the stream to read from + private String _host = null; // This is DNS name or IP of the host to talk to + private int _port = 0; // this is the port number to connect to + + /// + /// Connect to the host, populating the socket from the configuration options + /// + public Connection(String host, int port) { + ReportMessage.Log(Verbosity.Verbose,String.Format("Connecting to host {0}...",host)); + this._host = host; + this._port = port; + this._tcpClient = new TcpClient(this._host,this._port); + this._tcpClient.ReceiveTimeout = Connection._ircReadTimeout; + this._networkStream = this._tcpClient.GetStream(); + this._streamWriter = new StreamWriter(this._networkStream); + this._streamReader = new StreamReader(this._networkStream); + ReportMessage.Log(Verbosity.VeryVerbose,"... Connected."); + } + + /// + /// Reads a line from the socket + /// + /// A string read from the socket + public IRCServerMessage Read() { + String response = null; + while (response == null) { + try { + response = this._streamReader.ReadLine(); + } catch (IOException e) { // If the socket times out, make sure the host is still alive. + try { + IRCPongMessage pingHost = new IRCPongMessage(String.Format("PING :{0}",this._host)); + Write(pingHost); + response = this._streamReader.ReadLine(); + } catch (IOException f) { // If we get this, then the socket is dead and we need to signal + throw new RavenTimedOutException(String.Format("{0}\n{1}\n",e.Message,f.Message)); + } + } + if (response != null && response.Length > 3 && response.Substring(0,4).Equals("PING")) { // if the response is a PING message, PONG and read again. + IRCPongMessage pong = new IRCPongMessage(response); + Write(pong); + response = null; + } + } + IRCServerMessage readMessage = new IRCServerMessage(response); + ReportMessage.Log(Verbosity.VeryVerbose,readMessage.ToString()); + return readMessage; + + } + + /// + /// Writes a line to the socket + /// + /// + /// The string to write + /// + public void Write(IRCMessage toWrite) { + ReportMessage.Log(Verbosity.VeryVerbose,toWrite.ToString()); + this._streamWriter.WriteLine(String.Format("{0}\r\n",toWrite.GetOutgoingIRCString())); + this._streamWriter.Flush(); + } + + /// + /// Is the user logged in? + /// + /// + /// The username to check + /// + /// + /// A boolean value representing whether the user is logged in or not + /// + public bool IsLoggedIn(String userName) { + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("Asking for user {0} login status.",userName)); + String outgoing = String.Format("WHOIS {0}\r\n",userName); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("<<< {0}",outgoing.Trim())); + this._streamWriter.WriteLine(outgoing); + this._streamWriter.Flush(); + String[] bySpace; + do { + String response = this._streamReader.ReadLine(); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format(">>> {0}",response)); + bySpace = response.Split(' '); + if (bySpace.Length > 1 && bySpace[1].Equals("330")) { + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("User {0} is authenticated.",userName)); + return true; + } + } while (bySpace.Length < 2 || !bySpace[1].Equals("318")); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("User {0} is not authenticated.",userName)); + return false; + } + + /// + /// Get the modes for a user + /// + /// + /// the username to check + /// + /// + /// A string with the modes. + /// + public String GetModes(String userName) { + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("Asking for user {0} mode.",userName)); + String outgoing = String.Format("MODE {0}\r\n",userName); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("<<< {0}",outgoing.Trim())); + this._streamWriter.WriteLine(outgoing); + this._streamWriter.Flush(); + String[] bySpace; + do { + String response = this._streamReader.ReadLine(); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format(">>> {0}",response)); + bySpace = response.Split(' '); + if (bySpace.Length > 9 && bySpace[1].Equals("330")) { + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("User {0} has modes {1}.",userName,bySpace[4])); + return bySpace[4]; + } + } while (bySpace.Length < 2 || !bySpace[1].Equals("502")); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("Cannot get user modes -- not a netadmin.",userName)); + return ""; + } + + + + /* CONNECTION NEEDS TO BE DISPOSABLE BECAUSE IT HOLDS A SOCKET */ + + /// + /// Clean up this Connection, implementing IDisposable + /// + public void Dispose() { + Dispose(true); // Dispose of this instance + GC.SuppressFinalize(this); //The Garbage Collector doesn't need to finalize it. + } + + /// + /// Force the GarbageCollector to Dispose if programmer does not + /// + ~Connection() { + Dispose(false); + ReportMessage.Log(Verbosity.Error,"Programmer forgot to dispose of Connection. Marking for Garbage Collector"); + } + + // This bool indicates whether we are disposed of yet or not + bool _isDisposed = false; + + /// + /// Dispose of this Connection's resources responsibly. + /// + protected virtual void Dispose(bool disposing) { + if (!this._isDisposed) { //if we haven't already disposed of this, we should. + if (disposing) { + //No managed resources for this class. + } + // Cleaning unmanaged resources + this._streamReader.Dispose(); + this._streamWriter.Dispose(); + this._tcpClient.Close(); + this._networkStream.Dispose(); + } + this._isDisposed = true; + } + } +} diff --git a/exitedexception.csharp b/exitedexception.csharp new file mode 100644 index 0000000..c177f3c --- /dev/null +++ b/exitedexception.csharp @@ -0,0 +1,17 @@ +using System; + +namespace AniNIX.TheRaven { + public class RavenExitedException : System.Exception { + + /// + /// Create a new RavenTimedOutException to identify this event + /// + public RavenExitedException(String message) : base(message) { } + + /// + /// Create a new RavenTimedOutException to identify this event + /// + public RavenExitedException() : base(null) { } + + } +} diff --git a/ircclientmessage.csharp b/ircclientmessage.csharp new file mode 100644 index 0000000..8f14870 --- /dev/null +++ b/ircclientmessage.csharp @@ -0,0 +1,37 @@ +using System; +using System.Text; +namespace AniNIX.TheRaven { + + //IRC messages are a primitive data type for us to use. + public class IRCClientMessage : IRCMessage { + + //No incoming string + public new string GetIncomingIRCString() { + return null; + } + + public void CreateCustomMessage(String ircString) { + outgoingIRCString = ircString; + } + + public void NickServIdent(String nickServPass) { + outgoingIRCString = String.Format("PRIVMSG NickServ IDENTIFY {0}",nickServPass); + } + + public void CreateJoinMessage(String channel) { + outgoingIRCString = String.Format("JOIN {0}",channel); + } + + public void CreatePartMessage(String channel) { + outgoingIRCString = String.Format("PART {0}",channel); + } + + public void PrivMsg(String message, string destination) { + outgoingIRCString = String.Format("PRIVMSG {0} :{1}",destination,message); + } + + public void ActionMsg(String message, string destination) { + outgoingIRCString = String.Format("PRIVMSG {0} :\u0001ACTION {1}",destination,message); + } + } +} diff --git a/ircmessage.csharp b/ircmessage.csharp new file mode 100644 index 0000000..976c0ed --- /dev/null +++ b/ircmessage.csharp @@ -0,0 +1,28 @@ +using System; + +namespace AniNIX.TheRaven { + + public abstract class IRCMessage { + + protected string incomingIRCString; + protected string outgoingIRCString; + + public string GetOutgoingIRCString() { + return outgoingIRCString; + } + + public string GetIncomingIRCString() { + return incomingIRCString; + } + + public override string ToString() { + if (outgoingIRCString == null || outgoingIRCString.Length < 1) { + return String.Format(">>> {0}",incomingIRCString); + } else if (incomingIRCString == null || incomingIRCString.Length < 1) { + return String.Format("<<< {0}",outgoingIRCString); + } else { + return string.Format(">>> {1}\n<<< {0}",outgoingIRCString,incomingIRCString); + } + } + } +} diff --git a/ircpongmessage.csharp b/ircpongmessage.csharp new file mode 100644 index 0000000..b3cb81e --- /dev/null +++ b/ircpongmessage.csharp @@ -0,0 +1,17 @@ +using System; +using System.Text; + +namespace AniNIX.TheRaven { + + public class IRCPongMessage : IRCMessage { + + public IRCPongMessage(String serverString) { + incomingIRCString = String.Copy(serverString.Trim()); + outgoingIRCString = String.Copy(incomingIRCString); + char[] arr = outgoingIRCString.ToCharArray(); + arr[1] = 'O'; + outgoingIRCString = new String(arr); + } + + } +} diff --git a/ircservermessage.csharp b/ircservermessage.csharp new file mode 100644 index 0000000..2b2be85 --- /dev/null +++ b/ircservermessage.csharp @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace AniNIX.TheRaven { + + //IRC messages are a primitive data type for us to use. + public class IRCServerMessage : IRCMessage { + + public string user { get; protected set; } + public string server { get; protected set; } + public string msgCode { get; protected set; } + public string target { get; protected set; } + public string message { get; protected set; } + + public IRCServerMessage(String serverString) { + incomingIRCString = serverString.Trim(); + try { + String[] byColon = incomingIRCString.Split(':'); + user = byColon[0]; + String[] bySpace = byColon[1].Split(' '); + List messageL = new List(byColon); + messageL.RemoveAt(0); + messageL.RemoveAt(0); + message = String.Join(":",messageL.ToArray()); + if (bySpace[0].Contains("!")) { + String[] byExclamation = bySpace[0].Split('!'); + user = byExclamation[0]; + server = byExclamation[1]; + } else { + user = null; + server = bySpace[0]; + } + + msgCode = bySpace[1]; + target = bySpace[2]; + } catch (IndexOutOfRangeException e) { + ReportMessage.Log(Verbosity.Error,String.Format("!!! Can't translate string:\n{0}",serverString,e.ToString())); + user = null; + server = null; + msgCode = null; + target = null; + message = null; + } + } + + // There is no outgoing string. + public new string GetOutgoingIRCString() { + return null; + } + + public override string ToString() { + if (Raven.verbosity == Verbosity.Explicit) { + return String.Format(">>> {0}\nUser: {1}\nServer: {2}\nmsgCode: {3}\nTarget: {4}\nMessage: {5}\n",incomingIRCString,user,server,msgCode,target,message); + } else { + return String.Format(">>> {0}",incomingIRCString); + } + } + } +} diff --git a/make-conf-dir.bash b/make-conf-dir.bash new file mode 100644 index 0000000..9f8ff9f --- /dev/null +++ b/make-conf-dir.bash @@ -0,0 +1,16 @@ +#!/usr/bin/bash + +mkdir -p $1 +touch $1/blacklist.txt +touch $1/crowfacts.txt +touch $1/crowfacts.txt.bak +touch $1/hangman.txt +touch $1/keepalive-loginDefaults.txt +touch $1/magic8.txt +touch $1/todo.txt +touch $1/whitelist.txt +touch $1/loginDefaults.txt +touch $1/rooms.txt +touch $1/searches.txt +touch $1/helptext.txt + diff --git a/raven-local.service b/raven-local.service new file mode 100644 index 0000000..c781a9f --- /dev/null +++ b/raven-local.service @@ -0,0 +1,15 @@ +[Unit] +Description=AniNIX::Raven IRC Bot for Local +After=network.target + +[Service] +type=simple +ExecStart=/usr/bin/mono /opt/raven.mono -c /usr/local/etc/TheRaven-Local +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=always +User=ircd +Group=ircd + +[Install] +WantedBy=multi-user.target diff --git a/raven.csharp b/raven.csharp new file mode 100644 index 0000000..53193e5 --- /dev/null +++ b/raven.csharp @@ -0,0 +1,387 @@ +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 + private string _nick; // 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()); + } + + /// + /// 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("djinni admin \"Found {1} in {0}\"",response.target,result)); + } catch (Exception e) { + ReportMessage.Log(Verbosity.Error,e.ToString()); + } + + } + } + + //TODO Implement the dialog options and link reponse + + /* 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; + } + } + } + } +} diff --git a/raven.mono b/raven.mono new file mode 100755 index 0000000..c8563fa Binary files /dev/null and b/raven.mono differ diff --git a/raven.service b/raven.service new file mode 100644 index 0000000..a6a26ae --- /dev/null +++ b/raven.service @@ -0,0 +1,14 @@ +[Unit] +Description=AniNIX::Raven IRC Bot for ACWiki +After=network.target + +[Service] +ExecStart=/usr/bin/mono /opt/raven.mono -c /usr/local/etc/TheRaven +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=always +User=raven +Group=raven + +[Install] +WantedBy=multi-user.target diff --git a/ravencommand.csharp b/ravencommand.csharp new file mode 100644 index 0000000..277ed5f --- /dev/null +++ b/ravencommand.csharp @@ -0,0 +1,326 @@ +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; + +namespace AniNIX.TheRaven { + + public static class RavenCommand { + + // This is the string we return when people poorly format a command. + public static String helpString = "Bad formatting! Type r.help for help."; + + /// + /// This is a contextual match of commands with actions for the bot to take. + /// This will be a very long function, but watch for the /* HEADER */ sections to subdivide it. + /// + /// The socket being used represented by a Connection + /// The message from the IRC server to respond to + /// The Raven instance -- we use this to update various lists + public static void Respond(Connection connection,IRCServerMessage incoming, Raven theRaven) { + + IRCClientMessage send = new IRCClientMessage(); + + /* CANCEL ON BLACKLIST */ + if (theRaven.blacklist.Contains(incoming.user)) { return; } // Blacklisted people can't do anything. + + //Splits placed here for performance reasons + String[] bySpace = incoming.message.Split(' '); + String command = bySpace[0]; + + /* IDENTIFY the command and IRCClientMessage to send */ + + /* COMMON Commands everyone can use */ + switch (command) { + case "r.raven": + send.ActionMsg("quoth, \"Nevermore!\"",incoming.target); + connection.Write(send); + return; + case "r.help": + send.PrivMsg(theRaven.helpText,incoming.target); + connection.Write(send); + return; + case "r.magic8": + if (theRaven.magic8 == null) { + send.PrivMsg("Magic8 not loaded",incoming.target); + } else { + int location = theRaven.randomSeed.Next(theRaven.magic8.Length); + send.PrivMsg(theRaven.magic8[location],incoming.target); + } + connection.Write(send); + return; + case "r.tinyurl": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.user); + } else { + try { + send.PrivMsg(RavenExecute.Command(String.Format("wget -q -O - \"http://tiny-url.info/api/v1/create?format=text&apikey=\"$(api-keys tinyurl)\"&provider=x_co&url={0}\"",bySpace[1])),incoming.target); + } catch (Exception e) { + ReportMessage.Log(Verbosity.Error,e.ToString()); + send.PrivMsg("TinyURL error. Could not get link.",incoming.target); + } + } + connection.Write(send); + return; + case "r.msg": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.user); + } else { + if (!theRaven.MailerCount.ContainsKey(incoming.user)) theRaven.MailerCount.Add(incoming.user,0); + if (theRaven.MailerCount[incoming.user] >= 5) { + send.PrivMsg("You cannot send more than 5 messages to the admins between resets. This has been logged.",incoming.user); + connection.Write(send); + ReportMessage.Log(Verbosity.Error,String.Format("!!! {0} tried to send a message but couldn't -- attempts exceeded.",incoming.user)); + return; + } + theRaven.MailerCount[incoming.user] = theRaven.MailerCount[incoming.user] + 1; + //Try to append the message to the log + StringBuilder sb = new StringBuilder(bySpace[1]); + for (int i = 2; i < bySpace.Length; i++) { + sb.Append(" "); + sb.Append(bySpace[i]); + } + try { + StreamWriter writer = new StreamWriter("/var/log/r.msg.log",true); + writer.Write(String.Format("{0} - {1} left a message on {2} in {3}\n{4}\n\n",DateTime.Now,incoming.user,theRaven.Host,incoming.target,sb.ToString())); + writer.Close(); + } catch (Exception e) { + ReportMessage.Log(Verbosity.Error,e.Message); + ReportMessage.Log(Verbosity.Error,"Make sure user raven can write to /var/log/r.msg.log"); + } + try { + RavenExecute.Command(String.Format("djinni admin \"Page from {0}\"",incoming.user)); + send.PrivMsg("Sent!",incoming.user); + } catch (Exception e) { + ReportMessage.Log(Verbosity.Error,e.ToString()); + send.PrivMsg("Mailer error. Could not send.",incoming.user); + } + } + connection.Write(send); + return; + case "r.uptime": + try { + send.PrivMsg(RavenExecute.Command("uptime"),incoming.target); + } catch (Exception e) { + e.ToString(); + send.PrivMsg("Can't get uptime",incoming.target); + } + connection.Write(send); + return; + case "r.heartbeat": + try { + String[] byLine = RavenExecute.Command("heartbeat-client").Split('\n'); + for (int i = 0; i < byLine.Length; i++) { + send.PrivMsg(byLine[i],incoming.user); + connection.Write(send); + } + } catch (Exception e) { + e.ToString(); + send.PrivMsg("Can't get heartbeat",incoming.user); + } + return; + + } + + /* SEARCHES */ + foreach (String search in theRaven.searches) { + if (search == null) continue; + String[] byPipe = search.Split('|'); + if (byPipe.Length < 3) { + ReportMessage.Log(Verbosity.Error,String.Format("Incomplete search: {0}",search)); + continue; + } + if (byPipe[0].Equals(command)) { + send.PrivMsg(FormatSearch(byPipe[1],incoming.message,byPipe[2]),incoming.target); + connection.Write(send); + return; + } + } + + /* ADMIN commands are allowed to whitelisted users */ + if (theRaven.whitelist.Contains(incoming.user) && connection.IsLoggedIn(incoming.user)) switch (command) { + case "r.quit": + send.CreateCustomMessage("QUIT :Caw, caw, caw!"); + connection.Write(send); + throw new RavenExitedException(); + case "r.cf": + if (bySpace.Length < 2) { + send.PrivMsg("This is the CrowFacts list of subscribers:",incoming.user); + connection.Write(send); + foreach (String user in theRaven.crowFactsSubscribers) { + send.PrivMsg(user,incoming.user); + connection.Write(send); + } + send.PrivMsg("End subscribers",incoming.user); + + send.PrivMsg(theRaven.helpText,incoming.target); + } else if (theRaven.crowFacts == null) { + send.PrivMsg("CrowFacts not loaded.",incoming.target); + } else { + if (!theRaven.crowFactsSubscribers.Contains(bySpace[1])) { + theRaven.crowFactsSubscribers.Add(bySpace[1]); + send.PrivMsg(String.Format("{0} has been subscribed to CrowFacts!",bySpace[1]),incoming.target); + } else { + send.PrivMsg("Subscriber already added",incoming.target); + } + } + connection.Write(send); + return; + case "r.us": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + if (theRaven.crowFactsSubscribers.Contains(bySpace[1])) { + theRaven.crowFactsSubscribers.Remove(bySpace[1]); + send.PrivMsg(String.Format("{0} has been unsubscribed.",bySpace[1]),incoming.target); + } else { + send.PrivMsg("No such subscriber",incoming.target); + } + } + connection.Write(send); + return; + case "r.join": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + if (!theRaven.channels.Contains(bySpace[1])) { + theRaven.channels.Add(bySpace[1]); + send.CreateJoinMessage(bySpace[1]); + } else { + send.PrivMsg("Already joined channel",incoming.target); + } + } + connection.Write(send); + return; + case "r.part": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + if (theRaven.channels.Contains(bySpace[1])) { + theRaven.channels.Remove(bySpace[1]); + send.CreatePartMessage(bySpace[1]); + } else { + send.PrivMsg("No such channel",incoming.target); + } + } + connection.Write(send); + return; + case "r.say": + if (bySpace.Length < 3 || !bySpace[1].StartsWith("#")) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + StringBuilder newMsg = new StringBuilder(bySpace[2]); + for (int i = 3; i < bySpace.Length; i++) { + newMsg.Append(" "); + newMsg.Append(bySpace[i]); + } + send.PrivMsg(newMsg.ToString(),bySpace[1]); + } + connection.Write(send); + return; + case "r.act": + if (bySpace.Length < 3 || !bySpace[1].StartsWith("#")) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + StringBuilder newAction = new StringBuilder(bySpace[2]); + for (int i = 3; i < bySpace.Length; i++) { + newAction.Append(" "); + newAction.Append(bySpace[i]); + } + send.ActionMsg(newAction.ToString(),bySpace[1]); + } + connection.Write(send); + return; + case "r.whitelist": + if (bySpace.Length < 2) { + send.PrivMsg("This is the whitelist:",incoming.user); + connection.Write(send); + foreach (String user in theRaven.whitelist) { + send.PrivMsg(user,incoming.user); + connection.Write(send); + } + send.PrivMsg("End whitelist",incoming.user); + } else { + if (!theRaven.whitelist.Contains(bySpace[1]) && !theRaven.blacklist.Contains(bySpace[1])) { + theRaven.whitelist.Add(bySpace[1]); + send.PrivMsg(String.Format("{0} has been whitelisted.",bySpace[1]),incoming.user); + } else { + send.PrivMsg("Already whitelisted or is a blacklisted user",incoming.user); + } + } + connection.Write(send); + return; + case "r.blacklist": + if (bySpace.Length < 2) { + send.PrivMsg("This is the blacklist:",incoming.user); + connection.Write(send); + foreach (String user in theRaven.blacklist) { + send.PrivMsg(user,incoming.user); + connection.Write(send); + } + send.PrivMsg("End blacklist",incoming.user); + } else { + if (!theRaven.blacklist.Contains(bySpace[1]) && !theRaven.whitelist.Contains(bySpace[1])) { + theRaven.blacklist.Add(bySpace[1]); + send.PrivMsg(String.Format("{0} has been blacklisted.",bySpace[1]),incoming.user); + } else { + send.PrivMsg("Already blacklisted or is an admin user",incoming.user); + } + } + connection.Write(send); + return; + case "r.greylist": + if (bySpace.Length < 2) { + send.PrivMsg(theRaven.helpText,incoming.target); + } else { + if (theRaven.whitelist.Contains(bySpace[1])) { + theRaven.whitelist.Remove(bySpace[1]); + send.PrivMsg("User cleared from whitelist",incoming.user); + } else if (theRaven.blacklist.Contains(bySpace[1])) { + theRaven.blacklist.Remove(bySpace[1]); + send.PrivMsg("User cleared from blacklist",incoming.user); + } else { + send.PrivMsg("No action needed",incoming.user); + } + } + connection.Write(send); + return; + case "r.adminhelp": + send.PrivMsg("r.adminhelp, r.cf to crowfacts, r.us to unsubscribe, r.whitelist to make user bot admin, r.blacklist to block user, r.greylist to pull user off *list, r.say [channel] [message], r.join a channel, r.part a channel, r.quit to quit",incoming.user); + connection.Write(send); + return; + // Commenting because I believe this to be unneeded + // TODO Reevaluate + /* case "r.ident": + theRaven.IdentifySelfToServer(); + return;*/ + case "r.mailerreset": + theRaven.MailerCount = new Dictionary(); + send.PrivMsg("Mailer counts have been reset",incoming.user); + connection.Write(send); + return; + } + + return; + } + + /// + /// This function returns a search string for the user to browse. + /// + /// this is the base of the search URL + /// This is the string requested to be recombined. + /// This is the character to use to combine the search arguments + public static String FormatSearch(String searchBase,String request,String junction) { + StringBuilder formattedString = new StringBuilder(searchBase); + String[] elements = request.Split(' '); + if (elements.Length < 2) return helpString; + formattedString.Append(elements[1]); //First element is the command + for (int i = 2; i < elements.Length; i++) { + formattedString.Append(junction); + formattedString.Append(elements[i]); + } + return formattedString.ToString(); + } + } +} diff --git a/ravenconfigure.csharp b/ravenconfigure.csharp new file mode 100644 index 0000000..1179fc6 --- /dev/null +++ b/ravenconfigure.csharp @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace AniNIX.TheRaven { + + public static class RavenConfigure { + + /// + /// Create a new list from the line-delimited entries in a file + /// + /// the file to read + /// A List of Strings containing the lines. + public static List ReadLineDelimitedFile(String filename) { + String line = null; + int count = 0; + //Read all the file to join. + ReportMessage.Log(Verbosity.Verbose,String.Format("Reading {0}",filename)); //Path.GetFileName(filename))); + List newEntries = new List(); + StreamReader fileReader = new StreamReader(filename); + line = fileReader.ReadLine(); + while (line != null) { + if (line.Length < 1) { + line = fileReader.ReadLine(); + continue; + } + line = line.Trim(); + if (line[0] == '#') { + line = fileReader.ReadLine(); + continue; + } //Skip lines starting with a # + String[] byHash = line.Split('#'); //Ignore everything after a # + newEntries.Add(byHash[0]); + count++; + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("Added entry {0} from {1}",line,Path.GetFileName(filename))); + line = fileReader.ReadLine(); + } + fileReader.Close(); + ReportMessage.Log(Verbosity.VeryVerbose,String.Format("Found {0} newEntries.",newEntries.Count)); + return newEntries; + } + + /// + /// Get the String[] of lines in a file -- use this for random performance + /// + /// the file to read + /// A String[] + public static String[] ReadLineDelimitedFileToArr(String filename) { + return RavenConfigure.ReadLineDelimitedFile(filename).ToArray(); + } + + /// + /// Read the first line from a file -- this is useful for allowing configuration of single strings. + /// + /// the file to read + /// The first line as a String + public static String ReadFirstLineFromFile(String filename) { + StreamReader fileReader = new StreamReader(filename); + String readString = fileReader.ReadLine(); + fileReader.Close(); + return readString; + } + } +} diff --git a/ravenexecute.csharp b/ravenexecute.csharp new file mode 100644 index 0000000..ad24027 --- /dev/null +++ b/ravenexecute.csharp @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; + +namespace AniNIX.TheRaven { + + public static class RavenExecute { + + /// + /// This method allows TheRaven to execute a command on the OS. + /// + /// The command string to run as the string argument to "bash -c 'command'" + /// The effective replacement for the command's stdinThe stdout of the command + /// + public static String Command(String command, String input) { + //Sanitize inputs. + if (command.Contains("\'")) { + throw new Exception("Command strings cannot include \'."); + } + + //Create process. + Process proc = new Process(); + proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.FileName = "/bin/bash"; + proc.StartInfo.Arguments = String.Format("-c \'{0}\'",command); + proc.StartInfo.UseShellExecute=false; + + //Redirect input + proc.StartInfo.RedirectStandardOutput=true; + proc.StartInfo.RedirectStandardInput=true; + + //Start process + proc.Start(); + + //Add input and read output. + proc.StandardInput.Write(input); + proc.StandardInput.Close(); + proc.WaitForExit(); + if (proc.ExitCode != 0) { + throw new Exception(String.Format("Failed to exit command with return code {0}",proc.ExitCode)); + } + String stdoutString = proc.StandardOutput.ReadToEnd(); + + //Close up and return + proc.Close(); + return stdoutString; + } + + //Add polymorphism to allow no stdin + public static String Command(String command) { + return Command(command,null); + } + + } +} diff --git a/reportmessage.csharp b/reportmessage.csharp new file mode 100644 index 0000000..f6738c6 --- /dev/null +++ b/reportmessage.csharp @@ -0,0 +1,45 @@ +using System; + +namespace AniNIX.TheRaven { + + public enum Verbosity { + Always = -2, + Error, + Quiet = 0, + Verbose, + VeryVerbose, + Explicit, + } + + public static class ReportMessage { + + + /// + /// Log a new message for the user. + /// + public static void Log(Verbosity level,String message) { + + if (level == Verbosity.Error) { + Console.Error.WriteLine(message); + return; + } + + if (Raven.verbosity == Verbosity.Quiet) { + return; + } + + if (level == Verbosity.Always + || (Raven.verbosity == Verbosity.Verbose && level == Verbosity.Verbose) + || (Raven.verbosity == Verbosity.VeryVerbose && (level == Verbosity.Verbose || level == Verbosity.VeryVerbose)) + || (Raven.verbosity == Verbosity.Explicit && (level == Verbosity.Verbose || level == Verbosity.VeryVerbose || level == Verbosity.Explicit)) + ) { + Console.WriteLine(message); + } + + } + + public static void Log(String message) { + Log(Verbosity.VeryVerbose,message); + } + } +} diff --git a/timedoutexception.csharp b/timedoutexception.csharp new file mode 100644 index 0000000..6768874 --- /dev/null +++ b/timedoutexception.csharp @@ -0,0 +1,12 @@ +using System; + +namespace AniNIX.TheRaven { + public class RavenTimedOutException : System.Exception { + + /// + /// Create a new RavenTimedOutException to identify this event + /// + public RavenTimedOutException(String message) : base(message) { } + + } +}