This is a simple backend `telex` mod that takes care of encoding, storing, spooling and handling mail delivery for players. Everything is in StorageRef objects. The spool is global and contains undelivered msgs. Each user has an mbox. These can likely grow out of bounds and may need size checking to prevent corruption. The Terminal mod provides the UI. a `telex` command exists and it has subcommands for send/read/list and working with drafts. The draft is stored in the player StorageRef and is persistent. This makes editing and sending messages to more people doable, and you can re-edit your message later. There is no SENT folder or anything like it. There is no REPLY subcommand, but I do intend to include it. Messages do NOT get delivered to offline players. Those remain in the spool for 3 days. If the player does not log on, the mail is RETURNED UNDELIVERABLE. If the returning does not succeed within 30 days, the message is DROPPED.
245 lines
6.3 KiB
Lua
245 lines
6.3 KiB
Lua
|
|
--[[
|
|
|
|
telex mod for minetest
|
|
|
|
Copyright (C) 2019 Auke Kok <sofar@foo-projects.org>
|
|
|
|
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 <config>, 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
|
|
|
|
message table format:
|
|
{
|
|
"from" = <string>,
|
|
"to" = <string>,
|
|
"subject" = <string>,
|
|
"content" = <array of strings>,
|
|
"read" = nil or 1,
|
|
"age" = <int>
|
|
}
|
|
|
|
Spool/mbox format: array of messages
|
|
|
|
]]--
|
|
|
|
local S = minetest.get_mod_storage()
|
|
assert(S)
|
|
|
|
-- 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, msg in pairs(mbox) do
|
|
local unread = "!"
|
|
if msg.read == 1 then
|
|
unread = " "
|
|
end
|
|
list[#list + 1] = unread .. n .. ": <" .. msg.from .. "> \"" .. msg.subject .. "\""
|
|
end
|
|
return list
|
|
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 msg = mbox[no]
|
|
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
|
|
mbox[no] = msg
|
|
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 msg = mbox[no]
|
|
if not msg then
|
|
return { "No such message exists." }
|
|
end
|
|
|
|
table.remove(mbox, no)
|
|
pmeta:set_string("telex_mbox", telex.encode(mbox))
|
|
|
|
return { "Message deleted." }
|
|
end
|
|
|
|
-- returns nothing
|
|
function telex.deliver(message)
|
|
assert(message.from)
|
|
assert(message.to)
|
|
assert(message.subject)
|
|
assert(message.content)
|
|
|
|
local player = minetest.get_player_by_name(message.to)
|
|
if player then
|
|
-- retrieve inbox
|
|
local pmeta = player:get_meta()
|
|
local mbox = telex.decode(pmeta:get_string("telex_mbox"))
|
|
table.insert(mbox, message)
|
|
pmeta:set_string("telex_mbox", telex.encode(mbox))
|
|
minetest.chat_send_player(message.to, "You have a new message from <" ..
|
|
message.from .. ">, use a terminal to read your messages");
|
|
else
|
|
-- append to spool
|
|
message.age = 3 * 86400 -- 3 days max in spool
|
|
local spool = telex.decode(S:get_string("telex_spool"))
|
|
table.insert(spool, message)
|
|
S:set_string("telex_spool", telex.encode(spool))
|
|
end
|
|
end
|
|
|
|
-- returns nothing
|
|
function telex.retour(message)
|
|
if message.from == "MAILER-DEAMON" then
|
|
-- discard, we tried hard enough!
|
|
return
|
|
end
|
|
|
|
local to = message.to
|
|
local from = message.from
|
|
message.to = from
|
|
message.from = "MAILER-DAEMON"
|
|
message.subject = "UNDELIVERABLE: " .. message.subject
|
|
message.age = 86400 * 30 -- return mail for 30 days max, then discard.
|
|
table.insert(message.content, 1, "Your message to <" .. to .. "> was unable to be delivered.")
|
|
table.insert(message.content, 2, "================== ORIGINAL MESSAGE BELOW ================")
|
|
telex.deliver(message)
|
|
end
|
|
|
|
-- returns message/mbox
|
|
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)
|
|
-- 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 nos = {}
|
|
local del = {}
|
|
for k, msg in pairs(spool) do
|
|
if msg.to == name then
|
|
table.insert(nos, k)
|
|
table.insert(del, msg)
|
|
end
|
|
end
|
|
|
|
-- remove items from spool in reverse order
|
|
for i = #nos, 1, -1 do
|
|
table.remove(spool, nos[i])
|
|
end
|
|
S:set_string("telex_spool", telex.encode(spool))
|
|
|
|
-- now deliver them to player
|
|
for _, msg in pairs(del) do
|
|
telex.deliver(player, msg)
|
|
end
|
|
|
|
if #nos > 0 then
|
|
--FIXME minetest.after()
|
|
minetest.chat_send_player(name, "You have " .. #nos .. " new message(s), use a terminal to read your messages");
|
|
|
|
end
|
|
end)
|
|
|
|
function telex.process_spool()
|
|
local spool = telex.decode(S:get_string("telex_spool"))
|
|
local old = {}
|
|
local del = {}
|
|
for k, msg in pairs(spool) do --FIXME BAD
|
|
msg.age = msg.age - 3600
|
|
if msg.age < 0 then
|
|
old[#old + 1] = k
|
|
table.insert(del, msg)
|
|
end
|
|
end
|
|
|
|
-- remove old msgs from spool
|
|
for i = #old + 1, 1, -1 do
|
|
table.remove(spool, old[i])
|
|
end
|
|
S:set_string("telex_spool", telex.encode(spool))
|
|
|
|
-- return the actual messages
|
|
for _, msg in pairs(del) do
|
|
telex.retour(msg)
|
|
end
|
|
|
|
minetest.after(3600, telex.process_spool)
|
|
end
|
|
|
|
minetest.after(5, telex.process_spool)
|