Browse Source

Initial commit

master
luk3yx 3 months ago
commit
9a139e44b7
8 changed files with 702 additions and 0 deletions
  1. 22
    0
      LICENSE.md
  2. 49
    0
      README.md
  3. 238
    0
      console.lua
  4. 268
    0
      core.lua
  5. 20
    0
      init.lua
  6. 49
    0
      nodes.lua
  7. 56
    0
      persistence.lua
  8. BIN
      textures/snippets_button.png

+ 22
- 0
LICENSE.md View File

@@ -0,0 +1,22 @@

# The MIT License (MIT)

Copyright © 2019 by luk3yx.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 49
- 0
README.md View File

@@ -0,0 +1,49 @@
# Minetest snippets mod

A way for admins to run and save lua snippets.

More documentation coming soon.

## API

- `snippets.register_snippet(name, <code or def>)`: Registers a snippet.
`def` can be a table containing `code` (or `func`), and optionally `owner`.
If `persistent` is specified, this snippet will remain registered across
reboots.
- `snippets.unregister_snippet(name)`: The opposite of
`snippets.register_snippet`.
- `snippets.registered_snippets`: A table containing the above snippets.
- `snippets.log(level, msg)`: For use inside snippets: Logs a message. `level`
can be `none`, `debug`, `info`, `warning`, or `error`.
- `snippets.register_on_log(function(snippet, level, msg))`: Run when
snippets.log is called. `snippet` is the name of the snippet. Newest
functions are called first. If a callback returns `true`, any remaining
functions are not called (including the built-in log function). Callbacks
can check what player (if any) owns a snippet with
`snippets.registered_snippets[snippet].owner`.
- `snippets.log_levels`: A table containing functions that run
`minetest.colorize` on log levels (if applicable).
Example: `snippets.log_levels.error('Hello')` →
`minetest.colorize('red', 'Hello')`
- `snippets.exec_as_player(player_or_name, code)`: Executes `code` (a string)
inside an "anonymous snippet" owned by the player.
- `snippets.exec(code)`: Executes `code` inside a generic snippet.
- `snippets.run(name, ...)`: Executes a snippet.

## Example snippets

`get_connected_names`:
```lua
local res = {}
for _, player in ipairs(minetest.get_connected_players()) do
table.insert(res, player:get_player_name())
end
return res
```

`greeting_test`:
```lua
for _, name in ipairs(snippets.run 'get_connected_names') do
minetest.chat_send_player(name, 'Hello ' .. name .. '!')
end
```

+ 238
- 0
console.lua View File

@@ -0,0 +1,238 @@
--
-- Snippet console - Allows players to create and edit persistent snippets
--

local snippet_list = {}
local selected_snippet = {}
local console_code = {}
local console_text = {}

minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
if snippet_list[name] then
snippet_list[name] = nil
selected_snippet[name] = nil
console_code[name] = nil
console_text[name] = nil
end
end)

function snippets.show_console(name)
local formspec = 'size[14,10]' ..
'label[0,0;My snippets]' ..
'textlist[0,0.5;3.5,7.4;snippetlist;#aaaaaaNew snippet'

snippet_list[name] = {}
for k, v in pairs(snippets.registered_snippets) do
if v.persistent then
table.insert(snippet_list[name], k)
end
end
table.sort(snippet_list[name])

local selected = 0
local unsaved = false
for id, snippet in ipairs(snippet_list[name]) do
formspec = formspec .. ',##' .. minetest.formspec_escape(snippet)
if snippet == selected_snippet[name] then
selected = id
local def = snippets.registered_snippets[snippet]
if (def and def.code or '') ~= console_code[name] then
formspec = formspec .. ' (unsaved)'
end
end
end

formspec = formspec .. ';' .. tostring(selected + 1) .. ']' ..
'button[0,8.1;3.7,0.75;save;Save]' ..
'button[0,8.85;3.7,0.75;save_as;Save as]' ..
'button_exit[0,9.6;3.7,0.75;quit;Quit]'

