Initial Commit

This commit is contained in:
LeMagnesium 2015-09-17 20:51:25 +02:00
commit 9fae29e3b9
29 changed files with 2607 additions and 0 deletions

2
irc/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*~

3
irc/.gitmodules vendored Executable file
View File

@ -0,0 +1,3 @@
[submodule "src/LuaIRC"]
path = irc
url = https://github.com/ShadowNinja/LuaIRC.git

90
irc/API.md Executable file
View File

@ -0,0 +1,90 @@
IRC Mod API
===========
This file documents the Minetest IRC mod API.
Basics
------
In order to allow your mod to interface with this mod, you must add `irc`
to your mod's `depends.txt` file.
Reference
---------
irc:say([name,] message)
Sends <message> to either the channel (if <name> is nil or not specified),
or to the given user (if <name> is specified).
Example:
irc:say("Hello, Channel!")
irc:say("john1234", "How are you?")
irc:register_bot_command(name, cmdDef)
Registers a new bot command named <name>.
When an user sends a private message to the bot with the command name, the
command's function is called.
Here's the format of a command definition (<cmdDef>):
cmdDef = {
params = "<param1> ...", -- A description of the command's parameters
description = "My command", -- A description of what the command does. (one-liner)
func = function(user, args)
-- This function gets called when the command is invoked.
-- <user> is a user table for the user that ran the command.
-- (See the LuaIRC documentation for details.)
-- It contains fields such as 'nick' and 'ident'
-- <args> is a string of arguments to the command (may be "")
-- This function should return boolean success and a message.
end,
};
Example:
irc:register_bot_command("hello", {
params = "",
description = "Greet user",
func = function(user, param)
return true, "Hello!"
end,
});
irc.joined_players[name]
This table holds the players who are currently on the channel (may be less
than the players in the game). It is modified by the /part and /join chat
commands.
Example:
if irc.joined_players["joe"] then
-- Joe is talking on IRC
end
irc:register_hook(name, func)
Registers a function to be called when an event happens. <name> is the name
of the event, and <func> is the function to be called. See HOOKS below
for more information
Example:
irc:register_hook("OnSend", function(line)
print("SEND: "..line)
end)
This mod also supplies some utility functions:
string.expandvars(string, vars)
Expands all occurrences of the pattern "$(varname)" with the value of
'varname' in the <vars> table. Variable names not found on the table
are left verbatim in the string.
Example:
local tpl = "$(foo) $(bar) $(baz)"
local s = tpl:expandvars({foo=1, bar="Hello"})
assert(s == "1 Hello $(baz)")
In addition, all the configuration options decribed in `README.txt` are
available to other mods, though they should be considered read-only. Do
not modify these settings at runtime or you might crash the server!
Hooks
-----
The `irc:register_hook` function can register functions to be called
when some events happen. The events supported are the same as the LuaIRC
ones with a few added (mostly for internal use).
See src/LuaIRC/doc/irc.luadoc for more information.

22
irc/LICENSE.txt Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2013, Diego Martinez (kaeza)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

160
irc/README.md Executable file
View File

