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.
This commit is contained in:
Auke Kok 2019-07-30 23:19:04 -07:00
parent 8c8aa5cba6
commit 66f71baaec
8 changed files with 377 additions and 5 deletions

View File

@ -10,6 +10,7 @@ read_globals = {
"PseudoRandom", "ItemStack", "PseudoRandom", "ItemStack",
"SecureRandom", "SecureRandom",
table = { fields = { "copy" } }, table = { fields = { "copy" } },
string = { fields = { "split" } },
"unpack", "unpack",
"AreaStore", "AreaStore",
"ranks", "ranks",

0
mods/telex/depends.txt Normal file
View File

View File

@ -0,0 +1 @@
in-game message system.

244
mods/telex/init.lua Normal file
View File

@ -0,0 +1,244 @@
--[[
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)

1
mods/telex/mod.conf Normal file
View File

@ -0,0 +1 @@
name = telex

View File

@ -1,5 +0,0 @@
mech
rules
log
fsc
sounds

View File

@ -55,6 +55,17 @@ term.help = {
unlock = "unlocks the terminal", unlock = "unlocks the terminal",
write = "write text to a file", write = "write text to a file",
edit = "edits a file in an editor", edit = "edits a file in an editor",
telex = "run the telex command"
}
term.telex_help = {
help = "display help information for subcommands",
list = "list received telex messages",
draft = "create a new telex message",
discard = "discard the current telex draft message",
send = "send the current draft telex message",
remove = "remove a telex message by number",
read = "read a telex message by number"
} }
local function make_formspec(output, prompt) local function make_formspec(output, prompt)
@ -209,6 +220,92 @@ term.commands = {
unlock = function(output, params, c) unlock = function(output, params, c)
return output .. "\n" .. "Error: unable to connect to authentication service" return output .. "\n" .. "Error: unable to connect to authentication service"
end, end,
telex = function(output, params, c)
if params ~= "" then
local h, p = get_cmd_params(params)
if h == "help" then
if p == "" then
local o = ""
local ot = {}
for k, _ in pairs(term.telex_help) do
ot[#ot + 1] = k
end
table.sort(ot)
for _, v in ipairs(ot) do
o = o .. " " .. v .. "\n"
end
return output .. "\n" ..
"Available subcommands:\n" ..
o ..
"Type `telex help <subcommand>` for more help about that command"
elseif term.telex_help[p] then
return output .. "\n" .. term.telex_help[p]
else
return output .. "\nError: No help for \"" .. h .. "\""
end
elseif h == "list" then
local player = minetest.get_player_by_name(c.name)
return output .. "\n" .. table.concat(telex.list(player), "\n")
elseif h == "draft" then
local player = minetest.get_player_by_name(c.name)
local meta = player:get_meta()
local text = meta:get_string("telex_draft")
local subject = meta:get_string("telex_subject")
fsc.show(c.name, "size[12,8]" ..
"field[0.5,0.5;11.5,1;subject;subject;" ..
minetest.formspec_escape(subject) .. "]" ..
"textarea[0.5,1.5;11.5,7.0;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button_exit[5.2,7.7;1.6,0.5;exit;Save]",
c,
term.draft)
return false
elseif h == "discard" then
local player = minetest.get_player_by_name(c.name)
local meta = player:get_meta()
meta:set_string("telex_draft", "")
meta:set_string("telex_subject", "")
return output .. "\n" .. "Draft erased."
elseif h == "send" then
if p == "" then
return output .. "\n" .. "Need recipient name to send draft message to."
end
local player = minetest.get_player_by_name(c.name)
local meta = player:get_meta()
local text = meta:get_string("telex_draft")
local subject = meta:get_string("telex_subject")
if text == "" then
return output .. "\n" .. "No draft exists. Cannot send. run `telex draft` to create a draft."
end
if subject == "" then
return output .. "\n" .. "No draft subject. Edit your draft and enter a subject."
end
local msg = {
to = p,
subject = subject,
from = c.name,
content = string.split(text, "\n")
}
telex.deliver(msg)
return output .. "\n" .. "Mail sent to <" .. p .. ">."
elseif h == "read" then
local player = minetest.get_player_by_name(c.name)
return output .. "\n" .. table.concat(telex.read(player, tonumber(p)), "\n")
elseif h == "remove" then
local player = minetest.get_player_by_name(c.name)
return output .. "\n" .. table.concat(telex.delete(player, tonumber(p)), "\n")
end
else
return output .. "\n" ..
"Type `telex help` for more help about the telex command"
end
end,
help = function(output, params, c) help = function(output, params, c)
if params ~= "" then if params ~= "" then
local h, _ = get_cmd_params(params) local h, _ = get_cmd_params(params)
@ -392,6 +489,37 @@ function term.edit(player, fields, context)
return return
end end
function term.draft(player, fields, context)
if not fields.text then
return true
end
local name = player:get_player_name()
local c = context
if not c or not c.pos or not c.output then
log.fs_data(player, name, "terminal:", fields)
return true
end
local output = c.output
local meta = player:get_meta()
-- validate it fits
if string.len(fields.text) < 49152 then
meta:set_string("telex_draft", fields.text)
meta:set_string("telex_subject", fields.subject)
output = output .. "\n" .. "Draft saved.\n"
else
output = output .. "\n" .. "Error: no space left on device\n"
end
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local terminal_use = function(pos, node, clicker, itemstack, pointed_thing) local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
if not clicker then if not clicker then
return return

2
mods/terminal/mod.conf Normal file
View File

@ -0,0 +1,2 @@
name = terminal
depends = telex, mech, rules, log, fsc, sounds