generated from your-land/yl_template
611 lines
19 KiB
Lua
611 lines
19 KiB
Lua
-- The functions and variables in this file are only for use in the mod itself.
|
|
-- Those that do real work should be local and wrapped in public functions
|
|
local debug = yl_ticker.settings.debug or true
|
|
|
|
local function log(text)
|
|
if debug then minetest.log("action", "[MOD] yl_ticker : " .. text) end
|
|
end
|
|
|
|
function yl_ticker.log(text) return log(text) end
|
|
|
|
-- Storage
|
|
|
|
local function get_savepath()
|
|
local save_path = yl_ticker.settings.save_path
|
|
local path = yl_ticker.worldpath .. DIR_DELIM .. save_path
|
|
log("get_savepath : " .. dump(path))
|
|
return path
|
|
end
|
|
|
|
local function get_filepath(filename)
|
|
local path_to_file = get_savepath() .. DIR_DELIM .. filename
|
|
log("get_filepath : " .. dump(filename) .. ":" .. dump(path_to_file))
|
|
return path_to_file
|
|
end
|
|
|
|
local function save_json(filename, content)
|
|
if type(filename) ~= "string" or type(content) ~= "table" then
|
|
return false
|
|
end
|
|
local save_path = get_filepath(filename)
|
|
local save_content = minetest.write_json(content)
|
|
log("save_json : " .. dump(save_path) .. ":" .. dump(save_content))
|
|
return minetest.safe_file_write(save_path, save_content)
|
|
end
|
|
|
|
local function load_json(path_to_file)
|
|
local file = io.open(path_to_file, "r")
|
|
if not file then return false, "Error opening file: " .. path_to_file end
|
|
|
|
local content = file:read("*all")
|
|
file:close()
|
|
|
|
if not content then return false, "Error reading file: " .. path_to_file end
|
|
log("load_json : " .. dump(path_to_file) .. ":" .. dump(content))
|
|
return true, minetest.parse_json(content)
|
|
end
|
|
|
|
-- Public functions wrap the private ones, so they can be exchanged easily
|
|
function yl_ticker.load_json(filename, ...) return load_json(filename, ...) end
|
|
|
|
function yl_ticker.save_json(filename, content, ...)
|
|
return save_json(filename, content, ...)
|
|
end
|
|
|
|
-- load_all_data
|
|
--
|
|
|
|
local function is_visible(filename) return (string.sub(filename, 1, 1) ~= ".") end
|
|
|
|
local function is_json(filename) return (filename:match("%.json$")) end
|
|
|
|
local function validate_json(content, schema)
|
|
|
|
-- Are all fields mentioned in the schema?
|
|
for key, _ in pairs(content) do
|
|
if schema[key] == nil then
|
|
log("validate_json : Unexpected field in key = " .. dump(key))
|
|
return false, "Unexpected field in " .. dump(key)
|
|
end
|
|
end
|
|
|
|
-- Are all fields of the expected type?
|
|
for key, expected_type in pairs(schema) do
|
|
if type(content[key]) ~= expected_type then
|
|
log("validate_json : Validation error in key = " .. dump(key))
|
|
return false,
|
|
"Validation error in " .. dump(key) .. " not of type " ..
|
|
dump(expected_type)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local schema = {
|
|
id = "number",
|
|
creation_date = "number",
|
|
message = "string",
|
|
frequency = "number",
|
|
runtime = "number",
|
|
owner = "string",
|
|
param = "string"
|
|
}
|
|
|
|
local function load_all_data()
|
|
-- Get all json files from savepath
|
|
-- Excluding invisible
|
|
-- Excluding non-json files
|
|
local save_path = get_savepath()
|
|
local files = minetest.get_dir_list(save_path, false) or {}
|
|
local data = {}
|
|
local total = 0
|
|
local good = 0
|
|
local bad = 0
|
|
for key, filename in ipairs(files) do
|
|
if is_visible(filename) and is_json(filename) then
|
|
total = total + 1
|
|
local filepath = get_filepath(filename)
|
|
local success, content = load_json(filepath)
|
|
|
|
if success and content.id and
|
|
(validate_json(content, schema) == true) then
|
|
good = good + 1
|
|
data[content.id] = content
|
|
else
|
|
bad = bad + 1
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
yl_ticker.data = data
|
|
|
|
if bad == 0 then
|
|
minetest.log("action",
|
|
"[MOD] yl_ticker : bad = " .. tostring(bad) .. ", good = " ..
|
|
tostring(good) .. ", total = " .. tostring(total))
|
|
return true, good, bad
|
|
else
|
|
minetest.log("warning",
|
|
"[MOD] yl_ticker : bad = " .. tostring(bad) .. ", good = " ..
|
|
tostring(good) .. ", total = " .. tostring(total))
|
|
return false, good, bad
|
|
end
|
|
end
|
|
|
|
function yl_ticker.load_all_data() return load_all_data() end
|
|
|
|
-- check privs
|
|
--
|
|
|
|
local function ends_with(str, suffix) return str:sub(-suffix:len()) == suffix end
|
|
|
|
local function split(str)
|
|
local parts = {}
|
|
for part in str:gmatch("[^,%s]+") do table.insert(parts, part) end
|
|
return parts
|
|
end
|
|
|
|
local function priv_exists(priv)
|
|
return (minetest.registered_privileges[priv] ~= nil) or false
|
|
end
|
|
|
|
local function check_privs()
|
|
for key, value in pairs(yl_ticker.settings) do
|
|
if ends_with(key, "_privs") then
|
|
local parts = split(value)
|
|
for _, part in ipairs(parts) do
|
|
assert(priv_exists(part), "yl_ticker : configured priv " ..
|
|
dump(part) .. " does not exist.")
|
|
end
|
|
end
|
|
end
|
|
log("PASS priv check")
|
|
end
|
|
|
|
function yl_ticker.check_privs() return check_privs() end
|
|
|
|
-- Remove file
|
|
--
|
|
|
|
local function remove_file(filename)
|
|
local filepath = get_filepath(filename)
|
|
return os.remove(filepath)
|
|
end
|
|
|
|
function yl_ticker.remove_file(filename) return remove_file(filename) end
|
|
|
|
-- Help
|
|
--
|
|
|
|
local help_texts = {}
|
|
|
|
local function register_help(chatcommand_cmd, chatcommand_definition)
|
|
local definition = {
|
|
chatcommand = chatcommand_cmd,
|
|
params = chatcommand_definition.params,
|
|
description = chatcommand_definition.description,
|
|
privs = chatcommand_definition.privs
|
|
}
|
|
help_texts[chatcommand_cmd] = definition
|
|
end
|
|
|
|
function yl_ticker.register_help(chatcommand_cmd, chatcommand_definition)
|
|
return register_help(chatcommand_cmd, chatcommand_definition)
|
|
end
|
|
|
|
local function display_help()
|
|
if (type(help_texts) ~= "table") then
|
|
return false, "Help texts not a table"
|
|
end
|
|
if (next(help_texts) == nil) then return false, "Help text no content" end
|
|
|
|
local message = {}
|
|
|
|
for chatcommand, definition in pairs(help_texts) do
|
|
|
|
local privs = ""
|
|
|
|
if (definition.privs and (type(definition.privs) == "table")) then
|
|
for priv, _ in pairs(definition.privs) do
|
|
privs = privs .. priv .. ", "
|
|
end
|
|
end
|
|
|
|
table.insert(message, minetest.colorize("#FF6700", "/" .. chatcommand))
|
|
|
|
if (definition.params and (type(definition.params) == "string")) then
|
|
table.insert(message, minetest.colorize("#FFFF00", "Params:" ..
|
|
definition.params))
|
|
end
|
|
|
|
if (definition.description and
|
|
(type(definition.description) == "string")) then
|
|
table.insert(message, minetest.colorize("#FFFF00", "Description:" ..
|
|
definition.description))
|
|
end
|
|
|
|
table.insert(message,
|
|
minetest.colorize("#FFFF00", "Privs:" .. privs) .. "\n")
|
|
|
|
end
|
|
|
|
local s_message = table.concat(message, "\n")
|
|
|
|
return true, s_message
|
|
end
|
|
|
|
function yl_ticker.display_help() return display_help() end
|
|
|
|
local function display_examples()
|
|
if (type(yl_ticker.settings.examples) ~= "string") then
|
|
return false, "settings.examples not a string"
|
|
end
|
|
local content_raw = yl_ticker.settings.examples:gsub("\\n", "\n")
|
|
local content = minetest.formspec_escape(content_raw)
|
|
local formspec = "formspec_version[6]" .. "size[16,6]" ..
|
|
"button_exit[15.4,0.1;0.5,0.5;X;X]" ..
|
|
"textarea[0.05,0.05;15.3,5.9;;;" .. content .. "]"
|
|
return true, formspec
|
|
end
|
|
|
|
function yl_ticker.display_examples() return display_examples() end
|
|
|
|
-- Chatcommands
|
|
--
|
|
|
|
local function convert_to_seconds(time, unit)
|
|
|
|
local lower_unit = string.lower(unit)
|
|
|
|
local time_units = {s = 1, m = 60, h = 3600, d = 86400, w = 604800}
|
|
|
|
if (time == nil) then return false, "No time detected" end
|
|
|
|
local n_time = tonumber(time)
|
|
|
|
if (type(n_time) ~= "number") then return false, "Time must be a number" end
|
|
|
|
if (lower_unit == "") then
|
|
-- default to seconds
|
|
lower_unit = "s"
|
|
end
|
|
|
|
if (time_units[lower_unit] == nil) then lower_unit = "s" end
|
|
|
|
local seconds = n_time * time_units[lower_unit]
|
|
|
|
return true, seconds
|
|
end
|
|
|
|
local function to_frequency(time_string)
|
|
|
|
if (type(time_string) ~= "string") then return false, "Must be a string" end
|
|
|
|
local lower_time_string = string.lower(time_string)
|
|
|
|
local pattern = "^%s*(%d+)%s*([smhdw]?)%s*$"
|
|
local time, unit = string.match(lower_time_string, pattern)
|
|
|
|
if (time == nil) then return false, "No time detected" end
|
|
|
|
local n_time = tonumber(time)
|
|
|
|
if (type(n_time) ~= "number") then return false, "Time must be a number" end
|
|
|
|
if (n_time < 1) then return false, "Time must be greater than 1" end
|
|
|
|
if (unit == "") then
|
|
-- default to seconds
|
|
unit = "s"
|
|
end
|
|
|
|
return convert_to_seconds(time, unit)
|
|
end
|
|
|
|
local function to_runtime(time_string)
|
|
|
|
if (type(time_string) ~= "string") then return false, "Must be a string" end
|
|
|
|
local lower_time_string = string.lower(time_string)
|
|
|
|
local pattern = "^%s*(%d+)%s*(%l*)%s*$"
|
|
local time, unit = string.match(lower_time_string, pattern)
|
|
|
|
if (time == nil) then return false, "No time detected" end
|
|
|
|
local n_time = tonumber(time)
|
|
|
|
if (type(n_time) ~= "number") then return false, "Time must be a number" end
|
|
|
|
if (n_time < 1) then return false, "Time must be greater than 1" end
|
|
|
|
if (unit == "") then
|
|
-- default to seconds
|
|
unit = "s"
|
|
end
|
|
|
|
if (unit == "utc") then return true, n_time end
|
|
|
|
local current_time = os.time()
|
|
local c_success, seconds = convert_to_seconds(time, unit)
|
|
|
|
if c_success == false then return false, seconds end
|
|
|
|
if (type(seconds) ~= "number") then return false, "" end
|
|
|
|
local runtime = current_time + seconds
|
|
|
|
return true, runtime
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_add(name, param) -- param is a string containing a message and more
|
|
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if (not param) or (type(param) ~= "string") or (param == "") then
|
|
return false, "Requirements not met"
|
|
end
|
|
-- Create ticker
|
|
local ticker = string.split(param, "$", true)
|
|
|
|
local message = ticker[1] or ""
|
|
|
|
local f_success, frequency = to_frequency(ticker[2])
|
|
if (f_success == false) then
|
|
return false,
|
|
"Cannot understand frequency format: " .. tostring(frequency)
|
|
end
|
|
|
|
local r_success, runtime = to_runtime(ticker[3])
|
|
if (r_success == false) then
|
|
return false, "Cannot understand runtime format: " .. tostring(runtime)
|
|
end
|
|
local owner = name
|
|
|
|
local success, ticker_id = yl_ticker.set(message, frequency, runtime, owner,
|
|
param)
|
|
return success, "Ticker ID " .. tostring(ticker_id)
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_copy(name, param) -- param is a numerical ticker_id
|
|
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param == "" then return false, "Ticker ID missing" end
|
|
local ticker_id = tonumber(param)
|
|
if type(ticker_id) ~= "number" then
|
|
return false, "Ticker ID is not a number"
|
|
end
|
|
if (ticker_id <= 0) then
|
|
return false, "Ticker ID cannot be zero or negative"
|
|
end
|
|
|
|
local success, formspecstring = yl_ticker.formspec(ticker_id)
|
|
|
|
if (success == false) then return false, formspecstring end
|
|
|
|
-- Send the formspec
|
|
minetest.show_formspec(name, "yl_ticker:copy", formspecstring)
|
|
-- Report
|
|
return true, "Copied ticker ID " .. tostring(ticker_id)
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_delete(name, param) -- param is a numerical ticker_id
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param == "" then return false, "Ticker ID missing" end
|
|
local ticker_id = tonumber(param)
|
|
if type(ticker_id) ~= "number" then
|
|
return false, "Ticker ID not a number"
|
|
end
|
|
|
|
local success, ticker = yl_ticker.delete(ticker_id)
|
|
|
|
if success == false then
|
|
return false, ticker
|
|
else
|
|
return true, "Deleted ticker ID " .. tostring(ticker_id)
|
|
end
|
|
end
|
|
|
|
-- List ticker
|
|
|
|
-- taken from yl_cinema
|
|
-- TODO: Should we API-fy this?
|
|
|
|
local function format_table(t)
|
|
-- Format of t must be {{row1,row2,row3, ...},{row1,row2,row3, ...},...}
|
|
local blanks_between_rows = 3
|
|
local max_row_length = {}
|
|
for linenumber = 1, #t do
|
|
for rownumber = 1, #t[linenumber] do
|
|
local row_length = #tostring(t[linenumber][rownumber])
|
|
if (max_row_length[rownumber] or 0) < row_length then
|
|
max_row_length[rownumber] = row_length
|
|
end
|
|
end
|
|
end
|
|
|
|
local ret = {}
|
|
|
|
for linenumber = 1, #t do
|
|
local line_s = ""
|
|
for rownumber = 1, #t[linenumber] do
|
|
local text = t[linenumber][rownumber]
|
|
local text_length = #tostring(text)
|
|
local add_blanks = max_row_length[rownumber] - text_length
|
|
local newtext = t[linenumber][rownumber]
|
|
for add = 1, (add_blanks + blanks_between_rows) do
|
|
newtext = newtext .. " "
|
|
end
|
|
line_s = line_s .. newtext
|
|
end
|
|
table.insert(ret, line_s)
|
|
end
|
|
return table.concat(ret, "\n")
|
|
end
|
|
|
|
local function format_time_left(run_until)
|
|
|
|
if (type(run_until) ~= "number") then return "N/A" end
|
|
|
|
local seconds_left = run_until - os.time()
|
|
|
|
if (seconds_left <= 0) then return "N/A" end
|
|
|
|
local seconds = math.floor(seconds_left % 60)
|
|
local minutes = math.floor((seconds_left / 60) % 60)
|
|
local hours = math.floor((seconds_left / 3600) % 24)
|
|
local days = math.floor(seconds_left / 86400)
|
|
|
|
return string.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
|
|
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_list_all(name, param) -- param must be empty
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param ~= "" then
|
|
return false, "This command lists all tickers. " .. "Do /" ..
|
|
yl_ticker.settings.chatcommand_domain ..
|
|
"_list <ticker_id> if you want to have only one."
|
|
end
|
|
|
|
local success, data = yl_ticker.list()
|
|
|
|
if (success == false) then return false, data end
|
|
|
|
local f_ticker = {
|
|
{
|
|
"ID", "message", "created utc", "owner", "run until utc",
|
|
"time left", "frequency"
|
|
}
|
|
}
|
|
for _, ticker in pairs(data) do
|
|
|
|
local id = tostring(ticker.id) or "N/A"
|
|
local message = ticker.message or "N/A"
|
|
local created = os.date("!%Y-%m-%d %H:%M:%S",
|
|
(ticker.creation_date or 0)) or "N/A"
|
|
local owner = ticker.owner or "N/A"
|
|
local run_until =
|
|
os.date("!%Y-%m-%d %H:%M:%S", (ticker.runtime or 0)) or "N/A"
|
|
local time_left = format_time_left(ticker.runtime or 0) or "N/A"
|
|
local frequency = tostring(ticker.frequency) or "N/A"
|
|
|
|
local t = {id, message, created, owner, run_until, time_left, frequency}
|
|
table.insert(f_ticker, t)
|
|
end
|
|
|
|
return true, format_table(f_ticker)
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_list(name, param) -- param is a numerical ticker_id
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param == "" then
|
|
return false, "Ticker ID is missing"
|
|
end
|
|
local ticker_id = tonumber(param)
|
|
if type(ticker_id) ~= "number" then
|
|
return false, "Ticker ID not a number"
|
|
end
|
|
|
|
local success, ticker = yl_ticker.get(ticker_id)
|
|
|
|
if ((success == false) or (success == nil)) then
|
|
return false, "Ticker not found"
|
|
end
|
|
|
|
return true, dump(ticker)
|
|
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_say_all(name, param) -- param must be empty
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param ~= "" then
|
|
return false,
|
|
"This command sends all ticker to the main chat. " .. "Do /" ..
|
|
yl_ticker.settings.chatcommand_domain ..
|
|
"_say <ticker_id> if you want to send only one."
|
|
end
|
|
|
|
local success, data = yl_ticker.list()
|
|
|
|
if (success == false) then return false, "No data" end
|
|
|
|
local n = 0
|
|
for _, ticker in pairs(data) do
|
|
local s_success, s_message = yl_ticker.say(ticker.id, "*")
|
|
if (s_success == false) then return false, s_message end
|
|
n = n + 1
|
|
end
|
|
|
|
return true, "Sent " .. tostring(n) .. " ticker to public."
|
|
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_say(name, param) -- param is a numerical ticker_id
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param == "" then return false, "Ticker ID missing" end
|
|
local ticker_id = tonumber(param)
|
|
if type(ticker_id) ~= "number" then
|
|
return false, "Ticker ID not a number"
|
|
end
|
|
|
|
local s_success, s_message = yl_ticker.say(ticker_id, "*")
|
|
if (s_success == false) then return false, s_message end
|
|
|
|
return true, "Sent ticker " .. tostring(ticker_id) .. " to public."
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_help(name, param) -- param must be empty
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param ~= "" then
|
|
return false,
|
|
"This command displays the help for the ticker. " .. "Do /" ..
|
|
yl_ticker.settings.chatcommand_domain ..
|
|
"_help without parameters."
|
|
end
|
|
|
|
local success, message = yl_ticker.display_help()
|
|
|
|
if (success == false) then return false, message end
|
|
|
|
return true, message
|
|
end
|
|
|
|
function yl_ticker.chatcommand_ticker_examples(name, param)
|
|
-- defense
|
|
local player = minetest.get_player_by_name(name)
|
|
if not player then return false, "Player not online" end
|
|
if param ~= "" then
|
|
return false,
|
|
"This command displays the help for the ticker. " .. "Do /" ..
|
|
yl_ticker.settings.chatcommand_domain ..
|
|
"_examples without parameters."
|
|
end
|
|
|
|
local success, formspecstring = yl_ticker.display_examples()
|
|
|
|
if (success == false) then return false, formspecstring end
|
|
|
|
-- Send the formspec
|
|
minetest.show_formspec(name, "yl_ticker:examples", formspecstring)
|
|
-- Report
|
|
return true, "Showed examples"
|
|
end
|