diff --git a/README b/README index 82bd7d7..ecce753 100644 --- a/README +++ b/README @@ -37,6 +37,9 @@ advantage to this method is that your network client doesn't have to care about the transport mechanism: TCP, UDP, TCP6, SSL, or whatever else. +Other programs which work this way include `socat` and BSD `netcat`. +Use whatever you prefer. + bot --- diff --git a/contrib/modbot.lua b/contrib/modbot.lua new file mode 100755 index 0000000..89e7f02 --- /dev/null +++ b/contrib/modbot.lua @@ -0,0 +1,318 @@ +#! /usr/bin/lua + +channel = "testchan" + +-- +-- A bot framework with a bunch of nifty features you can +-- call from your privmsg handler. +-- + +-- You should override these in your handler. Don't change +-- them here, though, or you'll have to keep changing them +-- every time you install a new release. +bot = {} +bot.debug = false + +bot.nick = channel .. "-moderator" +bot.user = "moderator" +bot.desc = "Channel Moderation Fairy" +bot.channels = {"#" .. channel, "#mod-" .. channel} +bot.triggers = {} + +------------------------------------------------------------ +-- Some convenience functions +-- + +function shesc(s) + return "'" .. s:gsub("'", "'\\''") .. "'" +end + +function syscmd(argv) + local args = {} + local cmdline + local proc + + for _,a in ipairs(argv) do + table.insert(args, shesc(a)) + end + cmdline = table.concat(args, " ") + + proc = io.popen(cmdline, "r") + return function () + return proc:read() + end +end + +function system(argv) + local ret = {} + + for line in syscmd(argv) do + table.insert(ret, line) + end + return ret +end + + +function string:startswith(needle) + local len = needle:len() + + return self:sub(1, len) == needle +end + +function string:endswith(needle) + local len = needle:len() + + return self:sub(-len) == needle +end + + +------------------------------------------------------------ +-- Bot object +-- + + +bot.prefix = os.getenv("prefix") +bot.forum = os.getenv("forum") +bot.sender = os.getenv("sender") +bot.command = os.getenv("command") +bot.text = os.getenv("text") +bot.args = arg + +-- +-- Log to stderr +-- +function bot:log(text) + io.stderr:write(tostring(text) .. "\n") +end + +-- +-- Log what we are working with +-- +function bot:debug_input(text) + self:log(("< %-8s %8s/%-8s :%s"):format( + self.command, + self.sender or "-", + self.forum or "-", + self.text or "")) +end + +-- +-- Send a raw IRC command +-- +function bot:raw(text) + if self.debug then + self:log(" > " .. text) + end + print(text) +end + +-- +-- Send a message to the forum, falling back to sender if we're +-- spamming the channel. +-- +function bot:msg(text) + self.msgs_sent = self.msgs_sent + 1 + if ((self.msgs_sent == 5) and (self.forum ~= self.sender)) then + self:raw("PRIVMSG " .. self.forum .. " :Sending the rest in private") + self.msg_recip = self.sender + end + self:raw("PRIVMSG " .. self.msg_recip .. " :" .. text) +end +bot.msgs_sent = 0 +bot.msg_recip = bot.forum + +-- +-- Emote, like "* botname explodes" +-- +function bot:emote(text) + self:msg("\001ACTION " .. text .. "\001") +end + +-- +-- Run something in the shell +-- +function bot:syscmd(argv) + for line in syscmd(argv) do + if line:sub(1, 1) == ':' then + self:emote(line:sub(2)) + else + self:msg(line) + end + end +end + +-- Process self.triggers +function bot:do_triggers() + for pat,func in pairs(self.triggers) do + local a1, a2, a3, a4, a5 = self.text:match(pat) + + if a1 and func then + if type(func) == "string" then + self:msg(func) + else + func(self, a1, a2, a3, a4, a5, a6) + end + return true + end + end +end + +-- Use introspection to dispatch a command +function bot:run() + local func = self["handle_" .. self.command:lower()] + + if (self.debug) then + self:debug_input() + end + + if (func) then + func(self) + else + self:handle_default() + end +end + +-- Log in to IRC +function bot:handle__init_() + self:raw("NICK " .. self.nick) + self:raw(("USER %s %s %s :%s"):format(self.user, self.user, self.user, self.desc)) +end + +-- Join channels +function bot:handle_001() + self:raw("PRIVMSG nickserv :identify modbot4pescado.fleas") +end + +-- Deal with nickname collision +function bot:handle_433() + self:raw("NICK " .. self.nick .. (os.time() % 500)) +end + +-- Override this to handle messages +function bot:handle_privmsg() + self:do_triggers() +end + +-- Override this to handle any undefined command +function bot:handle_default() +end + + + +--------------------------------- +-- +-- Moderation +-- +-- +require('lfs') + +function queue() + ret = {} + for file in lfs.dir("queue") do + if file:sub(1, 1) ~= "." then + table.insert(ret, file) + end + end + return ret +end + +function push(text) + now = os.time() + f = io.open(("queue/%d"):format(now), "w") + f:write(text) + f:close() +end + +function pop(q, peek) + if not q then + q = queue() + end + if #q == 0 then + return + end + + table.sort(q) + fn = ("queue/%s"):format(q[1]) + + f = io.open(fn) + ret = f:read() + f:close() + + if peek == nil then + os.remove(fn) + table.remove(q, 1) + end + + return ret +end + +function peek(q) + return pop(q, true) +end + +function bot:handle_notice() + if (self.sender == 'NickServ') and self.text:startswith("You are successfully identified") then + for _,chan in ipairs(self.channels) do + self:raw("JOIN " .. chan) + end + end +end + +function bot:handle_privmsg() + if self.forum:sub(1, 1) == "#" then + self:handle_public() + else + self:handle_private() + end +end + +function bot:handle_public() + if self.forum:startswith("#mod-") then + q = queue() + if self.text:startswith("!") then + if self.text:startswith("!n") then + question = pop(q) + if not question then + return self:msg("[Queue empty]") + else + self:raw("PRIVMSG " .. self.channels[1] .. " :" .. question) + end + elseif self.text:startswith("!d") then + pop(q) + end + + question = pop(q, true) + if question then + self:msg(("[1/%d] %s"):format(#q, question)) + else + self:msg("[End of Queue]") + end + end + end +end + +function bot:handle_private() + ok = false + + for addr in io.lines("authorized.txt") do + if self.text:startswith(addr) then + ok = true + end + end + if not ok then + return + end + + q = queue() + question = ("<%s> %s"):format(self.prefix, self.text) + push(question) + self:msg("Your question has been enqueued. There are approximately " .. #q .. " questions before yours.") + + if (#q == 0) then + self:raw("PRIVMSG " .. self.channels[2] .. " :[1/1] " .. question) + end +end + +bot.debug = true +bot:run() +