--[[ telex mod for minetest Copyright (C) 2019 Auke Kok Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ]]-- telex = {} --[[ - messages are sent from player to another player. - messages have "from", "to", "subject", and "content", and "unread" properties - When a player sends a message, it is put in the global spool - the global spool is in mod_storage and persistent - periodically, the spool will attempt to deliver all messages to receivers - if the receiver does not exist or is not online, the message stays in the spool - if the message stays in the spool longer than , the message will be deleted. A new spool message, containing the old message in quoted form, will be returned to the sender. These messages will not expire from the spool. - if a player comes online, the spool attempts to deliver queued messages to the player - if the player is online, the spool immediately delivers the message - the player has their own mailbox, in player attributes/storage user actions: - list messages - read messages - reply to a message - delete a message msg = { "from" = , "to" = , "subject" = , "content" = , "read" = nil or 1, "age" = } mbox = { [1] = msgid, [2] = msgid, ... } spool = { [1] = msgid, [2] = msgid, ... } Spool/mbox format: array of messages ]]-- local S = minetest.get_mod_storage() assert(S) -- helper functions for handling msgid -- allocate a new msgid str function telex.msgid() local msgid = S:get_int("msgid") + 1 S:set_int("msgid", msgid) return "m" .. tostring(msgid) end -- get function telex.get_msg(msgid) if not msgid then minetest.log("action", "telex: get_msg() called with nil") return nil end local msg = S:get_string(msgid) if msg then return telex.decode(msg) else minetest.log("action", "telex: get_msg() unable to find " .. msgid) return nil end end -- set function telex.save_msg(msgid, msg) S:set_string(msgid, telex.encode(msg)) end -- remove function telex.remove_msg(msgid) S:set_string(msgid, "") end -- returns an array of strings function telex.list(player) local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) local list = {} for n, msgid in pairs(mbox) do local msg = telex.get_msg(msgid) local unread = "!" if msg.read == 1 then unread = " " end list[#list + 1] = unread .. n .. ": <" .. msg.from .. "> \"" .. msg.subject .. "\"" end return list end -- returns a table function telex.get(player, no) local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) if not mbox then return { "You have no messages." } end if not no then return { "You didn't specify a message number." } end local msgid = mbox[no] local msg = telex.get_msg(msgid) if not msg then minetest.log("action", "telex: get() unable to find msg no " .. no) return { "No such message exists." } end return msg end -- returns an array of strings function telex.read(player, no) local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) if not mbox then return { "You have no messages." } end local msgid = mbox[no] local msg = telex.get_msg(msgid) if not msg then return { "No such message exists." } end local list = { "From: <" .. msg.from .. ">", "Subject: " .. msg.subject, "" } for _, v in pairs(msg.content) do table.insert(list, v) end -- mark as read and store msg.read = 1 telex.save_msg(msgid, msg) mbox[no] = msgid pmeta:set_string("telex_mbox", telex.encode(mbox)) return list end -- returns array of strings function telex.delete(player, no) local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) if not mbox then return { "You have no messages." } end local msgid = mbox[no] local msg = telex.get_msg(msgid) if not msg then return { "No such message exists." } end table.remove(mbox, no) telex.remove_msg(msgid) pmeta:set_string("telex_mbox", telex.encode(mbox)) return { "Message deleted." } end -- external should use `send`, internal uses `deliver` function telex.send(msg) assert(msg.from) assert(msg.to) assert(msg.subject) assert(msg.content) assert(type(msg.content) == "table") local msgid = telex.msgid() telex.save_msg(msgid, msg) telex.deliver(msgid, msg) end -- returns nothing function telex.deliver(msgid, msg) -- msg is optional if not msg then msg = telex.get_msg(msgid) end local player = minetest.get_player_by_name(msg.to) if player then -- remove age, no longer needed msg.age = nil telex.save_msg(msgid, msg) -- retrieve inbox local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) table.insert(mbox, msgid) pmeta:set_string("telex_mbox", telex.encode(mbox)) minetest.chat_send_player(msg.to, "You have a new message from <" .. msg.from .. ">") minetest.log("action", "telex: delivered " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">") else -- append to spool msg.age = 14 * 86400 -- 7 days max in spool telex.save_msg(msgid, msg) local spool = telex.decode(S:get_string("telex_spool")) table.insert(spool, msgid) S:set_string("telex_spool", telex.encode(spool)) minetest.log("action", "telex: spooled " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">") announce.admins("spooled " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">") end end -- returns nothing function telex.retour(msgid) local msg = telex.get_msg(msgid) if msg.from == "MAILER-DEAMON" then -- discard, we tried hard enough! minetest.log("action", "telex: discarded " .. msgid .. " from <" .. msg.from .. "> to <" .. msg.to .. ">") announce.admins("discarded " .. msgid .. "from <" .. msg.from .. "> to <" .. msg.to .. ">") return end local to = msg.to local from = msg.from msg.to = from msg.from = "MAILER-DAEMON" msg.subject = "UNDELIVERABLE: " .. msg.subject msg.age = 86400 * 30 -- return mail for 30 days max, then discard. table.insert(msg.content, 1, "Your message to <" .. to .. "> was unable to be delivered.") table.insert(msg.content, 2, "================== ORIGINAL MESSAGE BELOW ================") -- remove the old msg, it's no longer in spool telex.remove_msg(msgid) -- allocate a new ID for the return msg local msgid2 = telex.msgid() telex.save_msg(msgid2, msg) telex.deliver(msgid2, msg) minetest.log("action", "telex: returned " .. msgid .. " -> " .. msgid2 .. " from <" .. from .. "> back to <" .. to .. ">") announce.admins("returned " .. msgid .. " -> " .. msgid2 .. " from <" .. from .. "> back to <" .. to .. ">") end -- returns message/mbox --FIXME compress/decompress function telex.decode(digest) if not digest or digest == "" then return {} end local tbl = minetest.parse_json(digest) if not tbl then return {} end return tbl end -- returns digest function telex.encode(message_or_mbox) return minetest.write_json(message_or_mbox) end minetest.register_on_joinplayer(function(player) -- convert old mbox local pmeta = player:get_meta() local mbox = telex.decode(pmeta:get_string("telex_mbox")) local save = false for k, v in pairs(mbox) do if type(v) == "table" then local msgid = telex.msgid() telex.save_msg(msgid, v) mbox[k] = msgid save = true end end if save then pmeta:set_string("telex_mbox", telex.encode(mbox)) end -- check the spool for messages for `player` local spool = telex.decode(S:get_string("telex_spool")) local name = player:get_player_name() -- deliver items for this player local old = {} local del = {} for k, msgid in pairs(spool) do local msg = telex.get_msg(msgid) if msg.to == name then old[#old + 1] = k del[#del + 1] = msgid end end -- remove items from spool in reverse order for i = #old, 1, -1 do table.remove(spool, old[i]) end S:set_string("telex_spool", telex.encode(spool)) -- now deliver them to player for _, msgid in pairs(del) do telex.deliver(msgid) end -- notify if there wasn't anything new but the player still had some unread if #old == 0 then -- reuse `mbox` for _, msgid in pairs(mbox) do local msg = telex.get_msg(msgid) if not msg.read then minetest.chat_send_player(name, "You have unread messages") break end end end end) function telex.process_spool() local spool = telex.decode(S:get_string("telex_spool")) minetest.log("action", "telex: processing spool: " .. #spool .. " messages" ) local old = {} local del = {} for k, msgid in pairs(spool) do local msg = telex.get_msg(msgid) msg.age = msg.age - 3600 if msg.age < 0 then old[#old + 1] = k del[#del + 1] = msgid end telex.save_msg(msgid, msg) end -- remove old msgs from spool for i = #old, 1, -1 do table.remove(spool, old[i]) end S:set_string("telex_spool", telex.encode(spool)) -- return the actual messages for _, msgid in pairs(del) do telex.retour(msgid) end minetest.after(3600, telex.process_spool) end minetest.after(5, telex.process_spool)