aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjluehrs2 <jluehrs2@uiuc.edu>2007-08-26 22:07:38 -0500
committerjluehrs2 <jluehrs2@uiuc.edu>2007-08-26 22:07:38 -0500
commit86a0d68f9920cbcd382d8bb255639b022991def7 (patch)
treed9ecb2ec46ababa1654818d06d3015f5da746162
parent27ea3f3876546997cfc838f6d605b8f4ca5adcd7 (diff)
downloadluairc-86a0d68f9920cbcd382d8bb255639b022991def7.tar.gz
luairc-86a0d68f9920cbcd382d8bb255639b022991def7.zip
add all of the current files
-rw-r--r--TODO9
-rw-r--r--src/irc.lua842
-rw-r--r--src/irc/channel.lua331
-rw-r--r--src/irc/constants.lua189
-rw-r--r--src/irc/ctcp.lua93
-rw-r--r--src/irc/dcc.lua122
-rw-r--r--src/irc/debug.lua64
-rw-r--r--src/irc/message.lua50
-rw-r--r--src/irc/misc.lua227
-rw-r--r--test/test.lua302
10 files changed, 2229 insertions, 0 deletions
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..8ba88e3
--- /dev/null
+++ b/TODO
@@ -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"}