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

579 lines
15 KiB
Lua

--[[
ITB (insidethebox) minetest game - Copyright (C) 2017-2018 sofar & nore
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation; either version 2.1
of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
MA 02111-1307 USA
]]--
--[[
terminal - an interactive terminal
]]--
local term = {}
local function get_cmd_params(line)
local cmd = ""
local params = ""
for w in line:gmatch("%w+") do
if cmd == "" then
cmd = w
elseif params == "" then
params = w
else
params = params .. " " .. w
end
end
return cmd, params
end
term.help = {
append = "append text to a file",
clear = "clear the output",
echo = "echoes the input back to you",
help = "display help information for commands",
list = "list available files",
lock = "lock the terminal",
read = "read the content of a file",
remove = "removes a file",
unlock = "unlocks the terminal",
write = "write text to a file",
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 f =
"size[12,8]" ..
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;11,6;output;"
local c = 1
for part in output:gmatch("[^\r\n]+") do
f = f .. minetest.formspec_escape(part) .. ","
c = c + 1
end
f = f .. minetest.formspec_escape(prompt) .. ";" .. c .. ";false]"
f = f .. "field[0.7,7;11.2,1;input;;]"
return f
end
term.commands = {
clear = function(output, params, c)
return ""
end,
append = function(output, params, c)
if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.writing = what
return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish"
end,
write = function(output, params, c)
if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.writing = what
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if meta_files and meta_files ~= "" then
local files = minetest.parse_json(meta_files) or {}
if files and files[what] then
files[what] = ""
meta:set_string("files", minetest.write_json(files))
meta:mark_as_private("files")
end
end
return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish"
end,
edit = function(output, params, c)
if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.what = what
c.output = output
local text = ""
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if meta_files and meta_files ~= "" then
local files = minetest.parse_json(meta_files) or {}
if files and files[what] then
text = files[what]
end
end
fsc.show(c.name, "size[12,8]" ..
"textarea[0.5,0.5;11.5,7.0;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button_exit[5.2,7.2;1.6,0.5;exit;Save]",
c,
term.edit)
return false
end,
remove = function(output, params, c)
if not c.rw then
return output .. "\nError: No write access"
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if not meta_files or meta_files == "" then
return output .. "\nError: No such file"
end
local files = minetest.parse_json(meta_files) or {}
local first, _ = get_cmd_params(params)
if files[first] then
files[first] = nil
else
return output .. "\nError: No such file"
end
meta:set_string("files", minetest.write_json(files))
meta:mark_as_private("files")
return output .. "\nRemoved \"" .. first .. "\""
end,
list = function(output, params, c)
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
if not meta_files or meta_files == "" then
return output .. "\nError: No files found"
end
files = minetest.parse_json(meta_files) or {}
if not files then
return output .. "\nError: No files found"
end
for k, _ in pairs(files) do
output = output .. "\n" .. k
end
return output
end,
echo = function(output, params, c)
return output .. "\n" .. params
end,
read = function(output, params, c)
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if not meta_files or meta_files == "" then
return output .. "\nError: No such file"
end
local files = minetest.parse_json(meta_files) or {}
local first, _ = get_cmd_params(params)
if files[first] then
if first == "rules" then
rules.show(c.name, "player")
return false
end
return output .. "\n" .. files[first]
else
return output .. "\nError: No such file"
end
end,
lock = function(output, params, c)
if not c.rw then
return output .. "\nError: no write access"
end
local meta = minetest.get_meta(c.pos)
meta:set_int("locked", 1)
meta:mark_as_private("locked")
return output .. "\n" .. "Terminal locked"
end,
unlock = function(output, params, c)
return output .. "\n" .. "Error: unable to connect to authentication service"
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)
if params ~= "" then
local h, _ = get_cmd_params(params)
if term.help[h] then
return output .. "\n" .. term.help[h]
else
return output .. "\nError: No help for \"" .. h .. "\""
end
end
local o = ""
local ot = {}
for k, _ in pairs(term.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 commands:\n" ..
o ..
"Type `help <command>` for more help about that command"
end,
}
function term.recv(player, fields, context)
-- input validation
local name = player:get_player_name()
local c = context
if not c or not c.pos then
log.fs_data(player, name, "terminal:", fields)
return true
end
local line = fields.input
if line and line ~= "" then
local output = c.output or ""
minetest.sound_play("terminal_keyboard_clicks", {pos = c.pos})
if c.writing then
-- this shouldn't get reached, but just to be safe, check ro
if not c.rw then
c.writing = nil
output = output .. "\n" .. line
output = output .. "\nError: no write access"
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
-- are we writing a file?
if line == "STOP" then
-- done writing a file
c.writing = nil
output = output .. "\n" .. line
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files = {}
if not meta_files or meta_files == "" then
files[c.writing] = line
else
files = minetest.parse_json(meta_files) or {}
if not files[c.writing] then
files[c.writing] = line
else
files[c.writing] = files[c.writing] .. "\n" .. line
end
end
if string.len(files[c.writing]) < 16384 then
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. line
else
output = output .. "\n" .. "Error: no space left on device"
end
else
output = output .. "\n" .. "Error: maximum file length exceeded"
end
c.output = output
fsc.show(name,
make_formspec(output, ""),
c,
term.recv)
return
else
-- else parse cmd
output = output .. "\n> " .. line
local meta = minetest.get_meta(c.pos)
local cmd, params = get_cmd_params(line)
if meta:get_int("locked") == 1 and cmd ~= "unlock" then
output = output .. "\nError: Terminal locked, type \"unlock\" to unlock it"
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local fn = term.commands[cmd]
if fn then
output = fn(output, params, c)
else
output = output .. "\n" .. "Error: Syntax Error. Try \"help\""
end
if output ~= false then
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
end
return
end
elseif fields.quit then
minetest.sound_play("terminal_power_off", {pos = c.pos})
return true
elseif fields.output then
-- CHG events - do not return true
return
end
log.fs_data(player, name, "terminal:", fields)
return true
end
function term.edit(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
if not c.what then
output = output .. "\n" .. "Error: no such file\n"
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
files = minetest.parse_json(meta_files) or {}
files[c.what] = fields.text
-- validate it fits
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. "Wrote: " .. c.what .. "\n"
else
output = output .. "\n" .. "Error: no space left on device\n"
end
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
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)
if not clicker then
return
end
local name = clicker:get_player_name()
local context = {
pos = pos,
rw = false,
output = "",
name = name,
}
if boxes.players_editing_boxes[name] or
(not boxes.players_in_boxes[name] and minetest.check_player_privs(clicker, "server")) then
-- allow rw access
context.rw = true
end
-- send formspec to player
fsc.show(name,
make_formspec("", "> "),
context,
term.recv)
minetest.sound_play("terminal_power_on", {pos = pos})
-- trigger on first use
local meta = minetest.get_meta(pos)
if meta:get_int("locked") ~= 1 then
mech.trigger(pos)
minetest.after(1.0, mech.untrigger, pos)
end
end
minetest.register_node("terminal:terminal", {
description = "Interactive terminal console emulator access interface unit controller",
drawtype = "mesh",
mesh = "terminal.obj",
groups = {mech = 1, trigger = 1},
tiles = {
{name = "terminal_base.png"},
{name = "terminal_idle.png", animation = {type = "vertical_frames", aspect_w = 14, aspect_h = 13, length = 4.0}},
},
paramtype = "light",
paramtype2 = "facedir",
on_trigger = function(pos)
local meta = minetest.get_meta(pos)
minetest.sound_play("terminal_power_on", {pos = pos})
meta:set_int("locked", 0)
meta:mark_as_private("locked")
end,
on_untrigger = function(pos)
local meta = minetest.get_meta(pos)
minetest.sound_play("terminal_power_off", {pos = pos})
meta:set_int("locked", 1)
meta:mark_as_private("locked")
end,
on_rightclick = terminal_use,
sounds = sounds.metal,
})