Converting to Git.

Bazaar log was:
------------------------------------------------------------
revno: 6
committer: dev <dev@aninix.net>
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 <dev@aninix.net>
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 <dev@aninix.net>
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 <ircd@aninix.net>
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 <root@aninix.net>
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 <ircd@aninix.net>
branch nick: TheRaven
timestamp: Thu 2015-12-10 21:58:41 -0600
message:
  Adding all my files.
This commit is contained in:
raven 2016-08-04 11:08:14 -05:00
parent ace6d41661
commit dede6245b9
18 changed files with 1322 additions and 0 deletions

36
Makefile Normal file
View File

@ -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

View File

@ -0,0 +1,13 @@
using System;
namespace AniNIX.TheRaven {
public class AlreadyIdentifiedException : System.Exception {
/// <summary>
/// Create a new AlreadyIdentifiedException to identify this event
/// </summary>
public AlreadyIdentifiedException(String message) : base(message) { }
public AlreadyIdentifiedException() : base(null) { }
}
}

177
connection.csharp Normal file
View File

@ -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
/// <summary>
/// Connect to the host, populating the socket from the configuration options
/// </summary>
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.");
}
/// <summary>
/// Reads a line from the socket
/// </summary>
/// <returns> A string read from the socket </returns>
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;
}
/// <summary>
/// Writes a line to the socket
/// </summary>
/// <param name="toWrite">
/// The string to write
/// </param>
public void Write(IRCMessage toWrite) {
ReportMessage.Log(Verbosity.VeryVerbose,toWrite.ToString());
this._streamWriter.WriteLine(String.Format("{0}\r\n",toWrite.GetOutgoingIRCString()));
this._streamWriter.Flush();
}
/// <summary>
/// Is the user logged in?
/// </summary>
/// <param name="userName">
/// The username to check
/// </param>
/// <returns>
/// A boolean value representing whether the user is logged in or not
/// </returns>
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;
}
/// <summary>
/// Get the modes for a user
/// </summary>
/// <param name="userName">
/// the username to check
/// </param>
/// <returns>
/// A string with the modes.
/// </returns>
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 */
/// <summary>
/// Clean up this Connection, implementing IDisposable
/// </summary>
public void Dispose() {
Dispose(true); // Dispose of this instance
GC.SuppressFinalize(this); //The Garbage Collector doesn't need to finalize it.
}
/// <summary>
/// Force the GarbageCollector to Dispose if programmer does not
/// </summary>
~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;
/// <summary>
/// Dispose of this Connection's resources responsibly.
/// </summary>
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;
}
}
}

17
exitedexception.csharp Normal file
View File

@ -0,0 +1,17 @@
using System;
namespace AniNIX.TheRaven {
public class RavenExitedException : System.Exception {
/// <summary>
/// Create a new RavenTimedOutException to identify this event
/// </summary>
public RavenExitedException(String message) : base(message) { }
/// <summary>
/// Create a new RavenTimedOutException to identify this event
/// </summary>
public RavenExitedException() : base(null) { }
}
}

37
ircclientmessage.csharp Normal file
View File

@ -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);
}
}
}

28
ircmessage.csharp Normal file
View File

@ -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);
}
}
}
}

17
ircpongmessage.csharp Normal file
View File

@ -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);
}
}
}

59
ircservermessage.csharp Normal file
View File

@ -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<String> messageL = new List<String>(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);
}
}
}
}

16
make-conf-dir.bash Normal file
View File

@ -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

15
raven-local.service Normal file
View File

@ -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

387
raven.csharp Normal file
View File