formspec = formspec ..
'textlist[3.9,6.01;10,4.04;ignore;'
if console_text[name] then
if #console_text[name] > 0 then
for id, msg in ipairs(console_text[name]) do
if id > 1 then formspec = formspec .. ',' end
formspec = formspec .. minetest.formspec_escape(msg)
end
formspec = formspec .. ',;' .. (#console_text[name] + 1)
else
formspec = formspec .. ';1'
end
formspec = formspec ..
']button[3.9,5.14;10.21,0.81;reset;Reset]' ..
'box[3.9,0.4;10,4.5;#ffffff]'
else
formspec = formspec .. ';1]' ..
'button[3.9,5.14;10.21,0.81;run;Run]'
end

if not console_code[name] then console_code[name] = '' end
local code = minetest.formspec_escape(console_code[name])
if code == '' and console_text[name] then code = '(no code)' end

local snippet, owner
if selected_snippet[name] then
snippet = minetest.colorize('#aaa', selected_snippet[name])
else
snippet = minetest.colorize('#888', 'New snippet')
end

local def = snippets.registered_snippets[selected_snippet[name]]
if def and def.owner then
owner = minetest.colorize('#aaa', def.owner)
elseif selected_snippet[name] then
owner = minetest.colorize('#888', 'none')
else
owner = minetest.colorize('#aaa', name)
end

formspec = formspec .. ']textarea[4.2,0.4;10.2,5.31;' ..
(console_text[name] and '' or 'code') .. ';Snippet: ' ..
minetest.formspec_escape(snippet .. ', owner: ' .. owner) .. ';' ..
code .. ']'

minetest.show_formspec(name, 'snippets:console', formspec)
end

function snippets.push_console_msg(name, msg, col)
if not col or col:sub(1, 1) ~= '#' or #col ~= 7 then
col = '##'
end

if console_text[name] then
table.insert(console_text[name], col .. tostring(msg))
snippets.show_console(name)
end
end

snippets.register_on_log(function(snippet, level, msg)
local owner = snippets.registered_snippets[snippet].owner
if not owner or not console_text[owner] then return end
if level ~= 'none' then
msg = level:sub(1, 1):upper() .. level:sub(2) .. ': ' .. msg
end

local col
if level == 'warning' then
col = '#FFFF00'
elseif level == 'error' then
col = '#FF0000'
elseif level == 'debug' then
col = '#888888'
end

local p = snippet:sub(1, 16) == 'snippets:player_'
if not p then msg = 'From snippet "' .. snippet .. '": ' .. msg end

snippets.push_console_msg(owner, msg, col)

if p then return true end
end)

minetest.register_chatcommand('snippets', {
description = 'Opens the snippets console.',
privs = {server=true},
func = function(name, param)
snippets.show_console(name)
return true, 'Opened the snippets console.'
end,
})

minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= 'snippets:console' and
formname ~= 'snippets:console_save_as' then
return
end
local name = player:get_player_name()

-- Sanity check
if not minetest.check_player_privs(name, 'server') then
if console_text[name] then
console_text[name] = nil
minetest.close_formspec(name, 'snippets:console')
elseif not fields.quit then
minetest.kick_player(name,
'You appear to be using a "hacked" client.')
end
return
elseif not console_code[name] then
return
end

-- Handle "Save as"
if formname == 'snippets:console_save_as' then
if not fields.filename or fields.filename == '' then
minetest.chat_send_player(name, 'Save operation cancelled.')
snippets.show_console(name)
return
end

-- Don't overwrite non-persistent snippets
local filename = fields.filename:gsub(':', '/')
while snippets.registered_snippets[filename] and
not snippets.registered_snippets[filename].persistent do
filename = filename .. '_'
end

-- Actually save it
snippets.register_snippet(filename, {
owner = name,
code = console_code[name],
persistent = true,
})

selected_snippet[name] = filename
snippets.show_console(name)
return
end

if fields.code then console_code[name] = fields.code end

if fields.ignore then
return
elseif fields.run then
local code = fields.code
console_text[name] = {}
snippets.show_console(name)
if not code or code == '' then return end
local good, msg = loadstring('return ' .. code)
if good then code = 'return ' .. code end
local res = snippets.exec_as_player(name, code)
if res ~= nil then
snippets.push_console_msg(name, res)
end
elseif fields.reset then
console_text[name] = nil
snippets.show_console(name)
elseif fields.snippetlist and snippet_list[name] then
local event = minetest.explode_textlist_event(fields.snippetlist)
local selected = snippet_list[name][event.index - 1]
if selected_snippet[name] == selected then return end
selected_snippet[name] = selected
if console_text[name] then console_text[name] = nil end
local def = snippets.registered_snippets[selected]
console_code[name] = def and def.code or ''
snippets.show_console(name)
elseif fields.save and selected_snippet[name] then
if console_code[name] == '' then
snippets.unregister_snippet(selected_snippet[name])
selected_snippet[name] = nil
else
snippets.register_snippet(selected_snippet[name], {
owner = name,
code = console_code[name],
persistent = true,
})
end
snippets.show_console(name)
elseif fields.save or fields.save_as and console_code[name] ~= '' then
console_text[name] = nil
minetest.show_formspec(name, 'snippets:console_save_as',
'field[filename;Please enter a new snippet name.;]')
elseif fields.quit then
-- console_code[name] = nil
console_text[name] = nil
end
end)

