Add support for the new arguments of `request_shutdown` to the `/shutdown` chatcomman...
[oweals/minetest.git] / builtin / game / chatcommands.lua
1 -- Minetest: builtin/chatcommands.lua
2
3 --
4 -- Chat command handler
5 --
6
7 core.registered_chatcommands = {}
8 core.chatcommands = core.registered_chatcommands -- BACKWARDS COMPATIBILITY
9 function core.register_chatcommand(cmd, def)
10         def = def or {}
11         def.params = def.params or ""
12         def.description = def.description or ""
13         def.privs = def.privs or {}
14         def.mod_origin = core.get_current_modname() or "??"
15         core.registered_chatcommands[cmd] = def
16 end
17
18 function core.unregister_chatcommand(name)
19         if core.registered_chatcommands[name] then
20                 core.registered_chatcommands[name] = nil
21         else
22                 core.log("warning", "Not unregistering chatcommand " ..name..
23                         " because it doesn't exist.")
24         end
25 end
26
27 function core.override_chatcommand(name, redefinition)
28         local chatcommand = core.registered_chatcommands[name]
29         assert(chatcommand, "Attempt to override non-existent chatcommand "..name)
30         for k, v in pairs(redefinition) do
31                 rawset(chatcommand, k, v)
32         end
33         core.registered_chatcommands[name] = chatcommand
34 end
35
36 core.register_on_chat_message(function(name, message)
37         local cmd, param = string.match(message, "^/([^ ]+) *(.*)")
38         if not param then
39                 param = ""
40         end
41         local cmd_def = core.registered_chatcommands[cmd]
42         if not cmd_def then
43                 return false
44         end
45         local has_privs, missing_privs = core.check_player_privs(name, cmd_def.privs)
46         if has_privs then
47                 core.set_last_run_mod(cmd_def.mod_origin)
48                 local success, message = cmd_def.func(name, param)
49                 if message then
50                         core.chat_send_player(name, message)
51                 end
52         else
53                 core.chat_send_player(name, "You don't have permission"
54                                 .. " to run this command (missing privileges: "
55                                 .. table.concat(missing_privs, ", ") .. ")")
56         end
57         return true  -- Handled chat message
58 end)
59
60 if core.setting_getbool("profiler.load") then
61         -- Run after register_chatcommand and its register_on_chat_message
62         -- Before any chattcommands that should be profiled
63         profiler.init_chatcommand()
64 end
65
66 -- Parses a "range" string in the format of "here (number)" or
67 -- "(x1, y1, z1) (x2, y2, z2)", returning two position vectors
68 local function parse_range_str(player_name, str)
69         local p1, p2
70         local args = str:split(" ")
71
72         if args[1] == "here" then
73                 p1, p2 = core.get_player_radius_area(player_name, tonumber(args[2]))
74                 if p1 == nil then
75                         return false, "Unable to get player " .. player_name .. " position"
76                 end
77         else
78                 p1, p2 = core.string_to_area(str)
79                 if p1 == nil then
80                         return false, "Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)"
81                 end
82         end
83
84         return p1, p2
85 end
86
87 --
88 -- Chat commands
89 --
90 core.register_chatcommand("me", {
91         params = "<action>",
92         description = "Display chat action (e.g., '/me orders a pizza' displays"
93                         .. " '<player name> orders a pizza')",
94         privs = {shout=true},
95         func = function(name, param)
96                 core.chat_send_all("* " .. name .. " " .. param)
97         end,
98 })
99
100 core.register_chatcommand("admin", {
101         description = "Show the name of the server owner",
102         func = function(name)
103                 local admin = minetest.setting_get("name")
104                 if admin then
105                         return true, "The administrator of this server is "..admin.."."
106                 else
107                         return false, "There's no administrator named in the config file."
108                 end
109         end,
110 })
111
112 core.register_chatcommand("help", {
113         privs = {},
114         params = "[all/privs/<cmd>]",
115         description = "Get help for commands or list privileges",
116         func = function(name, param)
117                 local function format_help_line(cmd, def)
118                         local msg = core.colorize("#00ffff", "/"..cmd)
119                         if def.params and def.params ~= "" then
120                                 msg = msg .. " " .. def.params
121                         end
122                         if def.description and def.description ~= "" then
123                                 msg = msg .. ": " .. def.description
124                         end
125                         return msg
126                 end
127                 if param == "" then
128                         local msg = ""
129                         local cmds = {}
130                         for cmd, def in pairs(core.registered_chatcommands) do
131                                 if core.check_player_privs(name, def.privs) then
132                                         cmds[#cmds + 1] = cmd
133                                 end
134                         end
135                         table.sort(cmds)
136                         return true, "Available commands: " .. table.concat(cmds, " ") .. "\n"
137                                         .. "Use '/help <cmd>' to get more information,"
138                                         .. " or '/help all' to list everything."
139                 elseif param == "all" then
140                         local cmds = {}
141                         for cmd, def in pairs(core.registered_chatcommands) do
142                                 if core.check_player_privs(name, def.privs) then
143                                         cmds[#cmds + 1] = format_help_line(cmd, def)
144                                 end
145                         end
146                         table.sort(cmds)
147                         return true, "Available commands:\n"..table.concat(cmds, "\n")
148                 elseif param == "privs" then
149                         local privs = {}
150                         for priv, def in pairs(core.registered_privileges) do
151                                 privs[#privs + 1] = priv .. ": " .. def.description
152                         end
153                         table.sort(privs)
154                         return true, "Available privileges:\n"..table.concat(privs, "\n")
155                 else
156                         local cmd = param
157                         local def = core.registered_chatcommands[cmd]
158                         if not def then
159                                 return false, "Command not available: "..cmd
160                         else
161                                 return true, format_help_line(cmd, def)
162                         end
163                 end
164         end,
165 })
166
167 core.register_chatcommand("privs", {
168         params = "<name>",
169         description = "Print privileges of player",
170         func = function(caller, param)
171                 param = param:trim()
172                 local name = (param ~= "" and param or caller)
173                 return true, "Privileges of " .. name .. ": "
174                         .. core.privs_to_string(
175                                 core.get_player_privs(name), ' ')
176         end,
177 })
178
179 local function handle_grant_command(caller, grantname, grantprivstr)
180         local caller_privs = minetest.get_player_privs(caller)
181         if not (caller_privs.privs or caller_privs.basic_privs) then
182                 return false, "Your privileges are insufficient."
183         end
184
185         if not core.get_auth_handler().get_auth(grantname) then
186                 return false, "Player " .. grantname .. " does not exist."
187         end
188         local grantprivs = core.string_to_privs(grantprivstr)
189         if grantprivstr == "all" then
190                 grantprivs = core.registered_privileges
191         end
192         local privs = core.get_player_privs(grantname)
193         local privs_unknown = ""
194         local basic_privs =
195                 core.string_to_privs(core.setting_get("basic_privs") or "interact,shout")
196         for priv, _ in pairs(grantprivs) do
197                 if not basic_privs[priv] and not caller_privs.privs then
198                         return false, "Your privileges are insufficient."
199                 end
200                 if not core.registered_privileges[priv] then
201                         privs_unknown = privs_unknown .. "Unknown privilege: " .. priv .. "\n"
202                 end
203                 privs[priv] = true
204         end
205         if privs_unknown ~= "" then
206                 return false, privs_unknown
207         end
208         core.set_player_privs(grantname, privs)
209         core.log("action", caller..' granted ('..core.privs_to_string(grantprivs, ', ')..') privileges to '..grantname)
210         if grantname ~= caller then
211                 core.chat_send_player(grantname, caller
212                                 .. " granted you privileges: "
213                                 .. core.privs_to_string(grantprivs, ' '))
214         end
215         return true, "Privileges of " .. grantname .. ": "
216                 .. core.privs_to_string(
217                         core.get_player_privs(grantname), ' ')
218 end
219
220 core.register_chatcommand("grant", {
221         params = "<name> <privilege>|all",
222         description = "Give privilege to player",
223         func = function(name, param)
224                 local grantname, grantprivstr = string.match(param, "([^ ]+) (.+)")
225                 if not grantname or not grantprivstr then
226                         return false, "Invalid parameters (see /help grant)"
227                 end
228                 return handle_grant_command(name, grantname, grantprivstr)
229         end,
230 })
231
232 core.register_chatcommand("grantme", {
233         params = "<privilege>|all",
234         description = "Grant privileges to yourself",
235         func = function(name, param)
236                 if param == "" then
237                         return false, "Invalid parameters (see /help grantme)"
238                 end
239                 return handle_grant_command(name, name, param)
240         end,
241 })
242
243 core.register_chatcommand("revoke", {
244         params = "<name> <privilege>|all",
245         description = "Remove privilege from player",
246         privs = {},
247         func = function(name, param)
248                 if not core.check_player_privs(name, {privs=true}) and
249                                 not core.check_player_privs(name, {basic_privs=true}) then
250                         return false, "Your privileges are insufficient."
251                 end
252                 local revoke_name, revoke_priv_str = string.match(param, "([^ ]+) (.+)")
253                 if not revoke_name or not revoke_priv_str then
254                         return false, "Invalid parameters (see /help revoke)"
255                 elseif not core.get_auth_handler().get_auth(revoke_name) then
256                         return false, "Player " .. revoke_name .. " does not exist."
257                 end
258                 local revoke_privs = core.string_to_privs(revoke_priv_str)
259                 local privs = core.get_player_privs(revoke_name)
260                 local basic_privs =
261                         core.string_to_privs(core.setting_get("basic_privs") or "interact,shout")
262                 for priv, _ in pairs(revoke_privs) do
263                         if not basic_privs[priv] and
264                                         not core.check_player_privs(name, {privs=true}) then
265                                 return false, "Your privileges are insufficient."
266                         end
267                 end
268                 if revoke_priv_str == "all" then
269                         privs = {}
270                 else
271                         for priv, _ in pairs(revoke_privs) do
272                                 privs[priv] = nil
273                         end
274                 end
275                 core.set_player_privs(revoke_name, privs)
276                 core.log("action", name..' revoked ('
277                                 ..core.privs_to_string(revoke_privs, ', ')
278                                 ..') privileges from '..revoke_name)
279                 if revoke_name ~= name then
280                         core.chat_send_player(revoke_name, name
281                                         .. " revoked privileges from you: "
282                                         .. core.privs_to_string(revoke_privs, ' '))
283                 end
284                 return true, "Privileges of " .. revoke_name .. ": "
285                         .. core.privs_to_string(
286                                 core.get_player_privs(revoke_name), ' ')
287         end,
288 })
289
290 core.register_chatcommand("setpassword", {
291         params = "<name> <password>",
292         description = "Set player's password",
293         privs = {password=true},
294         func = function(name, param)
295                 local toname, raw_password = string.match(param, "^([^ ]+) +(.+)$")
296                 if not toname then
297                         toname = param:match("^([^ ]+) *$")
298                         raw_password = nil
299                 end
300                 if not toname then
301                         return false, "Name field required"
302                 end
303                 local act_str_past = "?"
304                 local act_str_pres = "?"
305                 if not raw_password then
306                         core.set_player_password(toname, "")
307                         act_str_past = "cleared"
308                         act_str_pres = "clears"
309                 else
310                         core.set_player_password(toname,
311                                         core.get_password_hash(toname,
312                                                         raw_password))
313                         act_str_past = "set"
314                         act_str_pres = "sets"
315                 end
316                 if toname ~= name then
317                         core.chat_send_player(toname, "Your password was "
318                                         .. act_str_past .. " by " .. name)
319                 end
320
321                 core.log("action", name .. " " .. act_str_pres
322                 .. " password of " .. toname .. ".")
323
324                 return true, "Password of player \"" .. toname .. "\" " .. act_str_past
325         end,
326 })
327
328 core.register_chatcommand("clearpassword", {
329         params = "<name>",
330         description = "Set empty password",
331         privs = {password=true},
332         func = function(name, param)
333                 local toname = param
334                 if toname == "" then
335                         return false, "Name field required"
336                 end
337                 core.set_player_password(toname, '')
338
339                 core.log("action", name .. " clears password of " .. toname .. ".")
340
341                 return true, "Password of player \"" .. toname .. "\" cleared"
342         end,
343 })
344
345 core.register_chatcommand("auth_reload", {
346         params = "",
347         description = "Reload authentication data",
348         privs = {server=true},
349         func = function(name, param)
350                 local done = core.auth_reload()
351                 return done, (done and "Done." or "Failed.")
352         end,
353 })
354
355 core.register_chatcommand("teleport", {
356         params = "<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>",
357         description = "Teleport to player or position",
358         privs = {teleport=true},
359         func = function(name, param)
360                 -- Returns (pos, true) if found, otherwise (pos, false)
361                 local function find_free_position_near(pos)
362                         local tries = {
363                                 {x=1,y=0,z=0},
364                                 {x=-1,y=0,z=0},
365                                 {x=0,y=0,z=1},
366                                 {x=0,y=0,z=-1},
367                         }
368                         for _, d in ipairs(tries) do
369                                 local p = {x = pos.x+d.x, y = pos.y+d.y, z = pos.z+d.z}
370                                 local n = core.get_node_or_nil(p)
371                                 if n and n.name then
372                                         local def = core.registered_nodes[n.name]
373                                         if def and not def.walkable then
374                                                 return p, true
375                                         end
376                                 end
377                         end
378                         return pos, false
379                 end
380
381                 local teleportee = nil
382                 local p = {}
383                 p.x, p.y, p.z = string.match(param, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
384                 p.x = tonumber(p.x)
385                 p.y = tonumber(p.y)
386                 p.z = tonumber(p.z)
387                 if p.x and p.y and p.z then
388                         local lm = tonumber(minetest.setting_get("map_generation_limit") or 31000)
389                         if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm or p.z < -lm or p.z > lm then
390                                 return false, "Cannot teleport out of map bounds!"
391                         end
392                         teleportee = core.get_player_by_name(name)
393                         if teleportee then
394                                 teleportee:setpos(p)
395                                 return true, "Teleporting to "..core.pos_to_string(p)
396                         end
397                 end
398
399                 local teleportee = nil
400                 local p = nil
401                 local target_name = nil
402                 target_name = param:match("^([^ ]+)$")
403                 teleportee = core.get_player_by_name(name)
404                 if target_name then
405                         local target = core.get_player_by_name(target_name)
406                         if target then
407                                 p = target:getpos()
408                         end
409                 end
410                 if teleportee and p then
411                         p = find_free_position_near(p)
412                         teleportee:setpos(p)
413                         return true, "Teleporting to " .. target_name
414                                         .. " at "..core.pos_to_string(p)
415                 end
416
417                 if not core.check_player_privs(name, {bring=true}) then
418                         return false, "You don't have permission to teleport other players (missing bring privilege)"
419                 end
420
421                 local teleportee = nil
422                 local p = {}
423                 local teleportee_name = nil
424                 teleportee_name, p.x, p.y, p.z = param:match(
425                                 "^([^ ]+) +([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
426                 p.x, p.y, p.z = tonumber(p.x), tonumber(p.y), tonumber(p.z)
427                 if teleportee_name then
428                         teleportee = core.get_player_by_name(teleportee_name)
429                 end
430                 if teleportee and p.x and p.y and p.z then
431                         teleportee:setpos(p)
432                         return true, "Teleporting " .. teleportee_name
433                                         .. " to " .. core.pos_to_string(p)
434                 end
435
436                 local teleportee = nil
437                 local p = nil
438                 local teleportee_name = nil
439                 local target_name = nil
440                 teleportee_name, target_name = string.match(param, "^([^ ]+) +([^ ]+)$")
441                 if teleportee_name then
442                         teleportee = core.get_player_by_name(teleportee_name)
443                 end
444                 if target_name then
445                         local target = core.get_player_by_name(target_name)
446                         if target then
447                                 p = target:getpos()
448                         end
449                 end
450                 if teleportee and p then
451                         p = find_free_position_near(p)
452                         teleportee:setpos(p)
453                         return true, "Teleporting " .. teleportee_name
454                                         .. " to " .. target_name
455                                         .. " at " .. core.pos_to_string(p)
456                 end
457
458                 return false, 'Invalid parameters ("' .. param
459                                 .. '") or player not found (see /help teleport)'
460         end,
461 })
462
463 core.register_chatcommand("set", {
464         params = "[-n] <name> <value> | <name>",
465         description = "Set or read server configuration setting",
466         privs = {server=true},
467         func = function(name, param)
468                 local arg, setname, setvalue = string.match(param, "(-[n]) ([^ ]+) (.+)")
469                 if arg and arg == "-n" and setname and setvalue then
470                         core.setting_set(setname, setvalue)
471                         return true, setname .. " = " .. setvalue
472                 end
473                 local setname, setvalue = string.match(param, "([^ ]+) (.+)")
474                 if setname and setvalue then
475                         if not core.setting_get(setname) then
476                                 return false, "Failed. Use '/set -n <name> <value>' to create a new setting."
477                         end
478                         core.setting_set(setname, setvalue)
479                         return true, setname .. " = " .. setvalue
480                 end
481                 local setname = string.match(param, "([^ ]+)")
482                 if setname then
483                         local setvalue = core.setting_get(setname)
484                         if not setvalue then
485                                 setvalue = "<not set>"
486                         end
487                         return true, setname .. " = " .. setvalue
488                 end
489                 return false, "Invalid parameters (see /help set)."
490         end,
491 })
492
493 local function emergeblocks_callback(pos, action, num_calls_remaining, ctx)
494         if ctx.total_blocks == 0 then
495                 ctx.total_blocks   = num_calls_remaining + 1
496                 ctx.current_blocks = 0
497         end
498         ctx.current_blocks = ctx.current_blocks + 1
499
500         if ctx.current_blocks == ctx.total_blocks then
501                 core.chat_send_player(ctx.requestor_name,
502                         string.format("Finished emerging %d blocks in %.2fms.",
503                         ctx.total_blocks, (os.clock() - ctx.start_time) * 1000))
504         end
505 end
506
507 local function emergeblocks_progress_update(ctx)
508         if ctx.current_blocks ~= ctx.total_blocks then
509                 core.chat_send_player(ctx.requestor_name,
510                         string.format("emergeblocks update: %d/%d blocks emerged (%.1f%%)",
511                         ctx.current_blocks, ctx.total_blocks,
512                         (ctx.current_blocks / ctx.total_blocks) * 100))
513
514                 core.after(2, emergeblocks_progress_update, ctx)
515         end
516 end
517
518 core.register_chatcommand("emergeblocks", {
519         params = "(here [radius]) | (<pos1> <pos2>)",
520         description = "Load (or, if nonexistent, generate) map blocks "
521                 .. "contained in area pos1 to pos2",
522         privs = {server=true},
523         func = function(name, param)
524                 local p1, p2 = parse_range_str(name, param)
525                 if p1 == false then
526                         return false, p2
527                 end
528
529                 local context = {
530                         current_blocks = 0,
531                         total_blocks   = 0,
532                         start_time     = os.clock(),
533                         requestor_name = name
534                 }
535
536                 core.emerge_area(p1, p2, emergeblocks_callback, context)
537                 core.after(2, emergeblocks_progress_update, context)
538
539                 return true, "Started emerge of area ranging from " ..
540                         core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1)
541         end,
542 })
543
544 core.register_chatcommand("deleteblocks", {
545         params = "(here [radius]) | (<pos1> <pos2>)",
546         description = "Delete map blocks contained in area pos1 to pos2",
547         privs = {server=true},
548         func = function(name, param)
549                 local p1, p2 = parse_range_str(name, param)
550                 if p1 == false then
551                         return false, p2
552                 end
553
554                 if core.delete_area(p1, p2) then
555                         return true, "Successfully cleared area ranging from " ..
556                                 core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1)
557                 else
558                         return false, "Failed to clear one or more blocks in area"
559                 end
560         end,
561 })
562
563 core.register_chatcommand("mods", {
564         params = "",
565         description = "List mods installed on the server",
566         privs = {},
567         func = function(name, param)
568                 return true, table.concat(core.get_modnames(), ", ")
569         end,
570 })
571
572 local function handle_give_command(cmd, giver, receiver, stackstring)
573         core.log("action", giver .. " invoked " .. cmd
574                         .. ', stackstring="' .. stackstring .. '"')
575         local itemstack = ItemStack(stackstring)
576         if itemstack:is_empty() then
577                 return false, "Cannot give an empty item"
578         elseif not itemstack:is_known() then
579                 return false, "Cannot give an unknown item"
580         end
581         local receiverref = core.get_player_by_name(receiver)
582         if receiverref == nil then
583                 return false, receiver .. " is not a known player"
584         end
585         local leftover = receiverref:get_inventory():add_item("main", itemstack)
586         local partiality
587         if leftover:is_empty() then
588                 partiality = ""
589         elseif leftover:get_count() == itemstack:get_count() then
590                 partiality = "could not be "
591         else
592                 partiality = "partially "
593         end
594         -- The actual item stack string may be different from what the "giver"
595         -- entered (e.g. big numbers are always interpreted as 2^16-1).
596         stackstring = itemstack:to_string()
597         if giver == receiver then
598                 return true, ("%q %sadded to inventory.")
599                                 :format(stackstring, partiality)
600         else
601                 core.chat_send_player(receiver, ("%q %sadded to inventory.")
602                                 :format(stackstring, partiality))
603                 return true, ("%q %sadded to %s's inventory.")
604                                 :format(stackstring, partiality, receiver)
605         end
606 end
607
608 core.register_chatcommand("give", {
609         params = "<name> <ItemString>",
610         description = "Give item to player",
611         privs = {give=true},
612         func = function(name, param)
613                 local toname, itemstring = string.match(param, "^([^ ]+) +(.+)$")
614                 if not toname or not itemstring then
615                         return false, "Name and ItemString required"
616                 end
617                 return handle_give_command("/give", name, toname, itemstring)
618         end,
619 })
620
621 core.register_chatcommand("giveme", {
622         params = "<ItemString>",
623         description = "Give item to yourself",
624         privs = {give=true},
625         func = function(name, param)
626                 local itemstring = string.match(param, "(.+)$")
627                 if not itemstring then
628                         return false, "ItemString required"
629                 end
630                 return handle_give_command("/giveme", name, name, itemstring)
631         end,
632 })
633
634 core.register_chatcommand("spawnentity", {
635         params = "<EntityName> [<X>,<Y>,<Z>]",
636         description = "Spawn entity at given (or your) position",
637         privs = {give=true, interact=true},
638         func = function(name, param)
639                 local entityname, p = string.match(param, "^([^ ]+) *(.*)$")
640                 if not entityname then
641                         return false, "EntityName required"
642                 end
643                 core.log("action", ("%s invokes /spawnentity, entityname=%q")
644                                 :format(name, entityname))
645                 local player = core.get_player_by_name(name)
646                 if player == nil then
647                         core.log("error", "Unable to spawn entity, player is nil")
648                         return false, "Unable to spawn entity, player is nil"
649                 end
650                 if p == "" then
651                         p = player:getpos()
652                 else
653                         p = core.string_to_pos(p)
654                         if p == nil then
655                                 return false, "Invalid parameters ('" .. param .. "')"
656                         end
657                 end
658                 p.y = p.y + 1
659                 core.add_entity(p, entityname)
660                 return true, ("%q spawned."):format(entityname)
661         end,
662 })
663
664 core.register_chatcommand("pulverize", {
665         params = "",
666         description = "Destroy item in hand",
667         func = function(name, param)
668                 local player = core.get_player_by_name(name)
669                 if not player then
670                         core.log("error", "Unable to pulverize, no player.")
671                         return false, "Unable to pulverize, no player."
672                 end
673                 if player:get_wielded_item():is_empty() then
674                         return false, "Unable to pulverize, no item in hand."
675                 end
676                 player:set_wielded_item(nil)
677                 return true, "An item was pulverized."
678         end,
679 })
680
681 -- Key = player name
682 core.rollback_punch_callbacks = {}
683
684 core.register_on_punchnode(function(pos, node, puncher)
685         local name = puncher:get_player_name()
686         if core.rollback_punch_callbacks[name] then
687                 core.rollback_punch_callbacks[name](pos, node, puncher)
688                 core.rollback_punch_callbacks[name] = nil
689         end
690 end)
691
692 core.register_chatcommand("rollback_check", {
693         params = "[<range>] [<seconds>] [limit]",
694         description = "Check who last touched a node or a node near it"
695                         .. " within the time specified by <seconds>. Default: range = 0,"
696                         .. " seconds = 86400 = 24h, limit = 5",
697         privs = {rollback=true},
698         func = function(name, param)
699                 if not core.setting_getbool("enable_rollback_recording") then
700                         return false, "Rollback functions are disabled."
701                 end
702                 local range, seconds, limit =
703                         param:match("(%d+) *(%d*) *(%d*)")
704                 range = tonumber(range) or 0
705                 seconds = tonumber(seconds) or 86400
706                 limit = tonumber(limit) or 5
707                 if limit > 100 then
708                         return false, "That limit is too high!"
709                 end
710
711                 core.rollback_punch_callbacks[name] = function(pos, node, puncher)
712                         local name = puncher:get_player_name()
713                         core.chat_send_player(name, "Checking " .. core.pos_to_string(pos) .. "...")
714                         local actions = core.rollback_get_node_actions(pos, range, seconds, limit)
715                         if not actions then
716                                 core.chat_send_player(name, "Rollback functions are disabled")
717                                 return
718                         end
719                         local num_actions = #actions
720                         if num_actions == 0 then
721                                 core.chat_send_player(name, "Nobody has touched"
722                                                 .. " the specified location in "
723                                                 .. seconds .. " seconds")
724                                 return
725                         end
726                         local time = os.time()
727                         for i = num_actions, 1, -1 do
728                                 local action = actions[i]
729                                 core.chat_send_player(name,
730                                         ("%s %s %s -> %s %d seconds ago.")
731                                                 :format(
732                                                         core.pos_to_string(action.pos),
733                                                         action.actor,
734                                                         action.oldnode.name,
735                                                         action.newnode.name,
736                                                         time - action.time))
737                         end
738                 end
739
740                 return true, "Punch a node (range=" .. range .. ", seconds="
741                                 .. seconds .. "s, limit=" .. limit .. ")"
742         end,
743 })
744
745 core.register_chatcommand("rollback", {
746         params = "<player name> [<seconds>] | :<actor> [<seconds>]",
747         description = "Revert actions of a player. Default for <seconds> is 60",
748         privs = {rollback=true},
749         func = function(name, param)
750                 if not core.setting_getbool("enable_rollback_recording") then
751                         return false, "Rollback functions are disabled."
752                 end
753                 local target_name, seconds = string.match(param, ":([^ ]+) *(%d*)")
754                 if not target_name then
755                         local player_name = nil
756                         player_name, seconds = string.match(param, "([^ ]+) *(%d*)")
757                         if not player_name then
758                                 return false, "Invalid parameters. See /help rollback"
759                                                 .. " and /help rollback_check."
760                         end
761                         target_name = "player:"..player_name
762                 end
763                 seconds = tonumber(seconds) or 60
764                 core.chat_send_player(name, "Reverting actions of "
765                                 .. target_name .. " since "
766                                 .. seconds .. " seconds.")
767                 local success, log = core.rollback_revert_actions_by(
768                                 target_name, seconds)
769                 local response = ""
770                 if #log > 100 then
771                         response = "(log is too long to show)\n"
772                 else
773                         for _, line in pairs(log) do
774                                 response = response .. line .. "\n"
775                         end
776                 end
777                 response = response .. "Reverting actions "
778                                 .. (success and "succeeded." or "FAILED.")
779                 return success, response
780         end,
781 })
782
783 core.register_chatcommand("status", {
784         description = "Print server status",
785         func = function(name, param)
786                 return true, core.get_server_status()
787         end,
788 })
789
790 core.register_chatcommand("time", {
791         params = "<0..23>:<0..59> | <0..24000>",
792         description = "Set time of day",
793         privs = {},
794         func = function(name, param)
795                 if param == "" then
796                         local current_time = math.floor(core.get_timeofday() * 1440)
797                         local minutes = current_time % 60
798                         local hour = (current_time - minutes) / 60
799                         return true, ("Current time is %d:%02d"):format(hour, minutes)
800                 end
801                 local player_privs = core.get_player_privs(name)
802                 if not player_privs.settime then
803                         return false, "You don't have permission to run this command " ..
804                                 "(missing privilege: settime)."
805                 end
806                 local hour, minute = param:match("^(%d+):(%d+)$")
807                 if not hour then
808                         local new_time = tonumber(param)
809                         if not new_time then
810                                 return false, "Invalid time."
811                         end
812                         -- Backward compatibility.
813                         core.set_timeofday((new_time % 24000) / 24000)
814                         core.log("action", name .. " sets time to " .. new_time)
815                         return true, "Time of day changed."
816                 end
817                 hour = tonumber(hour)
818                 minute = tonumber(minute)
819                 if hour < 0 or hour > 23 then
820                         return false, "Invalid hour (must be between 0 and 23 inclusive)."
821                 elseif minute < 0 or minute > 59 then
822                         return false, "Invalid minute (must be between 0 and 59 inclusive)."
823                 end
824                 core.set_timeofday((hour * 60 + minute) / 1440)
825                 core.log("action", ("%s sets time to %d:%02d"):format(name, hour, minute))
826                 return true, "Time of day changed."
827         end,
828 })
829
830 core.register_chatcommand("days", {
831         description = "Display day count",
832         func = function(name, param)
833                 return true, "Current day is " .. core.get_day_count()
834         end
835 })
836
837 core.register_chatcommand("shutdown", {
838         description = "Shutdown server",
839         params = "[reconnect] [message]",
840         privs = {server=true},
841         func = function(name, param)
842                 core.log("action", name .. " shuts down server")
843                 core.chat_send_all("*** Server shutting down (operator request).")
844                 local reconnect, message = param:match("([^ ]+)(.*)")
845                 core.request_shutdown(message:trim(), minetest.is_yes(reconnect))
846         end,
847 })
848
849 core.register_chatcommand("ban", {
850         params = "<name>",
851         description = "Ban IP of player",
852         privs = {ban=true},
853         func = function(name, param)
854                 if param == "" then
855                         return true, "Ban list: " .. core.get_ban_list()
856                 end
857                 if not core.get_player_by_name(param) then
858                         return false, "No such player."
859                 end
860                 if not core.ban_player(param) then
861                         return false, "Failed to ban player."
862                 end
863                 local desc = core.get_ban_description(param)
864                 core.log("action", name .. " bans " .. desc .. ".")
865                 return true, "Banned " .. desc .. "."
866         end,
867 })
868
869 core.register_chatcommand("unban", {
870         params = "<name/ip>",
871         description = "Remove IP ban",
872         privs = {ban=true},
873         func = function(name, param)
874                 if not core.unban_player_or_ip(param) then
875                         return false, "Failed to unban player/IP."
876                 end
877                 core.log("action", name .. " unbans " .. param)
878                 return true, "Unbanned " .. param
879         end,
880 })
881
882 core.register_chatcommand("kick", {
883         params = "<name> [reason]",
884         description = "Kick a player",
885         privs = {kick=true},
886         func = function(name, param)
887                 local tokick, reason = param:match("([^ ]+) (.+)")
888                 tokick = tokick or param
889                 if not core.kick_player(tokick, reason) then
890                         return false, "Failed to kick player " .. tokick
891                 end
892                 local log_reason = ""
893                 if reason then
894                         log_reason = " with reason \"" .. reason .. "\""
895                 end
896                 core.log("action", name .. " kicks " .. tokick .. log_reason)
897                 return true, "Kicked " .. tokick
898         end,
899 })
900
901 core.register_chatcommand("clearobjects", {
902         params = "[full|quick]",
903         description = "Clear all objects in world",
904         privs = {server=true},
905         func = function(name, param)
906                 local options = {}
907                 if param == "" or param == "full" then
908                         options.mode = "full"
909                 elseif param == "quick" then
910                         options.mode = "quick"
911                 else
912                         return false, "Invalid usage, see /help clearobjects."
913                 end
914
915                 core.log("action", name .. " clears all objects ("
916                                 .. options.mode .. " mode).")
917                 core.chat_send_all("Clearing all objects.  This may take long."
918                                 .. "  You may experience a timeout.  (by "
919                                 .. name .. ")")
920                 core.clear_objects(options)
921                 core.log("action", "Object clearing done.")
922                 core.chat_send_all("*** Cleared all objects.")
923         end,
924 })
925
926 core.register_chatcommand("msg", {
927         params = "<name> <message>",
928         description = "Send a private message",
929         privs = {shout=true},
930         func = function(name, param)
931                 local sendto, message = param:match("^(%S+)%s(.+)$")
932                 if not sendto then
933                         return false, "Invalid usage, see /help msg."
934                 end
935                 if not core.get_player_by_name(sendto) then
936                         return false, "The player " .. sendto
937                                         .. " is not online."
938                 end
939                 core.log("action", "PM from " .. name .. " to " .. sendto
940                                 .. ": " .. message)
941                 core.chat_send_player(sendto, "PM from " .. name .. ": "
942                                 .. message)
943                 return true, "Message sent."
944         end,
945 })
946
947 core.register_chatcommand("last-login", {
948         params = "[name]",
949         description = "Get the last login time of a player",
950         func = function(name, param)
951                 if param == "" then
952                         param = name
953                 end
954                 local pauth = core.get_auth_handler().get_auth(param)
955                 if pauth and pauth.last_login then
956                         -- Time in UTC, ISO 8601 format
957                         return true, "Last login time was " ..
958                                 os.date("!%Y-%m-%dT%H:%M:%SZ", pauth.last_login)
959                 end
960                 return false, "Last login time is unknown"
961         end,
962 })