Forums » Community Projects

Lua-based IRC bridge bot

12»
May 10, 2007 jexkerome link
Okay, I threatened to do it on #vrelay, and here it is: the code for the CDC's IRC bridge bot.

This bot needs two things to work:

1. A VO account. You can't use your own unless you want to not play (it runs a bind continuously), and the account must be paid for unless you want to have a bot for only 8 hours. This is the Ingame Component.

2. The program below, which is the bot proper, and for which you need both Lua and the LuaSocket library. We call this the Lua Component.

The bot works in the following manner:

The Lua Component continually scans VO's errors.log, looking for guild chat which then parses into the guild's chosen IRC channel. At the same time, it's scanning said channel and anything said into it is written into a file, called output.txt, prefixed with VO's /say_guild command. Meanwhile, the Ingame Component is constantly /loading output.txt, which makes it execute any VO command it finds in it (usually the /say_guild command). This results in full duplex communication between the IRC channel and VO. Note that, unlike FiReMaGe's IRC bot, no special command is needed to send a message from IRC to VO; everything that is said in the IRC channel will be sent to VO, always. At the moment there's nothing written into the bot to stop a message on either side from reaching the other.

A couple of caveats: first, the last time I programmed on a high-level language was Turbo Pascal in 1994; the last time I programmed, period, was Motorola Assembler in 1998. Thus, this program is clunky and doesn't use many of the nifty features of Lua; any comments, suggestions, changes and optimizations are appreciated.

Second, the bot was not thoroughly stress-tested for large numbers of people both ingame and on IRC, as the CDC never had that many players to begin with. However, the bot performed flawlessly with as many as five people on either side.

Now, for the components:

The Ingame Component needs to be running the following bind:

/alias output "load output.txt; wait 0.5 output"

It's a closed loop bind that loads output.txt (executing any and all commands inside it), and then waits half a second before doing it again. The 0.5 second delay might need tweaking; the setting at the moment is for the Mac that used to run the bot. If the delay is too small the bot might start repeating itself, spamming guild chat; if it's too big, it might miss lines coming from IRC.

The Lua Component needs to be running on the Lua console on the same PC the Ingame Component is running. A number of specific information needs to be supplied to it, namely the bot's IRC nick( which must be identified and thus its password is also needed), the IRC channel the Guild is using, and the path to the VO application, where errors.log is and output.txt will be. The program listed below clearly indicates where each of these elements is to be written in; whenever you see a word in uppercase surrounded by << >> it means to change the whole thing (including the << >>) with the element named inside the << >>; for example, <<PASSWORD>> means the IRC identify password goes there, while <<PATH>> is to be replaced by the path where both errors.log and output.txt are located (the path where VO is located).

The bot right now accepts two commands (only one is alluded to in the comments inside the program): Quit and Shutup. Quit makes the Lua Component terminate; it does nothing to the Ingame Component. Shutup clears the output.txt file, making the bot shut up ingame. Both commands should work from either side (IRC or ingame) but I forget if they actually do (they should at least work from ingame).

*

