diff options
author | jluehrs2 <jluehrs2@uiuc.edu> | 2007-08-26 22:07:38 -0500 |
---|---|---|
committer | jluehrs2 <jluehrs2@uiuc.edu> | 2007-08-26 22:07:38 -0500 |
commit | 86a0d68f9920cbcd382d8bb255639b022991def7 (patch) | |
tree | d9ecb2ec46ababa1654818d06d3015f5da746162 | |
parent | 27ea3f3876546997cfc838f6d605b8f4ca5adcd7 (diff) | |
download | luairc-86a0d68f9920cbcd382d8bb255639b022991def7.tar.gz luairc-86a0d68f9920cbcd382d8bb255639b022991def7.zip |
add all of the current files
-rw-r--r-- | TODO | 9 | ||||
-rw-r--r-- | src/irc.lua | 842 | ||||
-rw-r--r-- | src/irc/channel.lua | 331 | ||||
-rw-r--r-- | src/irc/constants.lua | 189 | ||||
-rw-r--r-- | src/irc/ctcp.lua | 93 | ||||
-rw-r--r-- | src/irc/dcc.lua | 122 | ||||
-rw-r--r-- | src/irc/debug.lua | 64 | ||||
-rw-r--r-- | src/irc/message.lua | 50 | ||||
-rw-r--r-- | src/irc/misc.lua | 227 | ||||
-rw-r--r-- | test/test.lua | 302 |
10 files changed, 2229 insertions, 0 deletions
@@ -0,0 +1,9 @@ +implement information callbacks (modes, whois, etc) +- who +- whowas +- info +- stats +- links +- trace (not freenode supported) +add comments +allow the user to set the socket ip (for dcc, etc) diff --git a/src/irc.lua b/src/irc.lua new file mode 100644 index 0000000..605e74d --- /dev/null +++ b/src/irc.lua @@ -0,0 +1,842 @@ +-- initialization {{{ +local base = _G +local constants = require 'irc.constants' +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' +-- }}} + +module 'irc' + +-- constants {{{ +_VERSION = 'LuaIRC 0.2' +-- }}} + +-- 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 serverinfo = {} +-- }}} + +-- 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 +-- }}} +-- }}} + +-- 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 + misc.try_call(on_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) + misc.try_call(on_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) + misc.try_call(on_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") + misc.try_call(({["+"] = on_op, ["-"] = on_deop})[dir], + chan, from, target) + ind = ind + 1 + -- }}} + elseif mode == "v" then -- voice {{{ + chan:change_status(target, dir == "+", "v") + misc.try_call(({["+"] = on_voice, ["-"] = on_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 = (misc.parse_user(from)) + serverinfo.channels[chan]._topic.time = os.time() + if serverinfo.channels[chan].join_complete then + misc.try_call(on_topic_change, serverinfo.channels[chan]) + end +end +-- }}} + +-- on_invite {{{ +function handlers.on_invite(from, to, chan) + misc.try_call(on_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) + misc.try_call(on_kick, serverinfo.channels[chan], to, from) + end +end +-- }}} + +-- on_privmsg {{{ +function handlers.on_privmsg(from, to, msg) + local msgs = ctcp.ctcp_split(msg, true) + for _, v in base.ipairs(msgs) do + if base.type(v) == "string" then + -- normal message {{{ + if to:sub(1, 1) == "#" then + base.assert(serverinfo.channels[to], + "Received channel msg from unknown channel: " .. to) + misc.try_call(on_channel_msg, serverinfo.channels[to], from, v) + else + misc.try_call(on_private_msg, from, v) + end + -- }}} + elseif base.type(v) == "table" then + -- ctcp message {{{ + local words = misc.split(v[1]) + 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, {"ERRMSG Unknown query: " .. received_command}) + end + -- }}} + end + end +end +-- }}} + +-- on_notice {{{ +function handlers.on_notice(from, to, msg) + local msgs = ctcp.ctcp_split(msg, true) + for _, v in base.ipairs(msgs) do + if base.type(v) == "string" then + -- normal message {{{ + if to:sub(1, 1) == "#" then + base.assert(serverinfo.channels[to], + "Received channel msg from unknown channel: " .. to) + misc.try_call(on_channel_notice, serverinfo.channels[to], + from, v) + else + misc.try_call(on_private_notice, from, v) + end + -- }}} + elseif base.type(v) == "table" then + -- ctcp message {{{ + local words = misc.split(v[1]) + 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, ' ')) + -- }}} + 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 + misc.try_call(on_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 <user> at <time>" +function handlers.on_rpl_topicdate(from, chan, user, time) + base.assert(serverinfo.channels[chan], + "Received topic information about unknown channel: " .. chan) + serverinfo.channels[chan]._topic.user = user + serverinfo.channels[chan]._topic.time = base.tonumber(time) +end +-- }}} + +-- on_rpl_namreply {{{ +-- handles a NAMES reply +function handlers.on_rpl_namreply(from, chanmode, chan, userlist) + base.assert(serverinfo.channels[chan], + "Received user information about unknown channel: " .. chan) + serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode] + local users = misc.split(userlist) + for k,v in base.ipairs(users) do + if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then + local nick = v:sub(2) + serverinfo.channels[chan]:add_user(nick, v:sub(1, 1)) + else + serverinfo.channels[chan]:add_user(v) + end + end +end +-- }}} + +-- on_rpl_endofnames {{{ +-- when we get this message, the channel join has completed, so call the +-- external cb +function handlers.on_rpl_endofnames(from, chan) + base.assert(serverinfo.channels[chan], + "Received user information about unknown channel: " .. chan) + if not serverinfo.channels[chan].join_complete then + misc.try_call(on_me_join, serverinfo.channels[chan]) + serverinfo.channels[chan].join_complete = true + end +end +-- }}} + +-- on_rpl_welcome {{{ +function handlers.on_rpl_welcome(from) + serverinfo = { + connected = false, + connecting = true, + channels = {} + } +end +-- }}} + +-- on_rpl_yourhost {{{ +function handlers.on_rpl_yourhost(from, msg) + serverinfo.host = from +end +-- }}} + +-- on_rpl_motdstart {{{ +function handlers.on_rpl_motdstart(from) + serverinfo.motd = "" +end +-- }}} + +-- on_rpl_motd {{{ +function handlers.on_rpl_motd(from, motd) + serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n" +end +-- }}} + +-- on_rpl_endofmotd {{{ +function handlers.on_rpl_endofmotd(from) + if not serverinfo.connected then + serverinfo.connected = true + serverinfo.connecting = false + misc.try_call(on_connect) + end +end +-- }}} + +-- on_rpl_whoisuser {{{ +function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname) + nick = nick:lower() + requestinfo.whois[nick].user = user + requestinfo.whois[nick].host = host + requestinfo.whois[nick].realname = realname +end +-- }}} + +-- on_rpl_whoischannels {{{ +function handlers.on_rpl_whoischannels(from, nick, channel_list) + nick = nick:lower() + if not requestinfo.whois[nick].channels then + requestinfo.whois[nick].channels = {} + end + for _, channel in base.ipairs(misc.split(channel_list)) do + table.insert(requestinfo.whois[nick].channels, channel) + end +end +-- }}} + +-- on_rpl_whoisserver {{{ +function handlers.on_rpl_whoisserver(from, nick, server, serverinfo) + nick = nick:lower() + requestinfo.whois[nick].server = server + requestinfo.whois[nick].serverinfo = serverinfo +end +-- }}} + +-- on_rpl_away {{{ +function handlers.on_rpl_away(from, nick, away_msg) + nick = nick:lower() + if requestinfo.whois[nick] then + requestinfo.whois[nick].away_msg = away_msg + end +end +-- }}} + +-- on_rpl_whoisoperator {{{ +function handlers.on_rpl_whoisoperator(from, nick) + requestinfo.whois[nick:lower()].is_oper = true +end +-- }}} + +-- on_rpl_whoisidle {{{ +function handlers.on_rpl_whoisidle(from, nick, idle_seconds) + requestinfo.whois[nick:lower()].idle_time = idle_seconds +end +-- }}} + +-- on_rpl_endofwhois {{{ +function handlers.on_rpl_endofwhois(from, nick) + nick = nick:lower() + local cb = table.remove(icallbacks.whois[nick], 1) + cb(requestinfo.whois[nick]) + requestinfo.whois[nick] = nil + if #icallbacks.whois[nick] > 0 then send("WHOIS", nick) + else icallbacks.whois[nick] = nil + end +end +-- }}} + +-- on_rpl_version {{{ +function handlers.on_rpl_version(from, version, server, comments) + local cb = table.remove(icallbacks.serverversion[server], 1) + cb({version = version, server = server, comments = comments}) + if #icallbacks.serverversion[server] > 0 then send("VERSION", server) + else icallbacks.serverversion[server] = nil + end +end +-- }}} + +-- on_rpl_time {{{ +function on_rpl_time(from, server, time) + local cb = table.remove(icallbacks.servertime[server], 1) + cb({time = time, server = server}) + if #icallbacks.servertime[server] > 0 then send("TIME", server) + else icallbacks.servertime[server] = nil + end +end +-- }}} +-- }}} + +-- ctcp handlers {{{ +-- requests {{{ +-- on_action {{{ +function ctcp_handlers.on_action(from, to, message) + if to:sub(1, 1) == "#" then + base.assert(serverinfo.channels[to], + "Received channel msg from unknown channel: " .. to) + misc.try_call(on_channel_act, serverinfo.channels[to], from, message) + else + misc.try_call(on_private_act, from, message) + end +end +-- }}} + +-- on_dcc {{{ +function ctcp_handlers.on_dcc(from, to, message) + local type, argument, address, port, size = base.unpack(misc.split(message, " ", nil, '"', '"')) + if type == "SEND" then + if misc.try_call(on_dcc, from, to, argument, address, port, size) then + dcc.accept(argument, address, port, size) + end + elseif type == "CHAT" then + -- TODO: implement this? do people ever use this? + end +end +-- }}} + +-- on_version {{{ +function ctcp_handlers.on_version(from, to) + notice(from, {"VERSION " .. _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION}) +end +-- }}} + +-- on_errmsg {{{ +function ctcp_handlers.on_errmsg(from, to, message) + notice(from, {"ERRMSG " .. message .. "No error has occurred"}) +end +-- }}} + +-- on_ping {{{ +function ctcp_handlers.on_ping(from, to, timestamp) + notice(from, {"PING " .. timestamp}) +end +-- }}} + +-- on_time {{{ +function ctcp_handlers.on_time(from, to) + notice(from, {"TIME " .. os.date()}) +end +-- }}} +-- }}} + +-- responses {{{ +-- on_rpl_action {{{ +-- actions are handled the same, notice or not +ctcp_handlers.on_rpl_action = ctcp_handlers.on_action +-- }}} + +-- on_rpl_version {{{ +function ctcp_handlers.on_rpl_version(from, to, version) + local cb = table.remove(icallbacks.ctcp_version[from], 1) + cb({version = version, nick = from}) + if #icallbacks.ctcp_version[from] > 0 then say(from, {"VERSION"}) + else icallbacks.ctcp_version[from] = nil + end +end +-- }}} + +-- on_rpl_errmsg {{{ +function ctcp_handlers.on_rpl_errmsg(from, to, message) + try_call(on_ctcp_error, from, to, message) +end +-- }}} + +-- on_rpl_ping {{{ +function ctcp_handlers.on_rpl_ping(from, to, timestamp) + local cb = table.remove(icallbacks.ctcp_ping[from], 1) + cb({time = os.time() - timestamp, nick = from}) + if #icallbacks.ctcp_ping[from] > 0 then say(from, {"PING " .. os.time()}) + else icallbacks.ctcp_ping[from] = nil + end +end +-- }}} + +-- on_rpl_time {{{ +function ctcp_handlers.on_rpl_time(from, to, time) + local cb = table.remove(icallbacks.ctcp_time[from], 1) + cb({time = time, nick = from}) + if #icallbacks.ctcp_time[from] > 0 then say(from, {"TIME"}) + else icallbacks.ctcp_time[from] = nil + end +end +-- }}} +-- }}} +-- }}} +-- }}} + +-- module functions {{{ +-- socket handling functions {{{ +-- _register_socket() - register a socket to listen on {{{ +function _register_socket(sock, mode, cb) + local socks, cbs + if mode == 'r' then + socks = rsockets + cbs = rcallbacks + else + socks = wsockets + cbs = wcallbacks + end + base.assert(not cbs[sock], "socket already registered") + table.insert(socks, sock) + cbs[sock] = cb +end +-- }}} + +-- _unregister_socket() - remove a previously registered socket {{{ +function _unregister_socket(sock, mode) + local socks, cbs + if mode == 'r' then + socks = rsockets + cbs = rcallbacks + else + socks = wsockets + cbs = wcallbacks + end + for i, v in base.ipairs(socks) do + if v == sock then table.remove(socks, i); break; end + end + cbs[sock] = nil +end +-- }}} +-- }}} +-- }}} + +-- public functions {{{ +-- server commands {{{ +-- connect() - start a connection to the irc server {{{ +-- args: network - address of the irc network to connect to +-- port - port to connect to +-- pass - irc server password (if required) +-- nick - nickname to connect as +-- username - username to connect with +-- realname - realname to connect with +-- timeout - amount of time in seconds to wait before dropping an idle +-- connection +-- notes: this function uses a table and named arguments. defaults are specified +-- by the capitalized versions of the arguments at the top of this file. +-- all args are optional. +function connect(args) + local network = args.network or NETWORK + local port = args.port or PORT + local nick = args.nick or NICK + local username = args.username or USERNAME + local realname = args.realname or REALNAME + local timeout = args.timeout or TIMEOUT + serverinfo.connecting = true + if OUTFILE then irc_debug.set_output(OUTFILE) end + if DEBUG then irc_debug.enable() end + irc_sock = base.assert(socket.connect(network, port)) + irc_sock:settimeout(timeout) + _register_socket(irc_sock, 'r', incoming_message) + if args.pass then send("PASS", args.pass) end + send("NICK", nick) + send("USER", username, (irc_sock:getsockname()), network, realname) + begin_main_loop() +end +-- }}} + +-- quit() - close the connection to the irc server {{{ +-- args: message - quit message (optional) +function quit(message) + message = message or "Leaving" + send("QUIT", message) + serverinfo.connected = false +end +-- }}} + +-- join() - join a channel {{{ +-- args: channel - channel to join (required) +function join(channel) + if not channel then return end + serverinfo.channels[channel] = Channel.new(channel) + send("JOIN", channel) +end +-- }}} + +-- part() - leave a channel {{{ +-- args: channel - channel to leave (required) +function part(channel) + if not channel then return end + serverinfo.channels[channel] = nil + send("PART", channel) +end +-- }}} + +-- say() - send a message to a user or channel {{{ +-- args: name - user or channel to send the message to +-- message - message to send +function say(name, message) + if not name then return end + message = message or "" + send("PRIVMSG", name, message) +end +-- }}} + +-- notice() - send a notice to a user or channel {{{ +-- args: name - user or channel to send the notice to +-- message - message to send +function notice(name, message) + if not name then return end + message = message or "" + send("NOTICE", name, message) +end +-- }}} + +-- act() - perform a /me action {{{ +-- args: name - user or channel to send the action to +-- action - action to send +function act(name, action) + if not name then return end + action = action or "" + send("PRIVMSG", name, {"ACTION", action}) +end +-- }}} +-- }}} + +-- information requests {{{ +-- server_version {{{ +function server_version(cb, server) + -- apparently the optional server parameter isn't supported? + --server = server or serverinfo.host + server = serverinfo.host + if not icallbacks.serverversion[server] then + icallbacks.serverversion[server] = {cb} + send("VERSION", server) + else + table.insert(icallbacks.serverversion[server], cb) + end +end +-- }}} + +-- whois {{{ +-- TODO: allow server parameter (to get user idle time) +function whois(cb, nick) + nick = nick:lower() + requestinfo.whois[nick] = {nick = nick} + if not icallbacks.whois[nick] then + icallbacks.whois[nick] = {cb} + send("WHOIS", nick) + else + table.insert(icallbacks.whois[nick], cb) + end +end +-- }}} + +-- server_time {{{ +function server_time(cb, server) + -- apparently the optional server parameter isn't supported? + --server = server or serverinfo.host + server = serverinfo.host + if not icallbacks.servertime[server] then + icallbacks.servertime[server] = {cb} + send("TIME", server) + else + table.insert(icallbacks.servertime[server], cb) + end +end +-- }}} + +-- trace {{{ +--function trace(cb, server) +-- send("WHOWAS", "ekiM") +--end +-- }}} +-- }}} + +-- ctcp commands {{{ +-- ctcp_ping() - send a CTCP ping request {{{ +function ctcp_ping(cb, nick) + nick = nick:lower() + if not icallbacks.ctcp_ping[nick] then + icallbacks.ctcp_ping[nick] = {cb} + say(nick, {"PING " .. os.time()}) + else + table.insert(icallbacks.ctcp_ping[nick], cb) + end +end +-- }}} + +-- ctcp_time() - send a localtime request {{{ +function ctcp_time(cb, nick) + nick = nick:lower() + if not icallbacks.ctcp_time[nick] then + icallbacks.ctcp_time[nick] = {cb} + say(nick, {"TIME"}) + else + table.insert(icallbacks.ctcp_time[nick], cb) + end +end +-- }}} + +-- ctcp_version() - send a client version request {{{ +function ctcp_version(cb, nick) + nick = nick:lower() + if not icallbacks.ctcp_version[nick] then + icallbacks.ctcp_version[nick] = {cb} + say(nick, {"VERSION"}) + else + table.insert(icallbacks.ctcp_version[nick], cb) + end +end +-- }}} +-- }}} + +-- misc functions {{{ +-- send() - send a raw irc command {{{ +-- send takes a command and a variable number of arguments +-- if the argument is a string, it is sent literally +-- if the argument is a table, it is CTCP quoted +-- the last argument is preceded by a : +function send(command, ...) + if not serverinfo.connected and not serverinfo.connecting then return end + local message = command + for i, v in base.ipairs({...}) do + local arg + -- passing a table in as an argument means to treat that table as a + -- CTCP command, so quote it appropriately + if base.type(v) == "string" then + arg = v + elseif base.type(v) == "table" then + arg = ctcp.ctcp_quote(table.concat(v, " ")) + end + if i == #{...} then + arg = ":" .. arg + end + message = message .. " " .. arg + end + message = ctcp.low_quote(message) + -- we just truncate for now. -2 to account for the \r\n + message = message:sub(1, constants.IRC_MAX_MSG - 2) + irc_debug.message("SEND", message) + irc_sock:send(message .. "\r\n") +end +-- }}} + +-- get_ip() - get the local ip address for the server connection {{{ +function get_ip() + return (irc_sock:getsockname()) +end +-- }}} + +-- channels() - iterate over currently joined channels {{{ +function channels() + return function(state, arg) + return misc.value_iter(state, arg, + function(v) + return v.join_complete + end) + end, + serverinfo.channels, + nil +end +-- }}} +-- }}} +-- }}} diff --git a/src/irc/channel.lua b/src/irc/channel.lua new file mode 100644 index 0000000..4e77ae9 --- /dev/null +++ b/src/irc/channel.lua @@ -0,0 +1,331 @@ +-- initialization {{{ +local base = _G +local irc = require 'irc' +local misc = require 'irc.misc' +local socket = require 'socket' +local table = require 'table' +-- }}} + +module 'irc.channel' + +-- object metatable {{{ +local mt = { + -- __index() {{{ + __index = function(self, key) + if key == "name" then + return self._name + elseif key == "topic" then + return self._topic + elseif key == "chanmode" then + return self._chanmode + else + return _M[key] + end + end, + -- }}} + -- __newindex() {{{ + __newindex = function(self, key, value) + if key == "name" then + return + elseif key == "topic" then + irc.send("TOPIC", self._name, value) + elseif key == "chanmode" then + return + else + base.rawset(self, key, value) + end + end, + -- }}} + -- __concat() {{{ + __concat = function(first, second) + local first_str, second_str + + if base.type(first) == "table" then + first_str = first._name + else + first_str = first + end + if base.type(second) == "table" then + second_str = second._name + else + second_str = second + end + + return first_str .. second_str + end, + -- }}} + -- __tostring() {{{ + __tostring = function(self) + return self._name + end + -- }}} +} +-- }}} + +-- private methods {{{ +-- set_basic_mode() - sets a no-arg mode on a channel {{{ +local function set_basic_mode(self, set, letter) + if set then + irc.send("MODE", self.name, "+" .. letter) + else + irc.send("MODE", self.name, "-" .. letter) + end +end +-- }}} +-- }}} + +-- constructor {{{ +function new(chan) + return base.setmetatable({_name = chan, _topic = {}, _chanmode = "", + _members = {}}, mt) +end +-- }}} + +-- public methods {{{ +-- iterators {{{ +-- each_op() {{{ +function each_op(self) + return function(state, arg) + return misc.value_iter(state, arg, + function(v) + return v:sub(1, 1) == "@" + end) + end, + self._members, + nil +end +-- }}} + +-- each_voice() {{{ +function each_voice(self) + return function(state, arg) + return misc.value_iter(state, arg, + function(v) + return v:sub(1, 1) == "+" + end) + end, + self._members, + nil +end +-- }}} + +-- each_user() {{{ +function each_user(self) + return function(state, arg) + return misc.value_iter(state, arg, + function(v) + return v:sub(1, 1) ~= "@" and + v:sub(1, 1) ~= "+" + end) + end, + self._members, + nil +end +-- }}} + +-- each_member() {{{ +function each_member(self) + return misc.value_iter, self._members, nil +end +-- }}} +-- }}} + +-- return tables of users {{{ +-- ops() {{{ +function ops(self) + local ret = {} + for nick in self:each_op() do + table.insert(ret, nick) + end + return ret +end +-- }}} + +-- voices() {{{ +function voices(self) + local ret = {} + for nick in self:each_voice() do + table.insert(ret, nick) + end + return ret +end +-- }}} + +-- users() {{{ +function users(self) + local ret = {} + for nick in self:each_user() do + table.insert(ret, nick) + end + return ret +end +-- }}} + +-- members() {{{ +function members(self) + local ret = {} + -- not just returning self._members, since the return value shouldn't be + -- modifiable + for nick in self:each_member() do + table.insert(ret, nick) + end + return ret +end +-- }}} +-- }}} + +-- setting modes {{{ +-- ban() - ban a user from a channel {{{ +-- TODO: hmmm, this probably needs an appropriate mask, rather than a nick +function ban(self, name) + irc.send("MODE", self.name, "+b", name) +end +-- }}} + +-- unban() - remove a ban on a user {{{ +-- TODO: same here +function unban(self, name) + irc.send("MODE", self.name, "-b", name) +end +-- }}} + +-- voice() - give a user voice on a channel {{{ +function voice(self, name) + irc.send("MODE", self.name, "+v", name) +end +-- }}} + +-- devoice() - remove voice from a user {{{ +function devoice(self, name) + irc.send("MODE", self.name, "-v", name) +end +-- }}} + +-- op() - give a user ops on a channel {{{ +function op(self, name) + irc.send("MODE", self.name, "+o", name) +end +-- }}} + +-- deop() - remove ops from a user {{{ +function deop(self, name) + irc.send("MODE", self.name, "-o", name) +end +-- }}} + +-- set_limit() - set a channel limit {{{ +function set_limit(self, new_limit) + if new_limit then + irc.send("MODE", self.name, "+l", new_limit) + else + irc.send("MODE", self.name, "-l") + end +end +-- }}} + +-- set_key() - set a channel password {{{ +function set_key(self, key) + if key then + irc.send("MODE", self.name, "+k", key) + else + irc.send("MODE", self.name, "-k") + end +end +-- }}} + +-- set_private() - set the private state of a channel {{{ +function set_private(self, set) + set_basic_mode(self, set, "p") +end +-- }}} + +-- set_secret() - set the secret state of a channel {{{ +function set_secret(self, set) + set_basic_mode(self, set, "s") +end +-- }}} + +-- set_invite_only() - set whether joining the channel requires an invite {{{ +function set_invite_only(self, set) + set_basic_mode(self, set, "i") +end +-- }}} + +-- set_topic_lock() - if true, the topic can only be changed by an op {{{ +function set_topic_lock(self, set) + set_basic_mode(self, set, "t") +end +-- }}} + +-- set_no_outside_messages() - if true, users must be in the channel to send messages to it {{{ +function set_no_outside_messages(self, set) + set_basic_mode(self, set, "n") +end +-- }}} + +-- set moderated() - set whether voice is required to speak {{{ +function set_moderated(self, set) + set_basic_mode(self, set, "m") +end +-- }}} +-- }}} + +-- accessors {{{ +-- add_user() {{{ +function add_user(self, user, mode) + mode = mode or '' + self._members[user] = mode .. user +end +-- }}} + +-- remove_user() {{{ +function remove_user(self, user) + self._members[user] = nil +end +-- }}} + +-- change_status() {{{ +function change_status(self, user, on, mode) + if on then + if mode == 'o' then + self._members[user] = '@' .. user + elseif mode == 'v' then + self._members[user] = '+' .. user + end + else + if (mode == 'o' and self._members[user]:sub(1, 1) == '@') or + (mode == 'v' and self._members[user]:sub(1, 1) == '+') then + self._members[user] = user + end + end +end +-- }}} + +-- contains() {{{ +function contains(self, nick) + for member in self:each_member() do + local member_nick = member:gsub('@+', '') + if member_nick == nick then + return true + end + end + return false +end +-- }}} + +-- change_nick {{{ +function change_nick(self, old_nick, new_nick) + for member in self:each_member() do + local member_nick = member:gsub('@+', '') + if member_nick == old_nick then + local mode = self._members[old_nick]:sub(1, 1) + if mode ~= '@' and mode ~= '+' then mode = "" end + self._members[old_nick] = nil + self._members[new_nick] = mode .. new_nick + break + end + end +end +-- }}} +-- }}} +-- }}} diff --git a/src/irc/constants.lua b/src/irc/constants.lua new file mode 100644 index 0000000..864e4c9 --- /dev/null +++ b/src/irc/constants.lua @@ -0,0 +1,189 @@ +module "irc.constants" + +-- protocol constants {{{ +IRC_MAX_MSG = 512 +-- }}} + +-- server replies {{{ +replies = { +-- Command responses {{{ + [001] = "RPL_WELCOME", + [002] = "RPL_YOURHOST", + [003] = "RPL_CREATED", + [004] = "RPL_MYINFO", + [005] = "RPL_BOUNCE", + [302] = "RPL_USERHOST", + [303] = "RPL_ISON", + [301] = "RPL_AWAY", + [305] = "RPL_UNAWAY", + [306] = "RPL_NOWAWAY", + [311] = "RPL_WHOISUSER", + [312] = "RPL_WHOISSERVER", + [313] = "RPL_WHOISOPERATOR", + [317] = "RPL_WHOISIDLE", + [318] = "RPL_ENDOFWHOIS", + [319] = "RPL_WHOISCHANNELS", + [314] = "RPL_WHOWASUSER", + [369] = "RPL_ENDOFWHOWAS", + [321] = "RPL_LISTSTART", + [322] = "RPL_LIST", + [323] = "RPL_LISTEND", + [325] = "RPL_UNIQOPIS", + [324] = "RPL_CHANNELMODEIS", + [331] = "RPL_NOTOPIC", + [332] = "RPL_TOPIC", + [341] = "RPL_INVITING", + [342] = "RPL_SUMMONING", + [346] = "RPL_INVITELIST", + [347] = "RPL_ENDOFINVITELIST", + [348] = "RPL_EXCEPTLIST", + [349] = "RPL_ENDOFEXCEPTLIST", + [351] = "RPL_VERSION", + [352] = "RPL_WHOREPLY", + [315] = "RPL_ENDOFWHO", + [353] = "RPL_NAMREPLY", + [366] = "RPL_ENDOFNAMES", + [364] = "RPL_LINKS", + [365] = "RPL_ENDOFLINKS", + [367] = "RPL_BANLIST", + [368] = "RPL_ENDOFBANLIST", + [371] = "RPL_INFO", + [374] = "RPL_ENDOFINFO", + [375] = "RPL_MOTDSTART", + [372] = "RPL_MOTD", + [376] = "RPL_ENDOFMOTD", + [381] = "RPL_YOUREOPER", + [382] = "RPL_REHASHING", + [383] = "RPL_YOURESERVICE", + [391] = "RPL_TIME", + [392] = "RPL_USERSSTART", + [393] = "RPL_USERS", + [394] = "RPL_ENDOFUSERS", + [395] = "RPL_NOUSERS", + [200] = "RPL_TRACELINK", + [201] = "RPL_TRACECONNECTING", + [202] = "RPL_TRACEHANDSHAKE", + [203] = "RPL_TRACEUNKNOWN", + [204] = "RPL_TRACEOPERATOR", + [205] = "RPL_TRACEUSER", + [206] = "RPL_TRACESERVER", + [207] = "RPL_TRACESERVICE", + [208] = "RPL_TRACENEWTYPE", + [209] = "RPL_TRACECLASS", + [210] = "RPL_TRACERECONNECT", + [261] = "RPL_TRACELOG", + [262] = "RPL_TRACEEND", + [211] = "RPL_STATSLINKINFO", + [212] = "RPL_STATSCOMMANDS", + [219] = "RPL_ENDOFSTATS", + [242] = "RPL_STATSUPTIME", + [243] = "RPL_STATSOLINE", + [221] = "RPL_UMODEIS", + [234] = "RPL_SERVLIST", + [235] = "RPL_SERVLISTEND", + [221] = "RPL_UMODEIS", + [251] = "RPL_LUSERCLIENT", + [252] = "RPL_LUSEROP", + [253] = "RPL_LUSERUNKNOWN", + [254] = "RPL_LUSERCHANNELS", + [255] = "RPL_LUSERME", + [256] = "RPL_ADMINME", + [257] = "RPL_ADMINLOC1", + [258] = "RPL_ADMINLOC2", + [259] = "RPL_ADMINEMAIL", + [263] = "RPL_TRYAGAIN", +-- }}} +-- Error codes {{{ + [401] = "ERR_NOSUCHNICK", -- No such nick/channel + [402] = "ERR_NOSUCHSERVER", -- No such server + [403] = "ERR_NOSUCHCHANNEL", -- No such channel + [404] = "ERR_CANNOTSENDTOCHAN", -- Cannot send to channel + [405] = "ERR_TOOMANYCHANNELS", -- You have joined too many channels + [406] = "ERR_WASNOSUCHNICK", -- There was no such nickname + [407] = "ERR_TOOMANYTARGETS", -- Duplicate recipients. No message delivered + [408] = "ERR_NOSUCHSERVICE", -- No such service + [409] = "ERR_NOORIGIN", -- No origin specified + [411] = "ERR_NORECIPIENT", -- No recipient given + [412] = "ERR_NOTEXTTOSEND", -- No text to send + [413] = "ERR_NOTOPLEVEL", -- No toplevel domain specified + [414] = "ERR_WILDTOPLEVEL", -- Wildcard in toplevel domain + [415] = "ERR_BADMASK", -- Bad server/host mask + [421] = "ERR_UNKNOWNCOMMAND", -- Unknown command + [422] = "ERR_NOMOTD", -- MOTD file is missing + [423] = "ERR_NOADMININFO", -- No administrative info available + [424] = "ERR_FILEERROR", -- File error + [431] = "ERR_NONICKNAMEGIVEN", -- No nickname given + [432] = "ERR_ERRONEUSNICKNAME", -- Erroneus nickname + [433] = "ERR_NICKNAMEINUSE", -- Nickname is already in use + [436] = "ERR_NICKCOLLISION", -- Nickname collision KILL + [437] = "ERR_UNAVAILRESOURCE", -- Nick/channel is temporarily unavailable + [441] = "ERR_USERNOTINCHANNEL", -- They aren't on that channel + [442] = "ERR_NOTONCHANNEL", -- You're not on that channel + [443] = "ERR_USERONCHANNEL", -- User is already on channel + [444] = "ERR_NOLOGIN", -- User not logged in + [445] = "ERR_SUMMONDISABLED", -- SUMMON has been disabled + [446] = "ERR_USERSDISABLED", -- USERS has been disabled + [451] = "ERR_NOTREGISTERED", -- You have not registered + [461] = "ERR_NEEDMOREPARAMS", -- Not enough parameters + [462] = "ERR_ALREADYREGISTERED", -- You may not reregister + [463] = "ERR_NOPERMFORHOST", -- Your host isn't among the privileged + [464] = "ERR_PASSWDMISMATCH", -- Password incorrect + [465] = "ERR_YOUREBANNEDCREEP", -- You are banned from this server + [466] = "ERR_YOUWILLBEBANNED", + [467] = "ERR_KEYSET", -- Channel key already set + [471] = "ERR_CHANNELISFULL", -- Cannot join channel (+l) + [472] = "ERR_UNKNOWNMODE", -- Unknown mode char + [473] = "ERR_INVITEONLYCHAN", -- Cannot join channel (+i) + [474] = "ERR_BANNEDFROMCHAN", -- Cannot join channel (+b) + [475] = "ERR_BADCHANNELKEY", -- Cannot join channel (+k) + [476] = "ERR_BADCHANMASK", -- Bad channel mask + [477] = "ERR_NOCHANMODES", -- Channel doesn't support modes + [478] = "ERR_BANLISTFULL", -- Channel list is full + [481] = "ERR_NOPRIVILEGES", -- Permission denied- You're not an IRC operator + [482] = "ERR_CHANOPRIVSNEEDED", -- You're not channel operator + [483] = "ERR_CANTKILLSERVER", -- You can't kill a server! + [484] = "ERR_RESTRICTED", -- Your connection is restricted! + [485] = "ERR_UNIQOPPRIVSNEEDED", -- You're not the original channel operator + [491] = "ERR_NOOPERHOST", -- No O-lines for your host + [501] = "ERR_UMODEUNKNOWNFLAG", -- Unknown MODE flag + [502] = "ERR_USERSDONTMATCH", -- Can't change mode for other users +-- }}} +-- unused {{{ + [231] = "RPL_SERVICEINFO", + [232] = "RPL_ENDOFSERVICES", + [233] = "RPL_SERVICE", + [300] = "RPL_NONE", + [316] = "RPL_WHOISCHANOP", + [361] = "RPL_KILLDONE", + [362] = "RPL_CLOSING", + [363] = "RPL_CLOSEEND", + [373] = "RPL_INFOSTART", + [384] = "RPL_MYPORTIS", + [213] = "RPL_STATSCLINE", + [214] = "RPL_STATSNLINE", + [215] = "RPL_STATSILINE", + [216] = "RPL_STATSKLINE", + [217] = "RPL_STATSQLINE", + [218] = "RPL_STATSYLINE", + [240] = "RPL_STATSVLINE", + [241] = "RPL_STATSLLINE", + [244] = "RPL_STATSHLINE", + [246] = "RPL_STATSPING", + [247] = "RPL_STATSBLINE", + [250] = "RPL_STATSDLINE", + [492] = "ERR_NOSERVICEHOST", +-- }}} +-- guesses {{{ + [333] = "RPL_TOPICDATE", -- date the topic was set, in seconds since the epoch + [505] = "ERR_NOTREGISTERED" -- freenode blocking privmsg from unreged users +-- }}} +} +-- }}} + +-- chanmodes {{{ +chanmodes = { + ["@"] = "secret", + ["*"] = "private", + ["="] = "public" +} +-- }}} diff --git a/src/irc/ctcp.lua b/src/irc/ctcp.lua new file mode 100644 index 0000000..6a1877c --- /dev/null +++ b/src/irc/ctcp.lua @@ -0,0 +1,93 @@ +-- initialization {{{ +local base = _G +local table = require "table" +-- }}} + +module "irc.ctcp" + +-- public functions {{{ +-- low_quote {{{ +-- applies low level quoting to a string (escaping characters which +-- are illegal to appear in an irc packet) +function low_quote(str) + return str:gsub("[%z\n\r\020]", {["\000"] = "\0200", + ["\n"] = "\020n", + ["\r"] = "\020r", + ["\020"] = "\020\020"}) +end +-- }}} + +-- low_dequote {{{ +-- removes low level quoting done by low_quote +function low_dequote(str) + return str:gsub("\020(.?)", function(s) + if s == "0" then return "\000" end + if s == "n" then return "\n" end + if s == "r" then return "\r" end + if s == "\020" then return "\020" end + return "" + end) +end +-- }}} + +-- ctcp_quote {{{ +-- applies ctcp quoting to a block of text which has been identified +-- as ctcp data (by the calling program) +function ctcp_quote(str) + local ret = str:gsub("[\001\\]", {["\001"] = "\\a", + ["\\"] = "\\\\"}) + return "\001" .. ret .. "\001" +end +-- }}} + +-- ctcp_dequote {{{ +-- removes ctcp quoting from a block of text which has been +-- identified as ctcp data (likely by ctcp_split) +function ctcp_dequote(str) + local ret = str:gsub("^\001", ""):gsub("\001$", "") + return ret:gsub("\\(.?)", function(s) + if s == "a" then return "\001" end + if s == "\\" then return "\\" end + return "" + end) +end +-- }}} + +-- ctcp_split {{{ +-- takes in a mid_level (low level dequoted) string and splits it +-- up into normal text and ctcp messages. it returns an array, where string +-- values correspond to plain text and table values have t[1] as the ctcp +-- message. if dequote is true, each ctcp message will also be ctcp dequoted. +function ctcp_split(str, dequote) + local ret = {} + local iter = 1 + while true do + local s, e = str:find("\001.*\001", iter) + + local plain_string, ctcp_string + if not s then + plain_string = str:sub(iter, -1) + else + plain_string = str:sub(iter, s - 1) + ctcp_string = str:sub(s, e) + end + + if plain_string ~= "" then + table.insert(ret, plain_string) + end + if not s then break end + if ctcp_string ~= "" then + if dequote then + table.insert(ret, {ctcp_dequote(ctcp_string)}) + else + table.insert(ret, {ctcp_string}) + end + end + + iter = e + 1 + end + + return ret +end +-- }}} +-- }}} diff --git a/src/irc/dcc.lua b/src/irc/dcc.lua new file mode 100644 index 0000000..f227d4b --- /dev/null +++ b/src/irc/dcc.lua @@ -0,0 +1,122 @@ +-- initialization {{{ +local base = _G +local irc = require 'irc' +local irc_debug = require 'irc.debug' +local misc = require 'irc.misc' +local socket = require 'socket' +local coroutine = require 'coroutine' +local io = require 'io' +local string = require 'string' +-- }}} + +module 'irc.dcc' + +-- defaults {{{ +FIRST_PORT = 1028 +LAST_PORT = 5000 +-- }}} + +-- private functions {{{ +-- send_file {{{ +local function send_file(sock, file, size, packet_size) + local bytes = 0 + while true do + local packet = file:read(packet_size) + if not packet then break end + bytes = bytes + packet:len() + local index = 1 + while true do + sock:send(packet, index) + local new_bytes = misc.int_to_str(sock:receive(4)) + if new_bytes ~= bytes then + index = packet_size - bytes + new_bytes + 1 + else + break + end + end + if bytes >= size then break end + coroutine.yield(true) + end + file:close() + sock:close() + irc._unregister_socket(sock, 'w') + return true +end +-- }}} + +-- handle_connect {{{ +local function handle_connect(ssock, file, size, packet_size) + packet_size = packet_size or 1024 + local sock = ssock:accept() + sock:settimeout(0.1) + ssock:close() + irc._unregister_socket(ssock, 'r') + irc._register_socket(sock, 'w', + coroutine.wrap(function(sock) + return send_file(sock, file, size, packet_size) + end)) + return true +end +-- }}} + +-- accept_file {{{ +local function accept_file(sock, file, size, packet_size) + local bytes = 0 + while true do + local packet, err, partial_packet = sock:receive(packet_size) + if not packet and err == "timeout" then packet = partial_packet end + if not packet then break end + if packet:len() == 0 then break end + bytes = bytes + packet:len() + sock:send(misc.str_to_int(bytes)) + file:write(packet) + coroutine.yield(true) + end + file:close() + sock:close() + irc._unregister_socket(sock, 'r') + return true +end +-- }}} +-- }}} + +-- public functions {{{ +-- send {{{ +function send(nick, filename, port) + port = port or FIRST_PORT + local sock = base.assert(socket.tcp()) + repeat + err, msg = sock:bind('*', port) + port = port + 1 + until msg ~= "address already in use" and port <= LAST_PORT + 1 + base.assert(err, msg) + base.assert(sock:listen(1)) + local ip = misc.ip_str_to_int(irc.get_ip()) + local file = base.assert(io.open(filename)) + local size = file:seek("end") + file:seek("set") + irc._register_socket(sock, 'r', + coroutine.wrap(function(sock) + return handle_connect(sock, file, size) + end)) + filename = misc.basename(filename) + if filename:find(" ") then filename = '"' .. filename .. '"' end + irc.send("PRIVMSG", nick, {"DCC SEND " .. filename .. " " .. + ip .. " " .. port - 1 .. " " .. size}) +end +-- }}} + +-- accept {{{ +function accept(filename, address, port, size, packet_size) + packet_size = packet_size or 1024 + local sock = base.assert(socket.tcp()) + base.assert(sock:connect(misc.ip_int_to_str(address), port)) + sock:settimeout(0.1) + local file = base.assert(io.open(misc.get_unique_filename(filename), "w")) + irc._register_socket(sock, 'r', + coroutine.wrap(function(sock) + return accept_file(sock, file, size, packet_size) + end)) +end +-- }}} +-- }}} diff --git a/src/irc/debug.lua b/src/irc/debug.lua new file mode 100644 index 0000000..2e03d74 --- /dev/null +++ b/src/irc/debug.lua @@ -0,0 +1,64 @@ +-- initialization {{{ +local base = _G +local io = require 'io' +-- }}} + +module 'irc.debug' + +-- defaults {{{ +COLOR = true +-- }}} + +-- local variables {{{ +local ON = false +local outfile = io.output() +-- }}} + +-- public functions {{{ +-- enable {{{ +function enable() + ON = true +end +-- }}} + +-- disable {{{ +function disable() + ON = false +end +-- }}} + +-- set_output {{{ +function set_output(file) + outfile = base.assert(io.open(file)) +end +-- }}} + +-- message {{{ +function message(msg_type, msg, color) + if ON then + local endcolor = "" + if COLOR then + color = color or "\027[1;30m" + endcolor = "\027[0m" + else + color = "" + endcolor = "" + end + outfile:write(color .. msg_type .. ": " .. msg .. endcolor .. "\n") + end +end +-- }}} + +-- err {{{ +function err(msg) + message("ERR", msg, "\027[0;31m") + base.error(msg, 2) +end +-- }}} + +-- warn {{{ +function warn(msg) + message("WARN", msg, "\027[0;33m") +end +-- }}} +-- }}} diff --git a/src/irc/message.lua b/src/irc/message.lua new file mode 100644 index 0000000..27698d8 --- /dev/null +++ b/src/irc/message.lua @@ -0,0 +1,50 @@ +-- initialization {{{ +local base = _G +local constants = require 'irc.constants' +local ctcp = require 'irc.ctcp' +local irc_debug = require 'irc.debug' +local misc = require 'irc.misc' +local socket = require 'socket' +local string = require 'string' +local table = require 'table' +-- }}} + +module 'irc.message' + +-- local functions {{{ +-- parse() - parse a server command {{{ +function parse(str) + -- low-level ctcp quoting {{{ + str = ctcp.low_dequote(str) + -- }}} + -- parse the from field, if it exists (leading :) {{{ + local from = "" + if str:sub(1, 1) == ":" then + local e + e, from = socket.skip(1, str:find("^:([^ ]*) ")) + str = str:sub(e + 1) + end + -- }}} + -- get the command name or numerical reply value {{{ + local command, argstr = socket.skip(2, str:find("^([^ ]*) ?(.*)")) + local reply = false + if command:find("^%d%d%d$") then + reply = true + if constants.replies[base.tonumber(command)] then + command = constants.replies[base.tonumber(command)] + else + irc_debug.warn("Unknown server reply: " .. command) + end + end + -- }}} + -- get the args {{{ + local args = misc.split(argstr, " ", ":") + -- the first arg in a reply is always your nick + if reply then table.remove(args, 1) end + -- }}} + -- return the parsed message {{{ + return {from = from, command = command, args = args} + -- }}} +end +-- }}} +-- }}} diff --git a/src/irc/misc.lua b/src/irc/misc.lua new file mode 100644 index 0000000..7f77eea --- /dev/null +++ b/src/irc/misc.lua @@ -0,0 +1,227 @@ +-- initialization {{{ +local base = _G +local irc_debug = require 'irc.debug' +local socket = require 'socket' +local math = require 'math' +local os = require 'os' +local string = require 'string' +local table = require 'table' +-- }}} + +module 'irc.misc' + +-- defaults {{{ +DELIM = ' ' +PATH_SEP = '/' +ENDIANNESS = "big" +INT_BYTES = 4 +-- }}} + +-- private functions {{{ +local function exists(filename) + local _, err = os.rename(filename, filename) + if not err then return true end + return not err:find("No such file or directory") +end +-- }}} + +-- public functions {{{ +-- split() - splits str into substrings based on several options {{{ +function split(str, delim, end_delim, lquotes, rquotes) + -- handle arguments {{{ + delim = "["..(delim or DELIM).."]" + if end_delim then end_delim = "["..end_delim.."]" end + if lquotes then lquotes = "["..lquotes.."]" end + if rquotes then rquotes = "["..rquotes.."]" end + local optdelim = delim .. "?" + -- }}} + + local ret = {} + local instring = false + while str:len() > 0 do + -- handle case for not currently in a string {{{ + if not instring then + local end_delim_ind, lquote_ind, delim_ind + if end_delim then end_delim_ind = str:find(optdelim..end_delim) end + if lquotes then lquote_ind = str:find(optdelim..lquotes) end + local delim_ind = str:find(delim) + if not end_delim_ind then end_delim_ind = str:len() + 1 end + if not lquote_ind then lquote_ind = str:len() + 1 end + if not delim_ind then delim_ind = str:len() + 1 end + local next_ind = math.min(end_delim_ind, lquote_ind, delim_ind) + if next_ind == str:len() + 1 then + table.insert(ret, str) + break + elseif next_ind == end_delim_ind then + -- TODO: hackish here + if str:sub(next_ind, next_ind) == end_delim:gsub('[%[%]]', '') then + table.insert(ret, str:sub(next_ind + 1)) + else + table.insert(ret, str:sub(1, next_ind - 1)) + table.insert(ret, str:sub(next_ind + 2)) + end + break + elseif next_ind == lquote_ind then + table.insert(ret, str:sub(1, next_ind - 1)) + str = str:sub(next_ind + 2) + instring = true + else -- last because the top two contain it + table.insert(ret, str:sub(1, next_ind - 1)) + str = str:sub(next_ind + 1) + end + -- }}} + -- handle case for currently in a string {{{ + else + local endstr = str:find(rquotes..optdelim) + table.insert(ret, str:sub(1, endstr - 1)) + str = str:sub(endstr + 2) + instring = false + end + -- }}} + end + return ret +end +-- }}} + +-- basename() - returns the basename of a file {{{ +function basename(path, sep) + sep = sep or PATH_SEP + if not path:find(sep) then return path end + return socket.skip(2, path:find(".*" .. sep .. "(.*)")) +end +-- }}} + +-- dirname() - returns the dirname of a file {{{ +function dirname(path, sep) + sep = sep or PATH_SEP + if not path:find(sep) then return "." end + return socket.skip(2, path:find("(.*)" .. sep .. ".*")) +end +-- }}} + +-- str_to_int() - converts a number to a low-level int {{{ +function str_to_int(str, bytes, endian) + bytes = bytes or INT_BYTES + endian = endian or ENDIANNESS + local ret = "" + for i = 0, bytes - 1 do + local new_byte = string.char(math.fmod(str / (2^(8 * i)), 256)) + if endian == "big" or endian == "network" then ret = new_byte .. ret + else ret = ret .. new_byte + end + end + return ret +end +-- }}} + +-- int_to_str() - converts a low-level int to a number {{{ +function int_to_str(int, endian) + endian = endian or ENDIANNESS + local ret = 0 + for i = 1, int:len() do + if endian == "big" or endian == "network" then ind = int:len() - i + 1 + else ind = i + end + ret = ret + string.byte(int:sub(ind, ind)) * 2^(8 * (i - 1)) + end + return ret +end +-- }}} + +-- ip_str_to_int() - converts a string ip address to an int {{{ +function ip_str_to_int(ip_str) + local i = 3 + local ret = 0 + for num in ip_str:gmatch("%d+") do + ret = ret + num * 2^(i * 8) + i = i - 1 + end + return ret +end +-- }}} + +-- ip_int_to_str() - converts an int to a string ip address {{{ +function ip_int_to_str(ip_int) + local ip = {} + for i = 3, 0, -1 do + local new_num = math.floor(ip_int / 2^(i * 8)) + table.insert(ip, new_num) + ip_int = ip_int - new_num * 2^(i * 8) + end + return table.concat(ip, ".") +end +-- }}} + +-- get_unique_filename() - returns a unique filename {{{ +function get_unique_filename(filename) + if not exists(filename) then return filename end + + local count = 1 + while true do + if not exists(filename .. "." .. count) then + return filename .. "." .. count + end + count = count + 1 + end +end +-- }}} + +-- try_call() - call a function, if it exists {{{ +function try_call(fn, ...) + if base.type(fn) == "function" then + return fn(...) + end +end +-- }}} + +-- try_call_warn() - same as try_call, but complain if not {{{ +function try_call_warn(msg, fn, ...) + if base.type(fn) == "function" then + return fn(...) + else + irc_debug.warn(msg) + end +end +-- }}} + +-- parse_user() - gets the various parts of a full username {{{ +-- args: user - usermask (i.e. returned in the from field of a callback) +-- return: nick, username, hostname (these can be nil if nonexistant) +function parse_user(user) + local found, bang, nick = user:find("^([^!]*)!") + if found then + user = user:sub(bang + 1) + else + return user + end + local found, equals = user:find("^.=") + if found then + user = user:sub(3) + end + local found, at, username = user:find("^([^@]*)@") + if found then + return nick, username, user:sub(at + 1) + else + return nick, user + end +end +-- }}} + +-- value_iter() - iterate just over values of a table {{{ +function value_iter(state, arg, pred) + for k, v in base.pairs(state) do + if arg == v then arg = k end + end + local key, val = base.next(state, arg) + if not key then return end + + if base.type(pred) == "function" then + while not pred(val) do + key, val = base.next(state, key) + if not key then return end + end + end + return val +end +-- }}} +-- }}} diff --git a/test/test.lua b/test/test.lua new file mode 100644 index 0000000..eb40efa --- /dev/null +++ b/test/test.lua @@ -0,0 +1,302 @@ +#!/usr/bin/lua + +--[[ +--Implemented callbacks: +-- on_nick_change(user, old_nick) +-- on_join(chan, user) +-- on_part(chan, user, part_msg) +-- on_op(chan, from_user, to_user) +-- on_deop(chan, from_user, to_user) +-- on_voice(chan, from_user, to_user) +-- on_devoice(chan, from_user, to_user) +-- on_topic_change(chan) +-- on_invite(from_user, chan) +-- on_kick(chan, from_user, to_user) +-- on_channel_msg(chan, from_user, msg) +-- on_private_msg(from_user, msg) +-- on_channel_notice(chan, from_user, msg) +-- on_private_notice(from_user, msg) +-- on_quit(user, quit_msg) +-- on_me_join(chan) +-- on_connect() +-- on_channel_act(chan, from_user, msg) +-- on_private_act(from_user, msg) +-- on_dcc(from_user, to_user, arg, address, port, size) +-- on_ctcp_error(from_user, to_user, msg) +--]] + +--[[ +--Implemented functions: +-- connect(args) +-- quit(msg) +-- join(chan) +-- part(chan) +-- say(to, msg) +-- notice(to, msg) +-- act(to, msg) +-- server_version(cb) +-- whois(cb, nick) +-- server_time(cb) +-- ctcp_ping(cb, nick) +-- ctcp_time(cb, nick) +-- ctcp_version(cb, nick) +-- send(command, ...) +-- get_ip() +-- channels() +-- +-- chan:each_op() +-- chan:each_voice() +-- chan:each_user() +-- chan:each_member() +-- chan:ops() +-- chan:voices() +-- chan:users() +-- chan:members() +-- chan:ban(user) +-- chan:unban(user) +-- chan:voice(user) +-- chan:devoice(user) +-- chan:op(user) +-- chan:deop(user) +-- chan:set_limit(limit) +-- chan:set_key(key) +-- chan:set_private(b) +-- chan:set_secret(b) +-- chan:set_invite_only(b) +-- chan:set_topic_lock(b) +-- chan:set_no_outside_messages(b) +-- chan:set_moderated(b) +-- XXX: these should not be in the public interface... actually, this whole +-- handling needs to be rewritten +-- chan:add_user(user, mode) +-- chan:remove_user(user) +-- chan:change_status(user, b, mode) +-- chan:contains(user) +-- chan:change_nick(old_nick, new_nick +-- +-- dcc.send(nick, filename, [port]) +-- dcc.accept(filename, address, port, size, [packet_size]) +-- +-- debug.enable() +-- debug.disable() +-- debug.set_output(file) +-- debug.message(msg_type, msg, [color]) +-- debug.err(msg) +-- debug.warn(msg) +-- +-- XXX: do any of these need to be public? +-- misc.split(str, [delim], [end_delim], [lquotes], [rquotes]) +-- misc.basename(path, [sep]) +-- misc.dirname(path, [sep]) +-- misc.str_to_int(str, [bytes], [endian]) +-- misc.int_to_str(int, [endian]) +-- misc.ip_str_to_int(ip_str) +-- misc.ip_int_to_str(ip_int) +-- misc.get_unique_filename(filename) +-- misc.try_call(fn, [...]) +-- misc.try_call_warn(msg, fn, [...]) +-- misc.parse_user(user) +-- misc.value_iter(state, arg, pred) +--]] + +local irc = require "irc" +local dcc = require "irc.dcc" + +irc.DEBUG = true + +local function print_state() + for chan in irc.channels() do + print(chan..": Channel ops: "..table.concat(chan:ops(), " ")) + print(chan..": Channel voices: "..table.concat(chan:voices(), " ")) + print(chan..": Channel normal users: "..table.concat(chan:users(), " ")) + print(chan..": All channel members: "..table.concat(chan:members(), " ")) + end +end + +function irc.on_connect() + print("Joining channel #doytest...") + irc.join("#doytest") + print("Joining channel #doytest2...") + irc.join("#doytest2") +end + +function irc.on_me_join(chan) + print("Join to " .. chan .. " complete.") + print(chan .. ": Channel type: " .. chan.chanmode) + if chan.topic.text and chan.topic.text ~= "" then + print(chan .. ": Channel topic: " .. chan.topic.text) + print(" Set by " .. chan.topic.user .. + " at " .. os.date("%c", chan.topic.time)) + end + irc.act(chan.name, "is here") + print_state() +end + +function irc.on_join(chan, user) + print("I saw a join to " .. chan) + if tostring(user) ~= "doylua" then + irc.say(tostring(chan), "Hi, " .. user) + end + print_state() +end + +function irc.on_part(chan, user, part_msg) + print("I saw a part from " .. chan .. " saying " .. part_msg) + print_state() +end + +function irc.on_nick_change(new_nick, old_nick) + print("I saw a nick change: " .. old_nick .. " -> " .. new_nick) + print_state() +end + +function irc.on_kick(chan, user) + print("I saw a kick in " .. chan) + print_state() +end + +function irc.on_quit(chan, user) + print("I saw a quit from " .. chan) + print_state() +end + +local function whois_cb(cb_data) + print("WHOIS data for " .. cb_data.nick) + if cb_data.user then print("Username: " .. cb_data.user) end + if cb_data.host then print("Host: " .. cb_data.host) end + if cb_data.realname then print("Realname: " .. cb_data.realname) end + if cb_data.server then print("Server: " .. cb_data.server) end + if cb_data.serverinfo then print("Serverinfo: " .. cb_data.serverinfo) end + if cb_data.away_msg then print("Awaymsg: " .. cb_data.away_msg) end + if cb_data.is_oper then print(nick .. "is an IRCop") end + if cb_data.idle_time then print("Idletime: " .. cb_data.idle_time) end + if cb_data.channels then + print("Channel list for " .. cb_data.nick .. ":") + for _, channel in ipairs(cb_data.channels) do print(channel) end + end +end + +local function serverversion_cb(cb_data) + print("VERSION data for " .. cb_data.server) + print("Version: " .. cb_data.version) + print("Comments: " .. cb_data.comments) +end + +local function ping_cb(cb_data) + print("CTCP PING for " .. cb_data.nick) + print("Roundtrip time: " .. cb_data.time .. "s") +end + +local function time_cb(cb_data) + print("CTCP TIME for " .. cb_data.nick) + print("Localtime: " .. cb_data.time) +end + +local function version_cb(cb_data) + print("CTCP VERSION for " .. cb_data.nick) + print("Version: " .. cb_data.version) +end + +local function stime_cb(cb_data) + print("TIME for " .. cb_data.server) + print("Server time: " .. cb_data.time) +end + +function irc.on_channel_msg(chan, from, msg) + if from == "doy" then + if msg == "leave" then + irc.part(chan.name) + return + elseif msg:sub(1, 3) == "op " then + chan:op(msg:sub(4)) + return + elseif msg:sub(1, 5) == "deop " then + chan:deop(msg:sub(6)) + return + elseif msg:sub(1, 6) == "voice " then + chan:voice(msg:sub(7)) + return + elseif msg:sub(1, 8) == "devoice " then + chan:devoice(msg:sub(9)) + return + elseif msg:sub(1, 5) == "kick " then + chan:kick(msg:sub(6)) + return + elseif msg:sub(1, 5) == "send " then + dcc.send(from, msg:sub(6)) + return + elseif msg:sub(1, 6) == "whois " then + irc.whois(whois_cb, msg:sub(7)) + return + elseif msg:sub(1, 8) == "sversion" then + irc.server_version(serverversion_cb) + return + elseif msg:sub(1, 5) == "ping " then + irc.ctcp_ping(ping_cb, msg:sub(6)) + return + elseif msg:sub(1, 5) == "time " then + irc.ctcp_time(time_cb, msg:sub(6)) + return + elseif msg:sub(1, 8) == "version " then + irc.ctcp_version(version_cb, msg:sub(9)) + return + elseif msg:sub(1, 5) == "stime" then + irc.server_time(stime_cb) + return + elseif msg:sub(1, 6) == "trace " then + irc.trace(trace_cb, msg:sub(7)) + return + elseif msg:sub(1, 5) == "trace" then + irc.trace(trace_cb) + return + end + end + if from ~= "doylua" then + irc.say(chan.name, from .. ": " .. msg) + end +end + +function irc.on_private_msg(from, msg) + if from == "doy" then + if msg == "leave" then + irc.quit("gone") + return + elseif msg:sub(1, 5) == "send " then + dcc.send(from, msg:sub(6)) + return + end + end + if from ~= "doylua" then + irc.say(from, msg) + end +end + +function irc.on_channel_act(chan, from, msg) + irc.act(chan.name, "jumps on " .. from) +end + +function irc.on_private_act(from, msg) + irc.act(from, "jumps on you") +end + +function irc.on_op(chan, from, nick) + print_state() +end + +function irc.on_deop(chan, from, nick) + print_state() +end + +function irc.on_voice(chan, from, nick) + print_state() +end + +function irc.on_devoice(chan, from, nick) + print_state() +end + +function irc.on_dcc() + return true +end + +irc.connect{network = "irc.freenode.net", nick = "doylua", pass = "doylua"} |