commit 9fae29e3b91be9ce07f01a42761ab52788d0b500 Author: LeMagnesium Date: Thu Sep 17 20:51:25 2015 +0200 Initial Commit diff --git a/irc/.gitignore b/irc/.gitignore new file mode 100755 index 0000000..5236e1e --- /dev/null +++ b/irc/.gitignore @@ -0,0 +1,2 @@ +*~ + diff --git a/irc/.gitmodules b/irc/.gitmodules new file mode 100755 index 0000000..e56fee3 --- /dev/null +++ b/irc/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/LuaIRC"] + path = irc + url = https://github.com/ShadowNinja/LuaIRC.git diff --git a/irc/API.md b/irc/API.md new file mode 100755 index 0000000..e518f22 --- /dev/null +++ b/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/irc/LICENSE.txt b/irc/LICENSE.txt new file mode 100644 index 0000000..b184032 --- /dev/null +++ b/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/irc/README.md b/irc/README.md new file mode 100755 index 0000000..414b6e0 --- /dev/null +++ b/irc/README.md @@ -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 && git clone https://github.com/kaeza/minetest-irc.git irc && cd irc && git submodule update --init + +Please change `` 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 ` + 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 ` 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 + +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. + diff --git a/irc/botcmds.lua b/irc/botcmds.lua new file mode 100644 index 0000000..34978a8 --- /dev/null +++ b/irc/botcmds.lua @@ -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 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 = "", + 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 "", + cmd.description or "") + 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 ' to get" + .." help about a specific command." + end +}) + + +irc:register_bot_command("whereis", { + params = "", + description = "Tell the location of ", + 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 +}) + diff --git a/irc/callback.lua b/irc/callback.lua new file mode 100644 index 0000000..0356f91 --- /dev/null +++ b/irc/callback.lua @@ -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) + diff --git a/irc/chatcmds.lua b/irc/chatcmds.lua new file mode 100644 index 0000000..b8ac466 --- /dev/null +++ b/irc/chatcmds.lua @@ -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 = " ", + 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 = "", + 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 + diff --git a/irc/config.lua b/irc/config.lua new file mode 100644 index 0000000..8dfd381 --- /dev/null +++ b/irc/config.lua @@ -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-", 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 + diff --git a/irc/hooks.lua b/irc/hooks.lua new file mode 100644 index 0000000..6c8e243 --- /dev/null +++ b/irc/hooks.lua @@ -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(" "..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(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) + diff --git a/irc/init.lua b/irc/init.lua new file mode 100644 index 0000000..02f7996 --- /dev/null +++ b/irc/init.lua @@ -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 + diff --git a/irc/irc/.gitignore b/irc/irc/.gitignore new file mode 100755 index 0000000..3ad5d87 --- /dev/null +++ b/irc/irc/.gitignore @@ -0,0 +1,3 @@ +/gh-pages/ +*~ + diff --git a/irc/irc/LICENSE.txt b/irc/irc/LICENSE.txt new file mode 100644 index 0000000..0a68ca1 --- /dev/null +++ b/irc/irc/LICENSE.txt @@ -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.]] + diff --git a/irc/irc/README.markdown b/irc/irc/README.markdown new file mode 100644 index 0000000..d7c7714 --- /dev/null +++ b/irc/irc/README.markdown @@ -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). + diff --git a/irc/irc/asyncoperations.lua b/irc/irc/asyncoperations.lua new file mode 100644 index 0000000..127d170 --- /dev/null +++ b/irc/irc/asyncoperations.lua @@ -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 + diff --git a/irc/irc/doc/irc.luadoc b/irc/irc/doc/irc.luadoc new file mode 100755 index 0000000..7bf638e --- /dev/null +++ b/irc/irc/doc/irc.luadoc @@ -0,0 +1,184 @@ +--- LuaIRC is a low-level IRC library for Lua. +-- All functions raise Lua exceptions on error. +-- +-- Use new to create a new IRC object.
+-- Example:

+-- +--require "irc"
+--local sleep = require "socket".sleep
+--
+--local s = irc.new{nick = "example"}
+--
+--s:hook("OnChat", function(user, channel, message)
+-- print(("[%s] %s: %s"):format(channel, user.nick, message))
+--end)
+--
+--s:connect("irc.example.net")
+--s:join("#example")
+--
+--while true do
+-- s:think()
+-- sleep(0.5)
+--end
+--
+ +module "irc" + +--- Create a new IRC object. Use irc:connect to connect to a server. +-- @param user Table with fields nick, username and realname. +-- The nick field is required. +-- +-- @return Returns a new irc 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 id] +-- @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 irc 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 irc from the server. +-- @param message Quit message. +function irc:disconnect(message) + +--- Handle incoming data for irc, 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 userinfo, node, channels and account. +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 msg, with string.format 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 target, nick, add and/or rem. target or nick +-- specifies the user or channel to add/remove modes. add is a list of modes to add to the user or channel. +-- rem is a list of modes to remove from the user or channel. +-- @usage Example which sets +m (moderated) for #channel:
+-- irc:setMode{target = "#channel", add = "m"} +function irc:setMode(t) + +--internal +function irc:invoke(name, ...) +function irc:handle(msg) +function irc:shutdown() + +--- Table with connection information. +--
    +--
  • host - Server host name.
  • +--
  • port - Server port. [defaults to 6667]
  • +--
  • timeout - Connect timeout. [defaults to 30]
  • +--
  • password - Server password.
  • +--
  • secure - Boolean to enable TLS connection, pass a params table (described, [luasec]) to control
  • +--
