TheRaven/Raven.csharp
2023-01-20 14:23:39 -06:00

437 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.ip, 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");
Dictionary<String,String> apiDefaults = conf.ReadSection("API");
this.Host = loginDefaults["host"];
this.Port = Int32.Parse(loginDefaults["port"]);
this.Nick = loginDefaults["username"];
this._nickServPass = loginDefaults["password"];
this._netListener = new RavenNetListener(apiDefaults["password"],Int32.Parse(apiDefaults["port"]));
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 traffic 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;
}
}
}
}