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:
parent
8c8aa5cba6
commit
66f71baaec
@ -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
0
mods/telex/depends.txt
Normal file
1
mods/telex/description.txt
Normal file
1
mods/telex/description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
in-game message system.
|
244
mods/telex/init.lua
Normal file
244
mods/telex/init.lua
Normal 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
1
mods/telex/mod.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
name = telex
|
@ -1,5 +0,0 @@
|
|||||||
mech
|
|
||||||
rules
|
|
||||||
log
|
|
||||||
fsc
|
|
||||||
sounds
|
|
@ -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
2
mods/terminal/mod.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
name = terminal
|
||||||
|
depends = telex, mech, rules, log, fsc, sounds
|
Loading…
x
Reference in New Issue
Block a user