@ -0,0 +1,160 @@
IRC Mod for Minetest
====================
Introduction
------------
This mod is just a glue between IRC and Minetest. It provides two-way
communication between the in-game chat, and an arbitrary IRC channel.
The forum topic is at http://minetest.net/forum/viewtopic.php?id=3905
Installing
----------
Quick one line install for linux:
cd <Mod directory> && git clone https://github.com/kaeza/minetest-irc.git irc && cd irc && git submodule update --init
Please change `<Mod directory>` to fit your installation of minetest.
For more information, see [the wiki](http://wiki.minetest.net/Installing_mods).
The Minetest IRC mod uses submodules, therefore you will have to run
`git submodule init` when first installing the mod, and `git submodule update`
every time that a submodule is updated. These steps can be combined as
`git submodule update --init`.
The Minetest IRC mod also requires LuaSocket. This can be installed using your
package manager on many distributions, for example on Arch Linux:
# pacman -S lua51-socket
Settings
--------
All settings are changed in `minetest.conf`. If any of these settings
are not set, the default value is used.
* `irc.server` (string, default "irc.freenode.net")
This is the IRC server the mod connects to.
* `irc.channel` (string, default "##mt-irc-mod")
The IRC channel to join.
* `irc.interval` (number, default 2.0)
This prevents the server from flooding. It should be at
least 2.0 but can be higher. After four messages this much
time must pass between folowing messages.
* `irc.nick` (string, default "MT-FFFFFF")
Nickname used as "proxy" for the in-game chat.
'F' stands for a random base-16 number.
* `irc.password` (string, default "")
Password to use when connecting to the server.
* `irc.NSPass` (string, default nil)
NickServ password. Don't use this if you use SASL authentication.
* `irc.sasl.pass` (string, default nil)
SASL password, same as nickserv password.
You should use this instead of NickServ authentication
if the server supports it.
* `irc.sasl.user` (string, default `irc.nick`)
The SASL username. This should normaly be set to your main NickServ account name.
* `irc.debug` (boolean, default false)
Whether to output debug information.
* `irc.disable_auto_connect` (boolean, default false)
If false, the bot is connected by default. If true, a player with
the 'irc_admin' privilege has to use the /irc_connect command to
connect to the server.
* `irc.disable_auto_join` (boolean, default false)
If false, players join the channel automatically upon entering the
game. If true, each user must manually use the /join command to
join the channel. In any case, the players may use the /part
command to opt-out of being in the channel.
* `irc.send_join_part` (boolean, default true)
Determines whether to send player join and part messages to the channel.
Usage
-----
Once the game is connected to the IRC channel, chatting using the 'T' or
F10 hotkeys will send the messages to the channel, and will be visible
by anyone. Also, when someone sends a message to the channel, that text
will be visible in-game.
Messages that begin with `[off]` from in-game or IRC are not sent to the
other side.
This mod also adds a few chat commands:
* `/irc_msg <nick> <message>`
Sends a private message to a IRC user.
* `/join`
Join the IRC chat.
* `/part`
Part the IRC chat.
* `/irc_connect`
Connect the bot manually to the IRC network.
* `/irc_disconnect`
Disconnect the bot manually from the IRC network (this does not
shutdown the game).
* `/irc_reconnect`
Equivilant to `/irc_disconnect` followed by `/irc_connect`.
You can also send private messages from IRC to in-game players.
To do it, you must send a private message to the bot (set with
the `irc.nick` option above), in the following format:
@playername message
For example, if there's a player named `mtuser`, you can send him/her
a private message from IRC with:
/msg server_nick @mtuser Hello!
To avoid possible misunderstandings (since all in-game players use the
same IRC user to converse with you), the "proxy" user will reject any
private messages that are not in that format, and will send back a
nice reminder as a private message.
The bot also supports some basic commands, which are invoked by sending
a private message to it. Use `!list` to get a list of commands, and
`!help <command>` to get help about a specific command.
Thanks
------
I'd like to thank the users who supported this mod both on the Minetest
Forums and on the #minetest channel. In no particular order:
0gb.us, ShadowNinja, Shaun/kizeren, RAPHAEL, DARGON, Calinou, Exio,
vortexlabs/mrtux, marveidemanis, marktraceur, jmf/john\_minetest,
sdzen/Muadtralk, VanessaE, PilzAdam, sfan5, celeron55, KikaRz,
OldCoder, RealBadAngel, and all the people who commented in the
forum topic. Thanks to you all!
License
-------
(C) 2012-2013 Diego Martínez <kaeza@users.sf.net>
See LICENSE.txt for licensing information.
The files in the irc directory are part of the LuaIRC project.
See irc/LICENSE.txt for licensing information.

165
irc/botcmds.lua Normal file
View File

@ -0,0 +1,165 @@
irc.whereis_timer = {}
irc.whereis_timer_max_limit = 120
irc.bot_commands = {}
function irc:check_botcmd(msg)
local prefix = irc.config.command_prefix
local nick = irc.conn.nick:lower()
local text = msg.args[2]
local nickpart = text:sub(1, #nick + 2):lower()
-- First check for a nick prefix
if nickpart == nick..": " or
nickpart == nick..", " then
self:bot_command(msg, text:sub(#nick + 3))
return true
-- Then check for the configured prefix
elseif prefix and text:sub(1, #prefix):lower() == prefix:lower() then
self:bot_command(msg, text:sub(#prefix + 1))
return true
end
return false
end
function irc:bot_command(msg, text)
if text:sub(1, 1) == "@" then
local found, _, player_to, message = text:find("^.([^%s]+)%s(.+)$")
if not minetest.get_player_by_name(player_to) then
irc:reply("User '"..player_to.."' is not in the game.")
return
elseif not irc.joined_players[player_to] then
irc:reply("User '"..player_to.."' is not using IRC.")
return
end
minetest.chat_send_player(player_to,
"PM from "..msg.user.nick.."@IRC: "..message, false)
irc:reply("Message sent!")
return
end
local pos = text:find(" ", 1, true)
local cmd, args
if pos then
cmd = text:sub(1, pos - 1)
args = text:sub(pos + 1)
else
cmd = text
args = ""
end
if not self.bot_commands[cmd] then
self:reply("Unknown command '"..cmd.."'. Try 'list'."
.." Or use @playername <message> to send a private message")
return
end
local success, message = self.bot_commands[cmd].func(msg.user, args)
if message then
self:reply(message)
end
end
function irc:register_bot_command(name, def)
if (not def.func) or (type(def.func) ~= "function") then
error("Erroneous bot command definition. def.func missing.", 2)
elseif name:sub(1, 1) == "@" then
error("Erroneous bot command name. Command name begins with '@'.", 2)
end
self.bot_commands[name] = def
end
irc:register_bot_command("help", {
params = "<command>",
description = "Get help about a command",
func = function(user, args)
if args == "" then
return false, "No command name specified. Use 'list' for a list of commands."
end
local cmd = irc.bot_commands[args]
if not cmd then
return false, "Unknown command '"..cmdname.."'."
end
return true, ("Usage: %c%s %s -- %s"):format(
irc.config.command_prefix,
args,
cmd.params or "<no parameters>",
cmd.description or "<no description>")
end
})
irc:register_bot_command("list", {
params = "",
description = "List available commands.",
func = function(user, args)
local cmdlist = "Available commands: "
for name, cmd in pairs(irc.bot_commands) do
cmdlist = cmdlist..name..", "
end
return true, cmdlist.." -- Use 'help <command name>' to get"
.." help about a specific command."
end
})
irc:register_bot_command("whereis", {
params = "<player>",
description = "Tell the location of <player>",
func = function(user, args)
if args == "" then
return false, "Player name required."
end
local player = minetest.get_player_by_name(args)
if not player then
return false, "There is no player named '"..args.."'"
end
if irc.whereis_timer[user.nick] ~= nil then
local timer_player = os.difftime(os.time(),irc.whereis_timer[user.nick])
if timer_player < irc.whereis_timer_max_limit then
local answer = "Command used too often, retry in %d seconds."
return false,answer:format(irc.whereis_timer_max_limit - timer_player)
end
end
local fmt = "Player %s is at (%.2f,%.2f,%.2f)"
local pos = player:getpos()
irc.whereis_timer[user.nick] = os.time()
minetest.log("action","IRC user ".. user.nick.."!"..user.username.."@"..user.host.." asked for position of player "..player:get_player_name())
minetest.chat_send_player(player:get_player_name(),"IRC user ".. user.nick.."!"..user.username.."@"..user.host.." asked for your position")
return true, fmt:format(args, pos.x, pos.y, pos.z)
end
})
local starttime = os.time()
irc:register_bot_command("uptime", {
description = "Tell how much time the server has been up",
func = function(user, args)
local cur_time = os.time()
local diff = os.difftime(cur_time, starttime)
local fmt = "Server has been running for %d:%02d:%02d"
return true, fmt:format(
math.floor(diff / 60 / 60),
math.floor(diff / 60) % 60,
math.floor(diff) % 60
)
end
})
irc:register_bot_command("players", {
description = "List the players on the server",
func = function(user, args)
local players = minetest.get_connected_players()
local names = {}
for _, player in pairs(players) do
table.insert(names, player:get_player_name())
end
return true, "Connected players: "
..table.concat(names, ", ")
end
})

40
irc/callback.lua Normal file
View File

@ -0,0 +1,40 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
if irc.connected and irc.config.send_join_part then
irc:say("*** "..name.." joined the game")
end
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
if irc.connected and irc.config.send_join_part then
irc:say("*** "..name.." left the game")
end
end)
minetest.register_on_chat_message(function(name, message)
if not irc.connected
or message:sub(1, 1) == "/"
or message:sub(1, 5) == "[off]"
or not irc.joined_players[name]
or (not minetest.check_player_privs(name, {shout=true})) then
return
end
local nl = message:find("\n", 1, true)
if nl then
message = message:sub(1, nl - 1)
end
irc:say(irc:playerMessage(name, message))
end)
minetest.register_on_shutdown(function()
irc:disconnect("Game shutting down.")
end)

126
irc/chatcmds.lua Normal file
View File

@ -0,0 +1,126 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
-- Note: This file does NOT conatin every chat command, only general ones.
-- Feature-specific commands (like /join) are in their own files.
minetest.register_chatcommand("irc_msg", {
params = "<name> <message>",
description = "Send a private message to an IRC user",
privs = {shout=true},
func = function(name, param)
if not irc.connected then
minetest.chat_send_player(name, "Not connected to IRC. Use /irc_connect to connect.")
return
end
local found, _, toname, message = param:find("^([^%s]+)%s(.+)")
if not found then
minetest.chat_send_player(name, "Invalid usage, see /help irc_msg.")
return
end
local toname_l = toname:lower()
local validNick = false
for nick, user in pairs(irc.conn.channels[irc.config.channel].users) do
if nick:lower() == toname_l then
validNick = true
break
end
end
if toname_l:find("serv$") or toname_l:find("bot$") then
validNick = false
end
if not validNick then
minetest.chat_send_player(name,
"You can not message that user. (Hint: They have to be in the channel)")
return
end
irc:say(toname, irc:playerMessage(name, message))
minetest.chat_send_player(name, "Message sent!")
end
})
minetest.register_chatcommand("irc_names", {
params = "",
description = "List the users in IRC.",
func = function(name, params)
if not irc.connected then
minetest.chat_send_player(name, "Not connected to IRC. Use /irc_connect to connect.")
return
end
local users = { }
for k, v in pairs(irc.conn.channels[irc.config.channel].users) do
table.insert(users, k)
end
minetest.chat_send_player(name, "Users in IRC: "..table.concat(users, ", "))
end
})
minetest.register_chatcommand("irc_connect", {
description = "Connect to the IRC server.",
privs = {irc_admin=true},
func = function(name, param)
if irc.connected then
minetest.chat_send_player(name, "You are already connected to IRC.")
return
end
minetest.chat_send_player(name, "IRC: Connecting...")
irc:connect()
end
})
minetest.register_chatcommand("irc_disconnect", {
params = "[message]",
description = "Disconnect from the IRC server.",
privs = {irc_admin=true},
func = function(name, param)
if not irc.connected then
minetest.chat_send_player(name, "You are not connected to IRC.")
return
end
if params == "" then
params = "Manual disconnect by "..name
end
irc:disconnect(param)
end
})
minetest.register_chatcommand("irc_reconnect", {
description = "Reconnect to the IRC server.",
privs = {irc_admin=true},
func = function(name, param)
if not irc.connected then
minetest.chat_send_player(name, "You are not connected to IRC.")
return
end
irc:disconnect("Reconnecting...")
irc:connect()
end
})
minetest.register_chatcommand("irc_quote", {
params = "<command>",
description = "Send a raw command to the IRC server.",
privs = {irc_admin=true},
func = function(name, param)
if not irc.connected then
minetest.chat_send_player(name, "You are not connected to IRC.")
return
end
irc:queue(param)
minetest.chat_send_player(name, "Command sent!")
end
})
local oldme = minetest.chatcommands["me"].func
minetest.chatcommands["me"].func = function(name, param, ...)
oldme(name, param, ...)
irc:say(("* %s %s"):format(name, param))
end

59
irc/config.lua Normal file
View File

@ -0,0 +1,59 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
irc.config = {}
local function setting(stype, name, default)
local value
if stype == "bool" then
value = minetest.setting_getbool("irc."..name)
elseif stype == "string" then
value = minetest.setting_get("irc."..name)
elseif stype == "number" then
value = tonumber(minetest.setting_get("irc."..name))
end
if value == nil then
value = default
end
irc.config[name] = value
end
-------------------------
-- BASIC USER SETTINGS --
-------------------------
setting("string", "nick") -- Nickname (default "MT-<hash>", <hash> 6 random hexidecimal characters)
setting("string", "server", "irc.freenode.net") -- Server to connect on joinplayer
setting("number", "port", 6667) -- Port to connect on joinplayer
setting("string", "NSPass") -- NickServ password
setting("string", "sasl.user", irc.config.nick) -- SASL username
setting("string", "sasl.pass") -- SASL password
setting("string", "channel", "##mt-irc-mod") -- Channel to join
setting("string", "key") -- Key for the channel
setting("bool", "send_join_part", true) -- Whether to send player join and part messages to the channel
-----------------------
-- ADVANCED SETTINGS --
-----------------------
setting("string", "password") -- Server password
setting("bool", "secure", false) -- Enable a TLS connection, requires LuaSEC
setting("number", "timeout", 60) -- Underlying socket timeout in seconds.
setting("string", "command_prefix") -- Prefix to use for bot commands
setting("bool", "debug", false) -- Enable debug output
setting("bool", "enable_player_part", true) -- Whether to enable players joining and parting the channel
setting("bool", "auto_join", true) -- Whether to automatically show players in the channel when they join
setting("bool", "auto_connect", true) -- Whether to automatically connect to the server on mod load
-- Generate a random nickname if one isn't specified.
if not irc.config.nick then
local pr = PseudoRandom(os.time())
-- Workaround for bad distribution in minetest PRNG implementation.
irc.config.nick = ("MT-%02X%02X%02X"):format(
pr:next(0, 255),
pr:next(0, 255),
pr:next(0, 255)
)
end

260
irc/hooks.lua Normal file
View File

@ -0,0 +1,260 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
-- MIME is part of LuaSocket
local b64e = require("mime").b64
irc.hooks = {}
irc.registered_hooks = {}
-- TODO: Add proper conversion from CP1252 to UTF-8.
local stripped_chars = {"\2", "\31"}
for c = 127, 255 do
table.insert(stripped_chars, string.char(c))
end
stripped_chars = "["..table.concat(stripped_chars, "").."]"
local function normalize(text)
-- Strip colors
text = text:gsub("\3[0-9][0-9,]*", "")
return text:gsub(stripped_chars, "")
end
function irc:doHook(conn)
for name, hook in pairs(self.registered_hooks) do
for _, func in pairs(hook) do
conn:hook(name, func)
end
end
end
function irc:register_hook(name, func)
self.registered_hooks[name] = self.registered_hooks[name] or {}
table.insert(self.registered_hooks[name], func)
end
function irc.hooks.raw(line)
if irc.config.debug then
print("RECV: "..line)
end
end
function irc.hooks.send(line)
if irc.config.debug then
print("SEND: "..line)
end
end
function irc.hooks.chat(msg)
local channel, text = msg.args[1], msg.args[2]
if text:sub(1, 1) == string.char(1) then
irc.conn:invoke("OnCTCP", msg)
return
end
if channel == irc.conn.nick then
irc.last_from = msg.user.nick
irc.conn:invoke("PrivateMessage", msg)
else
irc.last_from = channel
irc.conn:invoke("OnChannelChat", msg)
end
end
local function get_core_version()
local status = minetest.get_server_status()
local start_pos = select(2, status:find("version=", 1, true))
local end_pos = status:find(",", start_pos, true)
return status:sub(start_pos + 1, end_pos - 1)
end
function irc.hooks.ctcp(msg)
local text = msg.args[2]:sub(2, -2) -- Remove ^C
local args = text:split(' ')
local command = args[1]:upper()
local function reply(s)
irc:queue(irc.msgs.notice(msg.user.nick,
("\1%s %s\1"):format(command, s)))
end
if command == "ACTION" and msg.args[1] == irc.config.channel then
local action = text:sub(8, -1)
irc:sendLocal(("* %s@IRC %s"):format(msg.user.nick, action))
elseif command == "VERSION" then
reply(("Minetest version %s, IRC mod version %s.")
:format(get_core_version(), irc.version))
elseif command == "PING" then
reply(args[2])
elseif command == "TIME" then
reply(os.date())
end
end
function irc.hooks.channelChat(msg)
local text = normalize(msg.args[2])
irc:check_botcmd(msg)
-- Don't let a user impersonate someone else by using the nick "IRC"
if msg.user.nick == "IRC" then
irc:sendLocal("<IRC@IRC> "..text)
return
end
-- Support multiple servers in a channel better by converting:
-- "<server@IRC> <player> message" into "<player@server> message"
-- "<server@IRC> *** player joined/left the game" into "*** player joined/left server"
-- and "<server@IRC> * player orders a pizza" into "* player@server orders a pizza"
local foundchat, _, chatnick, chatmessage =
text:find("^<([^>]+)> (.*)$")
local foundjoin, _, joinnick =
text:find("^%*%*%* ([^%s]+) joined the game$")
local foundleave, _, leavenick =
text:find("^%*%*%* ([^%s]+) left the game$")
local foundaction, _, actionnick, actionmessage =
text:find("^%* ([^%s]+) (.*)$")
if text:sub(1, 5) == "[off]" then
return
elseif foundchat then
irc:sendLocal(("<%s@%s> %s")
:format(chatnick, msg.user.nick, chatmessage))
elseif foundjoin then
irc:sendLocal(("*** %s joined %s")
:format(joinnick, msg.user.nick))
elseif foundleave then
irc:sendLocal(("*** %s left %s")
:format(leavenick, msg.user.nick))
elseif foundaction then
irc:sendLocal(("* %s@%s %s")
:format(actionnick, msg.user.nick, actionmessage))
else
irc:sendLocal(("<%s@IRC> %s"):format(msg.user.nick, text))
end
end
function irc.hooks.pm(msg)
-- Trim prefix if it is found
local text = msg.args[2]
local prefix = irc.config.command_prefix
if prefix and text:sub(1, #prefix) == prefix then
text = text:sub(#prefix + 1)
end
irc:bot_command(msg, text)
end
function irc.hooks.kick(channel, target, prefix, reason)
if target == irc.conn.nick then
minetest.chat_send_all("IRC: kicked from "..channel.." by "..prefix.nick..".")
irc:disconnect("Kicked")
else
irc:sendLocal(("-!- %s was kicked from %s by %s [%s]")
:format(target, channel, prefix.nick, reason))
end
end
function irc.hooks.notice(user, target, message)
if user.nick and target == irc.config.channel then
irc:sendLocal("-"..user.nick.."@IRC- "..message)
end
end
function irc.hooks.mode(user, target, modes, ...)
local by = ""
if user.nick then
by = " by "..user.nick
end
local options = ""
if select("#", ...) > 0 then
options = " "
end
options = options .. table.concat({...}, " ")
minetest.chat_send_all(("-!- mode/%s [%s%s]%s")
:format(target, modes, options, by))
end
function irc.hooks.nick(user, newNick)
irc:sendLocal(("-!- %s is now known as %s")
:format(user.nick, newNick))
end
function irc.hooks.join(user, channel)
irc:sendLocal(("-!- %s joined %s")
:format(user.nick, channel))
end
function irc.hooks.part(user, channel, reason)
reason = reason or ""
irc:sendLocal(("-!- %s has left %s [%s]")
:format(user.nick, channel, reason))
end
function irc.hooks.quit(user, reason)
irc:sendLocal(("-!- %s has quit [%s]")
:format(user.nick, reason))
end
function irc.hooks.disconnect(message, isError)
irc.connected = false
if isError then
minetest.log("error", "IRC: Error: Disconnected, reconnecting in one minute.")
minetest.chat_send_all("IRC: Error: Disconnected, reconnecting in one minute.")
minetest.after(60, irc.connect, irc)
else
minetest.log("action", "IRC: Disconnected.")
minetest.chat_send_all("IRC: Disconnected.")
end
end
function irc.hooks.preregister(conn)
if not (irc.config["sasl.user"] and irc.config["sasl.pass"]) then return end
local authString = b64e(
("%s\x00%s\x00%s"):format(
irc.config["sasl.user"],
irc.config["sasl.user"],
irc.config["sasl.pass"])
)
conn:send("CAP REQ sasl")
conn:send("AUTHENTICATE PLAIN")
conn:send("AUTHENTICATE "..authString)
--LuaIRC will send CAP END
end
irc:register_hook("PreRegister", irc.hooks.preregister)
irc:register_hook("OnRaw", irc.hooks.raw)
irc:register_hook("OnSend", irc.hooks.send)
irc:register_hook("DoPrivmsg", irc.hooks.chat)
irc:register_hook("OnPart", irc.hooks.part)
irc:register_hook("OnKick", irc.hooks.kick)
irc:register_hook("OnJoin", irc.hooks.join)
irc:register_hook("OnQuit", irc.hooks.quit)
irc:register_hook("NickChange", irc.hooks.nick)
irc:register_hook("OnCTCP", irc.hooks.ctcp)
irc:register_hook("PrivateMessage", irc.hooks.pm)
irc:register_hook("OnNotice", irc.hooks.notice)
irc:register_hook("OnChannelChat", irc.hooks.channelChat)
irc:register_hook("OnModeChange", irc.hooks.mode)
irc:register_hook("OnDisconnect", irc.hooks.disconnect)

154
irc/init.lua Normal file
View File

@ -0,0 +1,154 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
local modpath = minetest.get_modpath(minetest.get_current_modname())
package.path =
-- To find LuaIRC's init.lua
modpath.."/?/init.lua;"
-- For LuaIRC to find its files
..modpath.."/?.lua;"
..package.path
-- The build of Lua that Minetest comes with only looks for libraries under
-- /usr/local/share and /usr/local/lib but LuaSocket is often installed under
-- /usr/share and /usr/lib.
if not rawget(_G,"jit") and package.config:sub(1, 1) == "/" then
package.path = package.path..
";/usr/share/lua/5.1/?.lua"..
";/usr/share/lua/5.1/?/init.lua"
package.cpath = package.cpath..
-- ";/usr/lib/lua/5.1/?.so"
";/usr/lib/x86_64-linux-gnu/lua/5.1/?.so"
end
irc = {
version = "0.2.0",
connected = false,
cur_time = 0,
message_buffer = {},
recent_message_count = 0,
joined_players = {},
modpath = modpath,
lib = require("irc"),
}
-- Compatibility
mt_irc = irc
dofile(modpath.."/config.lua")
dofile(modpath.."/messages.lua")
dofile(modpath.."/hooks.lua")
dofile(modpath.."/callback.lua")
dofile(modpath.."/chatcmds.lua")
dofile(modpath.."/botcmds.lua")
if irc.config.enable_player_part then
dofile(modpath.."/player_part.lua")
else
setmetatable(irc.joined_players, {__index = function(index) return true end})
end
minetest.register_privilege("irc_admin", {
description = "Allow IRC administrative tasks to be performed.",
give_to_singleplayer = true
})
local stepnum = 0
minetest.register_globalstep(function(dtime) return irc:step(dtime) end)
function irc:step(dtime)
if stepnum == 3 then
if self.config.auto_connect then
self:connect()
end
end
stepnum = stepnum + 1
if not self.connected then return end
-- Hooks will manage incoming messages and errors
local good, err = xpcall(function() self.conn:think() end, debug.traceback)
if not good then
print(err)
return
end
end
function irc:connect()
if self.connected then
minetest.log("error", "IRC: Ignoring attempt to connect when already connected.")
return
end
self.conn = irc.lib.new({
nick = self.config.nick,
username = "Minetest",
realname = "Minetest",
})
self:doHook(self.conn)
local good, message = pcall(function()
self.conn:connect({
host = self.config.server,
port = self.config.port,
password = self.config.password,
timeout = self.config.timeout,
secure = self.config.secure
})
end)
if not good then
minetest.log("error", ("IRC: Connection error: %s: %s -- Reconnecting in ten minutes...")
:format(self.config.server, message))
minetest.after(600, function() self:connect() end)
return
end
if self.config.NSPass then
self:say("NickServ", "IDENTIFY "..self.config.NSPass)
end
self.conn:join(self.config.channel, self.config.key)
self.connected = true
minetest.log("action", "IRC: Connected!")
minetest.chat_send_all("IRC: Connected!")
end
function irc:disconnect(message)
if self.connected then
--The OnDisconnect hook will clear self.connected and print a disconnect message
self.conn:disconnect(message)
end
end
function irc:say(to, message)
if not message then
message = to
to = self.config.channel
end
to = to or self.config.channel
self:queue(irc.msgs.privmsg(to, message))
end
function irc:reply(message)
if not self.last_from then
return
end
message = message:gsub("[\r\n%z]", " \\n ")
self:say(self.last_from, message)
end
function irc:send(msg)
if not self.connected then return end
self.conn:send(msg)
end
function irc:queue(msg)
if not self.connected then return end
self.conn:queue(msg)
end

3
irc/irc/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
/gh-pages/
*~

26
irc/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.]]

18
irc/irc/README.markdown Normal file
View File

@ -0,0 +1,18 @@
LuaIRC
============
IRC 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).

View File

@ -0,0 +1,90 @@
local irc = require("irc.main")
local meta = irc.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(irc.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(irc.msgs.notice(verify(target, 3), line))
end
end
function meta:join(channel, key)
self:queue(irc.msgs.join(
verify(channel, 3),
key and verify(key, 3) or nil))
end
function meta:part(channel, reason)
channel = verify(channel, 3)
self:queue(irc.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(irc.msgs.mode(verify(target, 3), mode))
end

184
irc/irc/doc/irc.luadoc Executable file
View File

@ -0,0 +1,184 @@
--- LuaIRC is a low-level IRC library for Lua.
-- All functions raise Lua exceptions on error.
--
-- Use <code>new</code> to create a new IRC 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 IRC 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 <code>irc</code> object.
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(server, port)
-- @param table Table of connection details
-- @see Connection
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.
-- <ul>
-- <li><code>host</code> - Server host name.</li>
-- <li><code>port</code> - Server port. [defaults to <code>6667</code>]</li>
-- <li><code>timeout</code> - Connect timeout. [defaults to <code>30</code>]</li>
-- <li><code>password</code> - Server password.</li>
-- <li><code>secure</code> - Boolean to enable TLS connection, pass a params table (described, [luasec]) to control</li>
-- </ul>
-- [luasec]: http://www.inf.puc-rio.br/~brunoos/luasec/reference.html
-- @name Connection
-- @class table
--- 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(connection)</code>Useful for CAP commands and SASL.</li>
-- <li><code>OnRaw(line) - (any non false/nil return value assumes line handled and will not be further processed)</code></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>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

209
irc/irc/handlers.lua Normal file
View File

@ -0,0 +1,209 @@
local irc = require("irc.main")
irc.handlers = {}
local handlers = irc.handlers
handlers["PING"] = function(conn, msg)
conn:send(irc.Message({command="PONG", args=msg.args}))
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])
if msg.nick then
conn:queue(msg.nick(newnick))
end -- Ugly fix
end
-- ERR_ERRONEUSNICKNAME (Misspelt but remains for historical reasons)
handlers["432"] = needNewNick
-- ERR_NICKNAMEINUSE
handlers["433"] = needNewNick
-- 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 = irc.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
irc.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

9
irc/irc/init.lua Normal file
View File

@ -0,0 +1,9 @@
local irc = require("irc.main")
require("irc.util")
require("irc.asyncoperations")
require("irc.handlers")
require("irc.messages")
return irc

241
irc/irc/main.lua Normal file
View File

@ -0,0 +1,241 @@
local socket = require "socket"
-- Module table
local irc = {}
local meta = {}
meta.__index = meta
irc.meta = meta
local meta_preconnect = {}
function meta_preconnect.__index(o, k)
local v = rawget(meta_preconnect, k)
if not v and meta[k] then
error(("field '%s' is not accessible before connecting"):format(k), 2)
end
return v
end
function irc.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 irc.defaultNickGenerator;
hooks = {};
track_users = true;
supports = {};
messageQueue = {};
lastThought = 0;
recentMessages = 0;
}
assert(irc.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
if f(...) then
return true
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 = "tlsv1"}
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:queue(irc.Message({command="CAP", args={"REQ", "multi-prefix"}}))
self:invoke("PreRegister", self)
self:queue(irc.Message({command="CAP", args={"END"}}))
if password then
self:queue(irc.Message({command="PASS", args={password}}))
end
self:queue(irc.msgs.nick(self.nick))
self:queue(irc.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(irc.msgs.quit(message))
self:shutdown()
end
function meta:shutdown()
self.socket:close()
setmetatable(self, nil)
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(irc.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
local handlers = handlers
function meta:handle(msg)
local handler = irc.handlers[msg.command]
if handler then
handler(self, msg)
end
self:invoke("Do" .. irc.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(irc.msgs.whois(nick))
local result = {}
while true do
local line = getline(self, 3)
if line then
local msg = irc.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(irc.msgs.topic(channel))
end
return irc

200
irc/irc/messages.lua Normal file
View File

@ -0,0 +1,200 @@
local irc = require("irc.main")
irc.msgs = {}
local msgs = irc.msgs
local msg_meta = {}
msg_meta.__index = msg_meta
function irc.Message(opts)
opts = opts or {}
setmetatable(opts, msg_meta)
if opts.raw then
opts:fromRFC1459(opts.raw)
end
return opts
end
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 msgs.privmsg(to, text)
return irc.Message({command="PRIVMSG", args={to, text}})
end
function msgs.notice(to, text)
return irc.Message({command="NOTICE", args={to, text}})
end
function msgs.action(to, text)
return irc.Message({command="PRIVMSG", args={to, ("\x01ACTION %s\x01"):format(text)}})
end
function msgs.ctcp(command, to, args)
s = "\x01"..command
if args then
s = ' '..args
end
s = s..'\x01'
return irc.Message({command="PRIVMSG", args={to, s}})
end
function msgs.kick(channel, target, reason)
return irc.Message({command="KICK", args={channel, target, reason}})
end
function msgs.join(channel, key)
return irc.Message({command="JOIN", args={channel, key}})
end
function msgs.part(channel, reason)
return irc.Message({command="PART", args={channel, reason}})
end
function msgs.quit(reason)
return irc.Message({command="QUIT", args={reason}})
end
function msgs.kill(target, reason)
return irc.Message({command="KILL", args={target, reason}})
end
function msgs.kline(time, mask, reason, operreason)
local args = nil
if time then
args = {time, mask, reason..'|'..operreason}
else
args = {mask, reason..'|'..operreason}
end
return irc.Message({command="KLINE", args=args})
end
function msgs.whois(nick, server)
local args = nil
if server then
args = {server, nick}
else
args = {nick}
end
return irc.Message({command="WHOIS", args=args})
end
function msgs.topic(channel, text)
return irc.Message({command="TOPIC", args={channel, text}})
end
function msgs.invite(channel, target)
return irc.Message({command="INVITE", args={channel, target}})
end
function msgs.nick(nick)
return irc.Message({command="NICK", args={nick}})
end
function msgs.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 = irc.split(modes)
return irc.Message({command="MODE", args={target, unpack(mt)}})
end

19
irc/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/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

116
irc/irc/util.lua Normal file
View File

@ -0,0 +1,116 @@
local irc = require("irc.main")
function irc.parseNick(conn, nick)
local access = {}
irc.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
function irc.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
-- mIRC markup scheme (de-facto standard)
irc.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(irc.color, {__call = function(_, text, colornum)
colornum = (type(colornum) == "string" and
assert(irc.color[colornum], "Invalid color '"..colornum.."'") or
colornum)
return table.concat{colByte, tostring(colornum), text, colByte}
end})
local boldByte = string.char(2)
function irc.bold(text)
return boldByte..text..boldByte
end
local underlineByte = string.char(31)
function irc.underline(text)
return underlineByte..text..underlineByte
end
function irc.checkNick(nick)
return nick:find("^[a-zA-Z_%-%[|%]%^{|}`][a-zA-Z0-9_%-%[|%]%^{|}`]*$") ~= nil
end
function irc.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 irc.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 irc.split(str, sep)
local t = {}
for s in str:gmatch("%S+") do
table.insert(t, s)
end
return t
end

13
irc/messages.lua Normal file
View File

@ -0,0 +1,13 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
irc.msgs = irc.lib.msgs
function irc:sendLocal(message)
minetest.chat_send_all(message)
end
function irc:playerMessage(name, message)
return ("<%s> %s"):format(name, message)
end

69
irc/player_part.lua Normal file
View File

@ -0,0 +1,69 @@
-- This file is licensed under the terms of the BSD 2-clause license.
-- See LICENSE.txt for details.
function irc:player_part(name)
if not self.joined_players[name] then
minetest.chat_send_player(name, "IRC: You are not in the channel.")
return
end
self.joined_players[name] = nil
minetest.chat_send_player(name, "IRC: You are now out of the channel.")
end
function irc:player_join(name)
if self.joined_players[name] then
minetest.chat_send_player(name, "IRC: You are already in the channel.")
return
end
self.joined_players[name] = true
minetest.chat_send_player(name, "IRC: You are now in the channel.")
end
minetest.register_chatcommand("join", {
description = "Join the IRC channel",
privs = {shout=true},
func = function(name, param)
irc:player_join(name)
end
})
minetest.register_chatcommand("part", {
description = "Part the IRC channel",
privs = {shout=true},
func = function(name, param)
irc:player_part(name)
end
})
minetest.register_chatcommand("who", {
description = "Tell who is currently on the channel",
privs = {},
func = function(name, param)
local s = ""
for name, _ in pairs(irc.joined_players) do
s = s..", "..name
end
minetest.chat_send_player(name, "Players On Channel:"..s)
end
})
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
irc.joined_players[name] = irc.config.auto_join
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
irc.joined_players[name] = nil
end)
function irc:sendLocal(message)
for name, _ in pairs(self.joined_players) do
minetest.chat_send_player(name, message)
end
end

1
irc_commands/.gitignore vendored Executable file
View File

@ -0,0 +1 @@
*~

1
irc_commands/depends.txt Executable file
View File

@ -0,0 +1 @@
irc

274
irc_commands/init.lua Executable file
View File

@ -0,0 +1,274 @@
local irc_users = {}
local irc_tokens = {}
local tokens_file = minetest.get_worldpath() .. "/irc_tokens"
local old_chat_send_player = minetest.chat_send_player
minetest.chat_send_player = function(name, message)
for nick, loggedInAs in pairs(irc_users) do
if name == loggedInAs and not minetest.get_player_by_name(name) then
irc:say(nick, message)
end
end
return old_chat_send_player(name, message)
end
-- Load/Save tokens
local function load_tokens()
local f = io.open(tokens_file, "r")
local tokens = {}
if f then
tokens = minetest.deserialize(f:read())
f:close()
end
return tokens
end
irc_tokens = load_tokens()
local function save_tokens()
local f = io.open(tokens_file, "w")
if f then
f:write(minetest.serialize(irc_tokens))
f:close()
return true
else
minetest.log("error", "[IRC_Commands] Tokens storage file couldn't be created!")
return false
end
end
local function generate_token(name)
local passtr = ""
for i = 1, math.random(20,40) do
passtr = passtr .. tostring(math.random(1,65535))
end
return minetest.get_password_hash(name, passtr)
end
-- Note: You can and **should** regulary regenerate tokens
minetest.register_chatcommand("gen_token", {
description = "Generate irc token to log in",
privs = {shout = true},
func = function(name, param)
if not minetest.get_player_by_name(name) then
return false, "You need to be logged in to the server to generate tokens"
end
local h = ""
if irc_tokens[name] then
h = " new"
end
irc_tokens[name] = generate_token(name)
minetest.chat_send_player(name, "Here is you" .. h .. " token : " .. irc_tokens[name])
save_tokens()
-- Disconnect any user using this login
for nick, loggedInAs in pairs(irc_users) do
if loggedInAs == name then
minetest.log("action", nick.."@IRC has been logged out from "
..irc_users[nick] .. " (token regenerated)")
irc_users[nick] = nil
irc:say(nick, "Token regenerated. You are now logged off.")
end
end
return true
end
})
minetest.register_chatcommand("del_token", {
description = "Delete your entry in the token register",
privs = {shout = true},
func = function(name)
if not minetest.get_player_by_name(name) then
return false, "You need to be logged in to the server to generate tokens"
end
if not irc_tokens[name] then
return true, "You had no entry in the tokens' register"
else
irc_tokens[name] = nil
save_tokens()
-- Disconnect any user using this login
for nick, loggedInAs in pairs(irc_users) do
if loggedInAs == name then
minetest.log("action", nick.."@IRC has been logged out from "
..irc_users[nick] .. " (token regenerated)")
irc_users[nick] = nil
irc:say(nick, "Token regenerated. You are now logged off.")
end
end
return true, "Access for you using a token has been removed. Use /gen_token to create" ..
" a new token at any time"
end
end
})
irc:register_hook("NickChange", function(user, newNick)
for nick, player in pairs(irc_users) do
if nick == user.nick then
irc_users[newNick] = irc_users[user.nick]
irc_users[user.nick] = nil
end
end
end)
irc:register_hook("OnPart", function(user, channel, reason)
irc_users[user.nick] = nil
end)
irc:register_hook("OnKick", function(user, channel, target, reason)
irc_users[target] = nil
end)
irc:register_hook("OnQuit", function(user, reason)
irc_users[user.nick] = nil
end)
-- Pretty much a copypasta of the command right after this one
-- We'll keep "login" until passwords are broken. When it happens,
-- We'll remove "tlogin" and modify "login" to handle tokens
irc:register_bot_command("tlogin", {
params = "<username> <pass_token>",
description = "Login as an user to run commands, using a token",
func = function(user, args)
if args == "" then
return false, "You need a username and a token."
end
local playerName, token = args:match("^(%S+)%s(%S+)$")
if not playerName then
return false, "Player name and password required."
end
local inChannel = false
local users = irc.conn.channels[irc.config.channel].users
for cnick, cuser in pairs(users) do
if user.nick == cnick then
inChannel = true
break
end
end
if not inChannel then
return false, "You need to be in the server's channel to login."
end
if irc_tokens[playerName] and
irc_tokens[playerName] == token then
minetest.log("action", "User " .. user.nick
.." from IRC logs in as " .. playerName .. " using their token")
irc_users[user.nick] = playerName
return true, "You are now logged in as " .. playerName
else
minetest.log("action", user.nick.."@IRC attempted to log in as "
..playerName.." unsuccessfully using a token")
return false, "Incorrect token or player does not exist."
end
end
})
irc:register_bot_command("login", {
params = "<username> <password>",
description = "Login as a user to run commands",
func = function(user, args)
if args == "" then
return false, "You need a username and password."
end
local playerName, password = args:match("^(%S+)%s(%S+)$")
if not playerName then
return false, "Player name and password required."
end
local inChannel = false
local users = irc.conn.channels[irc.config.channel].users
for cnick, cuser in pairs(users) do
if user.nick == cnick then
inChannel = true
break
end
end
if not inChannel then
return false, "You need to be in the server's channel to login."
end
if minetest.auth_table[playerName] and
minetest.auth_table[playerName].password ==
minetest.get_password_hash(playerName, password) then
minetest.log("action", "User "..user.nick
.." from IRC logs in as "..playerName)
irc_users[user.nick] = playerName
return true, "You are now logged in as "..playerName
else
minetest.log("action", user.nick.."@IRC attempted to log in as "
..playerName.." unsuccessfully")
return false, "Incorrect password or player does not exist."
end
end
})
irc:register_bot_command("logout", {
description = "Logout",
func = function (user, args)
if irc_users[user.nick] then
minetest.log("action", user.nick.."@IRC logs out from "
..irc_users[user.nick])
irc_users[user.nick] = nil
return true, "You are now logged off."
else
return false, "You are not logged in."
end
end,
})
irc:register_bot_command("cmd", {
params = "<command>",
description = "Run a command on the server",
func = function (user, args)
if args == "" then
return false, "You need a command."
end
if not irc_users[user.nick] then
return false, "You are not logged in."
end
local found, _, commandname, params = args:find("^([^%s]+)%s(.+)$")
if not found then
commandname = args
end
local command = minetest.chatcommands[commandname]
if not command then
return false, "Not a valid command."
end
if not minetest.check_player_privs(irc_users[user.nick], command.privs) then
return false, "Your privileges are insufficient."
end
minetest.log("action", user.nick.."@IRC runs "
..args.." as "..irc_users[user.nick])
return command.func(irc_users[user.nick], (params or ""))
end
})
irc:register_bot_command("say", {
params = "message",
description = "Say something",
func = function (user, args)
if args == "" then
return false, "You need a message."
end
if not irc_users[user.nick] then
return false, "You are not logged in."
end
if not minetest.check_player_privs(irc_users[user.nick], {shout=true}) then
minetest.log("action", ("%s@IRC tried to say %q as %s"
.." without the shout privilege.")
:format(user.nick, args, irc_users[user.nick]))
return false, "You can not shout."
end
minetest.log("action", ("%s@IRC says %q as %s.")
:format(user.nick, args, irc_users[user.nick]))
minetest.chat_send_all("<"..irc_users[user.nick].."@IRC> "..args)
return true, "Message sent successfuly."
end
})
irc:register_bot_command("timeofday", {
description = "Tell the in-game time of day",
func = function(user, args)
local timeofday = minetest.get_timeofday()
local hours, minutes = math.modf(timeofday * 24)
minutes = math.floor(minutes * 60)
return true, "It's " .. hours .. " h " .. minutes .. " min."
end
})

1
modpack.txt Normal file
View File

@ -0,0 +1 @@
This is a modpack