Change irc module work in with tls https://forum.luanti.org/viewtopic.php?t=3905&start=125
Some checks failed
Check & Release / lint (push) Has been cancelled

This commit is contained in:
kitoy 2025-10-15 01:17:47 +02:00
parent 995750828c
commit d715c5ed73
10 changed files with 1267 additions and 0 deletions

26
irc/LICENSE.txt Normal file
View File

@ -0,0 +1,26 @@
--[[
Lua IRC library
Copyright (c) 2010 Jakob Ovrum
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.]]

19
irc/README.markdown Normal file
View File

@ -0,0 +1,19 @@
[![Build Status](https://travis-ci.org/JakobOvrum/LuaIRC.svg?branch=master)](https://travis-ci.org/JakobOvrum/LuaIRC)
LuaIRC
============
IRC client library for Lua.
Dependencies
-------------
* [LuaSocket](http://w3.impa.br/~diego/software/luasocket/)
**Only required if you want to make use of the TLS support**
* [LuaSec](http://www.inf.puc-rio.br/~brunoos/luasec/)
Documentation
-------------
Documentation can be automatically generated by passing irc.luadoc (in doc/) to [LuaDoc](http://luadoc.luaforge.net/), or pre-generated documentation can be found in the 'gh-pages' branch, which can also be browsed [online](http://jakobovrum.github.com/LuaIRC/doc/modules/irc.html).

92
irc/asyncoperations.lua Normal file
View File

@ -0,0 +1,92 @@
local msgs = require("irc.messages")
local meta = {}
function meta:send(msg, ...)
if type(msg) == "table" then
msg = msg:toRFC1459()
else
if select("#", ...) > 0 then
msg = msg:format(...)
end
end
self:invoke("OnSend", msg)
local bytes, err = self.socket:send(msg .. "\r\n")
if not bytes and err ~= "timeout" and err ~= "wantwrite" then
self:invoke("OnDisconnect", err, true)
self:shutdown()
error(err, errlevel)
end
end
function meta:queue(msg)
table.insert(self.messageQueue, msg)
end
local function verify(str, errLevel)
if str:find("^:") or str:find("%s%z") then
error(("malformed parameter '%s' to irc command"):format(str), errLevel)
end
return str
end
function meta:sendChat(target, msg)
-- Split the message into segments if it includes newlines.
for line in msg:gmatch("([^\r\n]+)") do
self:queue(msgs.privmsg(verify(target, 3), line))
end
end
function meta:sendNotice(target, msg)
-- Split the message into segments if it includes newlines.
for line in msg:gmatch("([^\r\n]+)") do
self:queue(msgs.notice(verify(target, 3), line))
end
end
function meta:join(channel, key)
self:queue(msgs.join(
verify(channel, 3),
key and verify(key, 3) or nil))
end
function meta:part(channel, reason)
channel = verify(channel, 3)
self:queue(msgs.part(channel, reason))
if self.track_users then
self.channels[channel] = nil
end
end
function meta:trackUsers(b)
self.track_users = b
if not b then
for k,v in pairs(self.channels) do
self.channels[k] = nil
end
end
end
function meta:setMode(t)
local target = t.target or self.nick
local mode = ""
local add, rem = t.add, t.remove
assert(add or rem, "table contains neither 'add' nor 'remove'")
if add then
mode = table.concat{"+", verify(add, 3)}
end
if rem then
mode = table.concat{mode, "-", verify(rem, 3)}
end
self:queue(msgs.mode(verify(target, 3), mode))
end
return meta

198
irc/doc/irc.luadoc Normal file
View File

@ -0,0 +1,198 @@
--- LuaIRC is a low-level IRC library for Lua.
-- All functions raise Lua exceptions on error.
--
-- Use <code>new</code> to create a new Connection object.<br/>
-- Example:<br/><br/>
--<code>
--require "irc"<br/>
--local sleep = require "socket".sleep<br/>
--<br/>
--local s = irc.new{nick = "example"}<br/>
--<br/>
--s:hook("OnChat", function(user, channel, message)<br/>
-- print(("[%s] %s: %s"):format(channel, user.nick, message))<br/>
--end)<br/>
--<br/>
--s:connect("irc.example.net")<br/>
--s:join("#example")<br/>
--<br/>
--while true do<br/>
-- s:think()<br/>
-- sleep(0.5)<br/>
--end<br/>
--</code>
module "irc"
--- Create a new Connection object. Use <code>irc:connect</code> to connect to a server.
-- @param user Table with fields <code>nick</code>, <code>username</code> and <code>realname</code>.
-- The <code>nick</code> field is required.
--
-- @return Returns a new Connection object.
-- @see Connection
function new(user)
--- Hook a function to an event.
-- @param name Name of event.
-- @param id Unique tag.
-- @param f Callback function. [defaults to <code>id</code>]
-- @see Hooks
function irc:hook(name, id, f)
--- Remove previous hooked callback.
-- @param name Name of event.
-- @param id Unique tag.
function irc:unhook(name, id)
--- Connect <code>irc</code> to an IRC server.
-- @param host Host address.
-- @param port Server port. [default 6667]
function irc:connect(host, port)
-- @param table Table of connection details
-- @see ConnectOptions
function irc:connect(table)
--- Disconnect <code>irc</code> from the server.
-- @param message Quit message.
function irc:disconnect(message)
--- Handle incoming data for <code>irc</code>, and invoke previously hooked callbacks based on new server input.
-- You should call this in some kind of main loop, or at least often enough to not time out.
function irc:think()
--- Look up user info.
-- @param nick Nick of user to query.
-- @return Table with fields <code>userinfo</code>, <code>node</code>, <code>channels</code> and <code>account</code>.
function irc:whois(nick)
--- Look up topic.
-- Use this to invoke the hooks OnTopic and OnTopicInfo at any time.
-- @param channel Channel to query.
function irc:topic(channel)
--- Send a IRC message to the server.
-- @param msg Message or raw line to send, excluding newline characters.
-- @param ... Format parameters for <code>msg</code>, with <code>string.format</code> semantics. [optional]
function irc:send(msg, ...)
--- Queue Message to be sent to the server.
-- @param msg Message to be sent.
function irc:queue(msg)
--- Send a message to a channel or user.
-- @param target Nick or channel to send to.
-- @param message Message text.
function irc:sendChat(target, message)
--- Send a notice to a channel or user.
-- @param target Nick or channel to send to.
-- @param message Notice text.
function irc:sendNotice(target, message)
--- Join a channel.
-- @param channel Channel to join.
-- @param key Channel key. [optional]
function irc:join(channel, key)
--- Leave a channel.
-- @param channel Channel to leave.
function irc:part(channel)
--- Turn user information tracking on or off. User tracking is enabled by default.
-- @param b Boolean whether or not to track user information.
function irc:trackUsers(b)
--- Add/remove modes for a channel or nick.
-- @param t Table with fields <code>target, nick, add</code> and/or <code>rem</code>. <code>target</code> or <code>nick</code>
-- specifies the user or channel to add/remove modes. <code>add</code> is a list of modes to add to the user or channel.
-- <code>rem</code> is a list of modes to remove from the user or channel.
-- @usage Example which sets +m (moderated) for #channel: <br/>
-- <code>irc:setMode{target = "#channel", add = "m"}</code>
function irc:setMode(t)
--internal
function irc:invoke(name, ...)
function irc:handle(msg)
function irc:shutdown()
--- Table with connection information.
-- @name ConnectOptions
-- @class table
-- @field host Server host name.
-- @field port Server port. [defaults to <code>6667</code>]
-- @field timeout Connect timeout. [defaults to <code>30</code>]
-- @field password Server password.
-- @field secure Boolean to enable TLS connection, pass a params table (described, [luasec]) to control
-- [luasec]: http://www.inf.puc-rio.br/~brunoos/luasec/reference.html
--- Class representing a connection.
-- @name Connection
-- @class table
-- @field authed Boolean indicating whether the connection has completed registration.
-- @field connected Whether the connection is currently connected.
-- @field motd The server's message of the day. Can be nil.
-- @field nick The current nickname.
-- @field realname The real name sent to the server.
-- @field username The username/ident sent to the server.
-- @field socket Raw socket used by the library.
-- @field supports What the server claims to support in it's ISUPPORT message.
--- Class representing an IRC message.
-- @name Message
-- @class table
-- @field args A list of the command arguments
-- @field command The IRC command
-- @field prefix The prefix of the message
-- @field raw A raw IRC line for this message
-- @field tags A table of IRCv3 tags
-- @field user A User object describing the sender of the message
-- Fields may be missing.
-- Messages have the following methods:
-- <ul>
-- <li><code>toRFC1459()</code> - Returns the message serialized in RFC 1459 format.</li>
-- </ul>
--- List of hooks you can use with irc:hook.
-- The parameter list describes the parameters passed to the callback function.
-- <ul>
-- <li><code>PreRegister()</code> - Usefull for requesting capabilities.</li>
-- <li><code>OnRaw(line)</code> - Any non false/nil return value assumes line handled and will not be further processed.</li>
-- <li><code>OnSend(line)</code></li>
-- <li><code>OnDisconnect(message, errorOccurred)</code></li>
-- <li><code>OnChat(user, channel, message)</code></li>
-- <li><code>OnNotice(user, channel, message)</code></li>
-- <li><code>OnJoin(user, channel)</code>*</li>
-- <li><code>OnPart(user, channel)</code>*</li>
-- <li><code>OnQuit(user, message)</code></li>
-- <li><code>NickChange(user, newnick, channel)</code>*†</li>
-- <li><code>NameList(channel, names)</code></li>
-- <li><code>OnTopic(channel, topic)</code></li>
-- <li><code>OnTopicInfo(channel, creator, timeCreated)</code></li>
-- <li><code>OnKick(channel, nick, kicker, reason)</code>* (kicker is a <code>user</code> table)</li>
-- <li><code>OnUserMode(modes)</code></li>
-- <li><code>OnChannelMode(user, channel, modes)</code></li>
-- <li><code>OnModeChange(user, target, modes, ...)</code>* ('...' contains mode options such as banmasks)</li>
-- <li><code>OnCapabilityList(caps)</code></li>
-- <li><code>OnCapabilityAvailable(cap, value)</code> Called only when a capability becomes available or changes.</li>
-- <li><code>OnCapabilitySet(cap, enabled)</code>*</li>
-- <li><code>DoX(msg)</code>* - 'X' is any IRC command or numeric with the first letter capitalized (eg, DoPing and Do001)</li>
-- </ul>
-- * Event also invoked for yourself.
-- † Channel passed only when user tracking is enabled
-- @name Hooks
-- @class table
--- Table with information about a user.
-- <ul>
-- <li><code>server</code> - Server name.</li>
-- <li><code>nick</code> - User nickname.</li>
-- <li><code>username</code> - User username.</li>
-- <li><code>host</code> - User hostname.</li>
-- <li><code>realname</code> - User real name.</li>
-- <li><code>access</code> - User access, available in channel-oriented callbacks. A table containing boolean fields for each access mode that the server supports. Eg: 'o', and 'v'.</li>
-- </ul>
-- Fields may be missing. To fill them in, enable user tracking and use irc:whois.
-- @name User
-- @class table

277
irc/handlers.lua Normal file
View File

@ -0,0 +1,277 @@
local util = require("irc.util")
local msgs = require("irc.messages")
local Message = msgs.Message
local handlers = {}
handlers["PING"] = function(conn, msg)
conn:send(Message({command="PONG", args=msg.args}))
end
local function requestWanted(conn, wanted)
local args = {}
for cap, value in pairs(wanted) do
if type(value) == "string" then
cap = cap .. "=" .. value
end
if not conn.capabilities[cap] then
table.insert(args, cap)
end
end
conn:queue(Message({
command = "CAP",
args = {"REQ", table.concat(args, " ")}
})
)
end
handlers["CAP"] = function(conn, msg)
local cmd = msg.args[2]
if not cmd then
return
end
if cmd == "LS" then
local list = msg.args[3]
local last = false
if list == "*" then
list = msg.args[4]
else
last = true
end
local avail = conn.availableCapabilities
local wanted = conn.wantedCapabilities
for item in list:gmatch("(%S+)") do
local eq = item:find("=", 1, true)
local k, v
if eq then
k, v = item:sub(1, eq - 1), item:sub(eq + 1)
else
k, v = item, true
end
if not avail[k] or avail[k] ~= v then
wanted[k] = conn:invoke("OnCapabilityAvailable", k, v)
end
avail[k] = v
end
if last then
if next(wanted) then
requestWanted(conn, wanted)
end
conn:invoke("OnCapabilityList", conn.availableCapabilities)
end
elseif cmd == "ACK" then
for item in msg.args[3]:gmatch("(%S+)") do
local enabled = (item:sub(1, 1) ~= "-")
local name = enabled and item or item:sub(2)
conn:invoke("OnCapabilitySet", name, enabled)
conn.capabilities[name] = enabled
end
end
end
handlers["001"] = function(conn, msg)
conn.authed = true
conn.nick = msg.args[1]
end
handlers["PRIVMSG"] = function(conn, msg)
conn:invoke("OnChat", msg.user, msg.args[1], msg.args[2])
end
handlers["NOTICE"] = function(conn, msg)
conn:invoke("OnNotice", msg.user, msg.args[1], msg.args[2])
end
handlers["JOIN"] = function(conn, msg)
local channel = msg.args[1]
if conn.track_users then
if msg.user.nick == conn.nick then
conn.channels[channel] = {users = {}}
else
conn.channels[channel].users[msg.user.nick] = msg.user
end
end
conn:invoke("OnJoin", msg.user, msg.args[1])
end
handlers["PART"] = function(conn, msg)
local channel = msg.args[1]
if conn.track_users then
if msg.user.nick == conn.nick then
conn.channels[channel] = nil
else
conn.channels[channel].users[msg.user.nick] = nil
end
end
conn:invoke("OnPart", msg.user, msg.args[1], msg.args[2])
end
handlers["QUIT"] = function(conn, msg)
if conn.track_users then
for chanName, chan in pairs(conn.channels) do
chan.users[msg.user.nick] = nil
end
end
conn:invoke("OnQuit", msg.user, msg.args[1], msg.args[2])
end
handlers["NICK"] = function(conn, msg)
local newNick = msg.args[1]
if conn.track_users then
for chanName, chan in pairs(conn.channels) do
local users = chan.users
local oldinfo = users[msg.user.nick]
if oldinfo then
users[newNick] = oldinfo
users[msg.user.nick] = nil
conn:invoke("NickChange", msg.user, newNick, chanName)
end
end
else
conn:invoke("NickChange", msg.user, newNick)
end
if msg.user.nick == conn.nick then
conn.nick = newNick
end
end
local function needNewNick(conn, msg)
local newnick = conn.nickGenerator(msg.args[2])
conn:queue(irc.msgs.nick(newnick))
end
-- ERR_ERRONEUSNICKNAME (Misspelt but remains for historical reasons)
handlers["432"] = needNewNick
-- ERR_NICKNAMEINUSE
handlers["433"] = needNewNick
-- ERR_UNAVAILRESOURCE
handlers["437"] = function(conn, msg)
if not conn.authed then
needNewNick(conn, msg)
end
end
-- RPL_ISUPPORT
handlers["005"] = function(conn, msg)
local arglen = #msg.args
-- Skip first and last parameters (nick and info)
for i = 2, arglen - 1 do
local item = msg.args[i]
local pos = item:find("=")
if pos then
conn.supports[item:sub(1, pos - 1)] = item:sub(pos + 1)
else
conn.supports[item] = true
end
end
end
-- RPL_MOTDSTART
handlers["375"] = function(conn, msg)
conn.motd = ""
end
-- RPL_MOTD
handlers["372"] = function(conn, msg)
-- MOTD lines have a "- " prefix, strip it.
conn.motd = conn.motd .. msg.args[2]:sub(3) .. '\n'
end
-- NAMES list
handlers["353"] = function(conn, msg)
local chanType = msg.args[2]
local channel = msg.args[3]
local names = msg.args[4]
if conn.track_users then
conn.channels[channel] = conn.channels[channel] or {users = {}, type = chanType}
local users = conn.channels[channel].users
for nick in names:gmatch("(%S+)") do
local access, name = util.parseNick(conn, nick)
users[name] = {access = access}
end
end
end
-- End of NAMES list
handlers["366"] = function(conn, msg)
if conn.track_users then
conn:invoke("NameList", msg.args[2], msg.args[3])
end
end
-- No topic
handlers["331"] = function(conn, msg)
conn:invoke("OnTopic", msg.args[2], nil)
end
handlers["TOPIC"] = function(conn, msg)
conn:invoke("OnTopic", msg.args[1], msg.args[2])
end
handlers["332"] = function(conn, msg)
conn:invoke("OnTopic", msg.args[2], msg.args[3])
end
-- Topic creation info
handlers["333"] = function(conn, msg)
conn:invoke("OnTopicInfo", msg.args[2], msg.args[3], tonumber(msg.args[4]))
end
handlers["KICK"] = function(conn, msg)
conn:invoke("OnKick", msg.args[1], msg.args[2], msg.user, msg.args[3])
end
-- RPL_UMODEIS
-- To answer a query about a client's own mode, RPL_UMODEIS is sent back
handlers["221"] = function(conn, msg)
conn:invoke("OnUserMode", msg.args[2])
end
-- RPL_CHANNELMODEIS
-- The result from common irc servers differs from that defined by the rfc
handlers["324"] = function(conn, msg)
conn:invoke("OnChannelMode", msg.args[2], msg.args[3])
end
handlers["MODE"] = function(conn, msg)
local target = msg.args[1]
local modes = msg.args[2]
local optList = {}
for i = 3, #msg.args do
table.insert(optList, msg.args[i])
end
if conn.track_users and target ~= conn.nick then
local add = true
local argNum = 1
util.updatePrefixModes(conn)
for c in modes:gmatch(".") do
if c == "+" then add = true
elseif c == "-" then add = false
elseif conn.modeprefix[c] then
local nick = optList[argNum]
argNum = argNum + 1
local user = conn.channels[target].users[nick]
user.access = user.access or {}
local access = user.access
access[c] = add
if c == "o" then access.op = add
elseif c == "v" then access.voice = add
end
end
end
end
conn:invoke("OnModeChange", msg.user, target, modes, unpack(optList))
end
handlers["ERROR"] = function(conn, msg)
conn:invoke("OnDisconnect", msg.args[1], true)
conn:shutdown()
error(msg.args[1], 3)
end
return handlers

257
irc/init.lua Normal file
View File

@ -0,0 +1,257 @@
local socket = require("socket")
local util = require("irc.util")
local handlers = require("irc.handlers")
local msgs = require("irc.messages")
local Message = msgs.Message
local meta = {}
meta.__index = meta
for k, v in pairs(require("irc.asyncoperations")) do
meta[k] = v
end
local meta_preconnect = {}
function meta_preconnect.__index(o, k)
local v = rawget(meta_preconnect, k)
if v == nil and meta[k] ~= nil then
error(("field '%s' is not accessible before connecting"):format(k), 2)
end
return v
end
meta.connected = true
meta_preconnect.connected = false
function new(data)
local o = {
nick = assert(data.nick, "Field 'nick' is required");
username = data.username or "lua";
realname = data.realname or "Lua owns";
nickGenerator = data.nickGenerator or util.defaultNickGenerator;
hooks = {};
track_users = true;
supports = {};
messageQueue = {};
lastThought = 0;
recentMessages = 0;
availableCapabilities = {};
wantedCapabilities = {};
capabilities = {};
}
assert(util.checkNick(o.nick), "Erroneous nickname passed to irc.new")
return setmetatable(o, meta_preconnect)
end
function meta:hook(name, id, f)
f = f or id
self.hooks[name] = self.hooks[name] or {}
self.hooks[name][id] = f
return id or f
end
meta_preconnect.hook = meta.hook
function meta:unhook(name, id)
local hooks = self.hooks[name]
assert(hooks, "no hooks exist for this event")
assert(hooks[id], "hook ID not found")
hooks[id] = nil
end
meta_preconnect.unhook = meta.unhook
function meta:invoke(name, ...)
local hooks = self.hooks[name]
if hooks then
for id, f in pairs(hooks) do
local ret = f(...)
if ret then
return ret
end
end
end
end
function meta_preconnect:connect(_host, _port)
local host, port, password, secure, timeout
if type(_host) == "table" then
host = _host.host
port = _host.port
timeout = _host.timeout
password = _host.password
secure = _host.secure
else
host = _host
port = _port
end
host = host or error("host name required to connect", 2)
port = port or 6667
local s = socket.tcp()
s:settimeout(timeout or 30)
assert(s:connect(host, port))
if secure then
local work, ssl = pcall(require, "ssl")
if not work then
error("LuaSec required for secure connections", 2)
end
local params
if type(secure) == "table" then
params = secure
else
params = {mode = "client", protocol = "any"}
end
s = ssl.wrap(s, params)
local success, errmsg = s:dohandshake()
if not success then
error(("could not make secure connection: %s"):format(errmsg), 2)
end
end
self.socket = s
setmetatable(self, meta)
self:invoke("PreRegister", self)
if password then
self:queue(Message({command="PASS", args={password}}))
end
self:queue(msgs.nick(self.nick))
self:queue(Message({command="USER", args={self.username, "0", "*", self.realname}}))
self.channels = {}
s:settimeout(0)
repeat
self:think()
socket.sleep(0.1)
until self.authed
end
function meta:disconnect(message)
message = message or "Bye!"
self:invoke("OnDisconnect", message, false)
self:send(msgs.quit(message))
self:shutdown()
end
function meta:shutdown()
self.socket:close()
setmetatable(self, meta_preconnect)
end
local function getline(self, errlevel)
local line, err = self.socket:receive("*l")
if not line and err ~= "timeout" and err ~= "wantread" then
self:invoke("OnDisconnect", err, true)
self:shutdown()
error(err, errlevel)
end
return line
end
function meta:think()
while true do
local line = getline(self, 3)
if line and #line > 0 then
if not self:invoke("OnRaw", line) then
self:handle(Message({raw=line}))
end
else
break
end
end
-- Handle outgoing message queue
local diff = socket.gettime() - self.lastThought
self.recentMessages = self.recentMessages - (diff * 2)
if self.recentMessages < 0 then
self.recentMessages = 0
end
for i = 1, #self.messageQueue do
if self.recentMessages > 4 then
break
end
self:send(table.remove(self.messageQueue, 1))
self.recentMessages = self.recentMessages + 1
end
self.lastThought = socket.gettime()
end
function meta:handle(msg)
local handler = handlers[msg.command]
if handler then
handler(self, msg)
end
self:invoke("Do" .. util.capitalize(msg.command), msg)
end
local whoisHandlers = {
["311"] = "userinfo";
["312"] = "node";
["319"] = "channels";
["330"] = "account"; -- Freenode
["307"] = "registered"; -- Unreal
}
function meta:whois(nick)
self:send(msgs.whois(nick))
local result = {}
while true do
local line = getline(self, 3)
if line then
local msg = Message({raw=line})
local handler = whoisHandlers[msg.command]
if handler then
result[handler] = msg.args
elseif msg.command == "318" then
break
else
self:handle(msg)
end
end
end
if result.account then
result.account = result.account[3]
elseif result.registered then
result.account = result.registered[2]
end
return result
end
function meta:topic(channel)
self:queue(msgs.topic(channel))
end
return {
new = new;
Message = Message;
msgs = msgs;
color = util.color;
bold = util.bold;
underline = util.underline;
}

207
irc/messages.lua Normal file
View File

@ -0,0 +1,207 @@
-- Module table
local m = {}
local msg_meta = {}
msg_meta.__index = msg_meta
local function Message(opts)
opts = opts or {}
setmetatable(opts, msg_meta)
if opts.raw then
opts:fromRFC1459(opts.raw)
end
return opts
end
m.Message = Message
local tag_escapes = {
[";"] = "\\:",
[" "] = "\\s",
["\0"] = "\\0",
["\\"] = "\\\\",
["\r"] = "\\r",
["\n"] = "\\n",
}
local tag_unescapes = {}
for x, y in pairs(tag_escapes) do tag_unescapes[y] = x end
function msg_meta:toRFC1459()
local s = ""
if self.tags then
s = s.."@"
for key, value in pairs(self.tags) do
s = s..key
if value ~= true then
value = value:gsub("[; %z\\\r\n]", tag_escapes)
s = s.."="..value
end
s = s..";"
end
-- Strip trailing semicolon
s = s:sub(1, -2)
s = s.." "
end
s = s..self.command
local argnum = #self.args
for i = 1, argnum do
local arg = self.args[i]
local startsWithColon = (arg:sub(1, 1) == ":")
local hasSpace = arg:find(" ")
if i == argnum and (hasSpace or startsWithColon) then
s = s.." :"
else
assert(not hasSpace and not startsWithColon,
"Message arguments can not be "
.."serialized to RFC1459 format")
s = s.." "
end
s = s..arg
end
return s
end
local function parsePrefix(prefix)
local user = {}
user.nick, user.username, user.host = prefix:match("^(.+)!(.+)@(.+)$")
if not user.nick and prefix:find(".", 1, true) then
user.server = prefix
end
return user
end
function msg_meta:fromRFC1459(line)
-- IRCv3 tags
if line:sub(1, 1) == "@" then
self.tags = {}
local space = line:find(" ", 1, true)
-- For each semicolon-delimited section from after
-- the @ character to before the space character.
for tag in line:sub(2, space - 1):gmatch("([^;]+)") do
local eq = tag:find("=", 1, true)
if eq then
self.tags[tag:sub(1, eq - 1)] =
tag:sub(eq + 1):gsub("\\([:s0\\rn])", tag_unescapes)
else
self.tags[tag] = true
end
end
line = line:sub(space + 1)
end
if line:sub(1, 1) == ":" then
local space = line:find(" ", 1, true)
self.prefix = line:sub(2, space - 1)
self.user = parsePrefix(self.prefix)
line = line:sub(space + 1)
end
local pos
self.command, pos = line:match("(%S+)()")
line = line:sub(pos)
self.args = self.args or {}
for pos, param in line:gmatch("()(%S+)") do
if param:sub(1, 1) == ":" then
param = line:sub(pos + 1)
table.insert(self.args, param)
break
end
table.insert(self.args, param)
end
end
function m.privmsg(to, text)
return Message({command="PRIVMSG", args={to, text}})
end
function m.notice(to, text)
return Message({command="NOTICE", args={to, text}})
end
function m.action(to, text)
return Message({command="PRIVMSG", args={to, ("\x01ACTION %s\x01"):format(text)}})
end
function m.ctcp(command, to, args)
s = "\x01"..command
if args then
s = ' '..args
end
s = s..'\x01'
return Message({command="PRIVMSG", args={to, s}})
end
function m.kick(channel, target, reason)
return Message({command="KICK", args={channel, target, reason}})
end
function m.join(channel, key)
return Message({command="JOIN", args={channel, key}})
end
function m.part(channel, reason)
return Message({command="PART", args={channel, reason}})
end
function m.quit(reason)
return Message({command="QUIT", args={reason}})
end
function m.kill(target, reason)
return Message({command="KILL", args={target, reason}})
end
function m.kline(time, mask, reason, operreason)
local args = nil
if time then
args = {time, mask, reason..'|'..operreason}
else
args = {mask, reason..'|'..operreason}
end
return Message({command="KLINE", args=args})
end
function m.whois(nick, server)
local args = nil
if server then
args = {server, nick}
else
args = {nick}
end
return Message({command="WHOIS", args=args})
end
function m.topic(channel, text)
return Message({command="TOPIC", args={channel, text}})
end
function m.invite(channel, target)
return Message({command="INVITE", args={channel, target}})
end
function m.nick(nick)
return Message({command="NICK", args={nick}})
end
function m.mode(target, modes)
-- We have to split the modes parameter because the mode string and
-- each parameter are seperate arguments (The first command is incorrect)
-- MODE foo :+ov Nick1 Nick2
-- MODE foo +ov Nick1 Nick2
local mt = util.split(modes)
return Message({command="MODE", args={target, unpack(mt)}})
end
function m.cap(cmd, ...)
return Message({command="CAP", args={cmd, ...}})
end
return m

19
irc/push-luadoc.sh Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
if [ "$TRAVIS_REPO_SLUG" == "JakobOvrum/LuaIRC" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
echo -e "Generating luadoc...\n"
git config --global user.email "travis@travis-ci.org"
git config --global user.name "travis-ci"
git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} gh-pages > /dev/null
cd gh-pages
git rm -rf ./doc
sh ./generate.sh
git add -f ./doc
git commit -m "Lastest documentation on successful travis build $TRAVIS_BUILD_NUMBER auto-pushed to gh-pages"
git push -fq origin gh-pages > /dev/null
echo -e "Published luadoc to gh-pages.\n"
fi

52
irc/set.lua Normal file
View File

@ -0,0 +1,52 @@
local select = require("socket").select
local m = {}
local set = {}
set.__index = set
function m.new(t)
t.connections = {}
t.sockets = {}
return setmetatable(t, set)
end
function set:add(connection)
local socket = connection.socket
insert(self.sockets, socket)
self.connections[socket] = connection
insert(self.connections, connection)
end
function set:remove(connection)
local socket = connection.socket
self.connections[socket] = nil
for k, s in ipairs(self.sockets) do
if socket == s then
remove(self.sockets, k)
remove(self.connections, k)
break
end
end
end
function set:select()
local read, write, err = select(self.sockets, nil, self.timeout)
if read then
for k, socket in ipairs(read) do
read[k] = self.connections[socket]
end
end
return read, err
end
-- Select - but if it times out, it returns all connections.
function set:poll()
local read, err = self:select()
return err == "timeout" and self.connections or read
end
return m

120
irc/util.lua Normal file
View File

@ -0,0 +1,120 @@
-- Module table
local m = {}
function m.updatePrefixModes(conn)
if conn.prefixmode and conn.modeprefix then
return
end
conn.prefixmode = {}
conn.modeprefix = {}
if conn.supports.PREFIX then
local modes, prefixes = conn.supports.PREFIX:match("%(([^%)]*)%)(.*)")
for i = 1, #modes do
conn.prefixmode[prefixes:sub(i, i)] = modes:sub(i, i)
conn.modeprefix[ modes:sub(i, i)] = prefixes:sub(i, i)
end
else
conn.prefixmode['@'] = 'o'
conn.prefixmode['+'] = 'v'
conn.modeprefix['o'] = '@'
conn.modeprefix['v'] = '+'
end
end
function m.parseNick(conn, nick)
local access = {}
m.updatePrefixModes(conn)
local namestart = 1
for i = 1, #nick - 1 do
local c = nick:sub(i, i)
if conn.prefixmode[c] then
access[conn.prefixmode[c]] = true
else
namestart = i
break
end
end
access.op = access.o
access.voice = access.v
local name = nick:sub(namestart)
return access, name
end
-- mIRC markup scheme (de-facto standard)
m.color = {
black = 1,
blue = 2,
green = 3,
red = 4,
lightred = 5,
purple = 6,
brown = 7,
yellow = 8,
lightgreen = 9,
navy = 10,
cyan = 11,
lightblue = 12,
violet = 13,
gray = 14,
lightgray = 15,
white = 16
}
local colByte = string.char(3)
setmetatable(m.color, {__call = function(_, text, colornum)
colornum = (type(colornum) == "string" and
assert(color[colornum], "Invalid color '"..colornum.."'") or
colornum)
return table.concat{colByte, tostring(colornum), text, colByte}
end})
local boldByte = string.char(2)
function m.bold(text)
return boldByte..text..boldByte
end
local underlineByte = string.char(31)
function m.underline(text)
return underlineByte..text..underlineByte
end
function m.checkNick(nick)
return nick:find("^[a-zA-Z_%-%[|%]%^{|}`][a-zA-Z0-9_%-%[|%]%^{|}`]*$") ~= nil
end
function m.defaultNickGenerator(nick)
-- LuaBot -> LuaCot -> LuaCou -> ...
-- We change a random character rather than appending to the
-- nickname as otherwise the new nick could exceed the ircd's
-- maximum nickname length.
local randindex = math.random(1, #nick)
local randchar = string.sub(nick, randindex, randindex)
local b = string.byte(randchar)
b = b + 1
if b < 65 or b > 125 then
b = 65
end
-- Get the halves before and after the changed character
local first = string.sub(nick, 1, randindex - 1)
local last = string.sub(nick, randindex + 1, #nick)
nick = first .. string.char(b) .. last -- Insert the new charachter
return nick
end
function m.capitalize(text)
-- Converts first character to upercase and the rest to lowercase.
-- "PING" -> "Ping" | "hello" -> "Hello" | "123" -> "123"
return text:sub(1, 1):upper()..text:sub(2):lower()
end
function m.split(str, sep)
local t = {}
for s in str:gmatch("%S+") do
table.insert(t, s)
end
return t
end
return m