+-- [luasec]: http://www.inf.puc-rio.br/~brunoos/luasec/reference.html +-- @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: +--
    +--
  • toRFC1459() - Returns the message serialized in RFC 1459 format.
  • +--
+ +--- List of hooks you can use with irc:hook. +-- The parameter list describes the parameters passed to the callback function. +--
    +--
  • PreRegister(connection)Useful for CAP commands and SASL.
  • +--
  • OnRaw(line) - (any non false/nil return value assumes line handled and will not be further processed)
  • +--
  • OnSend(line)
  • +--
  • OnDisconnect(message, errorOccurred)
  • +--
  • OnChat(user, channel, message)
  • +--
  • OnNotice(user, channel, message)
  • +--
  • OnJoin(user, channel)*
  • +--
  • OnPart(user, channel)*
  • +--
  • OnQuit(user, message)
  • +--
  • NickChange(user, newnick, channel)*†
  • +--
  • NameList(channel, names)
  • +--
  • OnTopic(channel, topic)
  • +--
  • OnTopicInfo(channel, creator, timeCreated)
  • +--
  • OnKick(channel, nick, kicker, reason)* (kicker is a user table)
  • +--
  • OnUserMode(modes)
  • +--
  • OnChannelMode(user, channel, modes)
  • +--
  • OnModeChange(user, target, modes, ...)* ('...' contains mode options such as banmasks)
  • +--
  • DoX(msg)'X' is any IRC command or numeric with the first letter capitalized (eg, DoPing and Do001)
  • +--
+-- * Event also invoked for yourself. +-- † Channel passed only when user tracking is enabled +-- @name Hooks +-- @class table + +--- Table with information about a user. +--
    +--
  • server - Server name.
  • +--
  • nick - User nickname.
  • +--
  • username - User username.
  • +--
  • host - User hostname.
  • +--
  • realname - User real name.
  • +--
  • access - User access, available in channel-oriented callbacks. A table containing boolean fields for each access mode that the server supports. Eg: 'o', and 'v'.
  • +--
+-- Fields may be missing. To fill them in, enable user tracking and use irc:whois. +-- @name User +-- @class table + diff --git a/irc/irc/handlers.lua b/irc/irc/handlers.lua new file mode 100644 index 0000000..1ea83e2 --- /dev/null +++ b/irc/irc/handlers.lua @@ -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 + diff --git a/irc/irc/init.lua b/irc/irc/init.lua new file mode 100644 index 0000000..e2eb71e --- /dev/null +++ b/irc/irc/init.lua @@ -0,0 +1,9 @@ + +local irc = require("irc.main") +require("irc.util") +require("irc.asyncoperations") +require("irc.handlers") +require("irc.messages") + +return irc + diff --git a/irc/irc/main.lua b/irc/irc/main.lua new file mode 100644 index 0000000..c3a7c8f --- /dev/null +++ b/irc/irc/main.lua @@ -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 + diff --git a/irc/irc/messages.lua b/irc/irc/messages.lua new file mode 100644 index 0000000..2f753ed --- /dev/null +++ b/irc/irc/messages.lua @@ -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 + diff --git a/irc/irc/push-luadoc.sh b/irc/irc/push-luadoc.sh new file mode 100644 index 0000000..6f77ff2 --- /dev/null +++ b/irc/irc/push-luadoc.sh @@ -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 diff --git a/irc/irc/set.lua b/irc/irc/set.lua new file mode 100644 index 0000000..feec00d --- /dev/null +++ b/irc/irc/set.lua @@ -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 + diff --git a/irc/irc/util.lua b/irc/irc/util.lua new file mode 100644 index 0000000..92bb76d --- /dev/null +++ b/irc/irc/util.lua @@ -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 + diff --git a/irc/messages.lua b/irc/messages.lua new file mode 100644 index 0000000..45ef4c4 --- /dev/null +++ b/irc/messages.lua @@ -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 + diff --git a/irc/player_part.lua b/irc/player_part.lua new file mode 100644 index 0000000..d39d2ab --- /dev/null +++ b/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 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 + diff --git a/irc_commands/.gitignore b/irc_commands/.gitignore new file mode 100755 index 0000000..b25c15b --- /dev/null +++ b/irc_commands/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/irc_commands/depends.txt b/irc_commands/depends.txt new file mode 100755 index 0000000..3661ef9 --- /dev/null +++ b/irc_commands/depends.txt @@ -0,0 +1 @@ +irc diff --git a/irc_commands/init.lua b/irc_commands/init.lua new file mode 100755 index 0000000..27da796 --- /dev/null +++ b/irc_commands/init.lua @@ -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 = " ", + 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 = " ", + 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 = "", + 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 +}) diff --git a/modpack.txt b/modpack.txt new file mode 100644 index 0000000..ecc2036 --- /dev/null +++ b/modpack.txt @@ -0,0 +1 @@ +This is a modpack