TheRaven/Raven.csharp
DarkFeather 5e360b0da2
Removed Pushbullet support
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
2020-09-10 16:20:40 -05:00

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