+ 268
- 0
core.lua View File

@@ -0,0 +1,268 @@
--
-- Minetest snippets mod: Attempt to prevent snippets from crashing the server
--

-- Make loadstring a local variable
local loadstring
if minetest.global_exists('loadstring') then
loadstring = _G.loadstring
else
loadstring = assert(load)
end

local copy = table.copy
local safe_funcs = {}
local orig_funcs, running_snippet

function snippets.get_current_snippet()
if running_snippet then return copy(running_snippet) end
end

-- Apply "safe functions": These wrap normal registration functions so that
-- snippets can't crash them as easily.
local function apply_safe_funcs()
if orig_funcs then return end
orig_funcs = {}
for k, v in pairs(safe_funcs) do
if k ~= 'print' then
orig_funcs[k] = minetest[k]
minetest[k] = v
end
end
orig_funcs.print, print = print, safe_funcs.print
end

local function remove_safe_funcs()
if not orig_funcs then return end
for k, v in pairs(orig_funcs) do
minetest[k] = orig_funcs[k]
end
print = orig_funcs.print
orig_funcs = nil
end

-- "Break out" of wrapped functions.
local function wrap_unsafe(func)
return function(...)
if orig_funcs then
remove_safe_funcs()
local res = {func(...)}
apply_safe_funcs()
return (table.unpack or unpack)(res)
else
return func(...)
end
end
end

-- Logging
snippets.registered_on_log = {}
snippets.log_levels = {}
function snippets.log_levels.error(n)
return minetest.colorize('red', n)
end
function snippets.log_levels.warning(n)
return minetest.colorize('yellow', n)
end
function snippets.log_levels.info(n)
return n
end
snippets.log_levels.none = snippets.log_levels.info
function snippets.log_levels.debug(n)
return minetest.colorize('grey', n)
end

function snippets.log(level, msg)
local snippet = running_snippet or 'snippets:anonymous'
if msg == nil then level, msg = 'none', level end
level, msg = tostring(level), tostring(msg)

if level == 'warn' then
level = 'warning'
elseif not snippets.log_levels[level] then
level = 'none'
end

for _, func in ipairs(snippets.registered_on_log) do
if func(snippet, level, msg) then return end
end
end
snippets.log = wrap_unsafe(snippets.log)

function snippets.register_on_log(func)
assert(type(func) == 'function')
table.insert(snippets.registered_on_log, 1, func)
end

