diff --git a/irc/.luacheckrc b/irc/.luacheckrc new file mode 100644 index 0000000..7453628 --- /dev/null +++ b/irc/.luacheckrc @@ -0,0 +1,14 @@ + +allow_defined_top = true + +read_globals = { + "minetest", +} + +exclude_files = { + "irc/*", +} + +globals = { + "irc", +} diff --git a/irc/API.md b/irc/API.md index e518f22..5de2c04 100755 --- a/irc/API.md +++ b/irc/API.md @@ -13,14 +13,14 @@ to your mod's `depends.txt` file. Reference --------- -irc:say([name,] message) +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.say("Hello, Channel!") + irc.say("john1234", "How are you?") -irc:register_bot_command(name, cmdDef) +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. @@ -38,7 +38,7 @@ irc:register_bot_command(name, cmdDef) end, }; Example: - irc:register_bot_command("hello", { + irc.register_bot_command("hello", { params = "", description = "Greet user", func = function(user, param) @@ -55,12 +55,12 @@ irc.joined_players[name] -- Joe is talking on IRC end -irc:register_hook(name, func) +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) + irc.register_hook("OnSend", function(line) print("SEND: "..line) end) @@ -83,7 +83,7 @@ not modify these settings at runtime or you might crash the server! Hooks ----- -The `irc:register_hook` function can register functions to be called +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/README.md b/irc/README.md index 414b6e0..99e0dcc 100755 --- a/irc/README.md +++ b/irc/README.md @@ -1,122 +1,138 @@ + 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 +The forum topic is [here][forum]. + +[forum]: https://forum.minetest.net/viewtopic.php?f=11&t=3905 + - Installing ---------- -Quick one line install for linux: +Quick one line install for Linux: - cd && git clone https://github.com/kaeza/minetest-irc.git irc && cd irc && git submodule update --init + 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](http://wiki.minetest.net/Installing_mods). +Please change `` to fit your installation of Minetest. +For more information, see [the wiki][wiki]. -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 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`. -The Minetest IRC mod also requires LuaSocket. This can be installed using your -package manager on many distributions, for example on Arch Linux: +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, default "irc.freenode.net") - This is the IRC server the mod connects to. +* `irc.server` (string): + The address of the IRC server to connect to. - * `irc.channel` (string, default "##mt-irc-mod") - The IRC channel to join. +* `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.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.nick` (string): + Nickname the server uses when it connects to IRC. - * `irc.password` (string, default "") - Password to use when connecting to the server. +* `irc.password` (string, default nil): + 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.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.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.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.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_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.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. - * `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. +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 ` - Sends a private message to a IRC user. +* `/irc_msg `: + Send a private message to a IRC user. - * `/join` - Join the IRC chat. +* `/join`: + Join the IRC chat. - * `/part` - Part the IRC chat. +* `/part`: + Part the IRC chat. - * `/irc_connect` - Connect the bot manually to the IRC network. +* `/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_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`. +* `/irc_reconnect`: + Equivalent 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: +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 @@ -125,21 +141,29 @@ 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 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`. -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. +* `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: +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, @@ -151,10 +175,7 @@ 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. +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/irc/botcmds.lua b/irc/botcmds.lua index 8ba4c6b..5d78917 100644 --- a/irc/botcmds.lua +++ b/irc/botcmds.lua @@ -1,40 +1,59 @@ -irc.whereis_timer = {} -irc.whereis_timer_max_limit = 120 + irc.bot_commands = {} -function irc:check_botcmd(msg) +-- 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:lower() + local nick = irc.conn.nick local text = msg.args[2] - local nickpart = text:sub(1, #nick + 2):lower() + local nickpart = text:sub(1, #nick) + local suffix = text:sub(#nick+1, #nick+2) -- First check for a nick prefix - if nickpart == nick..": " or - nickpart == nick..", " then - self:bot_command(msg, text:sub(#nick + 3)) + 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 - self:bot_command(msg, text:sub(#prefix + 1)) + irc.bot_command(msg, text:sub(#prefix + 1)) return true end return false end -function irc:bot_command(msg, text) +function irc.bot_command(msg, text) + -- Remove leading whitespace + text = text:match("^%s*(.*)") 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.") + 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.") + 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!") + minetest.colorize(irc.config.pm_color, + "PM from "..msg.user.nick.."@IRC: "..message, false)) + irc.reply("Message sent!") return end local pos = text:find(" ", 1, true) @@ -47,35 +66,41 @@ function irc:bot_command(msg, text) args = "" end - if not self.bot_commands[cmd] then - self:reply("Unknown command '"..cmd.."'. Try 'list'." + 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 success, message = self.bot_commands[cmd].func(msg.user, args) + local _, message = irc.bot_commands[cmd].func(msg.user, args) if message then - self:reply(message) + irc.reply(message) end end -function irc:register_bot_command(name, def) +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 + irc.bot_commands[name] = def end -irc:register_bot_command("help", { +irc.register_bot_command("help", { params = "", description = "Get help about a command", - func = function(user, args) + func = function(_, args) if args == "" then - return false, "No command name specified. Use 'list' for a list of commands." + 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] @@ -84,7 +109,7 @@ irc:register_bot_command("help", { end return true, ("Usage: %s%s %s -- %s"):format( - irc.config.command_prefix, + irc.config.command_prefix or "", args, cmd.params or "", cmd.description or "") @@ -92,24 +117,20 @@ irc:register_bot_command("help", { }) -irc:register_bot_command("list", { +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." + 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", { +irc.register_bot_command("whereis", { params = "", description = "Tell the location of ", - func = function(user, args) + func = function(_, args) if args == "" then return false, "Player name required." end @@ -117,27 +138,17 @@ irc:register_bot_command("whereis", { 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") + local pos = player:get_pos() return true, fmt:format(args, pos.x, pos.y, pos.z) end }) local starttime = os.time() -irc:register_bot_command("uptime", { +irc.register_bot_command("uptime", { description = "Tell how much time the server has been up", - func = function(user, args) + func = function() local cur_time = os.time() local diff = os.difftime(cur_time, starttime) local fmt = "Server has been running for %d:%02d:%02d" @@ -150,9 +161,9 @@ irc:register_bot_command("uptime", { }) -irc:register_bot_command("players", { +irc.register_bot_command("players", { description = "List the players on the server", - func = function(user, args) + func = function() local players = minetest.get_connected_players() local names = {} for _, player in pairs(players) do diff --git a/irc/callback.lua b/irc/callback.lua index 0356f91..29667ba 100644 --- a/irc/callback.lua +++ b/irc/callback.lua @@ -5,15 +5,16 @@ 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") + irc.say("*** "..name.." joined the game") end end) -minetest.register_on_leaveplayer(function(player) +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") + irc.say("*** "..name.." left the game".. + (timed_out and " (Timed out)" or "")) end end) @@ -30,11 +31,11 @@ minetest.register_on_chat_message(function(name, message) if nl then message = message:sub(1, nl - 1) end - irc:say(irc:playerMessage(name, message)) + irc.say(irc.playerMessage(name, core.strip_colors(message))) end) minetest.register_on_shutdown(function() - irc:disconnect("Game shutting down.") + irc.disconnect("Game shutting down.") end) diff --git a/irc/chatcmds.lua b/irc/chatcmds.lua index 7a56264..34283d7 100644 --- a/irc/chatcmds.lua +++ b/irc/chatcmds.lua @@ -11,32 +11,30 @@ minetest.register_chatcommand("irc_msg", { 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 + return false, "Not connected to IRC. Use /irc_connect to connect." 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 + return false, "Invalid usage, see /help irc_msg." end local toname_l = toname:lower() local validNick = false - for nick, user in pairs(irc.conn.channels[irc.config.channel].users) do + 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 - minetest.chat_send_player(name, - "You can not message that user. (Hint: They have to be in the channel)") - return + return false, "You can not message that user. ("..hint..")" end - irc:say(toname, irc:playerMessage(name, message)) - minetest.chat_send_player(name, "Message sent!") + irc.say(toname, irc.playerMessage(name, message)) + return true, "Message sent!" end }) @@ -44,16 +42,15 @@ minetest.register_chatcommand("irc_msg", { minetest.register_chatcommand("irc_names", { params = "", description = "List the users in IRC.", - func = function(name, param) + func = function() if not irc.connected then - minetest.chat_send_player(name, "Not connected to IRC. Use /irc_connect to connect.") - return + return false, "Not connected to IRC. Use /irc_connect to connect." end local users = { } - for k, v in pairs(irc.conn.channels[irc.config.channel].users) do - table.insert(users, k) + for nick in pairs(irc.conn.channels[irc.config.channel].users) do + table.insert(users, nick) end - minetest.chat_send_player(name, "Users in IRC: "..table.concat(users, ", ")) + return true, "Users in IRC: "..table.concat(users, ", ") end }) @@ -61,13 +58,12 @@ minetest.register_chatcommand("irc_names", { minetest.register_chatcommand("irc_connect", { description = "Connect to the IRC server.", privs = {irc_admin=true}, - func = function(name, param) + func = function(name) if irc.connected then - minetest.chat_send_player(name, "You are already connected to IRC.") - return + return false, "You are already connected to IRC." end minetest.chat_send_player(name, "IRC: Connecting...") - irc:connect() + irc.connect() end }) @@ -78,13 +74,12 @@ minetest.register_chatcommand("irc_disconnect", { 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 + return false, "Not connected to IRC. Use /irc_connect to connect." end if param == "" then param = "Manual disconnect by "..name end - irc:disconnect(param) + irc.disconnect(param) end }) @@ -92,13 +87,13 @@ minetest.register_chatcommand("irc_disconnect", { minetest.register_chatcommand("irc_reconnect", { description = "Reconnect to the IRC server.", privs = {irc_admin=true}, - func = function(name, param) + func = function(name) if not irc.connected then - minetest.chat_send_player(name, "You are not connected to IRC.") - return + return false, "Not connected to IRC. Use /irc_connect to connect." end - irc:disconnect("Reconnecting...") - irc:connect() + minetest.chat_send_player(name, "IRC: Reconnecting...") + irc.disconnect("Reconnecting...") + irc.connect() end }) @@ -109,34 +104,31 @@ minetest.register_chatcommand("irc_quote", { 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 + return false, "Not connected to IRC. Use /irc_connect to connect." end - irc:queue(param) + 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, ...) - oldme(name, param, ...) - irc:say(("* %s %s"):format(name, param)) + irc.say(("* %s %s"):format(name, param)) + return oldme(name, param, ...) end -minetest.register_chatcommand("irc_change_server", { - params = "", - description = "Change the IRC server.", - privs = {irc_admin=true}, - func = function(name, param) - if param == "" then - minetest.chat_send_player(name, "Missing argument servername") - return +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.config.server = param - irc.save_config() - minetest.chat_send_player(name, "New server IRC is \"".. param.."\".") - minetest.chat_send_player(name, "type \"/irc_reconnect\" to reconnect IRC.") + irc.say(("*** Kicked %s.%s"):format(name, + reason~="" and " Reason: "..reason or "")) + return oldkick(name, param, ...) end -}) - +end diff --git a/irc/config.lua b/irc/config.lua index 83dc09a..2f72066 100644 --- a/irc/config.lua +++ b/irc/config.lua @@ -4,16 +4,22 @@ irc.config = {} -local function setting(stype, name, default) +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)) + if minetest.settings and minetest.settings.get and minetest.settings.get_bool then + if stype == "bool" then + value = minetest.settings:get_bool("irc."..name) + elseif stype == "string" then + value = minetest.settings:get("irc."..name) + elseif stype == "number" then + value = tonumber(minetest.settings:get("irc."..name)) + end end if value == nil then + if required then + error("Required configuration option irc.".. + name.." missing.") + end value = default end irc.config[name] = value @@ -23,15 +29,18 @@ 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", "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", "##mt-irc-mod") -- Channel to join +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 -- @@ -40,50 +49,11 @@ setting("bool", "send_join_part", true) -- Whether to send player join and par 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("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 - --- 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 - - -local config_file = minetest.get_worldpath() .. "/irc.txt" - -function irc.save_config() - local input, err = io.open(config_file, "w") - if input then - local conf = {server = irc.config.server} - input:write(minetest.serialize(conf)) - input:close() - else - minetest.log("error", "open(" .. config_file .. ", 'w') failed: " .. err) - end -end - -function irc.load_config() - local file = io.open(config_file, "r") - local settings = {} - if file then - settings = minetest.deserialize(file:read("*all")) - file:close() - if settings and type(settings) == "table" then - if settings["server"] ~= nil then - irc.config.server = settings["server"] - end - end - end -end - -irc.load_config() - +setting("string", "chat_color", "#339933") -- Color of IRC chat in-game, green by default +setting("string", "pm_color", "#8800AA") -- Color of IRC PMs in-game, purple by default diff --git a/irc/hooks.lua b/irc/hooks.lua index 61acd44..19a85ae 100644 --- a/irc/hooks.lua +++ b/irc/hooks.lua @@ -1,30 +1,27 @@ -- 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 = require("mime").b64 +local b64e = ie.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 stripped_chars = "[\2\31]" local function normalize(text) -- Strip colors text = text:gsub("\3[0-9][0-9,]*", "") - return text --:gsub(stripped_chars, "") + return text:gsub(stripped_chars, "") end -function irc:doHook(conn) - for name, hook in pairs(self.registered_hooks) do +function irc.doHook(conn) + for name, hook in pairs(irc.registered_hooks) do for _, func in pairs(hook) do conn:hook(name, func) end @@ -32,9 +29,9 @@ function irc:doHook(conn) end -function irc:register_hook(name, func) - self.registered_hooks[name] = self.registered_hooks[name] or {} - table.insert(self.registered_hooks[name], func) +function irc.register_hook(name, func) + irc.registered_hooks[name] = irc.registered_hooks[name] or {} + table.insert(irc.registered_hooks[name], func) end @@ -83,13 +80,13 @@ function irc.hooks.ctcp(msg) local command = args[1]:upper() local function reply(s) - irc:queue(irc.msgs.notice(msg.user.nick, + 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)) + 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)) @@ -104,11 +101,12 @@ end function irc.hooks.channelChat(msg) local text = normalize(msg.args[2]) - irc:check_botcmd(msg) + 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) + local fake = msg.user.nick:lower():match("^[il|]rc$") + if fake then + irc.sendLocal("<"..msg.user.nick.."@IRC> "..text) return end @@ -122,25 +120,30 @@ function irc.hooks.channelChat(msg) text:find("^%*%*%* ([^%s]+) joined the game$") local foundleave, _, leavenick = text:find("^%*%*%* ([^%s]+) left the game$") + local foundtimedout, _, timedoutnick = + text:find("^%*%*%* ([^%s]+) left the game %(Timed out%)$") local foundaction, _, actionnick, actionmessage = text:find("^%* ([^%s]+) (.*)$") if text:sub(1, 5) == "[off]" then return elseif foundchat then - irc:sendLocal(("<%s@%s> %s") + irc.sendLocal(("<%s@%s> %s") :format(chatnick, msg.user.nick, chatmessage)) elseif foundjoin then - irc:sendLocal(("*** %s joined %s") + irc.sendLocal(("*** %s joined %s") :format(joinnick, msg.user.nick)) elseif foundleave then - irc:sendLocal(("*** %s left %s") + irc.sendLocal(("*** %s left %s") :format(leavenick, msg.user.nick)) + elseif foundtimedout then + irc.sendLocal(("*** %s left %s (Timed out)") + :format(timedoutnick, msg.user.nick)) elseif foundaction then - irc:sendLocal(("* %s@%s %s") + irc.sendLocal(("* %s@%s %s") :format(actionnick, msg.user.nick, actionmessage)) else - irc:sendLocal(("<%s@IRC> %s"):format(msg.user.nick, text)) + irc.sendLocal(("<%s@IRC> %s"):format(msg.user.nick, text)) end end @@ -152,16 +155,16 @@ function irc.hooks.pm(msg) if prefix and text:sub(1, #prefix) == prefix then text = text:sub(#prefix + 1) end - irc:bot_command(msg, text) + 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") + irc.disconnect("Kicked") else - irc:sendLocal(("-!- %s was kicked from %s by %s [%s]") + irc.sendLocal(("-!- %s was kicked from %s by %s [%s]") :format(target, channel, prefix.nick, reason)) end end @@ -169,7 +172,7 @@ end function irc.hooks.notice(user, target, message) if user.nick and target == irc.config.channel then - irc:sendLocal("-"..user.nick.."@IRC- "..message) + irc.sendLocal("-"..user.nick.."@IRC- "..message) end end @@ -190,31 +193,31 @@ end function irc.hooks.nick(user, newNick) - irc:sendLocal(("-!- %s is now known as %s") + irc.sendLocal(("-!- %s is now known as %s") :format(user.nick, newNick)) end function irc.hooks.join(user, channel) - irc:sendLocal(("-!- %s joined %s") + 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]") + 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]") + irc.sendLocal(("-!- %s has quit [%s]") :format(user.nick, reason)) end -function irc.hooks.disconnect(message, isError) +function irc.hooks.disconnect(_, isError) irc.connected = false if isError then minetest.log("error", "IRC: Error: Disconnected, reconnecting in one minute.") @@ -238,23 +241,23 @@ function irc.hooks.preregister(conn) conn:send("CAP REQ sasl") conn:send("AUTHENTICATE PLAIN") conn:send("AUTHENTICATE "..authString) - --LuaIRC will send CAP END + 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) +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 index 02f7996..2dfaa7d 100644 --- a/irc/init.lua +++ b/irc/init.lua @@ -3,25 +3,49 @@ local modpath = minetest.get_modpath(minetest.get_current_modname()) -package.path = +-- 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;" - ..package.path + ..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 - package.path = package.path.. +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" - package.cpath = package.cpath.. - -- ";/usr/lib/lua/5.1/?.so" - ";/usr/lib/x86_64-linux-gnu/lua/5.1/?.so" + + ie.package.cpath = ie.package.cpath.. + ";/usr/lib/lua/5.1/?.so" + + ie.package.cpath = "/usr/lib/x86_64-linux-gnu/lua/5.1/?.so;"..ie.package.cpath + + 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, @@ -30,45 +54,82 @@ irc = { recent_message_count = 0, joined_players = {}, modpath = modpath, - lib = require("irc"), + lib = lib, } -- Compatibility -mt_irc = irc +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") -dofile(modpath.."/hooks.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(index) return true end}) + 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 + give_to_singleplayer = true, + give_to_admin = true, }) local stepnum = 0 -minetest.register_globalstep(function(dtime) return irc:step(dtime) end) +minetest.register_globalstep(function(dtime) return irc.step(dtime) end) -function irc:step(dtime) +function irc.step() if stepnum == 3 then - if self.config.auto_connect then - self:connect() + if irc.config.auto_connect then + irc.connect() end end stepnum = stepnum + 1 - if not self.connected then return end + if not irc.connected then return end -- Hooks will manage incoming messages and errors - local good, err = xpcall(function() self.conn:think() end, debug.traceback) + local good, err = xpcall(function() irc.conn:think() end, debug.traceback) if not good then print(err) return @@ -76,79 +137,89 @@ function irc:step(dtime) end -function irc:connect() - if self.connected then +function irc.connect() + if irc.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", + irc.conn = irc.lib.new({ + nick = irc.config.nick, + username = irc.config.username, + realname = irc.config.realname, }) - self:doHook(self.conn) + 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() - self.conn:connect({ - host = self.config.server, - port = self.config.port, - password = self.config.password, - timeout = self.config.timeout, - secure = self.config.secure + 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 ten minutes...") - :format(self.config.server, message)) - minetest.after(600, function() self:connect() end) + 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 self.config.NSPass then - self:say("NickServ", "IDENTIFY "..self.config.NSPass) + if irc.config.NSPass then + irc.conn:queue(irc.msgs.privmsg( + "NickServ", "IDENTIFY "..irc.config.NSPass)) end - self.conn:join(self.config.channel, self.config.key) - self.connected = true + 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 self.connected then - --The OnDisconnect hook will clear self.connected and print a disconnect message - self.conn:disconnect(message) +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) +function irc.say(to, message) if not message then message = to - to = self.config.channel + to = irc.config.channel end - to = to or self.config.channel + to = to or irc.config.channel - self:queue(irc.msgs.privmsg(to, message)) + irc.queue(irc.msgs.privmsg(to, message)) end -function irc:reply(message) - if not self.last_from then +function irc.reply(message) + if not irc.last_from then return end message = message:gsub("[\r\n%z]", " \\n ") - self:say(self.last_from, message) + irc.say(irc.last_from, message) end -function irc:send(msg) - if not self.connected then return end - self.conn:send(msg) +function irc.send(msg) + if not irc.connected then return end + irc.conn:send(msg) end -function irc:queue(msg) - if not self.connected then return end - self.conn:queue(msg) +function irc.queue(msg) + if not irc.connected then return end + irc.conn:queue(msg) end diff --git a/irc/irc/.travis.yml b/irc/irc/.travis.yml new file mode 100644 index 0000000..f072070 --- /dev/null +++ b/irc/irc/.travis.yml @@ -0,0 +1,15 @@ +language: lua + +notifications: + email: false + +env: + global: + - secure: "kFhU+DZjhq/KbDt0DIDWnlskXMa12miNelmhhy30fQGgVIdiibDGKMNGyLahWp8CnPu1DARb5AZWK2TDfARdnURT2pgcsG83M7bYIY6cR647BWjL7oAhJ6CYEzTWJTBjeUjpN/o4vIgfXSDR0c7vboDi7Xz8ilfrBujPL2Oi/og=" + +install: + - sudo apt-get -y update + - sudo apt-get -y install lua5.1 luadoc + +script: + - ./push-luadoc.sh diff --git a/irc/irc/README.markdown b/irc/irc/README.markdown index d7c7714..8cff1d2 100644 --- a/irc/irc/README.markdown +++ b/irc/irc/README.markdown @@ -1,7 +1,8 @@ +[![Build Status](https://travis-ci.org/JakobOvrum/LuaIRC.svg?branch=master)](https://travis-ci.org/JakobOvrum/LuaIRC) LuaIRC ============ -IRC library for Lua. +IRC client library for Lua. Dependencies ------------- diff --git a/irc/irc/asyncoperations.lua b/irc/irc/asyncoperations.lua index 127d170..7531b28 100644 --- a/irc/irc/asyncoperations.lua +++ b/irc/irc/asyncoperations.lua @@ -1,6 +1,6 @@ -local irc = require("irc.main") +local msgs = require("irc.messages") -local meta = irc.meta +local meta = {} function meta:send(msg, ...) if type(msg) == "table" then @@ -36,26 +36,26 @@ 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)) + self:queue(msgs.privmsg(verify(target, 3), line)) end end function meta:sendNotice(target, msg) -- Split the message into segments if it includes newlines. for line in msg:gmatch("([^\r\n]+)") do - self:queue(irc.msgs.notice(verify(target, 3), line)) + self:queue(msgs.notice(verify(target, 3), line)) end end function meta:join(channel, key) - self:queue(irc.msgs.join( + self:queue(msgs.join( verify(channel, 3), key and verify(key, 3) or nil)) end function meta:part(channel, reason) channel = verify(channel, 3) - self:queue(irc.msgs.part(channel, reason)) + self:queue(msgs.part(channel, reason)) if self.track_users then self.channels[channel] = nil end @@ -85,6 +85,8 @@ function meta:setMode(t) mode = table.concat{mode, "-", verify(rem, 3)} end - self:queue(irc.msgs.mode(verify(target, 3), mode)) + self:queue(msgs.mode(verify(target, 3), mode)) end +return meta + diff --git a/irc/irc/doc/irc.luadoc b/irc/irc/doc/irc.luadoc index 7bf638e..c33a0b1 100755 --- a/irc/irc/doc/irc.luadoc +++ b/irc/irc/doc/irc.luadoc @@ -1,7 +1,7 @@ --- LuaIRC is a low-level IRC library for Lua. -- All functions raise Lua exceptions on error. -- --- Use new to create a new IRC object.
+-- Use new to create a new Connection object.
-- Example:

