From dede6245b9d647875affa65a6db3cf96d7e1aa0e Mon Sep 17 00:00:00 2001 From: raven Date: Thu, 4 Aug 2016 11:08:14 -0500 Subject: [PATCH] Converting to Git. Bazaar log was: ------------------------------------------------------------ revno: 6 committer: dev branch nick: TheRaven timestamp: Thu 2016-06-02 23:21:56 -0500 message: Updating to include dedicated raven user. Adding RavenExecute.Command() static function to handle OS integration. Updating RavenCommand to use Djinni package for mailing. ------------------------------------------------------------ revno: 5 committer: dev branch nick: TheRaven timestamp: Wed 2016-04-20 16:59:51 -0500 message: Updated Makefile to include OS dependencies Updated ircservermessage to fix dropped sections of searches from things like "r.image :blar:" Added new tinyURL feature -- WARNING: this requires that the OS has a script api-keys in the path that will return an API key for the TinyURL service ------------------------------------------------------------ revno: 4 committer: dev branch nick: TheRaven timestamp: Mon 2016-03-28 14:40:00 -0500 message: MailerCount is now added to limit the number of pages sent to admins. ------------------------------------------------------------ revno: 3 committer: ircd branch nick: TheRaven timestamp: Fri 2016-01-15 14:36:54 -0600 message: Removing an unneeded file -- requests are tracked in keep.google.com now Makefile had a run rule added to run the bot in the foreground with the verbose flag. This prevents the user from accidentally compiling and installing TheRaven with higher verbosity and filling system logs. Ravencommand was modified to check for user modes r (registerd on UnrealIRCd) or G (registered on InspIRCd) before running whitelist commands. connection.csharp had functions added to check for users being authenticated (IRC response code 330 in a WHOIS request) and do get modes ------------------------------------------------------------ revno: 2 committer: root branch nick: TheRaven timestamp: Mon 2015-12-14 14:58:02 -0600 message: Updated commenting Removed requests file in favor of keep.google.com Updated ircservermessage.csharp et. al. for better verbosity options Edited connection.csharp to not need a Raven instance and instead take host and port Updated private globals to standard Moved Verbosity enum to not be inside ReportMessage class ------------------------------------------------------------ revno: 1 committer: ircd branch nick: TheRaven timestamp: Thu 2015-12-10 21:58:41 -0600 message: Adding all my files. --- Makefile | 36 +++ alreadyidentifiedexception.csharp | 13 + connection.csharp | 177 ++++++++++++++ exitedexception.csharp | 17 ++ ircclientmessage.csharp | 37 +++ ircmessage.csharp | 28 +++ ircpongmessage.csharp | 17 ++ ircservermessage.csharp | 59 +++++ make-conf-dir.bash | 16 ++ raven-local.service | 15 ++ raven.csharp | 387 ++++++++++++++++++++++++++++++ raven.mono | Bin 0 -> 25600 bytes raven.service | 14 ++ ravencommand.csharp | 326 +++++++++++++++++++++++++ ravenconfigure.csharp | 65 +++++ ravenexecute.csharp | 58 +++++ reportmessage.csharp | 45 ++++ timedoutexception.csharp | 12 + 18 files changed, 1322 insertions(+) create mode 100644 Makefile create mode 100644 alreadyidentifiedexception.csharp create mode 100644 connection.csharp create mode 100644 exitedexception.csharp create mode 100644 ircclientmessage.csharp create mode 100644 ircmessage.csharp create mode 100644 ircpongmessage.csharp create mode 100644 ircservermessage.csharp create mode 100644 make-conf-dir.bash create mode 100644 raven-local.service create mode 100644 raven.csharp create mode 100755 raven.mono create mode 100644 raven.service create mode 100644 ravencommand.csharp create mode 100644 ravenconfigure.csharp create mode 100644 ravenexecute.csharp create mode 100644 reportmessage.csharp create mode 100644 timedoutexception.csharp 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 0000000000000000000000000000000000000000..c8563fa9d3ed8fb3f042c987422b4eab4ea70254 GIT binary patch literal 25600 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 VueHFq`5WGP92S4``L}0*{{!&5L1F*^ literal 0 HcmV?d00001 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) { } + + } +}