-- Create the default log action
-- Only notify the player of errors or warnings
snippets.register_on_log(function(snippet, level, msg)
local rawmsg
if level == 'warning' then
rawmsg = 'Warning'
elseif level == 'error' then
rawmsg = 'Error'
else
return
end

rawmsg = snippets.log_levels[level](rawmsg .. ' in snippet "' .. snippet ..
'": ' .. msg)

local def = snippets.registered_snippets[snippet]
if def and def.owner then
minetest.chat_send_player(def.owner, rawmsg)
else
minetest.chat_send_all(rawmsg)
end
end)

-- Create a safe print()
function safe_funcs.print(...)
local msg = ''
for i = 1, select('#', ...) do
if i > 1 then msg = msg .. '\t' end
msg = msg .. tostring(select(i, ...))
end
snippets.log('none', msg)
end

-- Mostly copied from https://stackoverflow.com/a/26367080
local function wrap_raw(snippet, func, ...)
local old_running = running_snippet
running_snippet = snippet
local use_safe_funcs = not orig_funcs
if use_safe_funcs then apply_safe_funcs() end
local good, msg = pcall(func, ...)
if use_safe_funcs then remove_safe_funcs() end
if good then
running_snippet = old_running
return msg
else
snippets.log('error', msg)
running_snippet = old_running
end
end

local function wrap(snippet, func)
if not snippet then return func end
return function(...) return wrap_raw(snippet, func, ...) end
end

do
local after_ = minetest.after
function safe_funcs.after(after, func, ...)
after = tonumber(after)
assert(after and after == after, 'Invalid core.ater invocation')
after_(after, wrap_raw, running_snippet, func, ...)
end

function snippets.wrap_register_on(orig)
return function(func, ...)
return orig(wrap(running_snippet, func), ...)
end
end

for k, v in pairs(minetest) do
if type(k) == 'string' and k:sub(1, 12) == 'register_on_' then
safe_funcs[k] = snippets.wrap_register_on(v)
end
end
end

-- Register a snippet
snippets.registered_snippets = {}
function snippets.register_snippet(name, def)
if def == nil and type(name) == 'table' then
name, def = name.name, name
elseif type(name) ~= 'string' then
error('Invalid name passed to snippets.register_snippet!', 2)
elseif type(def) == 'string' then
def = {code=def}
elseif type(def) ~= 'table' then
error('Invalid definition passed to snippets.register_snippet!', 2)
elseif def.owner and type(def.owner) ~= 'string' then
error('Invalid owner passed to snippets.register_snippet!', 2)
end
def = table.copy(def)
def.name = name

if def.code then
local msg
def.func, msg = loadstring(def.code, name)
if def.func then
if name ~= 'snippets:anonymous' then
local old_def = snippets.registered_snippets[name]
def.env = old_def and old_def.env
end
if not def.env then
local g = {}
def.env = setmetatable({}, {__index = function(self, key)
local res = rawget(_G, key)
if res == nil and not g[key] then
snippets.log('warning', 'Undeclared global variable "'
.. key .. '" accessed.')
g[key] = true
end
return res
end})
end
setfenv(def.func, def.env)
else
local r, s = running_snippet, snippets.registered_snippets[name]
function def.func() end
running_snippet, snippets.registered_snippets[name] = name, def
snippets.log('error', 'Load error: ' .. tostring(msg))
running_snippet, snippets.registered_snippets[name] = r, s
end
else
def.persistent = nil
end
if not def.persistent then def.code = nil end
if type(def.func) ~= 'function' then return false end

snippets.registered_snippets[name] = def
return true
end
snippets.register_snippet('snippets:anonymous', '')

-- Run a snippet
function snippets.run(snippet, ...)
local def = snippets.registered_snippets[snippet]
if not def then error('Invalid snippet specified!', 2) end
return wrap_raw(snippet, def.func, ...)
end

-- Run code as player
function snippets.exec_as_player(name, code)
if minetest.is_player(name) then name = name:get_player_name() end
local owner
if name and name ~= '' then
owner = name
name = 'snippets:player_' .. tostring(name)
else
name = 'snippets:anonymous'
end

local def = {
code = tostring(code),
owner = owner,
}
if not snippets.register_snippet(name, def) then return end

return snippets.run(name)
end

