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.
579 lines
15 KiB
Lua
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,
|
|
})
|