-- --require "irc"
@@ -24,11 +24,12 @@ module "irc" ---- Create a new IRC object. Use irc:connect to connect to a server. +--- Create a new Connection 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. +-- @return Returns a new Connection object. +-- @see Connection function new(user) --- Hook a function to an event. @@ -46,10 +47,10 @@ 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) +function irc:connect(host, port) -- @param table Table of connection details --- @see Connection +-- @see ConnectOptions function irc:connect(table) --- Disconnect irc from the server. @@ -116,16 +117,26 @@ 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
  • ---
+-- @name ConnectOptions +-- @class table +-- @field host Server host name. +-- @field port Server port. [defaults to 6667] +-- @field timeout Connect timeout. [defaults to 30] +-- @field password Server password. +-- @field secure Boolean to enable TLS connection, pass a params table (described, [luasec]) to control -- [luasec]: http://www.inf.puc-rio.br/~brunoos/luasec/reference.html + +--- Class representing a connection. -- @name Connection -- @class table +-- @field authed Boolean indicating whether the connection has completed registration. +-- @field connected Whether the connection is currently connected. +-- @field motd The server's message of the day. Can be nil. +-- @field nick The current nickname. +-- @field realname The real name sent to the server. +-- @field username The username/ident sent to the server. +-- @field socket Raw socket used by the library. +-- @field supports What the server claims to support in it's ISUPPORT message. --- Class representing an IRC message. -- @name Message @@ -145,8 +156,8 @@ function irc:shutdown() --- 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)
  • +--
  • PreRegister() - Usefull for requesting capabilities.
  • +--
  • 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)
  • @@ -162,7 +173,10 @@ function irc:shutdown() --
  • 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)
  • +--
  • OnCapabilityList(caps)
  • +--
  • OnCapabilityAvailable(cap, value) Called only when a capability becomes available or changes.
  • +--
  • OnCapabilitySet(cap, enabled)*
  • +--
  • 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 diff --git a/irc/irc/handlers.lua b/irc/irc/handlers.lua index 1ea83e2..f01936f 100644 --- a/irc/irc/handlers.lua +++ b/irc/irc/handlers.lua @@ -1,10 +1,72 @@ -local irc = require("irc.main") +local util = require("irc.util") +local msgs = require("irc.messages") +local Message = msgs.Message -irc.handlers = {} -local handlers = irc.handlers +local handlers = {} handlers["PING"] = function(conn, msg) - conn:send(irc.Message({command="PONG", args=msg.args})) + conn:send(Message({command="PONG", args=msg.args})) +end + +local function requestWanted(conn, wanted) + local args = {} + for cap, value in pairs(wanted) do + if type(value) == "string" then + cap = cap .. "=" .. value + end + if not conn.capabilities[cap] then + table.insert(args, cap) + end + end + conn:queue(Message({ + command = "CAP", + args = {"REQ", table.concat(args, " ")} + }) + ) +end + +handlers["CAP"] = function(conn, msg) + local cmd = msg.args[2] + if not cmd then + return + end + if cmd == "LS" then + local list = msg.args[3] + local last = false + if list == "*" then + list = msg.args[4] + else + last = true + end + local avail = conn.availableCapabilities + local wanted = conn.wantedCapabilities + for item in list:gmatch("(%S+)") do + local eq = item:find("=", 1, true) + local k, v + if eq then + k, v = item:sub(1, eq - 1), item:sub(eq + 1) + else + k, v = item, true + end + if not avail[k] or avail[k] ~= v then + wanted[k] = conn:invoke("OnCapabilityAvailable", k, v) + end + avail[k] = v + end + if last then + if next(wanted) then + requestWanted(conn, wanted) + end + conn:invoke("OnCapabilityList", conn.availableCapabilities) + end + elseif cmd == "ACK" then + for item in msg.args[3]:gmatch("(%S+)") do + local enabled = (item:sub(1, 1) ~= "-") + local name = enabled and item or item:sub(2) + conn:invoke("OnCapabilitySet", name, enabled) + conn.capabilities[name] = enabled + end + end end handlers["001"] = function(conn, msg) @@ -16,7 +78,6 @@ 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 @@ -71,15 +132,13 @@ handlers["NICK"] = function(conn, msg) conn:invoke("NickChange", msg.user, newNick) end if msg.user.nick == conn.nick then - conn.nick = newnick + 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 + conn:queue(irc.msgs.nick(newnick)) end -- ERR_ERRONEUSNICKNAME (Misspelt but remains for historical reasons) @@ -88,6 +147,13 @@ handlers["432"] = needNewNick -- ERR_NICKNAMEINUSE handlers["433"] = needNewNick +-- ERR_UNAVAILRESOURCE +handlers["437"] = function(conn, msg) + if not conn.authed then + needNewNick(conn, msg) + end +end + -- RPL_ISUPPORT handlers["005"] = function(conn, msg) local arglen = #msg.args @@ -124,7 +190,7 @@ handlers["353"] = function(conn, msg) local users = conn.channels[channel].users for nick in names:gmatch("(%S+)") do - local access, name = irc.parseNick(conn, nick) + local access, name = util.parseNick(conn, nick) users[name] = {access = access} end end @@ -181,7 +247,7 @@ handlers["MODE"] = function(conn, msg) if conn.track_users and target ~= conn.nick then local add = true local argNum = 1 - irc.updatePrefixModes(conn) + util.updatePrefixModes(conn) for c in modes:gmatch(".") do if c == "+" then add = true elseif c == "-" then add = false @@ -207,3 +273,5 @@ handlers["ERROR"] = function(conn, msg) error(msg.args[1], 3) end +return handlers + diff --git a/irc/irc/init.lua b/irc/irc/init.lua index e2eb71e..194b692 100644 --- a/irc/irc/init.lua +++ b/irc/irc/init.lua @@ -1,9 +1,257 @@ +local socket = require("socket") +local util = require("irc.util") +local handlers = require("irc.handlers") +local msgs = require("irc.messages") +local Message = msgs.Message -local irc = require("irc.main") -require("irc.util") -require("irc.asyncoperations") -require("irc.handlers") -require("irc.messages") +local meta = {} +meta.__index = meta -return irc + +for k, v in pairs(require("irc.asyncoperations")) do + meta[k] = v +end + +local meta_preconnect = {} +function meta_preconnect.__index(o, k) + local v = rawget(meta_preconnect, k) + + if v == nil and meta[k] ~= nil then + error(("field '%s' is not accessible before connecting"):format(k), 2) + end + return v +end + +meta.connected = true +meta_preconnect.connected = false + +function new(data) + local o = { + nick = assert(data.nick, "Field 'nick' is required"); + username = data.username or "lua"; + realname = data.realname or "Lua owns"; + nickGenerator = data.nickGenerator or util.defaultNickGenerator; + hooks = {}; + track_users = true; + supports = {}; + messageQueue = {}; + lastThought = 0; + recentMessages = 0; + availableCapabilities = {}; + wantedCapabilities = {}; + capabilities = {}; + } + assert(util.checkNick(o.nick), "Erroneous nickname passed to irc.new") + return setmetatable(o, meta_preconnect) +end + +function meta:hook(name, id, f) + f = f or id + self.hooks[name] = self.hooks[name] or {} + self.hooks[name][id] = f + return id or f +end +meta_preconnect.hook = meta.hook + + +function meta:unhook(name, id) + local hooks = self.hooks[name] + + assert(hooks, "no hooks exist for this event") + assert(hooks[id], "hook ID not found") + + hooks[id] = nil +end +meta_preconnect.unhook = meta.unhook + +function meta:invoke(name, ...) + local hooks = self.hooks[name] + if hooks then + for id, f in pairs(hooks) do + local ret = f(...) + if ret then + return ret + end + end + end +end + +function meta_preconnect:connect(_host, _port) + local host, port, password, secure, timeout + + if type(_host) == "table" then + host = _host.host + port = _host.port + timeout = _host.timeout + password = _host.password + secure = _host.secure + else + host = _host + port = _port + end + + host = host or error("host name required to connect", 2) + port = port or 6667 + + local s = socket.tcp() + + s:settimeout(timeout or 30) + assert(s:connect(host, port)) + + if secure then + local work, ssl = pcall(require, "ssl") + if not work then + error("LuaSec required for secure connections", 2) + end + + local params + if type(secure) == "table" then + params = secure + else + params = {mode = "client", protocol = "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:invoke("PreRegister", self) + + if password then + self:queue(Message({command="PASS", args={password}})) + end + + self:queue(msgs.nick(self.nick)) + self:queue(Message({command="USER", args={self.username, "0", "*", self.realname}})) + + self.channels = {} + + s:settimeout(0) + + repeat + self:think() + socket.sleep(0.1) + until self.authed +end + +function meta:disconnect(message) + message = message or "Bye!" + + self:invoke("OnDisconnect", message, false) + self:send(msgs.quit(message)) + + self:shutdown() +end + +function meta:shutdown() + self.socket:close() + setmetatable(self, meta_preconnect) +end + +local function getline(self, errlevel) + local line, err = self.socket:receive("*l") + + if not line and err ~= "timeout" and err ~= "wantread" then + self:invoke("OnDisconnect", err, true) + self:shutdown() + error(err, errlevel) + end + + return line +end + +function meta:think() + while true do + local line = getline(self, 3) + if line and #line > 0 then + if not self:invoke("OnRaw", line) then + self:handle(Message({raw=line})) + end + else + break + end + end + + -- Handle outgoing message queue + local diff = socket.gettime() - self.lastThought + self.recentMessages = self.recentMessages - (diff * 2) + if self.recentMessages < 0 then + self.recentMessages = 0 + end + for i = 1, #self.messageQueue do + if self.recentMessages > 4 then + break + end + self:send(table.remove(self.messageQueue, 1)) + self.recentMessages = self.recentMessages + 1 + end + self.lastThought = socket.gettime() +end + +function meta:handle(msg) + local handler = handlers[msg.command] + if handler then + handler(self, msg) + end + self:invoke("Do" .. util.capitalize(msg.command), msg) +end + +local whoisHandlers = { + ["311"] = "userinfo"; + ["312"] = "node"; + ["319"] = "channels"; + ["330"] = "account"; -- Freenode + ["307"] = "registered"; -- Unreal +} + +function meta:whois(nick) + self:send(msgs.whois(nick)) + + local result = {} + + while true do + local line = getline(self, 3) + if line then + local msg = Message({raw=line}) + + local handler = whoisHandlers[msg.command] + if handler then + result[handler] = msg.args + elseif msg.command == "318" then + break + else + self:handle(msg) + end + end + end + + if result.account then + result.account = result.account[3] + elseif result.registered then + result.account = result.registered[2] + end + + return result +end + +function meta:topic(channel) + self:queue(msgs.topic(channel)) +end + +return { + new = new; + + Message = Message; + msgs = msgs; + + color = util.color; + bold = util.bold; + underline = util.underline; +} diff --git a/irc/irc/messages.lua b/irc/irc/messages.lua index 2f753ed..8ec1e5a 100644 --- a/irc/irc/messages.lua +++ b/irc/irc/messages.lua @@ -1,12 +1,11 @@ -local irc = require("irc.main") -irc.msgs = {} -local msgs = irc.msgs +-- Module table +local m = {} local msg_meta = {} msg_meta.__index = msg_meta -function irc.Message(opts) +local function Message(opts) opts = opts or {} setmetatable(opts, msg_meta) if opts.raw then @@ -15,6 +14,8 @@ function irc.Message(opts) return opts end +m.Message = Message + local tag_escapes = { [";"] = "\\:", [" "] = "\\s", @@ -47,7 +48,7 @@ function msg_meta:toRFC1459() s = s..self.command - local argnum = #self.args + local argnum = self.args and #self.args or 0 for i = 1, argnum do local arg = self.args[i] local startsWithColon = (arg:sub(1, 1) == ":") @@ -116,85 +117,91 @@ function msg_meta:fromRFC1459(line) end end -function msgs.privmsg(to, text) - return irc.Message({command="PRIVMSG", args={to, text}}) +function m.privmsg(to, text) + return Message({command="PRIVMSG", args={to, text}}) end -function msgs.notice(to, text) - return irc.Message({command="NOTICE", args={to, text}}) +function m.notice(to, text) + return Message({command="NOTICE", args={to, text}}) end -function msgs.action(to, text) - return irc.Message({command="PRIVMSG", args={to, ("\x01ACTION %s\x01"):format(text)}}) +function m.action(to, text) + return Message({command="PRIVMSG", args={to, ("\x01ACTION %s\x01"):format(text)}}) end -function msgs.ctcp(command, to, args) +function m.ctcp(command, to, args) s = "\x01"..command if args then s = ' '..args end s = s..'\x01' - return irc.Message({command="PRIVMSG", args={to, s}}) + return Message({command="PRIVMSG", args={to, s}}) end -function msgs.kick(channel, target, reason) - return irc.Message({command="KICK", args={channel, target, reason}}) +function m.kick(channel, target, reason) + return Message({command="KICK", args={channel, target, reason}}) end -function msgs.join(channel, key) - return irc.Message({command="JOIN", args={channel, key}}) +function m.join(channel, key) + return Message({command="JOIN", args={channel, key}}) end -function msgs.part(channel, reason) - return irc.Message({command="PART", args={channel, reason}}) +function m.part(channel, reason) + return Message({command="PART", args={channel, reason}}) end -function msgs.quit(reason) - return irc.Message({command="QUIT", args={reason}}) +function m.quit(reason) + return Message({command="QUIT", args={reason}}) end -function msgs.kill(target, reason) - return irc.Message({command="KILL", args={target, reason}}) +function m.kill(target, reason) + return Message({command="KILL", args={target, reason}}) end -function msgs.kline(time, mask, reason, operreason) +function m.kline(time, mask, reason, operreason) local args = nil if time then args = {time, mask, reason..'|'..operreason} else args = {mask, reason..'|'..operreason} end - return irc.Message({command="KLINE", args=args}) + return Message({command="KLINE", args=args}) end -function msgs.whois(nick, server) +function m.whois(nick, server) local args = nil if server then args = {server, nick} else args = {nick} end - return irc.Message({command="WHOIS", args=args}) + return Message({command="WHOIS", args=args}) end -function msgs.topic(channel, text) - return irc.Message({command="TOPIC", args={channel, text}}) +function m.topic(channel, text) + return Message({command="TOPIC", args={channel, text}}) end -function msgs.invite(channel, target) - return irc.Message({command="INVITE", args={channel, target}}) +function m.invite(channel, target) + return Message({command="INVITE", args={channel, target}}) end -function msgs.nick(nick) - return irc.Message({command="NICK", args={nick}}) +function m.nick(nick) + return Message({command="NICK", args={nick}}) end -function msgs.mode(target, modes) +function m.mode(target, modes) -- We have to split the modes parameter because the mode string and -- each parameter are seperate arguments (The first command is incorrect) -- MODE foo :+ov Nick1 Nick2 -- MODE foo +ov Nick1 Nick2 - local mt = irc.split(modes) - return irc.Message({command="MODE", args={target, unpack(mt)}}) + local mt = util.split(modes) + return Message({command="MODE", args={target, unpack(mt)}}) end +function m.cap(cmd, ...) + return Message({command="CAP", args={cmd, ...}}) +end + +return m + diff --git a/irc/irc/set.lua b/irc/irc/set.lua index 8841953..a4c6881 100644 --- a/irc/irc/set.lua +++ b/irc/irc/set.lua @@ -1,4 +1,4 @@ -local select = require "socket".select +local select = require("socket").select local m = {} local set = {} @@ -13,7 +13,7 @@ end function set:add(connection) local socket = connection.socket insert(self.sockets, socket) - + self.connections[socket] = connection insert(self.connections, connection) end @@ -32,13 +32,13 @@ 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 diff --git a/irc/irc/util.lua b/irc/irc/util.lua index 92bb76d..5916857 100644 --- a/irc/irc/util.lua +++ b/irc/irc/util.lua @@ -1,25 +1,8 @@ -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 +-- Module table +local m = {} -function irc.updatePrefixModes(conn) +function m.updatePrefixModes(conn) if conn.prefixmode and conn.modeprefix then return end @@ -39,8 +22,27 @@ function irc.updatePrefixModes(conn) end end +function m.parseNick(conn, nick) + local access = {} + m.updatePrefixModes(conn) + local namestart = 1 + for i = 1, #nick - 1 do + local c = nick:sub(i, i) + if conn.prefixmode[c] then + access[conn.prefixmode[c]] = true + else + namestart = i + break + end + end + access.op = access.o + access.voice = access.v + local name = nick:sub(namestart) + return access, name +end + -- mIRC markup scheme (de-facto standard) -irc.color = { +m.color = { black = 1, blue = 2, green = 3, @@ -60,28 +62,28 @@ irc.color = { } local colByte = string.char(3) -setmetatable(irc.color, {__call = function(_, text, colornum) +setmetatable(m.color, {__call = function(_, text, colornum) colornum = (type(colornum) == "string" and - assert(irc.color[colornum], "Invalid color '"..colornum.."'") or + assert(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) +function m.bold(text) return boldByte..text..boldByte end local underlineByte = string.char(31) -function irc.underline(text) +function m.underline(text) return underlineByte..text..underlineByte end -function irc.checkNick(nick) +function m.checkNick(nick) return nick:find("^[a-zA-Z_%-%[|%]%^{|}`][a-zA-Z0-9_%-%[|%]%^{|}`]*$") ~= nil end -function irc.defaultNickGenerator(nick) +function m.defaultNickGenerator(nick) -- LuaBot -> LuaCot -> LuaCou -> ... -- We change a random character rather than appending to the -- nickname as otherwise the new nick could exceed the ircd's @@ -100,13 +102,13 @@ function irc.defaultNickGenerator(nick) return nick end -function irc.capitalize(text) +function m.capitalize(text) -- Converts first character to upercase and the rest to lowercase. -- "PING" -> "Ping" | "hello" -> "Hello" | "123" -> "123" return text:sub(1, 1):upper()..text:sub(2):lower() end -function irc.split(str, sep) +function m.split(str, sep) local t = {} for s in str:gmatch("%S+") do table.insert(t, s) @@ -114,3 +116,5 @@ function irc.split(str, sep) return t end +return m + diff --git a/irc/messages.lua b/irc/messages.lua index 45ef4c4..52b1d4c 100644 --- a/irc/messages.lua +++ b/irc/messages.lua @@ -3,11 +3,15 @@ irc.msgs = irc.lib.msgs -function irc:sendLocal(message) - minetest.chat_send_all(message) +function irc.logChat(message) + minetest.log("action", "IRC CHAT: "..message) end -function irc:playerMessage(name, message) +function irc.sendLocal(message) + minetest.chat_send_all(minetest.colorize(irc.config.chat_color, message)) + irc.logChat(message) +end + +function irc.playerMessage(name, message) return ("<%s> %s"):format(name, message) end - diff --git a/irc/mod.conf b/irc/mod.conf new file mode 100644 index 0000000..e5286b4 --- /dev/null +++ b/irc/mod.conf @@ -0,0 +1,2 @@ +name = irc +description = 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. \ No newline at end of file diff --git a/irc/player_part.lua b/irc/player_part.lua index c9916c4..0e7b1c6 100644 --- a/irc/player_part.lua +++ b/irc/player_part.lua @@ -2,50 +2,52 @@ -- 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 +function irc.player_part(name) + if not irc.joined_players[name] then + return false, "You are not in the channel" end - self.joined_players[name] = nil - minetest.chat_send_player(name, "IRC: You are now out of the channel.") + irc.joined_players[name] = nil + return true, "You left 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 +function irc.player_join(name) + if irc.joined_players[name] then + return false, "You are already in the channel" + elseif not minetest.get_player_by_name(name) then + return false, "You need to be in-game to join the channel" end - self.joined_players[name] = true - minetest.chat_send_player(name, "IRC: You are now in the channel.") + 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, param) - irc:player_join(name) + func = function(name) + return 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) + func = function(name) + return 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 + func = function() + local out, n = { }, 0 + for plname in pairs(irc.joined_players) do + n = n + 1 + out[n] = plname end - minetest.chat_send_player(name, "Players On Channel:"..s) + table.sort(out) + return true, "Players in channel: "..table.concat(out, ", ") end }) @@ -61,9 +63,10 @@ minetest.register_on_leaveplayer(function(player) irc.joined_players[name] = nil end) -function irc:sendLocal(message) - for name, _ in pairs(self.joined_players) do - minetest.chat_send_player(name, message) +function irc.sendLocal(message) + for name, _ in pairs(irc.joined_players) do + minetest.chat_send_player(name, + minetest.colorize(irc.config.chat_color, message)) end + irc.logChat(message) end - diff --git a/irc/settingtypes.txt b/irc/settingtypes.txt new file mode 100644 index 0000000..0814167 --- /dev/null +++ b/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/irc_commands/depends.txt b/irc_commands/depends.txt deleted file mode 100755 index 3661ef9..0000000 --- a/irc_commands/depends.txt +++ /dev/null @@ -1 +0,0 @@ -irc diff --git a/irc_commands/mod.conf b/irc_commands/mod.conf new file mode 100644 index 0000000..76803ee --- /dev/null +++ b/irc_commands/mod.conf @@ -0,0 +1,2 @@ +name = irc_commands +depends = irc \ No newline at end of file