diff --git a/mods/irc/.luacheckrc b/mods/irc/.luacheckrc new file mode 100644 index 0000000..7453628 --- /dev/null +++ b/mods/irc/.luacheckrc @@ -0,0 +1,14 @@ + +allow_defined_top = true + +read_globals = { + "minetest", +} + +exclude_files = { + "irc/*", +} + +globals = { + "irc", +} diff --git a/mods/irc/API.md b/mods/irc/API.md new file mode 100644 index 0000000..5de2c04 --- /dev/null +++ b/mods/irc/API.md @@ -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 to either the channel (if is nil or not specified), +or to the given user (if 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 . + 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 = { + params = " ...", -- 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. + -- 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' + -- 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. is the name + of the event, and 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 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. + diff --git a/mods/irc/LICENSE.txt b/mods/irc/LICENSE.txt new file mode 100644 index 0000000..b184032 --- /dev/null +++ b/mods/irc/LICENSE.txt @@ -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. + diff --git a/mods/irc/README.md b/mods/irc/README.md new file mode 100644 index 0000000..99e0dcc --- /dev/null +++ b/mods/irc/README.md @@ -0,0 +1,181 @@ + +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 [here][forum]. + +[forum]: https://forum.minetest.net/viewtopic.php?f=11&t=3905 + + +Installing +---------- + +Quick one line install for Linux: + + cd && git clone --recursive https://github.com/minetest-mods/irc.git + +Please change `` to fit your installation of Minetest. +For more information, see [the wiki][wiki]. + +The IRC mod's git repository uses submodules, therefore you will have to run +`git submodule init` when first installing the mod (unless you used +`--recursive` as above), and `git submodule update` every time that a submodule +is updated. These steps can be combined into `git submodule update --init`. + +You'll need to install LuaSocket. You can do so with your package manager on +many distributions, for example: + + # # On Arch Linux: + # pacman -S lua51-socket + # # On Debian/Ubuntu: + # # Debian/Ubuntu's LuaSocket packages are broken, so use LuaRocks. + # apt-get install luarocks + # luarocks install luasocket + +You will also need to add IRC to your trusted mods if you haven't disabled mod +security. Here's an example configuration line: + + secure.trusted_mods = irc + +[wiki]: https://wiki.minetest.net/Installing_mods + + +Settings +-------- + +All settings are changed in `minetest.conf`. If any of these settings +are not set, the default value is used. + +* `irc.server` (string): + The address of the IRC server to connect to. + +* `irc.channel` (string): + 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): + Nickname the server uses when it connects to IRC. + +* `irc.password` (string, default nil): + Password to use when connecting to the server. + +* `irc.NSPass` (string, default nil): + NickServ password. Don't set 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 + 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 in-game will send +messages to the channel, and will be visible by anyone. Also, messages sent +to the channel 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 `: + Send 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`: + Equivalent to `/irc_disconnect` followed by `/irc_connect`. + +You can also send private messages from IRC to in-game players +by sending 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! + +The bot also supports some basic commands, which are invoked by saying +the bot name followed by either a colon or a comma and the command, or +sending a private message to it. For example: `ServerBot: help whereis`. + +* `help []`: + Prints help about a command, or a list of supported commands if no + command is given. + +* `uptime`: + Prints the server's running time. + +* `whereis `: + Prints the coordinates of the given player. + +* `players`: + Lists players currently in the server. + + +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 +------- + +See `LICENSE.txt` for details. + +The files in the `irc` directory are part of the LuaIRC project. +See `irc/LICENSE.txt` for details. diff --git a/mods/irc/botcmds.lua b/mods/irc/botcmds.lua new file mode 100644 index 0000000..5c6020a --- /dev/null +++ b/mods/irc/botcmds.lua @@ -0,0 +1,175 @@ + +irc.bot_commands = {} + +-- From RFC1459: +-- "Because of IRC’s scandanavian origin, the characters {}| are +-- considered to be the lower case equivalents of the characters +-- []\, respectively." +local irctolower = { ["["]="{", ["\\"]="|", ["]"]="}" } + +local function irclower(s) + return (s:lower():gsub("[%[%]\\]", irctolower)) +end + +local function nickequals(nick1, nick2) + return irclower(nick1) == irclower(nick2) +end + +function irc.check_botcmd(msg) + local prefix = irc.config.command_prefix + local nick = irc.conn.nick + local text = msg.args[2] + local nickpart = text:sub(1, #nick) + local suffix = text:sub(#nick+1, #nick+2) + + -- First check for a nick prefix + if nickequals(nickpart, nick) + and (suffix == ": " or suffix == ", ") then + irc.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 + irc.bot_command(msg, text:sub(#prefix + 1)) + return true + end + return false +end + + +function irc.bot_command(msg, text) + -- Remove leading whitespace + text = text:match("^%s*(.*)") + if text:sub(1, 1) == "@" then + local _, _, player_to, message = text:find("^.([^%s]+)%s(.+)$") + if not player_to then + return + elseif 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 irc.bot_commands[cmd] then + irc.reply("Unknown command '"..cmd.."'. Try 'help'." + .." Or use @playername to send a private message") + return + end + + local _, message = irc.bot_commands[cmd].func(msg.user, args) + if message then + irc.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 + irc.bot_commands[name] = def +end + + +irc.register_bot_command("help", { + params = "", + description = "Get help about a command", + func = function(_, args) + if args == "" then + local cmdlist = { } + for name in pairs(irc.bot_commands) do + cmdlist[#cmdlist+1] = name + end + return true, "Available commands: "..table.concat(cmdlist, ", ") + .." -- Use 'help ' to get" + .." help about a specific command." + end + + local cmd = irc.bot_commands[args] + if not cmd then + return false, "Unknown command '"..args.."'." + end + + return true, ("Usage: %s%s %s -- %s"):format( + irc.config.command_prefix or "", + args, + cmd.params or "", + cmd.description or "") + end +}) + + +irc.register_bot_command("list", { + params = "", + description = "List available commands.", + func = function() + return false, "The `list` command has been merged into `help`." + .." Use `help` with no arguments to get a list." + end +}) + +--[[ +irc.register_bot_command("whereis", { + params = "", + description = "Tell the location of ", + func = function(_, 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 + local fmt = "Player %s is at (%.2f,%.2f,%.2f)" + local pos = player:getpos() + 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() + 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() + 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 +}) + diff --git a/mods/irc/callback.lua b/mods/irc/callback.lua new file mode 100644 index 0000000..29667ba --- /dev/null +++ b/mods/irc/callback.lua @@ -0,0 +1,41 @@ +-- 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, timed_out) + local name = player:get_player_name() + if irc.connected and irc.config.send_join_part then + irc.say("*** "..name.." left the game".. + (timed_out and " (Timed out)" or "")) + 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, core.strip_colors(message))) +end) + + +minetest.register_on_shutdown(function() + irc.disconnect("Game shutting down.") +end) + diff --git a/mods/irc/chatcmds.lua b/mods/irc/chatcmds.lua new file mode 100644 index 0000000..34283d7 --- /dev/null +++ b/mods/irc/chatcmds.lua @@ -0,0 +1,134 @@ +-- 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 = " ", + description = "Send a private message to an IRC user", + privs = {shout=true}, + func = function(name, param) + if not irc.connected then + return false, "Not connected to IRC. Use /irc_connect to connect." + end + local found, _, toname, message = param:find("^([^%s]+)%s(.+)") + if not found then + return false, "Invalid usage, see /help irc_msg." + end + local toname_l = toname:lower() + local validNick = false + local hint = "They have to be in the channel" + for nick 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 + hint = "it looks like a bot or service" + validNick = false + end + if not validNick then + return false, "You can not message that user. ("..hint..")" + end + irc.say(toname, irc.playerMessage(name, message)) + return true, "Message sent!" + end +}) + + +minetest.register_chatcommand("irc_names", { + params = "", + description = "List the users in IRC.", + func = function() + if not irc.connected then + return false, "Not connected to IRC. Use /irc_connect to connect." + end + local users = { } + for nick in pairs(irc.conn.channels[irc.config.channel].users) do + table.insert(users, nick) + end + return true, "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) + if irc.connected then + return false, "You are already connected to IRC." + 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 + return false, "Not connected to IRC. Use /irc_connect to connect." + end + if param == "" then + param = "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) + if not irc.connected then + return false, "Not connected to IRC. Use /irc_connect to connect." + end + minetest.chat_send_player(name, "IRC: Reconnecting...") + irc.disconnect("Reconnecting...") + irc.connect() + end +}) + + +minetest.register_chatcommand("irc_quote", { + params = "", + description = "Send a raw command to the IRC server.", + privs = {irc_admin=true}, + func = function(name, param) + if not irc.connected then + return false, "Not connected to IRC. Use /irc_connect to connect." + end + irc.queue(param) + minetest.chat_send_player(name, "Command sent!") + end +}) + + +local oldme = minetest.chatcommands["me"].func +-- luacheck: ignore +minetest.chatcommands["me"].func = function(name, param, ...) + irc.say(("* %s %s"):format(name, param)) + return oldme(name, param, ...) +end + +if irc.config.send_kicks and minetest.chatcommands["kick"] then + local oldkick = minetest.chatcommands["kick"].func + -- luacheck: ignore + minetest.chatcommands["kick"].func = function(name, param, ...) + local plname, reason = param:match("^(%S+)%s*(.*)$") + if not plname then + return false, "Usage: /kick player [reason]" + end + irc.say(("*** Kicked %s.%s"):format(name, + reason~="" and " Reason: "..reason or "")) + return oldkick(name, param, ...) + end +end diff --git a/mods/irc/config.lua b/mods/irc/config.lua new file mode 100644 index 0000000..c629e47 --- /dev/null +++ b/mods/irc/config.lua @@ -0,0 +1,56 @@ +-- 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, required) + 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 + if required then + error("Required configuration option irc.".. + name.." missing.") + end + value = default + end + irc.config[name] = value +end + +------------------------- +-- BASIC USER SETTINGS -- +------------------------- + +setting("string", "nick", nil, true) -- Nickname +setting("string", "server", nil, true) -- Server address to connect to +setting("number", "port", 6667) -- Server port to connect to +setting("string", "NSPass") -- NickServ password +setting("string", "sasl.user", irc.config.nick) -- SASL username +setting("string", "username", "Minetest") -- Username/ident +setting("string", "realname", "Minetest") -- Real name/GECOS +setting("string", "sasl.pass") -- SASL password +setting("string", "channel", nil, true) -- 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 +setting("bool", "send_kicks", false) -- Whether to send player kicked 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("number", "reconnect", 600) -- Time between reconnection attempts, 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 + diff --git a/mods/irc/description.txt b/mods/irc/description.txt new file mode 100644 index 0000000..58ba37a --- /dev/null +++ b/mods/irc/description.txt @@ -0,0 +1,4 @@ +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. diff --git a/mods/irc/hooks.lua b/mods/irc/hooks.lua new file mode 100644 index 0000000..0ac3597 --- /dev/null +++ b/mods/irc/hooks.lua @@ -0,0 +1,258 @@ +-- This file is licensed under the terms of the BSD 2-clause license. +-- See LICENSE.txt for details. + +local ie = ... + +-- MIME is part of LuaSocket +local b64e = ie.require("mime").b64 + +irc.hooks = {} +irc.registered_hooks = {} + + +local stripped_chars = "[\2\31]" + +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(irc.registered_hooks) do + for _, func in pairs(hook) do + conn:hook(name, func) + end + end +end + + +function irc.register_hook(name, func) + irc.registered_hooks[name] = irc.registered_hooks[name] or {} + table.insert(irc.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" + local fake = msg.user.nick:lower():match("^[il|]rc$") + if fake then + irc.sendLocal("<"..msg.user.nick.."@IRC> "..text) + return + end + + -- Support multiple servers in a channel better by converting: + -- " message" into " message" + -- " *** player joined/left the game" into "*** player joined/left server" + -- and " * 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(_, 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) + conn: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) + diff --git a/mods/irc/init.lua b/mods/irc/init.lua new file mode 100644 index 0000000..67dec28 --- /dev/null +++ b/mods/irc/init.lua @@ -0,0 +1,218 @@ +-- 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()) + +-- Handle mod security if needed +local ie, req_ie = _G, minetest.request_insecure_environment +if req_ie then ie = req_ie() end +if not ie then + error("The IRC mod requires access to insecure functions in order ".. + "to work. Please add the irc mod to your secure.trusted_mods ".. + "setting or disable the irc mod.") +end + +ie.package.path = + -- To find LuaIRC's init.lua + modpath.."/?/init.lua;" + -- For LuaIRC to find its files + ..modpath.."/?.lua;" + ..ie.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 + ie.package.path = ie.package.path.. + ";/usr/share/lua/5.1/?.lua".. + ";/usr/share/lua/5.1/?/init.lua" + ie.package.cpath = ie.package.cpath.. + ";/usr/lib/lua/5.1/?.so" +end + +-- Temporarily set require so that LuaIRC can access it +local old_require = require +require = ie.require + +-- Silence warnings about `module` in `ltn12`. +local old_module = rawget(_G, "module") +rawset(_G, "module", ie.module) + +local lib = ie.require("irc") + +irc = { + version = "0.2.0", + connected = false, + cur_time = 0, + message_buffer = {}, + recent_message_count = 0, + joined_players = {}, + modpath = modpath, + lib = lib, +} + +-- Compatibility +rawset(_G, "mt_irc", irc) + +local getinfo = debug.getinfo +local warned = { } + +local function warn_deprecated(k) + local info = getinfo(3) + local loc = info.source..":"..info.currentline + if warned[loc] then return end + warned[loc] = true + print("COLON: "..tostring(k)) + minetest.log("warning", "Deprecated use of colon notation when calling" + .." method `"..tostring(k).."` at "..loc) +end + +-- This is a hack. +setmetatable(irc, { + __newindex = function(t, k, v) + if type(v) == "function" then + local f = v + v = function(me, ...) + if rawequal(me, t) then + warn_deprecated(k) + return f(...) + else + return f(me, ...) + end + end + end + rawset(t, k, v) + end, +}) + +dofile(modpath.."/config.lua") +dofile(modpath.."/messages.lua") +loadfile(modpath.."/hooks.lua")(ie) +dofile(modpath.."/callback.lua") +dofile(modpath.."/chatcmds.lua") +dofile(modpath.."/botcmds.lua") + +-- Restore old (safe) functions +require = old_require +rawset(_G, "module", old_module) + +if irc.config.enable_player_part then + dofile(modpath.."/player_part.lua") +else + setmetatable(irc.joined_players, {__index = function() 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() + if stepnum == 3 then + if irc.config.auto_connect then + irc.connect() + end + end + stepnum = stepnum + 1 + + if not irc.connected then return end + + -- Hooks will manage incoming messages and errors + local good, err = xpcall(function() irc.conn:think() end, debug.traceback) + if not good then + print(err) + return + end +end + + +function irc.connect() + if irc.connected then + minetest.log("error", "IRC: Ignoring attempt to connect when already connected.") + return + end + irc.conn = irc.lib.new({ + nick = irc.config.nick, + username = irc.config.username, + realname = irc.config.realname, + }) + irc.doHook(irc.conn) + + -- We need to swap the `require` function again since + -- LuaIRC `require`s `ssl` if `irc.secure` is true. + old_require = require + require = ie.require + + local good, message = pcall(function() + irc.conn:connect({ + host = irc.config.server, + port = irc.config.port, + password = irc.config.password, + timeout = irc.config.timeout, + reconnect = irc.config.reconnect, + secure = irc.config.secure + }) + end) + + require = old_require + + if not good then + minetest.log("error", ("IRC: Connection error: %s: %s -- Reconnecting in %d seconds...") + :format(irc.config.server, message, irc.config.reconnect)) + minetest.after(irc.config.reconnect, function() irc.connect() end) + return + end + + if irc.config.NSPass then + irc.conn:queue(irc.msgs.privmsg( + "NickServ", "IDENTIFY "..irc.config.NSPass)) + end + + irc.conn:join(irc.config.channel, irc.config.key) + irc.connected = true + minetest.log("action", "IRC: Connected!") + minetest.chat_send_all("IRC: Connected!") +end + + +function irc.disconnect(message) + if irc.connected then + --The OnDisconnect hook will clear irc.connected and print a disconnect message + irc.conn:disconnect(message) + end +end + + +function irc.say(to, message) + if not message then + message = to + to = irc.config.channel + end + to = to or irc.config.channel + + irc.queue(irc.msgs.privmsg(to, message)) +end + + +function irc.reply(message) + if not irc.last_from then + return + end + message = message:gsub("[\r\n%z]", " \\n ") + irc.say(irc.last_from, message) +end + +function irc.send(msg) + if not irc.connected then return end + irc.conn:send(msg) +end + +function irc.queue(msg) + if not irc.connected then return end + irc.conn:queue(msg) +end + diff --git a/mods/irc/messages.lua b/mods/irc/messages.lua new file mode 100644 index 0000000..83e4f7f --- /dev/null +++ b/mods/irc/messages.lua @@ -0,0 +1,17 @@ +-- 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.logChat(message) + minetest.log("action", "IRC CHAT: "..message) +end + +function irc.sendLocal(message) + minetest.chat_send_all(message) + irc.logChat(message) +end + +function irc.playerMessage(name, message) + return ("<%s> %s"):format(name, message) +end diff --git a/mods/irc/mod.conf b/mods/irc/mod.conf new file mode 100644 index 0000000..ba3caea --- /dev/null +++ b/mods/irc/mod.conf @@ -0,0 +1 @@ +name = irc diff --git a/mods/irc/player_part.lua b/mods/irc/player_part.lua new file mode 100644 index 0000000..e703316 --- /dev/null +++ b/mods/irc/player_part.lua @@ -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 irc.joined_players[name] then + return false, "You are not in the channel" + end + irc.joined_players[name] = nil + return true, "You left the channel" +end + +function irc.player_join(name) + if irc.joined_players[name] then + return false, "You are already in the channel" + end + irc.joined_players[name] = true + return true, "You joined the channel" +end + + +minetest.register_chatcommand("join", { + description = "Join the IRC channel", + privs = {shout=true}, + func = function(name) + return irc.player_join(name) + end +}) + +minetest.register_chatcommand("part", { + description = "Part the IRC channel", + privs = {shout=true}, + func = function(name) + return irc.player_part(name) + end +}) + +minetest.register_chatcommand("who", { + description = "Tell who is currently on the channel", + privs = {}, + func = function() + local out, n = { }, 0 + for plname in pairs(irc.joined_players) do + n = n + 1 + out[n] = plname + end + table.sort(out) + return true, "Players in channel: "..table.concat(out, ", ") + 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(irc.joined_players) do + minetest.chat_send_player(name, message) + end + irc.logChat(message) +end diff --git a/mods/irc/settingtypes.txt b/mods/irc/settingtypes.txt new file mode 100644 index 0000000..0814167 --- /dev/null +++ b/mods/irc/settingtypes.txt @@ -0,0 +1,75 @@ + +[Basic] + +# Whether to automatically connect to the server on mod load. +# If false, you must use /irc_connect to connect. +irc.auto_connect (Auto-connect on load) bool true + +# Nickname for the bot. May only contain characters A-Z, 0-9 +# '{', '}', '[', ']', '|', '^', '-', or '_'. +irc.nick (Bot nickname) string Minetest + +# Server to connect to. +irc.server (IRC server) string irc.freenode.net + +# Server port. +# The standard IRC protocol port is 6667 for regular servers, +# or 6697 for SSL-enabled servers. +# If unsure, leave at 6667. +irc.port (IRC server port) int 6667 1 65535 + +# Channel the bot joins after connecting. +irc.channel (Channel to join) string ##mt-irc-mod + +[Authentication] + +# Password for authenticating to NickServ. +# Leave empty to not authenticate with NickServ. +irc.NSPass (NickServ password) string + +# IRC server password. +# Leave empty for no password. +irc.password (Server password) string + +# Password for joining the channel. +# Leave empty if your channel is not protected. +irc.key (Channel key) string + +# Enable TLS connection. +# Requires LuaSEC . +irc.secure (Use TLS) bool false + +# Username for SASL authentication. +# Leave empty to use the nickname. +irc.sasl.user (SASL username) string + +# Password for SASL authentication. +# Leave empty to not authenticate via SASL. +irc.sasl.pass (SASL password) string + +[Advanced] + +# Enable this to make the bot send messages when players join +# or leave the game server. +irc.send_join_part (Send join and part messages) bool true + +# Enable this to make the bot send messages when players are kicked. +irc.send_kicks (Send kick messages) bool false + +# Underlying socket timeout in seconds. +irc.timeout (Timeout) int 60 1 + +# Time between reconnection attempts, in seconds. +irc.reconnect (Reconnect delay) int 600 1 + +# Prefix to use for bot commands. +irc.command_prefix (Command prefix) string + +# Enable debug output. +irc.debug (Debug mode) bool false + +# Whether to enable players joining and parting the channel. +irc.enable_player_part (Allow player join/part) bool true + +# Whether to automatically show players in the channel when they join. +irc.auto_join (Auto join players) bool true diff --git a/mods/irc_commands/depends.txt b/mods/irc_commands/depends.txt new file mode 100644 index 0000000..3661ef9 --- /dev/null +++ b/mods/irc_commands/depends.txt @@ -0,0 +1 @@ +irc diff --git a/mods/irc_commands/init.lua b/mods/irc_commands/init.lua new file mode 100644 index 0000000..5aef834 --- /dev/null +++ b/mods/irc_commands/init.lua @@ -0,0 +1,140 @@ + +local irc_users = {} + +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 + +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) + +irc:register_bot_command("login", { + params = " ", + 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 channel = irc.conn.channels[irc.config.channel] + if not channel then + return false, "The server needs to be in its ".. + "channel for anyone to log in." + end + for cnick, cuser in pairs(channel.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 log in." + end + local handler = minetest.get_auth_handler() + local auth = handler.get_auth(playerName) + if auth and minetest.check_password_entry(playerName, auth.password, password) then + minetest.log("action", "User "..user.nick + .." from IRC logs in as "..playerName) + irc_users[user.nick] = playerName + handler.record_login(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 = "", + 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 +}) +