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