diff --git a/README.md b/README.md index 93893e3..d00ed43 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # LuaMeat -Lua IRC bot +A simple, stupid IRC bot to provide some support functions for twitch channels. + +Doesn't do much, yet. diff --git a/config.lua.template b/config.lua.template new file mode 100644 index 0000000..651b21c --- /dev/null +++ b/config.lua.template @@ -0,0 +1,20 @@ +-- config file template - copy this to config.lua + +-- IRC Config +network = "irc.twitch.tv" -- network to connect (my default for quakenet) +nick = "YourStupidBot" +defaulChannels = {"#YourStupidBot", "#otherchannel"} --channels to join on startup +username = "YourStupidBot" +realname = "Stupid Lua IRC bot for twitch" +password = "oauth:" + +-- Plugin configs +yt_api_key = "" + +--------------------------- +-- Add plugins here! +--------------------------- +require('plugin.twitch') +require('plugin.google') +require('plugin.calc') +require('plugin.youtube') diff --git a/irc.lua b/irc.lua new file mode 100644 index 0000000..a0c22b4 --- /dev/null +++ b/irc.lua @@ -0,0 +1,994 @@ +--- +-- Implementation of the main LuaIRC module + +-- initialization {{{ +local base = _G +local constants = require 'irc.constants' +local ctcp = require 'irc.ctcp' +local c = ctcp._ctcp_quote +local irc_debug = require 'irc.debug' +local message = require 'irc.message' +local misc = require 'irc.misc' +local socket = require 'socket' +local os = require 'os' +local string = require 'string' +local table = require 'table' +-- }}} + +--- +-- LuaIRC - IRC framework written in Lua +-- @release 0.3 +module 'irc' + +-- constants {{{ +_VERSION = 'LuaIRC 0.3' +-- }}} + +-- classes {{{ +local Channel = base.require 'irc.channel' +-- }}} + +-- local variables {{{ +local irc_sock = nil +local rsockets = {} +local wsockets = {} +local rcallbacks = {} +local wcallbacks = {} +local icallbacks = { + whois = {}, + serverversion = {}, + servertime = {}, + ctcp_ping = {}, + ctcp_time = {}, + ctcp_version = {}, +} +local requestinfo = {whois = {}} +local handlers = {} +local ctcp_handlers = {} +local user_handlers = {} +local serverinfo = {} +local ip = nil +-- }}} + +-- defaults {{{ +TIMEOUT = 60 -- connection timeout +NETWORK = "localhost" -- default network +PORT = 6667 -- default port +NICK = "luabot" -- default nick +USERNAME = "LuaIRC" -- default username +REALNAME = "LuaIRC" -- default realname +DEBUG = false -- whether we want extra debug information +OUTFILE = nil -- file to send debug output to - nil is stdout +-- }}} + +-- private functions {{{ +-- main_loop_iter {{{ +local function main_loop_iter() + if #rsockets == 0 and #wsockets == 0 then return false end + local rready, wready, err = socket.select(rsockets, wsockets) + if err then irc_debug._err(err); return false; end + + for _, sock in base.ipairs(rready) do + local cb = socket.protect(rcallbacks[sock]) + local ret, err = cb(sock) + if not ret then + irc_debug._warn("socket error: " .. err) + _unregister_socket(sock, 'r') + end + end + + for _, sock in base.ipairs(wready) do + local cb = socket.protect(wcallbacks[sock]) + local ret, err = cb(sock) + if not ret then + irc_debug._warn("socket error: " .. err) + _unregister_socket(sock, 'w') + end + end + + return true +end +-- }}} + +-- begin_main_loop {{{ +local function begin_main_loop() + while main_loop_iter() do end +end +-- }}} + +-- incoming_message {{{ +local function incoming_message(sock) + local raw_msg = socket.try(sock:receive()) + irc_debug._message("RECV", raw_msg) + local msg = message._parse(raw_msg) + misc._try_call_warn("Unhandled server message: " .. msg.command, + handlers["on_" .. msg.command:lower()], + (misc._parse_user(msg.from)), base.unpack(msg.args)) + return true +end +-- }}} + +-- callback {{{ +local function callback(name, ...) + return misc._try_call(user_handlers[name], ...) +end +-- }}} +-- }}} + +-- internal message handlers {{{ +-- command handlers {{{ +-- on_nick {{{ +function handlers.on_nick(from, new_nick) + for chan in channels() do + chan:_change_nick(from, new_nick) + end + callback("nick_change", new_nick, from) +end +-- }}} + +-- on_join {{{ +function handlers.on_join(from, chan) + base.assert(serverinfo.channels[chan], + "Received join message for unknown channel: " .. chan) + if serverinfo.channels[chan].join_complete then + serverinfo.channels[chan]:_add_user(from) + callback("join", serverinfo.channels[chan], from) + end +end +-- }}} + +-- on_part {{{ +function handlers.on_part(from, chan, part_msg) + -- don't assert on chan here, since we get part messages for ourselves + -- after we remove the channel from the channel list + if not serverinfo.channels[chan] then return end + if serverinfo.channels[chan].join_complete then + serverinfo.channels[chan]:_remove_user(from) + callback("part", serverinfo.channels[chan], from, part_msg) + end +end +-- }}} + +-- on_mode {{{ +function handlers.on_mode(from, to, mode_string, ...) + local dir = mode_string:sub(1, 1) + mode_string = mode_string:sub(2) + local args = {...} + + if to:sub(1, 1) == "#" then + -- handle channel mode requests {{{ + base.assert(serverinfo.channels[to], + "Received mode change for unknown channel: " .. to) + local chan = serverinfo.channels[to] + local ind = 1 + for i = 1, mode_string:len() do + local mode = mode_string:sub(i, i) + local target = args[ind] + -- channel modes other than op/voice will be implemented as + -- information request commands + if mode == "o" then -- channel op {{{ + chan:_change_status(target, dir == "+", "o") + callback(({["+"] = "op", ["-"] = "deop"})[dir], + chan, from, target) + ind = ind + 1 + -- }}} + elseif mode == "v" then -- voice {{{ + chan:_change_status(target, dir == "+", "v") + callback(({["+"] = "voice", ["-"] = "devoice"})[dir], + chan, from, target) + ind = ind + 1 + -- }}} + end + end + -- }}} + elseif from == to then + -- handle user mode requests {{{ + -- TODO: make users more easily accessible so this is actually + -- reasonably possible + for i = 1, mode_string:len() do + local mode = mode_string:sub(i, i) + if mode == "i" then -- invisible {{{ + -- }}} + elseif mode == "s" then -- server messages {{{ + -- }}} + elseif mode == "w" then -- wallops messages {{{ + -- }}} + elseif mode == "o" then -- ircop {{{ + -- }}} + end + end + -- }}} + end +end +-- }}} + +-- on_topic {{{ +function handlers.on_topic(from, chan, new_topic) + base.assert(serverinfo.channels[chan], + "Received topic message for unknown channel: " .. chan) + serverinfo.channels[chan]._topic.text = new_topic + serverinfo.channels[chan]._topic.user = from + serverinfo.channels[chan]._topic.time = os.time() + if serverinfo.channels[chan].join_complete then + callback("topic_change", serverinfo.channels[chan]) + end +end +-- }}} + +-- on_invite {{{ +function handlers.on_invite(from, to, chan) + callback("invite", from, chan) +end +-- }}} + +-- on_kick {{{ +function handlers.on_kick(from, chan, to) + base.assert(serverinfo.channels[chan], + "Received kick message for unknown channel: " .. chan) + if serverinfo.channels[chan].join_complete then + serverinfo.channels[chan]:_remove_user(to) + callback("kick", serverinfo.channels[chan], to, from) + end +end +-- }}} + +-- on_privmsg {{{ +function handlers.on_privmsg(from, to, msg) + local msgs = ctcp._ctcp_split(msg) + for _, v in base.ipairs(msgs) do + local msg = v.str + if v.ctcp then + -- ctcp message {{{ + local words = misc._split(msg) + local received_command = words[1] + local cb = "on_" .. received_command:lower() + table.remove(words, 1) + -- not using try_call here because the ctcp specification requires + -- an error response to nonexistant commands + if base.type(ctcp_handlers[cb]) == "function" then + ctcp_handlers[cb](from, to, table.concat(words, " ")) + else + notice(from, c("ERRMSG", received_command, ":Unknown query")) + end + -- }}} + else + -- normal message {{{ + if to:sub(1, 1) == "#" then + base.assert(serverinfo.channels[to], + "Received channel msg from unknown channel: " .. to) + callback("channel_msg", serverinfo.channels[to], from, msg) + else + callback("private_msg", from, msg) + end + -- }}} + end + end +end +-- }}} + +-- on_notice {{{ +function handlers.on_notice(from, to, msg) + local msgs = ctcp._ctcp_split(msg) + for _, v in base.ipairs(msgs) do + local msg = v.str + if v.ctcp then + -- ctcp message {{{ + local words = misc._split(msg) + local command = words[1]:lower() + table.remove(words, 1) + misc._try_call_warn("Unknown CTCP message: " .. command, + ctcp_handlers["on_rpl_"..command], from, to, + table.concat(words, ' ')) + -- }}} + else + -- normal message {{{ + if to:sub(1, 1) == "#" then + base.assert(serverinfo.channels[to], + "Received channel msg from unknown channel: " .. to) + callback("channel_notice", serverinfo.channels[to], from, msg) + else + callback("private_notice", from, msg) + end + -- }}} + end + end +end +-- }}} + +-- on_quit {{{ +function handlers.on_quit(from, quit_msg) + for name, chan in base.pairs(serverinfo.channels) do + chan:_remove_user(from) + end + callback("quit", from, quit_msg) +end +-- }}} + +-- on_ping {{{ +-- respond to server pings to make sure it knows we are alive +function handlers.on_ping(from, respond_to) + send("PONG", respond_to) +end +-- }}} +-- }}} + +-- server replies {{{ +-- on_rpl_topic {{{ +-- catch topic changes +function handlers.on_rpl_topic(from, chan, topic) + base.assert(serverinfo.channels[chan], + "Received topic information about unknown channel: " .. chan) + serverinfo.channels[chan]._topic.text = topic +end +-- }}} + +-- on_rpl_notopic {{{ +function handlers.on_rpl_notopic(from, chan) + base.assert(serverinfo.channels[chan], + "Received topic information about unknown channel: " .. chan) + serverinfo.channels[chan]._topic.text = "" +end +-- }}} + +-- on_rpl_topicdate {{{ +-- "topic was set by at