DarkFeather
5e360b0da2
Added Usage function Reformated PKGBUILD to fit standards Corrections to README.md Removed static paths in favor of referential. Test case update Removed unneeded statements from crowfacts
436 lines
20 KiB
Plaintext
436 lines
20 KiB
Plaintext
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using AniNIX.Shared;
|
|
|
|
#pragma warning disable 0168
|
|
#pragma warning disable 2002
|
|
|
|
namespace AniNIX.TheRaven {
|
|
|
|
public sealed class Raven : IDisposable {
|
|
|
|
/* These are the private globals for this instance
|
|
* They should never be accessible outside this Raven
|
|
*/
|
|
//These are the basic configuration information to be overwritten
|
|
public String Host { get; private set; } //This is the Host we are connecting to.
|
|
public int Port { get; private set; } // This is Port to connect on
|
|
public string Nick { get; private set; } // This is the Nickname for this Raven to use.
|
|
private string _nickServPass; // This is the password we will send to NickServ to identify
|
|
private string _autoSend; // This is the command we will automatically send to the Host
|
|
private string _configFile; // This is the configuration directory.
|
|
private Connection _connection; //This is the socket to the Host
|
|
|
|
public List<String> channels; //This is the list of channels to join
|
|
public List<String> whitelist; //This is the list of admin users.
|
|
public List<String> blacklist; // This is the list of blocked people.
|
|
public String helpText = "Available commands are r.d <dice test>, r.heartbeat, r.magic8, r.math <math problem>, r.msg <memo for admin>, r.raven, r.searches, r.tinyurl <url>, r.wikidiff \"one\" \"other\", and r.uptime";
|
|
// This is the text to send when people ask for help -- this is configurable to allow for skinning
|
|
public List<String> searches; //These are the searches
|
|
public String searchesIndex; //This is the helptext for the searches
|
|
public List<String> magic8; //These are the strings to return like a Magic 8-ball to questions.
|
|
public List<String> crowFacts; //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.
|
|
|
|
private RavenNetListener _netListener;
|
|
|
|
/// <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",_configFile));
|
|
sb.Append(String.Format("Verbosity: {0}\n",ReportMessage.verbosity));
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read from the files in the current directory to configure this Raven
|
|
/// </summary>
|
|
// TODO: This and ParseArgs may get punted into their own static class to improve readability.
|
|
private void ConfigureSelfFromFiles() {
|
|
|
|
if (!File.Exists(_configFile)) {
|
|
throw new Exception(String.Format("Configuration file {0} doesn't exist.",_configFile));
|
|
}
|
|
|
|
ReportMessage.Log(Verbosity.Always,String.Format("Reading from config file in {0} and the global files in the same directory...",_configFile));
|
|
Configure conf = new Configure(_configFile);
|
|
|
|
//These are locals that will be used throughout
|
|
ReportMessage.Log(Verbosity.Verbose,"Reading login info");
|
|
try {
|
|
Dictionary<String,String> loginDefaults = conf.ReadSection("Login");
|
|
this.Host = loginDefaults["host"];
|
|
this.Port = Int32.Parse(loginDefaults["port"]);
|
|
this.Nick = loginDefaults["username"];
|
|
this._nickServPass = loginDefaults["password"];
|
|
this._netListener = new RavenNetListener(loginDefaults["password"]);
|
|
|
|
channels=new List<String>();
|
|
foreach (String channel in conf.ReadSectionLines("Rooms")) {
|
|
channels.Add(String.Format("#{0}",channel));
|
|
}
|
|
} catch (Exception e) {
|
|
throw new Exception("Failed to read default login info from Login section in configuration.");
|
|
}
|
|
|
|
//Parse the lists.
|
|
ReportMessage.Log(Verbosity.Verbose,"Building lists.");
|
|
try {
|
|
notifications = conf.ReadSection("Notifications");
|
|
} catch (Exception e) {
|
|
throw new Exception("Couldn't read Notifications section.");
|
|
}
|
|
try {
|
|
whitelist = conf.ReadSectionLines("Whitelist");
|
|
} catch (Exception e) {
|
|
throw new Exception("Couldn't read Whitelist section.");
|
|
}
|
|
try {
|
|
blacklist = conf.ReadSectionLines("Blacklist");
|
|
} catch (Exception e) {
|
|
throw new Exception("Couldn't read Blacklist section.");
|
|
}
|
|
try {
|
|
searches = conf.ReadSectionLines("Searches");
|
|
} catch (Exception e) {
|
|
throw new Exception("Couldn't read Searches section.");
|
|
}
|
|
StringBuilder searchIndexBuilder = new StringBuilder();
|
|
foreach (String searchLine in searches) {
|
|
String[] byPipe = searchLine.Split('|');
|
|
if (byPipe.Length > 3) searchIndexBuilder.Append(String.Format("{0} <{1} search>, ",byPipe[0],byPipe[3]));
|
|
}
|
|
searchesIndex = searchIndexBuilder.ToString();
|
|
|
|
//Read the globals
|
|
magic8 = (new Configure("magic8.txt")).GetLines();
|
|
crowFacts = (new Configure("crowfacts.txt")).GetLines();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Print helptext and exit
|
|
/// param retcode: what to return to the system.
|
|
/// </summary>
|
|
public void Usage() {
|
|
ReportMessage.Log(Verbosity.Always,"Usage: mono ./raven.mono -c conf # start the Raven with the conf file");
|
|
ReportMessage.Log(Verbosity.Always,"Usage: mono ./raven.mono -h # Get help");
|
|
ReportMessage.Log(Verbosity.Always,"");
|
|
ReportMessage.Log(Verbosity.Always,"The following flags are optional:");
|
|
ReportMessage.Log(Verbosity.Always,"-n Nickname");
|
|
ReportMessage.Log(Verbosity.Always,"-t Host");
|
|
ReportMessage.Log(Verbosity.Always,"-p Port");
|
|
ReportMessage.Log(Verbosity.Always,"-v Verbose");
|
|
ReportMessage.Log(Verbosity.Always,"-q Quiet");
|
|
ReportMessage.Log(Verbosity.Always,"-P NickServ passphrase");
|
|
ReportMessage.Log(Verbosity.Always,"-a Autosend command");
|
|
throw new RavenExitedException();
|
|
}
|
|
|
|
|
|
/// <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 "-t":
|
|
if (i < args.Length-1) Host = args[++i];
|
|
break;
|
|
case "-p":
|
|
if (i < args.Length-1) try {
|
|
Port = Int32.Parse(args[++i]);
|
|
} catch (Exception e) {
|
|
e.ToString();
|
|
Port = 6667;
|
|
}
|
|
break;
|
|
case "-v":
|
|
ReportMessage.verbosity = Verbosity.VeryVerbose;
|
|
break;
|
|
case "-q":
|
|
ReportMessage.verbosity = Verbosity.Quiet;
|
|
break;
|
|
case "-P":
|
|
if (i < args.Length-1) _nickServPass = args[++i];
|
|
break;
|
|
case "-a":
|
|
if (i < args.Length-1) _autoSend = args[++i];
|
|
break;
|
|
case "-h":
|
|
Usage();
|
|
return;
|
|
case "-c":
|
|
if (i < args.Length-1) _configFile = 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();
|
|
this._isDisposed = false;
|
|
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 configFile = "raven.conf", Verbosity verbosity = Verbosity.Verbose) {
|
|
this.Host = host;
|
|
Port = port;
|
|
Nick = nick;
|
|
_nickServPass = nickServPass;
|
|
_autoSend = autoSend;
|
|
this._configFile = String.Format("/usr/local/etc/TheRaven/{0}",configFile);
|
|
ReportMessage.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!");
|
|
// Start a network listener to allow relaying traffic via ncat into IRCd.
|
|
this._netListener.NetListener(this._connection);
|
|
// Loop on main connect to ircd
|
|
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) {
|
|
// Integrate with the ALICE chatbot project.
|
|
// TODO Create a local instance instead
|
|
if (response.msgCode.Equals("PRIVMSG") && !String.IsNullOrWhiteSpace(response.message) && (response.target.Equals(Nick) || response.message.StartsWith(String.Format("{0}:",Nick)) || response.message.EndsWith(String.Format("{0}!",Nick)) || response.message.EndsWith(String.Format("{0}?",Nick)) || response.message.EndsWith(String.Format("{0}.",Nick)) || response.message.EndsWith(String.Format("{0}",Nick)))) {
|
|
IRCClientMessage send = new IRCClientMessage();
|
|
try {
|
|
String aliceResponse = ExecuteCommand.Run(String.Format("bash ./chatbot-support.bash \"{0}\" {1}",response.message.Replace("'","").Replace("\"","").Split('\n')[0].Trim(),Nick)).Trim();
|
|
if (String.IsNullOrWhiteSpace(aliceResponse)) throw new Exception("No response from ALICE chatbot service");
|
|
send.PrivMsg(aliceResponse,(response.target.Equals(Nick))?response.user:response.target);
|
|
} catch (Exception e) {
|
|
e.ToString();
|
|
send.PrivMsg("Cannot talk right now.",(response.target.Equals(Nick))?response.user:response.target);
|
|
}
|
|
_connection.Write(send);
|
|
}
|
|
|
|
/* CROWFACTS the deserving */
|
|
else if (crowFactsSubscribers.Contains(response.user) && randomSeed.Next(10) < 8) {
|
|
IRCClientMessage send = new IRCClientMessage();
|
|
int location = randomSeed.Next(crowFacts.Count);
|
|
send.PrivMsg(crowFacts[location],response.user);
|
|
_connection.Write(send);
|
|
}
|
|
|
|
// If the WebPage
|
|
if (WebPageAPI.URLRegEx.Match(response.message).Success) {
|
|
try {
|
|
String title = WebPageAPI.GetPageTitle(WebPageAPI.URLRegEx.Match(response.message).Value);
|
|
if (!String.IsNullOrWhiteSpace(title)) {
|
|
IRCClientMessage send = new IRCClientMessage();
|
|
send.PrivMsg(String.Format("Web page title: {0}",title),(response.target.Equals(Nick))?response.user:response.target);
|
|
_connection.Write(send);
|
|
}
|
|
} catch (Exception e) {
|
|
e.ToString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close the _connection
|
|
/// </summary>
|
|
private void CloseConnection() {
|
|
if (this._connection != null) {
|
|
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 = true;
|
|
|
|
/// <summary>
|
|
/// Dispose of this Raven's's resources responsibly.
|
|
/// </summary>
|
|
private void Dispose(bool disposing) {
|
|
if (this == null) {
|
|
return;
|
|
}
|
|
if (!_isDisposed) {
|
|
if (disposing) {
|
|
Host = null;
|
|
Port = 0;
|
|
_nickServPass = null;
|
|
_autoSend = null;
|
|
_configFile = null;
|
|
whitelist = null;
|
|
blacklist = null;
|
|
magic8 = null;
|
|
crowFacts = null;
|
|
crowFactsSubscribers = null;
|
|
channels = null;
|
|
searches = null;
|
|
|
|
}
|
|
_connection.Dispose();
|
|
}
|
|
this._isDisposed = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default function
|
|
/// </summary>
|
|
static int Main(string[] args) {
|
|
try {
|
|
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.Message);
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
catch (RavenExitedException e) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|