aboutsummaryrefslogtreecommitdiff
path: root/src/irc
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 /src/irc
parent27ea3f3876546997cfc838f6d605b8f4ca5adcd7 (diff)
downloadluairc-86a0d68f9920cbcd382d8bb255639b022991def7.tar.xz
add all of the current files
Diffstat (limited to 'src/irc')
-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
7 files changed, 1076 insertions, 0 deletions
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
+-- }}}
+-- }}}