@ -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<String> channels = new List<String>(); //This is the list of channels to join
public List<String> whitelist = new List<String>(); //This is the list of admin users.
public List<String> blacklist = new List<String>(); // 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<String> searches = new List<String>(); //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<String> crowFactsSubscribers = new List<String>(); //These are the subscribers to CrowFacts
public Dictionary<String,String> notifications = new Dictionary<String,String>(); // This is the notifications list for TheRaven.
public Random randomSeed = new Random(); //This is our random seed
public Dictionary<String,int> MailerCount = new Dictionary<String,int>(); // 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.
/// <summary>
/// Show the settings used by this Raven.
/// </summary>
/// <returns>A string representing this Raven</returns>
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();
}
/// <summary>
/// Read from the files in the directory to configure this Raven
/// </summary>
// 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<String> 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]);
}
}
/// <summary>
/// Parse arguments from the command line.
/// </summary>
//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;
}
}
}
}
/// <summary>
/// Create a new Raven instance
/// </summary>
/// <param name="args">
/// The arguments for creating the bot
/// </param>
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());
}
/// <summary>
/// Create a raven with default settings.
/// </summary>
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;
}
/// <summary>
/// Identify to the server and join the initial channels
/// </summary>
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);
}
}
}
/// <summary>
/// Read from the connection, and for each message act appropriately.
/// </summary>
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);
}
}
}
}
/// <summary>
/// Close the _connection
/// </summary>
private void CloseConnection() {
this._connection.Dispose();
}
/// <summary>
/// Execute the work of connecting the IRCbot to the network and handle the traffic.
/// </summaray>
/// <returns> The proper OS-level exit status -- if there are problems, return 1; else return 0 </returns>
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 */
/// <summary>
/// Clean up this Connection, implementing IDisposable
/// </summary>
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Force the GarbageCollector to Dispose if programmer does not
/// </summary>
~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;
/// <summary>
/// Dispose of this Raven's's resources responsibly.
/// </summary>
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;
}
/// <summary>
/// The default function
/// </summary>
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;
}
}
}
}
}

BIN
raven.mono Executable file

Binary file not shown.

14
raven.service Normal file
View File

@ -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

326
ravencommand.csharp Normal file
View File

@ -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.";
/// <summary>
/// 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.
/// <summary>
/// <param name=connection>The socket being used represented by a Connection</param>
/// <param name=incoming>The message from the IRC server to respond to</param>
/// <param name=theRaven>The Raven instance -- we use this to update various lists</param>
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<String,int>();
send.PrivMsg("Mailer counts have been reset",incoming.user);
connection.Write(send);
return;
}
return;
}
/// <summary>
/// This function returns a search string for the user to browse.
/// </summary>
/// <param name=searchBase> this is the base of the search URL</param>
/// <param name=request> This is the string requested to be recombined.</param>
/// <param name=junction>This is the character to use to combine the search arguments</param>
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();
}
}
}

65
ravenconfigure.csharp Normal file
View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace AniNIX.TheRaven {
public static class RavenConfigure {
/// <summary>
/// Create a new list from the line-delimited entries in a file
/// </summary>
/// <param name=filename> the file to read </param>
/// <returns> A List of Strings containing the lines.</returns>
public static List<String> 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<String> newEntries = new List<String>();
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;
}
/// <summary>
/// Get the String[] of lines in a file -- use this for random performance
/// </summary>
/// <param name=filename> the file to read </param>
/// <returns> A String[] </returns>
public static String[] ReadLineDelimitedFileToArr(String filename) {
return RavenConfigure.ReadLineDelimitedFile(filename).ToArray();
}
/// <summary>
/// Read the first line from a file -- this is useful for allowing configuration of single strings.
/// </summary>
/// <param name=filename> the file to read </param>
/// <returns>The first line as a String</returns>
public static String ReadFirstLineFromFile(String filename) {
StreamReader fileReader = new StreamReader(filename);
String readString = fileReader.ReadLine();
fileReader.Close();
return readString;
}
}
}

58
ravenexecute.csharp Normal file
View File

@ -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 {
/// <summary>
/// This method allows TheRaven to execute a command on the OS.
/// </summary>
/// <param name=command>The command string to run as the string argument to "bash -c 'command'"</param>
/// <param name=input>The effective replacement for the command's stdin</param
/// <return>The stdout of the command</return>
/// </summary>
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);
}
}
}

45
reportmessage.csharp Normal file
View File

@ -0,0 +1,45 @@
using System;
namespace AniNIX.TheRaven {
public enum Verbosity {
Always = -2,
Error,
Quiet = 0,
Verbose,
VeryVerbose,
Explicit,
}
public static class ReportMessage {
/// <summary>
/// Log a new message for the user.
/// </summary>
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);
}
}
}

12
timedoutexception.csharp Normal file
View File

@ -0,0 +1,12 @@
using System;
namespace AniNIX.TheRaven {
public class RavenTimedOutException : System.Exception {
/// <summary>
/// Create a new RavenTimedOutException to identify this event
/// </summary>
public RavenTimedOutException(String message) : base(message) { }
}
}