This lengthy howto will show you how to hook up C# to an eggdrop IRC bot. I've taken this approach because it avoids the overhead of managing a fully fledged IRC client in C# whilst still providing 2-way command functionality between IRC and the application.
The app works by:
- Connecting to the eggdrop
- Logging in to the eggdrop
- Sending .say and .topic commands to the eggdrop
- Providing a web service to which the eggdrop may send a SOAP request to perform specific actions
So, let's get started!
The first part is the main IRC class. The version implemented here comes with a log4net appender implementation so that errors can be logged directly to IRC.
IRC.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Collections.Specialized;
using System.Configuration;
using System.IO;
using System.Threading;
namespace Logging
{
public class IRCLogger : log4net.Appender.AppenderSkeleton
{
protected override void Append(log4net.Core.LoggingEvent loggingEvent)
{
if (loggingEvent.Level == log4net.Core.Level.Error)
{
if (loggingEvent.ExceptionObject != null)
{
IRC.SendAdminMessage(loggingEvent.MessageObject.ToString() + ": " + loggingEvent.ExceptionObject.Message);
}
else
{
IRC.SendAdminMessage(loggingEvent.MessageObject.ToString());
}
DateTime gmt = DateTime.Now.AddHours(5);
IRC.SetAdminTopic("Last error rececived on " + gmt.ToShortDateString() + " at " + gmt.ToShortTimeString() + " (GMT).");
}
}
}
public class IRC
{
static TcpClient client = null;
static NetworkStream ns = null;
static StreamWriter sw = null;
static StreamReader sr = null;
static bool isConnecting = false;
static bool isSending = false;
static List<string> commands = new List<string>();
private static void GetConnection()
{
bool ret = false;
if (isConnecting) ret = true;
while (isConnecting) { Thread.Sleep(1000); }
if (ret) return;
isConnecting = true;
try
{
NameValueCollection botConfig =
ConfigurationManager.GetSection("ircSetup/botDetails")
as NameValueCollection;
client = new TcpClient(botConfig["host"], int.Parse(botConfig["port"]));
if (client.Connected)
{
ns = client.GetStream();
sw = new StreamWriter(ns);
sr = new StreamReader(ns);
sw.WriteLine(botConfig["username"]);
sr.ReadLine();
sw.WriteLine(botConfig["password"]);
sr.ReadLine();
sw.Flush();
}
}
finally
{
isConnecting = false;
}
}
public static void SetTopic(string topic)
{
commands.Add(".topic #default [-http://mysite.com-] " + topic);
DoSend();
}
public static void SendMessage(string msg)
{
commands.Add(".say #default [-default-] " + msg);
DoSend();
}
public static void SendMessage(string msg, string channel)
{
commands.Add(".say " + channel + " [-default-] " + msg);
DoSend();
}
public static void SetAdminTopic(string topic)
{
commands.Add(".topic #adminchannel [-admin-] " + topic);
DoSend();
}
public static void SendAdminMessage(string msg)
{
commands.Add(".say #adminchannel [-admin-] " + msg);
DoSend();
}
private static void DoSend()
{
ThreadStart pts = new ThreadStart(SendToBot);
Thread t = new Thread(pts);
t.Start();
}
private static void SendToBot()
{
while (isSending) { System.Threading.Thread.Sleep(1000); }
isSending = true;
try
{
while (commands.Count > 0)
{
//Filter the data using safe whitelist
string msg = commands[0].ToString();
string regex = @".*[\r\n].*";
if (System.Text.RegularExpressions.Regex.IsMatch(msg, regex))
return;
int attempts = 0;
if (client == null)
{
GetConnection();
attempts++;
if (attempts > 3) return;
}
while (client == null || !client.Connected)
{
GetConnection();
attempts++;
if (attempts > 3) return;
}
if (client.Connected)
{
sw.WriteLine(msg);
sw.Flush();
sr.ReadLine();
}
while (ns.DataAvailable)
{
sr.ReadLine();
}
commands.RemoveAt(0);
}
}
catch (Exception ex)
{
Logging.Logger.Log.Info("Error sending to IRC: " + ex.Message);
}
finally
{
isSending = false;
}
}
}
}
Then, put the relevant stuff into your web.config file:
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler
, log4net"
requirePermission="false"/>
<sectionGroup name="ircSetup">
<section name="botDetails" type="System.Configuration.NameValueSectionHandler"/>
</sectionGroup>
</configSections>
<ircSetup>
<botDetails>
<add key="host" value="your.eggdrop.host"/>
<add key="port" value="31337"/>
<add key="username" value="youreggdropusername" />
<add key="password" value="youreggdroppassword" />
</botDetails>
</ircSetup>
<log4net>
<appender name="ircAppender" type="Logging.IRCLogger, Logging">
</log4net>
Tada - it should hook up! But what about getting commands from the bot I hear you cry? Well, first of all you'll need to hook up the following TCL to your eggdrop:
package require http 2.7
bind pubm - * webServiceRequest
proc webServiceRequest {nick uhost hand chan text} {
if {[string index $text 0] == "!"} {
set token [::http::geturl "http://www.yoursite.com/webservice.asmx" \
-type "text/xml; charset=utf-8" \
-headers {"SOAPAction" "http://www.yoursite.com/IrcMessage"} \
-query "<?xml version=\"1.0\" encoding=\"utf-8\"?> \
<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"> \
<soap:Header> \
<authentication xmlns=\"http://www.yoursite.com/\"> \
<user>YourUsername</user> \
<password>YourPassword</password> \
<version>Bot_1.0</version> \
</authentication> \
</soap:Header> \
<soap:Body> \
<ircMessage xmlns=\"http://www.yoursite.com/\"> \
<user>$nick</user> \
<hostmark>$uhost</hostmark> \
<channel>$chan</channel> \
<message>$text</message> \
</ircMessage> \
</soap:Body> \
</soap:Envelope>"]
upvar #0 $token state
if {$state(status) != "ok"} {
putserv "PRIVMSG $chan :ERROR"
}
}
}
Then, the webservice at http://www.yoursite.com/webservice.asmx should expose the following web method:
[WebMethod]
[PrincipalPermissionAttribute(SecurityAction.Demand, Role = "Bot")]
[SoapHeader("authentication")]
public void IrcMessage(string user, string hostmark, string channel, string message)
{
IRC.ParseIrcMessage(user, hostmark, channel, message);
}
With which you can do what you like! The only proviso you might require is the following setup for the authentication SOAP header:
/// <summary>
/// An Authentication class
/// </summary>
public class Authentication : SoapHeader
{
public string User;
public string Password;
public string Version;
}
And the following HttpModule:
using System;
using System.Web;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Text;
using System.Web.Services.Protocols;
using System.Security.Principal;
using System.Web.Security;
using System.Diagnostics;
using Logging;
namespace YourWebService
{
public sealed class WebServiceAuthenticationModule : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication app)
{
app.AuthenticateRequest += new
EventHandler(this.OnEnter);
}
public string ModuleName
{
get { return "WebServiceAuthentication"; }
}
void OnEnter(Object source, EventArgs eventArgs)
{
HttpApplication app = (HttpApplication)source;
HttpContext context = app.Context;
Stream HttpStream = context.Request.InputStream;
// Save the current position of stream.
long posStream = HttpStream.Position;
// If the request contains an HTTP_SOAPACTION
// header, look at this message.
if (context.Request.ServerVariables["HTTP_SOAPACTION"]
== null)
return;
// Load the body of the HTTP message
// into an XML document.
XmlDocument dom = new XmlDocument();
string soapUser;
string soapPassword;
string soapVersion;
try
{
dom.Load(HttpStream);
// Reset the stream position.
HttpStream.Position = posStream;
// Bind to the Authentication header.
soapUser = dom.GetElementsByTagName("User").Item(0).InnerText;
soapPassword = dom.GetElementsByTagName("Password").Item(0).InnerText;
soapVersion = dom.GetElementsByTagName("Version").Item(0).InnerText;
if (Membership.ValidateUser(soapUser, soapPassword))
{
MembershipUser user = Membership.GetUser(soapUser);
GenericIdentity identity = new GenericIdentity(user.UserName);
RolePrincipal rp = new RolePrincipal(identity);
HttpContext.Current.User = rp;
user.Comment = soapVersion;
Membership.UpdateUser(user);
Logger.Log.Info("Authenticated: " + soapUser + " :: " + soapVersion);
}
else
{
Logger.Log.Info("Failed to login as " + soapUser);
Logging.IRC.SendAdminMessage("Attempt to login as " + soapUser + " with password " + soapPassword + " failed.");
}
}
catch (Exception e)
{
Logging.IRC.SendAdminMessage("Error in SOAP parser: " + e.Message);
// Reset the position of stream.
HttpStream.Position = posStream;
// Throw a SOAP exception.
XmlQualifiedName name = new
XmlQualifiedName("Load");
SoapException soapException = new SoapException(
"Unable to read SOAP request", name, e);
throw soapException;
}
return;
}
}
}
Which, of course, you will need to load in web.config:
<httpModules>
<add name="WebServiceAuthenticationModule" type="YourWebService.WebServiceAuthenticationModule" />
</httpModules>