Make sure telex + random command doesn't crash the server.
[insidethebox.git] / mods / terminal / init.lua
blob330c3c0155a4b276b9356f31aa1ad0308d89bbf7
2 --[[
4 ITB (insidethebox) minetest game - Copyright (C) 2017-2018 sofar & nore
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public License
8 as published by the Free Software Foundation; either version 2.1
9 of the License, or (at your option) any later version.
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free
18 Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
19 MA 02111-1307 USA
21 ]]--
23 --[[
25 terminal - an interactive terminal
27 ]]--
29 local term = {}
31 local function get_cmd_params(line)
32 local cmd = ""
33 local params = ""
34 for w in line:gmatch("%w+") do
35 if cmd == "" then
36 cmd = w
37 elseif params == "" then
38 params = w
39 else
40 params = params .. " " .. w
41 end
42 end
43 return cmd, params
44 end
46 term.help = {
47 append = "append text to a file",
48 clear = "clear the output",
49 echo = "echoes the input back to you",
50 help = "display help information for commands",
51 list = "list available files",
52 lock = "lock the terminal",
53 read = "read the content of a file",
54 remove = "removes a file",
55 unlock = "unlocks the terminal",
56 write = "write text to a file",
57 edit = "edits a file in an editor",
58 telex = "run the telex command - send and receive messages"
61 term.telex_help = {
62 help = "display help information for subcommands",
63 list = "list received telex messages",
64 draft = "create a new, or edit an outgoing telex message",
65 discard = "discard the current telex draft message",
66 send = "send the current draft telex message to a recipient",
67 remove = "remove a received telex message by number",
68 read = "read a received telex message by number",
69 reply = "create a draft reply to a message by number"
72 local function make_formspec(output, prompt)
73 local f =
74 "size[12,8]" ..
75 "field_close_on_enter[input;false]" ..
76 "textlist[0.4,0.5;11,6;output;"
78 local c = 1
79 for part in output:gmatch("[^\r\n]+") do
80 f = f .. minetest.formspec_escape(part) .. ","
81 c = c + 1
82 end
83 f = f .. minetest.formspec_escape(prompt) .. ";" .. c .. ";false]"
85 f = f .. "field[0.7,7;11.2,1;input;;]"
86 return f
87 end
89 term.commands = {
90 clear = function(output, params, c)
91 return ""
92 end,
93 append = function(output, params, c)
94 if not c.rw then
95 return output .. "\nError: No write access"
96 end
97 local what, _ = get_cmd_params(params)
98 if what == "" then
99 return output .. "\nError: Missing file name"
101 c.writing = what
102 return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish"
103 end,
104 write = function(output, params, c)
105 if not c.rw then
106 return output .. "\nError: No write access"
108 local what, _ = get_cmd_params(params)
109 if what == "" then
110 return output .. "\nError: Missing file name"
112 c.writing = what
113 local meta = minetest.get_meta(c.pos)
114 local meta_files = meta:get_string("files")
115 if meta_files and meta_files ~= "" then
116 local files = minetest.parse_json(meta_files) or {}
117 if files and files[what] then
118 files[what] = ""
119 meta:set_string("files", minetest.write_json(files))
120 meta:mark_as_private("files")
123 return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish"
124 end,
125 edit = function(output, params, c)
126 if not c.rw then
127 return output .. "\nError: No write access"
129 local what, _ = get_cmd_params(params)
130 if what == "" then
131 return output .. "\nError: Missing file name"
133 c.what = what
134 c.output = output
136 local text = ""
137 local meta = minetest.get_meta(c.pos)
138 local meta_files = meta:get_string("files")
139 if meta_files and meta_files ~= "" then
140 local files = minetest.parse_json(meta_files) or {}
141 if files and files[what] then
142 text = files[what]
146 fsc.show(c.name, "size[12,8]" ..
147 "textarea[0.5,0.5;11.5,7.0;text;text;" ..
148 minetest.formspec_escape(text) .. "]" ..
149 "button[5.2,7.2;1.6,0.5;exit;Save]",
151 term.edit)
153 return false
154 end,
155 remove = function(output, params, c)
156 if not c.rw then
157 return output .. "\nError: No write access"
159 local meta = minetest.get_meta(c.pos)
160 local meta_files = meta:get_string("files")
161 if not meta_files or meta_files == "" then
162 return output .. "\nError: No such file"
164 local files = minetest.parse_json(meta_files) or {}
165 local first, _ = get_cmd_params(params)
166 if files[first] then
167 files[first] = nil
168 else
169 return output .. "\nError: No such file"
171 meta:set_string("files", minetest.write_json(files))
172 meta:mark_as_private("files")
173 return output .. "\nRemoved \"" .. first .. "\""
174 end,
175 list = function(output, params, c)
176 local meta = minetest.get_meta(c.pos)
177 local meta_files = meta:get_string("files")
178 local files
179 if not meta_files or meta_files == "" then
180 return output .. "\nError: No files found"
182 files = minetest.parse_json(meta_files) or {}
183 if not files then
184 return output .. "\nError: No files found"
186 for k, _ in pairs(files) do
187 output = output .. "\n" .. k
189 return output
190 end,
191 echo = function(output, params, c)
192 return output .. "\n" .. params
193 end,
194 read = function(output, params, c)
195 local meta = minetest.get_meta(c.pos)
196 local meta_files = meta:get_string("files")
197 if not meta_files or meta_files == "" then
198 return output .. "\nError: No such file"
200 local files = minetest.parse_json(meta_files) or {}
201 local first, _ = get_cmd_params(params)
202 if files[first] then
203 if first == "rules" then
204 rules.show(c.name, "player")
205 return false
207 return output .. "\n" .. files[first]
208 else
209 return output .. "\nError: No such file"
211 end,
212 lock = function(output, params, c)
213 if not c.rw then
214 return output .. "\nError: no write access"
216 local meta = minetest.get_meta(c.pos)
217 meta:set_int("locked", 1)
218 meta:mark_as_private("locked")
219 return output .. "\n" .. "Terminal locked"
220 end,
221 unlock = function(output, params, c)
222 return output .. "\n" .. "Error: unable to connect to authentication service"
223 end,
224 telex = function(output, params, c)
225 if params ~= "" then
226 local h, p = get_cmd_params(params)
227 if h == "help" then
228 if p == "" then
229 local o = ""
230 local ot = {}
231 for k, _ in pairs(term.telex_help) do
232 ot[#ot + 1] = k
234 table.sort(ot)
235 for _, v in ipairs(ot) do
236 o = o .. " " .. v .. "\n"
238 return output .. "\n" ..
239 "Available subcommands:\n" ..
240 o ..
241 "Type `telex help <subcommand>` for more help about that command"
242 elseif term.telex_help[p] then
243 return output .. "\n" .. term.telex_help[p]
244 else
245 return output .. "\nError: No help for \"" .. h .. "\""
247 elseif h == "list" then
248 local player = minetest.get_player_by_name(c.name)
249 return output .. "\n" .. table.concat(telex.list(player), "\n")
250 elseif h == "draft" then
251 local player = minetest.get_player_by_name(c.name)
252 local meta = player:get_meta()
253 local text = meta:get_string("telex_draft")
254 local subject = meta:get_string("telex_subject")
255 fsc.show(c.name, "size[12,8]" ..
256 "field[0.5,0.5;11.5,1;subject;subject;" ..
257 minetest.formspec_escape(subject) .. "]" ..
258 "textarea[0.5,1.5;11.5,7.0;text;text;" ..
259 minetest.formspec_escape(text) .. "]" ..
260 "button[5.2,7.7;1.6,0.5;exit;Save]",
262 term.draft)
264 return false
265 elseif h == "discard" then
266 local player = minetest.get_player_by_name(c.name)
267 local meta = player:get_meta()
268 meta:set_string("telex_draft", "")
269 meta:set_string("telex_subject", "")
270 return output .. "\n" .. "Draft erased."
271 elseif h == "send" then
272 if p == "" then
273 return output .. "\n" .. "Need recipient name to send draft message to."
276 local player = minetest.get_player_by_name(c.name)
277 local meta = player:get_meta()
278 local text = meta:get_string("telex_draft")
279 local subject = meta:get_string("telex_subject")
281 if text == "" then
282 return output .. "\n" .. "No draft exists. Cannot send. run `telex draft` to create a draft."
285 if subject == "" then
286 return output .. "\n" .. "No draft subject. Edit your draft and enter a subject."
289 local msg = {
290 to = p,
291 subject = subject,
292 from = c.name,
293 content = string.split(text, "\n")
295 telex.deliver(msg)
297 return output .. "\n" .. "Mail sent to <" .. p .. ">."
298 elseif h == "read" then
299 local player = minetest.get_player_by_name(c.name)
300 return output .. "\n" .. table.concat(telex.read(player, tonumber(p)), "\n")
301 elseif h == "remove" then
302 local player = minetest.get_player_by_name(c.name)
303 return output .. "\n" .. table.concat(telex.delete(player, tonumber(p)), "\n")
304 elseif h == "reply" then
305 local player = minetest.get_player_by_name(c.name)
306 local msg = telex.get(player, tonumber(p))
307 if not msg.subject then
308 return output .. "\n" .. table.concat(msg, "\n")
310 fsc.show(c.name, "size[12,8]" ..
311 "field[0.5,0.5;11.5,1;subject;subject;" ..
312 minetest.formspec_escape("Re: " .. msg.subject) .. "]" ..
313 "textarea[0.5,1.5;11.5,7.0;text;text;" ..
314 "\n\n> " ..
315 minetest.formspec_escape(table.concat(msg.content, "\n> ")) .. "]" ..
316 "button[5.2,7.7;1.6,0.5;exit;Save]",
318 term.draft)
320 return false
321 else
322 return output .. "\n" .. "Invalid command passed. Use `telex help` to list available commands."
324 else
325 return output .. "\n" ..
326 "Type `telex help` for more help about the telex command"
328 end,
329 help = function(output, params, c)
330 if params ~= "" then
331 local h, _ = get_cmd_params(params)
332 if term.help[h] then
333 return output .. "\n" .. term.help[h]
334 else
335 return output .. "\nError: No help for \"" .. h .. "\""
338 local o = ""
339 local ot = {}
340 for k, _ in pairs(term.help) do
341 ot[#ot + 1] = k
343 table.sort(ot)
344 for _, v in ipairs(ot) do
345 o = o .. " " .. v .. "\n"
347 return output .. "\n" ..
348 "Available commands:\n" ..
349 o ..
350 "Type `help <command>` for more help about that command"
351 end,
354 function term.recv(player, fields, context)
355 -- input validation
356 local name = player:get_player_name()
357 local c = context
358 if not c or not c.pos then
359 log.fs_data(player, name, "term.recv/context", fields)
360 return true
363 local line = fields.input
364 if line and line ~= "" then
365 local output = c.output or ""
366 minetest.sound_play("terminal_keyboard_clicks", {pos = c.pos})
368 if c.writing then
369 -- this shouldn't get reached, but just to be safe, check ro
370 if not c.rw then
371 c.writing = nil
372 output = output .. "\n" .. line
373 output = output .. "\nError: no write access"
374 c.output = output
375 fsc.show(name,
376 make_formspec(output, "> "),
378 term.recv)
379 return
381 -- are we writing a file?
382 if line == "STOP" then
383 -- done writing a file
384 c.writing = nil
385 output = output .. "\n" .. line
386 c.output = output
387 fsc.show(name,
388 make_formspec(output, "> "),
390 term.recv)
391 return
393 local meta = minetest.get_meta(c.pos)
394 local meta_files = meta:get_string("files")
395 local files = {}
396 if not meta_files or meta_files == "" then
397 files[c.writing] = line
398 else
399 files = minetest.parse_json(meta_files) or {}
400 if not files[c.writing] then
401 files[c.writing] = line
402 else
403 files[c.writing] = files[c.writing] .. "\n" .. line
406 if string.len(files[c.writing]) < 16384 then
407 local json = minetest.write_json(files)
408 if string.len(json) < 49152 then
409 meta:set_string("files", json)
410 meta:mark_as_private("files")
411 output = output .. "\n" .. line
412 else
413 output = output .. "\n" .. "Error: no space left on device"
415 else
416 output = output .. "\n" .. "Error: maximum file length exceeded"
418 c.output = output
419 fsc.show(name,
420 make_formspec(output, ""),
422 term.recv)
423 return
424 else
425 -- else parse cmd
426 output = output .. "\n> " .. line
428 local meta = minetest.get_meta(c.pos)
429 local cmd, params = get_cmd_params(line)
430 if meta:get_int("locked") == 1 and cmd ~= "unlock" then
431 output = output .. "\nError: Terminal locked, type \"unlock\" to unlock it"
432 c.output = output
433 fsc.show(name,
434 make_formspec(output, "> "),
436 term.recv)
437 return
440 local fn = term.commands[cmd]
441 if fn then
442 output = fn(output, params, c)
443 else
444 output = output .. "\n" .. "Error: Syntax Error. Try \"help\""
446 if output ~= false then
447 c.output = output
448 fsc.show(name,
449 make_formspec(output, "> "),
451 term.recv)
453 return
455 elseif fields.quit then
456 minetest.sound_play("terminal_power_off", {pos = c.pos})
457 return true
458 elseif fields.output then
459 -- CHG events - do not return true
460 return
461 elseif fields.input then
462 -- KEYBOARD events - do not return true
463 return
466 log.fs_data(player, name, "term.recv/default", fields)
467 return true
470 function term.edit(player, fields, context)
471 if not fields.text then
472 return true
475 local name = player:get_player_name()
476 local c = context
477 if not c or not c.pos or not c.output then
478 log.fs_data(player, name, "term.edit/terminal", fields)
479 return true
481 local output = c.output
483 if not c.what then
484 output = output .. "\n" .. "Error: no such file\n"
485 fsc.show(name,
486 make_formspec(output, "> "),
488 term.recv)
489 return
492 local meta = minetest.get_meta(c.pos)
493 local meta_files = meta:get_string("files")
494 local files
495 files = minetest.parse_json(meta_files) or {}
496 files[c.what] = fields.text
498 -- validate it fits
499 local json = minetest.write_json(files)
500 if string.len(json) < 49152 then
501 meta:set_string("files", json)
502 meta:mark_as_private("files")
503 output = output .. "\n" .. "Wrote: " .. c.what .. "\n"
504 else
505 output = output .. "\n" .. "Error: no space left on device\n"
508 c.output = output
510 fsc.show(name,
511 make_formspec(output, "> "),
513 term.recv)
514 return
517 function term.draft(player, fields, context)
518 if not fields.text then
519 return true
522 local name = player:get_player_name()
523 local c = context
524 if not c or not c.pos or not c.output then
525 log.fs_data(player, name, "term.draft/terminal", fields)
526 return true
528 local output = c.output
530 local meta = player:get_meta()
532 -- validate it fits
533 if string.len(fields.text) < 49152 then
534 meta:set_string("telex_draft", fields.text)
535 meta:set_string("telex_subject", fields.subject)
536 output = output .. "\n" .. "Draft saved.\n"
537 else
538 output = output .. "\n" .. "Error: no space left on device\n"
541 c.output = output
543 fsc.show(name,
544 make_formspec(output, "> "),
546 term.recv)
547 return
550 local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
551 if not clicker then
552 return
554 local name = clicker:get_player_name()
555 local context = {
556 pos = pos,
557 rw = false,
558 output = "",
559 name = name,
561 if boxes.players_editing_boxes[name] or
562 (not boxes.players_in_boxes[name] and minetest.check_player_privs(clicker, "server")) then
563 -- allow rw access
564 context.rw = true
566 -- send formspec to player
567 fsc.show(name,
568 make_formspec("", "> "),
569 context,
570 term.recv)
571 minetest.sound_play("terminal_power_on", {pos = pos})
572 -- trigger on first use
573 local meta = minetest.get_meta(pos)
574 if meta:get_int("locked") ~= 1 then
575 mech.trigger(pos)
576 minetest.after(1.0, mech.untrigger, pos)
580 minetest.register_node("terminal:terminal", {
581 description = "Interactive terminal console emulator access interface unit controller",
582 drawtype = "mesh",
583 mesh = "terminal.obj",
584 groups = {mech = 1, trigger = 1},
585 tiles = {
586 {name = "terminal_base.png"},
587 {name = "terminal_idle.png", animation = {type = "vertical_frames", aspect_w = 14, aspect_h = 13, length = 4.0}},
589 paramtype = "light",
590 paramtype2 = "facedir",
591 on_trigger = function(pos)
592 local meta = minetest.get_meta(pos)
593 minetest.sound_play("terminal_power_on", {pos = pos})
594 meta:set_int("locked", 0)
595 meta:mark_as_private("locked")
596 end,
597 on_untrigger = function(pos)
598 local meta = minetest.get_meta(pos)
599 minetest.sound_play("terminal_power_off", {pos = pos})
600 meta:set_int("locked", 1)
601 meta:mark_as_private("locked")
602 end,
603 on_rightclick = terminal_use,
604 sounds = sounds.metal,