Basic bot in a working state.
This commit is contained in:
parent
47abf398a1
commit
6c4161f17a
15 changed files with 2710 additions and 1 deletions
488
irc/channel.lua
Normal file
488
irc/channel.lua
Normal file
|
@ -0,0 +1,488 @@
|
|||
---
|
||||
-- Implementation of the Channel class
|
||||
|
||||
-- initialization {{{
|
||||
local base = _G
|
||||
local irc = require 'irc'
|
||||
local misc = require 'irc.misc'
|
||||
local socket = require 'socket'
|
||||
local table = require 'table'
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module implements a channel object representing a single channel we
|
||||
-- have joined.
|
||||
module 'irc.channel'
|
||||
|
||||
-- object metatable {{{
|
||||
-- TODO: this <br /> shouldn't be necessary - bug in luadoc
|
||||
---
|
||||
-- An object of the Channel class represents a single joined channel. It has
|
||||
-- several table fields, and can be used in string contexts (returning the
|
||||
-- channel name).<br />
|
||||
-- @class table
|
||||
-- @name Channel
|
||||
-- @field name Name of the channel (read only)
|
||||
-- @field topic Channel topic, if set (read/write, writing to this sends a
|
||||
-- topic change request to the server for this channel)
|
||||
-- @field chanmode Channel mode (public/private/secret) (read only)
|
||||
-- @field members Array of all members of this channel
|
||||
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.
|
||||
-- @name chan:set_basic_mode
|
||||
-- @param self Channel object
|
||||
-- @param set True to set the mode, false to unset it
|
||||
-- @param letter Letter of the mode
|
||||
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
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- internal methods {{{
|
||||
-- TODO: is there a better way to do this? also, storing op/voice as initial
|
||||
-- substrings of the username is just ugly
|
||||
-- _add_user {{{
|
||||
--
|
||||
-- Add a user to the channel's internal user list.
|
||||
-- @param self Channel object
|
||||
-- @param user Nick of the user to add
|
||||
-- @param mode Mode (op/voice) of the user, in symbolic form (@/+)
|
||||
function _add_user(self, user, mode)
|
||||
mode = mode or ''
|
||||
self._members[user] = mode .. user
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- _remove_user {{{
|
||||
--
|
||||
-- Remove a user from the channel's internal user list.
|
||||
-- @param self Channel object
|
||||
-- @param user Nick of the user to remove
|
||||
function _remove_user(self, user)
|
||||
self._members[user] = nil
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- _change_status {{{
|
||||
--
|
||||
-- Change the op/voice status of a user in the channel's internal user list.
|
||||
-- @param self Channel object
|
||||
-- @param user Nick of the user to affect
|
||||
-- @param on True if the mode is being set, false if it's being unset
|
||||
-- @param mode 'o' for op, 'v' for voice
|
||||
function _change_status(self, user, on, mode)
|
||||
if not self._members[user] then return end
|
||||
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
|
||||
-- }}}
|
||||
|
||||
-- _change_nick {{{
|
||||
--
|
||||
-- Change the nick of a user in the channel's internal user list.
|
||||
-- @param self Channel object
|
||||
-- @param old_nick User's old nick
|
||||
-- @param new_nick User's new 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
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- constructor {{{
|
||||
---
|
||||
-- Creates a new Channel object.
|
||||
-- @param chan Name of the new channel
|
||||
-- @return The new channel instance
|
||||
function new(chan)
|
||||
return base.setmetatable({_name = chan, _topic = {}, _chanmode = "",
|
||||
_members = {}}, mt)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- public methods {{{
|
||||
-- iterators {{{
|
||||
-- each_op {{{
|
||||
---
|
||||
-- Iterator over the ops in the channel
|
||||
-- @param self Channel object
|
||||
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 {{{
|
||||
---
|
||||
-- Iterator over the voiced users in the channel
|
||||
-- @param self Channel object
|
||||
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 {{{
|
||||
---
|
||||
-- Iterator over the normal users in the channel
|
||||
-- @param self Channel object
|
||||
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 {{{
|
||||
---
|
||||
-- Iterator over all users in the channel
|
||||
-- @param self Channel object
|
||||
function each_member(self)
|
||||
return misc._value_iter, self._members, nil
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- return tables of users {{{
|
||||
-- ops {{{
|
||||
---
|
||||
-- Gets an array of all the ops in the channel.
|
||||
-- @param self Channel object
|
||||
-- @return Array of channel ops
|
||||
function ops(self)
|
||||
local ret = {}
|
||||
for nick in self:each_op() do
|
||||
table.insert(ret, nick)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- voices {{{
|
||||
---
|
||||
-- Gets an array of all the voiced users in the channel.
|
||||
-- @param self Channel object
|
||||
-- @return Array of channel voiced users
|
||||
function voices(self)
|
||||
local ret = {}
|
||||
for nick in self:each_voice() do
|
||||
table.insert(ret, nick)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- users {{{
|
||||
---
|
||||
-- Gets an array of all the normal users in the channel.
|
||||
-- @param self Channel object
|
||||
-- @return Array of channel normal users
|
||||
function users(self)
|
||||
local ret = {}
|
||||
for nick in self:each_user() do
|
||||
table.insert(ret, nick)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- members {{{
|
||||
---
|
||||
-- Gets an array of all the users in the channel.
|
||||
-- @param self Channel object
|
||||
-- @return Array of channel users
|
||||
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 {{{
|
||||
-- TODO: hmmm, this probably needs an appropriate mask, rather than a nick
|
||||
---
|
||||
-- Ban a user from a channel.
|
||||
-- @param self Channel object
|
||||
-- @param name User to ban
|
||||
function ban(self, name)
|
||||
irc.send("MODE", self.name, "+b", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- unban {{{
|
||||
-- TODO: same here
|
||||
---
|
||||
-- Remove a ban on a user.
|
||||
-- @param self Channel object
|
||||
-- @param name User to unban
|
||||
function unban(self, name)
|
||||
irc.send("MODE", self.name, "-b", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- voice {{{
|
||||
---
|
||||
-- Give a user voice on a channel.
|
||||
-- @param self Channel object
|
||||
-- @param name User to give voice to
|
||||
function voice(self, name)
|
||||
irc.send("MODE", self.name, "+v", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- devoice {{{
|
||||
---
|
||||
-- Remove voice from a user.
|
||||
-- @param self Channel object
|
||||
-- @param name User to remove voice from
|
||||
function devoice(self, name)
|
||||
irc.send("MODE", self.name, "-v", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
|
||||
-- kick {{{
|
||||
---
|
||||
-- Kicks a user.
|
||||
-- @param self Channel object
|
||||
-- @param name User to kick
|
||||
-- @param reason Reason for kicking(optional)
|
||||
function kick(self, name)
|
||||
irc.send("KICK", self.name, name, reason)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- op {{{
|
||||
---
|
||||
-- Give a user ops on a channel.
|
||||
-- @param self Channel object
|
||||
-- @param name User to op
|
||||
function op(self, name)
|
||||
irc.send("MODE", self.name, "+o", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- deop {{{
|
||||
---
|
||||
-- Remove ops from a user.
|
||||
-- @param self Channel object
|
||||
-- @param name User to remove ops from
|
||||
function deop(self, name)
|
||||
irc.send("MODE", self.name, "-o", name)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- set_limit {{{
|
||||
---
|
||||
-- Set a channel limit.
|
||||
-- @param self Channel object
|
||||
-- @param new_limit New value for the channel limit (optional; limit is unset
|
||||
-- if this argument isn't passed)
|
||||
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.
|
||||
-- @param self Channel object
|
||||
-- @param key New channel password (optional; password is unset if this
|
||||
-- argument isn't passed)
|
||||
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.
|
||||
-- @param self Channel object
|
||||
-- @param set True to set the channel as private, false to unset it
|
||||
function set_private(self, set)
|
||||
set_basic_mode(self, set, "p")
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- set_secret {{{
|
||||
---
|
||||
-- Set the secret state of a channel.
|
||||
-- @param self Channel object
|
||||
-- @param set True to set the channel as secret, false to unset it
|
||||
function set_secret(self, set)
|
||||
set_basic_mode(self, set, "s")
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- set_invite_only {{{
|
||||
---
|
||||
-- Set whether joining the channel requires an invite.
|
||||
-- @param self Channel object
|
||||
-- @param set True to set the channel invite only, false to unset it
|
||||
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.
|
||||
-- @param self Channel object
|
||||
-- @param set True to lock the topic, false to unlock it
|
||||
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.
|
||||
-- @param self Channel object
|
||||
-- @param set True to require users to be in the channel to send messages to
|
||||
-- it, false to remove this restriction
|
||||
function set_no_outside_messages(self, set)
|
||||
set_basic_mode(self, set, "n")
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- set moderated {{{
|
||||
---
|
||||
-- Set whether voice is required to speak.
|
||||
-- @param self Channel object
|
||||
-- @param set True to set the channel as moderated, false to unset it
|
||||
function set_moderated(self, set)
|
||||
set_basic_mode(self, set, "m")
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- accessors {{{
|
||||
-- contains {{{
|
||||
---
|
||||
-- Test if a user is in the channel.
|
||||
-- @param self Channel object
|
||||
-- @param nick Nick to search for
|
||||
-- @return True if the nick is in the channel, false otherwise
|
||||
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
|
||||
-- }}}
|
||||
-- }}}
|
||||
-- }}}
|
191
irc/constants.lua
Normal file
191
irc/constants.lua
Normal file
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
-- This module holds various constants used by the IRC protocol.
|
||||
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"
|
||||
}
|
||||
-- }}}
|
115
irc/ctcp.lua
Normal file
115
irc/ctcp.lua
Normal file
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
-- Implementation of the CTCP protocol
|
||||
-- initialization {{{
|
||||
local base = _G
|
||||
local table = require "table"
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module implements the various quoting and escaping requirements of the
|
||||
-- CTCP protocol.
|
||||
module "irc.ctcp"
|
||||
|
||||
-- internal functions {{{
|
||||
-- _low_quote {{{
|
||||
--
|
||||
-- Applies low level quoting to a string (escaping characters which are illegal
|
||||
-- to appear in an IRC packet).
|
||||
-- @param ... Strings to quote together, space separated
|
||||
-- @return Quoted string
|
||||
function _low_quote(...)
|
||||
local str = table.concat({...}, " ")
|
||||
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.
|
||||
-- @param str String with low level quoting applied to it
|
||||
-- @return String with those quoting methods stripped off
|
||||
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).
|
||||
-- @param ... Strings to apply CTCP quoting to together, space separated
|
||||
-- @return String with CTCP quoting applied
|
||||
function _ctcp_quote(...)
|
||||
local str = table.concat({...}, " ")
|
||||
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).
|
||||
-- @param str String with CTCP quoting
|
||||
-- @return String with all CTCP quoting stripped
|
||||
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 {{{
|
||||
--
|
||||
-- Splits a low level dequoted string into normal text and unquoted CTCP
|
||||
-- messages.
|
||||
-- @param str Low level dequoted string
|
||||
-- @return Array of tables, with each entry in the array corresponding to one
|
||||
-- part of the split message. These tables will have these fields:
|
||||
-- <ul>
|
||||
-- <li><i>str:</i> The text of the split section</li>
|
||||
-- <li><i>ctcp:</i> True if the section was a CTCP message, false
|
||||
-- otherwise</li>
|
||||
-- </ul>
|
||||
function _ctcp_split(str)
|
||||
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, {str = plain_string, ctcp = false})
|
||||
end
|
||||
if not s then break end
|
||||
if ctcp_string ~= "" then
|
||||
table.insert(ret, {str = _ctcp_dequote(ctcp_string), ctcp = true})
|
||||
end
|
||||
|
||||
iter = e + 1
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
196
irc/dcc.lua
Normal file
196
irc/dcc.lua
Normal file
|
@ -0,0 +1,196 @@
|
|||
---
|
||||
-- Implementation of the DCC protocol
|
||||
-- initialization {{{
|
||||
local base = _G
|
||||
local irc = require 'irc'
|
||||
local ctcp = require 'irc.ctcp'
|
||||
local c = ctcp._ctcp_quote
|
||||
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'
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module implements the DCC protocol. File transfers (DCC SEND) are
|
||||
-- handled, but DCC CHAT is not, as of yet.
|
||||
module 'irc.dcc'
|
||||
|
||||
-- defaults {{{
|
||||
FIRST_PORT = 1028
|
||||
LAST_PORT = 5000
|
||||
-- }}}
|
||||
|
||||
-- private functions {{{
|
||||
-- debug_dcc {{{
|
||||
--
|
||||
-- Prints a debug message about DCC events similar to irc.debug.warn, etc.
|
||||
-- @param msg Debug message
|
||||
local function debug_dcc(msg)
|
||||
irc_debug._message("DCC", msg, "\027[0;32m")
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- send_file {{{
|
||||
--
|
||||
-- Sends a file to a remote user, after that user has accepted our DCC SEND
|
||||
-- invitation
|
||||
-- @param sock Socket to send the file on
|
||||
-- @param file Lua file object corresponding to the file we want to send
|
||||
-- @param packet_size Size of the packets to send the file in
|
||||
local function send_file(sock, file, 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
|
||||
local skip = false
|
||||
sock:send(packet, index)
|
||||
local new_bytes, err = sock:receive(4)
|
||||
if not new_bytes then
|
||||
if err == "timeout" then
|
||||
skip = true
|
||||
else
|
||||
irc_debug._warn(err)
|
||||
break
|
||||
end
|
||||
else
|
||||
new_bytes = misc._int_to_str(new_bytes)
|
||||
end
|
||||
if not skip then
|
||||
if new_bytes ~= bytes then
|
||||
index = packet_size - bytes + new_bytes + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
coroutine.yield(true)
|
||||
end
|
||||
debug_dcc("File completely sent")
|
||||
file:close()
|
||||
sock:close()
|
||||
irc._unregister_socket(sock, 'w')
|
||||
return true
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- handle_connect {{{
|
||||
--
|
||||
-- Handle the connection attempt by a remote user to get our file. Basically
|
||||
-- just swaps out the server socket we were listening on for a client socket
|
||||
-- that we can send data on
|
||||
-- @param ssock Server socket that the remote user connected to
|
||||
-- @param file Lua file object corresponding to the file we want to send
|
||||
-- @param packet_size Size of the packets to send the file in
|
||||
local function handle_connect(ssock, file, packet_size)
|
||||
debug_dcc("Offer accepted, beginning to send")
|
||||
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(s)
|
||||
return send_file(s, file, packet_size)
|
||||
end))
|
||||
return true
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- accept_file {{{
|
||||
--
|
||||
-- Accepts a file from a remote user which has offered it to us.
|
||||
-- @param sock Socket to receive the file on
|
||||
-- @param file Lua file object corresponding to the file we want to save
|
||||
-- @param packet_size Size of the packets to receive the file in
|
||||
local function accept_file(sock, file, 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
|
||||
debug_dcc("File completely received")
|
||||
file:close()
|
||||
sock:close()
|
||||
irc._unregister_socket(sock, 'r')
|
||||
return true
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- internal functions {{{
|
||||
-- _accept {{{
|
||||
--
|
||||
-- Accepts a file offer from a remote user. Called when the on_dcc callback
|
||||
-- retuns true.
|
||||
-- @param filename Name to save the file as
|
||||
-- @param address IP address of the remote user in low level int form
|
||||
-- @param port Port to connect to at the remote user
|
||||
-- @param packet_size Size of the packets the remote user will be sending
|
||||
function _accept(filename, address, port, packet_size)
|
||||
debug_dcc("Accepting a DCC SEND request from " .. address .. ":" .. port)
|
||||
packet_size = packet_size or 1024
|
||||
local sock = base.assert(socket.tcp())
|
||||
base.assert(sock:connect(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(s)
|
||||
return accept_file(s, file, packet_size)
|
||||
end))
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- public functions {{{
|
||||
-- send {{{
|
||||
---
|
||||
-- Offers a file to a remote user.
|
||||
-- @param nick User to offer the file to
|
||||
-- @param filename Filename to offer
|
||||
-- @param port Port to accept connections on (optional, defaults to
|
||||
-- choosing an available port between FIRST_PORT and LAST_PORT
|
||||
-- above)
|
||||
function send(nick, filename, port)
|
||||
port = port or FIRST_PORT
|
||||
local sock
|
||||
repeat
|
||||
sock = base.assert(socket.tcp())
|
||||
err, msg = sock:bind('*', port)
|
||||
port = port + 1
|
||||
until msg ~= "address already in use" and port <= LAST_PORT + 1
|
||||
port = port - 1
|
||||
base.assert(err, msg)
|
||||
base.assert(sock:listen(1))
|
||||
local ip = misc._ip_str_to_int(irc.get_ip())
|
||||
local file, err = io.open(filename)
|
||||
if not file then
|
||||
irc_debug._warn(err)
|
||||
sock:close()
|
||||
return
|
||||
end
|
||||
local size = file:seek("end")
|
||||
file:seek("set")
|
||||
irc._register_socket(sock, 'r',
|
||||
coroutine.wrap(function(s)
|
||||
return handle_connect(s, file)
|
||||
end))
|
||||
filename = misc._basename(filename)
|
||||
if filename:find(" ") then filename = '"' .. filename .. '"' end
|
||||
debug_dcc("Offering " .. filename .. " to " .. nick .. " from " ..
|
||||
irc.get_ip() .. ":" .. port)
|
||||
irc.send("PRIVMSG", nick, c("DCC", "SEND", filename, ip, port, size))
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
92
irc/debug.lua
Normal file
92
irc/debug.lua
Normal file
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
-- Basic debug output
|
||||
-- initialization {{{
|
||||
local base = _G
|
||||
local io = require 'io'
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module implements a few useful debug functions for use throughout the
|
||||
-- rest of the code.
|
||||
module 'irc.debug'
|
||||
|
||||
-- defaults {{{
|
||||
COLOR = true
|
||||
-- }}}
|
||||
|
||||
-- local variables {{{
|
||||
local ON = false
|
||||
local outfile = io.output()
|
||||
-- }}}
|
||||
|
||||
-- internal functions {{{
|
||||
-- _message {{{
|
||||
--
|
||||
-- Output a debug message.
|
||||
-- @param msg_type Arbitrary string corresponding to the type of message
|
||||
-- @param msg Message text
|
||||
-- @param color Which terminal code to use for color output (defaults to
|
||||
-- dark gray)
|
||||
function _message(msg_type, msg, color)
|
||||
if ON then
|
||||
local endcolor = ""
|
||||
if COLOR and outfile == io.stdout 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 {{{
|
||||
--
|
||||
-- Signal an error. Writes the error message to the screen in red and calls
|
||||
-- error().
|
||||
-- @param msg Error message
|
||||
-- @see error
|
||||
function _err(msg)
|
||||
_message("ERR", msg, "\027[0;31m")
|
||||
base.error(msg, 2)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- _warn {{{
|
||||
--
|
||||
-- Signal a warning. Writes the warning message to the screen in yellow.
|
||||
-- @param msg Warning message
|
||||
function _warn(msg)
|
||||
_message("WARN", msg, "\027[0;33m")
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
||||
|
||||
-- public functions {{{
|
||||
-- enable {{{
|
||||
---
|
||||
-- Turns on debug output.
|
||||
function enable()
|
||||
ON = true
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- disable {{{
|
||||
---
|
||||
-- Turns off debug output.
|
||||
function disable()
|
||||
ON = false
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- set_output {{{
|
||||
---
|
||||
-- Redirects output to a file rather than stdout.
|
||||
-- @param file File to write debug output to
|
||||
function set_output(file)
|
||||
outfile = base.assert(io.open(file))
|
||||
end
|
||||
-- }}}
|
||||
-- }}}
|
69
irc/message.lua
Normal file
69
irc/message.lua
Normal file
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
-- Implementation of IRC server message parsing
|
||||
-- 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'
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module contains parsing functions for IRC server messages.
|
||||
module 'irc.message'
|
||||
|
||||
-- internal functions {{{
|
||||
-- _parse {{{
|
||||
--
|
||||
-- Parse a server command.
|
||||
-- @param str Command to parse
|
||||
-- @return Table containing the parsed message. It contains:
|
||||
-- <ul>
|
||||
-- <li><i>from:</i> The source of this message, in full usermask
|
||||
-- form (nick!user@host) for messages originating
|
||||
-- from users, and as a hostname for messages from
|
||||
-- servers</li>
|
||||
-- <li><i>command:</i> The command sent, in name form if possible,
|
||||
-- otherwise as a numeric code</li>
|
||||
-- <li><i>args:</i> Array of strings corresponding to the arguments
|
||||
-- to the received command</li>
|
||||
--
|
||||
-- </ul>
|
||||
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
|
||||
-- }}}
|
||||
-- }}}
|
303
irc/misc.lua
Normal file
303
irc/misc.lua
Normal file
|
@ -0,0 +1,303 @@
|
|||
---
|
||||
-- Various useful functions that didn't fit anywhere else
|
||||
-- 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'
|
||||
-- }}}
|
||||
|
||||
---
|
||||
-- This module contains various useful functions which didn't fit in any of the
|
||||
-- other modules.
|
||||
module 'irc.misc'
|
||||
|
||||
-- defaults {{{
|
||||
DELIM = ' '
|
||||
PATH_SEP = '/'
|
||||
ENDIANNESS = "big"
|
||||
INT_BYTES = 4
|
||||
-- }}}
|
||||
|
||||
-- private functions {{{
|
||||
--
|
||||
-- Check for existence of a file. This returns true if renaming a file to
|
||||
-- itself succeeds. This isn't ideal (I think anyway) but it works here, and
|
||||
-- lets me not have to bring in LFS as a dependency.
|
||||
-- @param filename File to check for existence
|
||||
-- @return True if the file exists, false otherwise
|
||||
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
|
||||
-- }}}
|
||||
|
||||
-- internal functions {{{
|
||||
-- _split {{{
|
||||
--
|
||||
-- Splits str into substrings based on several options.
|
||||
-- @param str String to split
|
||||
-- @param delim String of characters to use as the beginning of substring
|
||||
-- delimiter
|
||||
-- @param end_delim String of characters to use as the end of substring
|
||||
-- delimiter
|
||||
-- @param lquotes String of characters to use as opening quotes (quoted strings
|
||||
-- in str will be considered one substring)
|
||||
-- @param rquotes String of characters to use as closing quotes
|
||||
-- @return Array of strings, one for each substring that was separated out
|
||||
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 (the part after the last directory separator).
|
||||
-- @param path Path to the file
|
||||
-- @param sep Directory separator (optional, defaults to PATH_SEP)
|
||||
-- @return The basename of the 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 (the part before the last directory separator).
|
||||
-- @param path Path to the file
|
||||
-- @param sep Directory separator (optional, defaults to PATH_SEP)
|
||||
-- @return The dirname of the 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.
|
||||
-- @param str String representation of the int
|
||||
-- @param bytes Number of bytes in an int (defaults to INT_BYTES)
|
||||
-- @param endian Which endianness to use (big, little, host, network) (defaultsi
|
||||
-- to ENDIANNESS)
|
||||
-- @return A string whose first INT_BYTES characters make 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.
|
||||
-- @param int String whose bytes correspond to the bytes of a low-level int
|
||||
-- @param endian Endianness of the int argument (defaults to ENDIANNESS)
|
||||
-- @return String representation of the low-level int argument
|
||||
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 {{{
|
||||
-- TODO: handle endianness here
|
||||
--
|
||||
-- Converts a string IP address to a low-level int.
|
||||
-- @param ip_str String representation of an IP address
|
||||
-- @return Low-level int representation of that IP address
|
||||
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 {{{
|
||||
-- TODO: handle endianness here
|
||||
--
|
||||
-- Converts an int to a string IP address.
|
||||
-- @param ip_int Low-level int representation of an IP address
|
||||
-- @return String representation of that 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.
|
||||
-- @param filename Filename to start with
|
||||
-- @return Filename (same as the one we started with, except possibly with some
|
||||
-- numbers appended) which does not currently exist on the filesystem
|
||||
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.
|
||||
-- @param fn Function to try to call
|
||||
-- @param ... Arguments to fn
|
||||
-- @return The return values of fn, if it was successfully called
|
||||
function _try_call(fn, ...)
|
||||
if base.type(fn) == "function" then
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- _try_call_warn {{{
|
||||
--
|
||||
-- Same as try_call, but complain if the function doesn't exist.
|
||||
-- @param msg Warning message to use if the function doesn't exist
|
||||
-- @param fn Function to try to call
|
||||
-- @param ... Arguments to fn
|
||||
-- @return The return values of fn, if it was successfully called
|
||||
function _try_call_warn(msg, fn, ...)
|
||||
if base.type(fn) == "function" then
|
||||
return fn(...)
|
||||
else
|
||||
irc_debug._warn(msg)
|
||||
end
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- _value_iter {{{
|
||||
--
|
||||
-- Iterator to iterate over just the 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
|
||||
-- }}}
|
||||
|
||||
-- _parse_user {{{
|
||||
--
|
||||
-- Gets the various parts of a full username.
|
||||
-- @param user A usermask (i.e. returned in the from field of a callback)
|
||||
-- @return nick
|
||||
-- @return username (if it exists)
|
||||
-- @return hostname (if it exists)
|
||||
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
|
||||
-- }}}
|
||||
-- }}}
|
Loading…
Add table
Add a link
Reference in a new issue