git @ Cat's Eye Technologies mzstorkipiwanbotbotbot / master mzstorkipiwanbotbotbot.lua
master

Tree @master (Download .tar.gz)

mzstorkipiwanbotbotbot.lua @masterraw · history · blame

#!/usr/bin/env lua

-- An IRC bot with no plan or purpose, written in Lua.
-- This work is in the public domain.

require "config"

--[[ GLOBALS ]]--

local MSG_PATTERN = "^" .. BOTNAME .. "%s*[:,!]%s*(.-)%s*$"
local CMD_PATTERN = "^%|%s*(.-)%s*$"

math.randomseed(os.time())

-- variable scopes

local server -- for server scope
local room   -- for channel scope 
local user   -- for nick scope

-- syntax error messages

local snark = {
    "?SYNTAX ERROR",
    "omg u errored teh syntax!!1!",
    "What is this I don't even",
    "wat.",
    "I disagree!",
    "That's wonderful for you!",
}

--[[ FUNCTIONS ]]--

local p = function(s)
    io.stderr:write(">>> " .. s .. "\n")
    io.stdout:write(s)
    io.stdout:write("\n")
    io.stdout:flush()
end

local eval
eval = function(expr, nick, channel, context, reply)

    context.iter = context.iter + 1
    if context.iter > 10000 then
        reply('Out of stack space!  Well no, but I stopped it anyway.')
        return nil
    end

    -- define the user's space
    local userspace = user[nick]
    if userspace == nil then
        userspace = {}
        user[nick] = userspace
    end

    -- FIRST, we find and reduce any expressions.
    expr = string.gsub(expr, "(%b[])", function (subexpr)
        subexpr = string.sub(subexpr, 2, -2)
        local value = eval(subexpr, nick, channel, context, reply)
        if value == nil then
            return ''
        else
            return value
        end
    end)

    -- Assignment to server variable
    local match, _, name, value = string.find(expr, "^%/(%a-)%s*%=%s*(.-)$")
    if match then
        server[name] = value
        return value
    end

    -- Assignment to channel variable
    local match, _, name, value = string.find(expr, "^%#(%a-)%s*%=%s*(.-)$")
    if match then
        if room[channel] == nil then room[channel] = {} end
        room[channel][name] = value
        return value
    end

    -- Assignment to user variable
    local match, _, name, value = string.find(expr, "^%~%/(%a-)%s*%=%s*(.-)$")
    if match then
        userspace[name] = value
        return value
    end

    -- Print
    local match, _, value = string.find(expr, "^print%s*(.-)%s*$")
    if match then
        reply(value)
        return nil
    end

    -- Goto
    local match, _, newexpr = string.find(expr, "^goto%s*(.-)%s*$")
    if match then
        return eval(newexpr, nick, channel, context, reply)
    end

    -- Reading a server variable's value
    local match, _, name = string.find(expr, '^%/(%a-)$')
    if match then
        return server[name] or ''
    end

    -- Reading a channel variable's value
    local match, _, name = string.find(expr, '^%#(%a-)$')
    if match then
        local roomspace = room[channel]
        if roomspace == nil then return '' end
        return roomspace[name] or ''
    end

    -- Reading this user variable's value
    local match, _, name = string.find(expr, '^%~%/(%a-)$')
    if match then
        return userspace[name] or ''
    end

    -- Reading any user variable's value
    local match, _, fromnick, name = string.find(expr, '^%~([%w_]-)%/(%a-)$')
    if match then
        local userspace = user[fromnick]
        if userspace == nil then return '' end
        return userspace[name] or ''
    end

    -- Head and tail
    local match, _, fst = string.find(expr, '^hd%s*(.).*$')
    if match then
        return fst
    end
    local match, _, rst = string.find(expr, '^tl%s*.(.*)$')
    if match then
        return rst
    end

    -- tell
    local match, _, to_nick, msg = string.find(expr, "^tell%s+(.-)%s+(.-)$")
    if match then
        message = {from=nick, msg=msg}
        if user[to_nick] == nil then user[to_nick] = {} end
        if type(user[to_nick].msgs) ~= table then
            user[to_nick].msgs = {}
        end
        table.insert(user[to_nick].msgs, message)
        reply("Consider it noted.")
        return
    end

    -- source code
    local match = string.find(expr, "^source%s*$")
    if match then
        reply("http://bitbucket.org/catseye/mzstorkipiwanbotbotbot/src/tip/mzstorkipiwanbotbotbot.lua")
        return
    end

    -- save state
    local match = string.find(expr, "^save%s*$")
    if match then
        -- TODO save state here
        -- reply("State saved.")
        return
    end

    -- Help
    local match, _, topic = string.find(expr, "^help%s*(.-)$")
    if match then
        if string.find(topic, '^ass') then
            reply("Assign a nick-scope variable with ~/foo=1.  Assign a server-scope variable with /bar=1.  Assign a channel-scope variable with #baz=1.")
            return
        elseif string.find(topic, '^exp') then
            reply("All items in [brackets] are replaced by their value, in a recursive, depth-first manner.")
            return
        elseif string.find(topic, '^pr') then
            reply("To print a string, issue the command 'print string'.")
            return
        elseif string.find(topic, '^go') then
            reply("To evaluate a string as a command, issue 'goto command'.  This discards control context.")
            return
        elseif string.find(topic, '^tell') then
            reply("To enqueue a message for another user, which they will see publicly when they next speak up, issue the comand 'tell nick <stuff>'.")
            return
        elseif string.find(topic, '^sou') then
            reply("To get a link to the source code for this bot, issue the command 'source'.")
            return
        elseif string.find(topic, '^err') then
            reply("To get more interesting error messages, set ~/errmsgs=snark.")
            return
        else
            reply("Help is available for: assignment expressions print goto tell source errors")
            return
        end
    end

    -- Syntax error
    if userspace.errmsgs == 'snark' then
        reply(snark[math.random(#snark)])
    else
        reply("Unknown command.  Type '|help' for help.")
    end
end

--[[ MAIN ]]--

local done = False
if state == nil then state = 0 end

server = {}
room = {}
user = {}
user[BOTNAME] = {
    BRA="[",
    KET="]"
}

while not done do
    local line = io.stdin:read("*line")
    line = string.gsub(line, "[\r\n]+$", "") -- chomp
    io.stderr:write("--- " .. line .. "\n")
    if state == 0 then
        if string.find(line, "No Ident response") then
            p(string.format("USER %s %s %s %s", BOTNAME, BOTNAME, BOTNAME, BOTNAME))
            p(string.format("NICK %s", BOTNAME))
            if PASSWORD ~= nil then
                p(string.format("PRIVMSG NickServ :identify %s", PASSWORD))
            end
            for i,channel in ipairs(CHANNELS) do
                p(string.format("JOIN %s", channel))
            end
            state = 1
        end
    else -- state == 1
        local match, _, nick, channel, chatline = string.find(line, '^%:(.-)%!.-%s+PRIVMSG%s*(.-)%s*%:(.-)$')
        if match and string.find(channel, '^\#') then
            -- someone said something in the channel we're in.
            local reply = function(s)
                if s ~= nil then
                    if type(s) == "table" then s = "<table>" end -- TODO:  table.tostring(s)
                    p(string.format("PRIVMSG %s :%s: %s", channel, nick, s))
                end
            end

            -- was it someone I have a message for?
            if user[nick] ~= nil and type(user[nick].msgs) == "table" then
                local c = 0
                for key,message in pairs(user[nick].msgs) do
                    c = c + 1
                    if c > 5 then
                        reply("And there's more.  I'll tell you later.")
                        break
                    else
                        reply(string.format("%s told me to tell you: %s", message.from, message.msg))
                        user[nick].msgs[key] = nil
                    end
                end
            end

            -- was it addressed to me?
            match, _, msg = string.find(chatline, MSG_PATTERN)
            if not match then
                match, _, msg = string.find(chatline, CMD_PATTERN)
            end
            if match then
                local context = { iter=0 }
                reply(eval(msg, nick, channel, context, reply))
            end
        end
        local match, _, nick, botname, msg = string.find(line, '^%:(.-)%!.-%s+PRIVMSG%s+(.-)%s*%:%s*(.-)%s*$')
        if match then
            if botname == BOTNAME and nick ~= BOTNAME then
                local context = { iter=0 }
                local reply = function(s)
                    if s ~= nil then
                        p(string.format("PRIVMSG %s :%s", nick, s))
                    end
                end
                reply(eval(msg, nick, nil, context, reply))
            end
        end
        local match, _, serv = string.find(line, "^PING%s+%:(.-)$")
        if match then
            p(string.format("PONG :%s", serv))
            -- TODO save state here
        end
    end
end