--[[
Function to parse RFC 1459 (IRC) lines into their components. I wrote this intending to do something with it, but that never went anywhere. Do what you
want with it.

The return value of parseline is a table. `prefix', if set, is the prefix of the line. `command' is the name of the command. The array elements are set to
the parameters, use table.getn (5.0, for 5.1 use #), ipairs, etc. as though it were a regular array.

Copyright (C)2006 Tejat !INC, all rights reserved. Or whatever.
]]

local function regexp(string, exp)
local ret
string.gsub(string, exp, function(...) ret = arg end)
if(ret) then
return unpack(ret)
else
return nil
end
end

function parseline(line)
local nl
local prefix,command
prefix,ln = regexp(line, "^%:([^ ]+) +(.+)$")
if(prefix) then line = ln end
command,ln = regexp(line, "^([^ ]+)(.*)$")
if(not command) then
command,ln = regexp(line, "^([0-9][0-9][0-9])( +.+)$")
if(not command) then return nil end
else
if(string.gsub(command, "[^-A-Za-z0-9_]", "") ~= command) then return nil end
end
line = ln
local ret = {prefix=prefix, command=command}
repeat
local param
param,ln = regexp(line, "^ +([^:\10\13 ][^\10\13 ]*)(.*)$")
if(not param) then
param = regexp(line, "^ +:([^\10\13]*)$")
if(not param) then
return ret
end
table.insert(ret, param)
return ret
else
table.insert(ret, param)
line = ln

end
until false
return ret
end

-- printtable takes the table created by Lua1459 and prints it onscreen; good only for debugging

function printtable(table)
for k,v in pairs(table) do
if(type(k) ~= "number") then
print(tostring(k).."= '"..tostring(v).."'")
end
end
for i,v in ipairs(table) do
print("["..i.."]="..tostring(v))
end
end

-- MAIN PROGRAM BLOCK START %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-- Call on lua sockets

socket = require("socket")

-- The bot connects to IRC, ensures the nick is free via a GHOST command, and then takes the nick

print("Connecting to IRC...")
socket = socket.connect("radon.slashnet.org",6667)
socket:send("PRIVMSG NickServ :ghost <<NICK>> <<PASSWORD>>\n")
socket:send("NICK <<NICK>>\n")
socket:send("USER <<NICK>> bogus. this.is.ignored :<<NICK>>\n")

-- [[ The bot displays the logon information IRC sends back to ensure we're making the connection. Using the parseline function, the bot looks for any PINGs, PONGing back if it finds any, and we also look for IRC's prompt to identify ourselves

repeat
feedback = socket:receive("*l")
print(feedback)
cmd = parseline(feedback)
-- printtable(cmd)

if (cmd["command"] == "PING") then
print("Found PING!!!")
socket:send("PONG "..cmd[1].."\n")
end
until ( feedback == ":NickServ!services@services.slashnet.org NOTICE <<NICK>> :If you do not change your nickname within one minute, it will be changed automatically.")

-- Having found the prompt to identify itself, the bot sends the IDENTIFY command

print("Sending IDENTIFY command.")
socket:send("PRIVMSG NickServ :IDENTIFY <<PASSWORD>>\n")
feedback = socket:receive("*l")
print(feedback)

-- The bot is now connected and identified, and asks for a keypress before proceeding to enter the proper channel
-- io.input()
print("Connected. Ready to join the channel on your command:")
x = io.read("*l")
print(x)
socket:send("JOIN <<CHANNEL>>\n")

-- The bot reads errors.log and stores its size in filesize. It then sets up errors.log as the default input file and output.txt as the default output file

io.input("<<PATH>>errors.log")
filesize = io.input():seek("end")
file = io.open("<<PATH>>output.txt","w+")
file:write("say_guild")
file:close()
socket:settimeout(0.1)

--[[ The bot starts a loop. The loop does a number of things:
1. It looks for modifications to the errors.log; if it finds some, it scans for guild chat, and displays any it finds.
2. It reads what is in IRC; if it's a normal message, it sends it to a file that VO can read.
2b. If it's a ping, it pongs back.
3. It scans for commands from both IRC and VO.
The loop will not end until it detects a chat line with the command "<<NICK>>-quit", at which point it logs out and quits. ]]

repeat

-- [[ The bot checks errors.log for any size changes. If there are any, it takes out the timestamp and looks for lines with (guild) on them. It then takes these lines and parses them to IRC; at this time, it also looks for commands, which are always prefixed with <<NICK>>-. It does this by further stripping the line of [location] and <player> and then looking at what is left. A debugging line can display the end result on IRC, to see what the bot has captured. ]]

currentsize = io.input():seek("end")
-- socket:send("PRIVMSG <<CHANNEL>> :"..currentsize..","..filesize..","..startingposition.."\n")
if (currentsize > filesize) then
io.input():seek("set",filesize)
line = io.input():read("*l")
-- socket:send("PRIVMSG <<CHANNEL>> :change in errors.logs detected!\n")
while line do
if (string.find(line,"> <") or string.find(line,"> "))then
file = io.open("<<PATH>>output.txt","w+")
file:write("say_guild")
file:close()
end
line = string.gsub(line, "^%[[^%]]+%] ", "")
if (string.find(line,"^%(guild%)") and not(string.find(line,"> <")) and not(string.find(line,"> "))) then
line = string.gsub(line, "^%(guild%) ", "")
socket:send("PRIVMSG <<CHANNEL>> :"..line.."\n")
line = string.gsub(line, "^%[[^%]]+%] ", "")
line = string.gsub(line, "^%<[^%]]+%> ", "")
-- socket:send("PRIVMSG <<CHANNEL>> :"..line.."\n")
if (line == "<<NICK>>-quit") then
socket:send("PRIVMSG <<CHANNEL>> :Signing Off\n")
break
end
if (line == "<<NICK>>-shutup") then
socket:send("PRIVMSG <<CHANNEL>> :Shutting Up\n")
file = io.open("<<PATH>>output.txt","w+")
file:write("say_guild")
file:close()
end
-- else
-- socket:send("PRIVMSG <<CHANNEL>> :\n") end
end
if (line == "<<NICK>>-quit") then break end
line = io.input():read("*l")
end
end

-- [[ Once the bot is done processing the new data on errors.log it sets the new size as the default size, so it can detect further changes to the file ]]

filesize = currentsize

--[[ Now it's time to read the IRC socket and find any messages suitable for sending back to VO. To be eligible, they have to start with a name, but the bot ignores those sent by any Serv bot (NickServ, ChanServ, etc.); it also formats properly any emotes it finds. ]]

feedback = socket:receive("*l")
if feedback then
name = string.match(feedback, "^:([^!]+)!")
if (name and (string.sub(name,-4,-1) ~= "Serv")) then
feedback1 = string.gsub(feedback, "^%b::", "")
feedback1 = string.gsub(feedback1,"[\1-\31\128-\255]", "")
if string.find(feedback1,"^ACTION") then
feedback1 = string.gsub(feedback1,"^ACTION","")
feedback1 = " "..name..feedback1
name = ""
else
name = "<"..name.."> "
end
-- io.output():write("say_guild ",name,feedback1)
file = io.open("<<PATH>>output.txt","w+")
file:write('say_guild ',name,'"',feedback1,'"')
file:close()
-- socket:send("PRIVMSG <<CHANNEL>> :"..name..": "..feedback1.."\n")
else
file = io.open("<<PATH>>output.txt","w+")
file:write("say_guild")
file:close()
end
cmd = parseline(feedback)
if (cmd["command"] == "PING") then
socket:send("PONG "..cmd[1].."\n")
-- socket:send("PRIVMSG <<CHANNEL>> :Pinging\n")
end
end
until (line == "<<NICK>>-quit")

-- The quit command has been issued; the bot uses QUIT to log off IRC, closes the socket, and terminates.

socket:send("QUIT\n")
-- io.input():close()
file = io.open("<<PATH>>output.txt","w+")
file:write("say_guild")
file:close()
print("done.")
socket:close()

*

Useful references:

Lua Reference Manual:
http://www.lua.org/manual/5.1/

LuaSocket page:
http://www.cs.princeton.edu/~diego/professional/luasocket/

The most current IRC protocol:
http://www.irchelp.org/irchelp/rfc/rfc.html


Special thanks to Solra Bizna for the initial guidance, plus the regexp, parseline and printtable functions, and to firsm, for the idea of using the /load command to communicate with VO.
Jun 27, 2007 maq link
I tried to run it but i get this error:
attempt to index global 'socket' (a nil value)
stack traceback:
relay.lua:77: in main chunk
[C]:

Any ideas?
Jun 27, 2007 mr_spuck link
you probably don't have the luaasocket library installed or installed where the script can find it
Jun 27, 2007 moldyman link
I believe the spuck has it. I had a similar error when I was retesting the code after a long hiatus. I had forgotten/misinstalled lua sockets, easily the most annoying thing to install out of all this.
Jun 27, 2007 maq link
Odd i installed it and it seemed to work. The socket that is.
Btw does relay.lua:77 refer to 77 line?
Jun 27, 2007 moldyman link
Yes

In your case:

socket = socket.connect("radon.slashnet.org",6667)

So something in the I/O is borken.
Jun 27, 2007 maq link
hmm but that wouldn't be the first time socket shows up in the code, shouldn't it break earlier if it was problem with socket?
Jun 27, 2007 mr_spuck link
no .. I guess require("socket") is supposed to return a socket "object", In your case it returns nil. the script dies when it tries to use one of its methods the first time.
Jun 27, 2007 maq link
Right.
Oh well i guess no bot for me.
Jul 16, 2007 slime73 link
luasocket doesn't work on OSX :/
Jul 17, 2007 moldyman link
It does, but it is pain in assholes to make work. No promises but I might try it in another language, without something that requires painful installation (ie luasockets).
Jul 18, 2007 Demonen link
assholes? You have more than one?

Anyway, LuaSocket isn't really that big of a problem if installed using FINK in OSX, or so I've heard.
I've never even witnessed Fink first hand, but it sounds reasonable considering all the other goodies it does automagically.
Jul 18, 2007 slime73 link
I can get it to install without Fink, but it doesn't work.
http://lua-users.org/lists/lua-l/2005-02/msg00197.html

EDIT: and fink doesn't have luasocket, at least I didn't see it.
Jul 19, 2007 Demonen link
*shrug* alright, my bad.
Jul 19, 2007 moldyman link
Borat reference. Saw it a few months ago and can't get it out of my head.
Jul 19, 2007 MSKanaka link
Very nice.

Slime's right--Fink is not listed among the packages that Fink can install, nor, for that matter, are any other lua-related packages. :/
Sep 28, 2007 slime73 link
On a Mac, if you install luasocket using MacPorts (sudo /opt/local/bin/port install -cv lua-luasocket), and then run the port install of lua, it will work.
Sep 28, 2007 csgno1 link
Huh, probably why I can't get any socket examples to work....

Then again vokb used to work on my machine...
Oct 02, 2007 maq link
i played with it a bit and found something interesting

it wouldn't connect right? but turns out when i tried different network (not slashnet) it did work...

only it still doesn't work with slashnet

and then after half an hour it hit me... i don't need it to use slashnet...

ok, now it connects but only works one way, from irc to vo

it works if i edit errors.log manually (or actually no)
it didn't work if a line had '> ' in it, as in for example if there was a space after a nick. And of course there's always a space after a nick...
i deleted that part of the code and it seems to work now.
Nov 08, 2007 moldyman link
Made minor change. When Jex and I wrote this, we got lazy. Didn't write up code to make this pong back to the IRCNet when pinged, so we used one of SlashNET's two servers which don't require pongs to stay connected. Obviously, 95% of IRCNets do require pongs. So here's the code, make sure to add it right before:

until (line == "<<NICK>>-quit")

So add this:

feedback = socket:receive("*l")
print(feedback)
cmd = parseline(feedback)
-- printtable(cmd)

if (cmd["command"] == "PING") then
print("Ponging!!!")
socket:send("PONG "..cmd[1].."\n")
end

It'll iterate every loop that the bot goes to chec IRC messages and changes to VO's errors log. If the IRCNet sends a ping, the bot sends a pong. Simple as that. Hope it helps.