function snippets.exec(code) return snippets.exec_as_player(nil, code) end

minetest.register_on_leaveplayer(function(player)
snippets.registered_snippets['snippets:player_' ..
player:get_player_name()] = nil
end)

-- In case console.lua isn't loaded
function snippets.unregister_snippet(name)
if snippets.registered_snippets[name] ~= nil then
snippets.registered_snippets[name] = nil
end
end

+ 20
- 0
init.lua View File

@@ -0,0 +1,20 @@
--
-- Minetest snippets mod: Allows admins to run a bunch of predefined snippets
--

assert(minetest.get_current_modname() == 'snippets')
snippets = {}

local modpath = minetest.get_modpath('snippets')

-- Load the core sandbox
dofile(modpath .. '/core.lua')

-- Load persistence
loadfile(modpath .. '/persistence.lua')(minetest.get_mod_storage())

-- Load the "console"
dofile(modpath .. '/console.lua')

-- Load "snippet buttons"
dofile(modpath .. '/nodes.lua')

+ 49
- 0
nodes.lua View File

@@ -0,0 +1,49 @@
--
-- Buttons that run snippets
--

minetest.register_node('snippets:button', {
description = 'Snippets button',
tiles = {'default_steel_block.png', 'default_steel_block.png',
'default_steel_block.png^snippets_button.png'},
groups = {cracky = 2},

on_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string('infotext', 'Unconfigured snippets button')
meta:set_string('formspec', 'field[snippet;Snippet to run:;]')
end,

on_receive_fields = function(pos, formname, fields, sender)
if not fields.snippet or fields.snippet == '' then return end

local name = sender:get_player_name()
if not minetest.check_player_privs(name, {server=true}) then
minetest.chat_send_player(name, 'Insufficient privileges!')
return
end

local snippet = fields.snippet
if not snippets.registered_snippets[snippet] or
snippet:sub(1, 9) == 'snippets:' then
minetest.chat_send_player(name, 'Unknown snippet!')
else
local meta = minetest.get_meta(pos)
meta:set_string('snippet', snippet)
meta:set_string('infotext', 'Snippet: ' .. fields.snippet)
meta:set_string('formspec', '')
end
end,

on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local meta, name = minetest.get_meta(pos), clicker:get_player_name()
local snippet = meta:get_string('snippet')
if not snippet or snippet == '' then return end
if snippets.registered_snippets[snippet] then
snippets.run(snippet, name)
else
minetest.chat_send_player(name, 'Invalid snippet: "' .. snippet ..
'"')
end
end,
})

+ 56
- 0
persistence.lua View File

@@ -0,0 +1,56 @@
--
-- Persistent snippets
--

-- Get storage
local storage = ...
assert(storage)

-- Load persistent snippets
local register_snippet_raw = snippets.register_snippet
do
for name, def in pairs(storage:to_table().fields) do
if name:sub(1, 1) == '>' then
def = minetest.deserialize(def)
if def then
def.persistent = true
register_snippet_raw(name:sub(2), def)
end
end
end
end

-- Override snippets.register_snippet so it accepts the "persistent" field.
function snippets.register_snippet(name, def)
if def == nil and type(name) == 'table' then
name, def = name.name, name
end

-- Fix tracebacks
local good, msg = pcall(register_snippet_raw, name, def)
if not good then error(msg, 2) end
if not msg then return msg end

-- Check for def.persistent
def = snippets.registered_snippets[name]
if type(def) == 'table' and def.persistent and def.code then
print('Saving snippet', name)
storage:set_string('>' .. name, minetest.serialize({
code = def.code,
owner = def.owner,
}))
end

-- Return the same value as register_snippet_raw.
return msg
end

-- Override snippets.unregister_snippet
local unregister_snippet_raw = snippets.unregister_snippet
function snippets.unregister_snippet(name)
local def = snippets.registered_snippets[name]
if def and def.persistent then
storage:set_string('>' .. name, '')
end
return unregister_snippet_raw(name)
end

BIN
textures/snippets_button.png View File


Loading…
Cancel
Save