Auke Kok 66f71baaec Telex mod and terminal integration.
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.
2019-07-30 23:19:04 -07:00

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)