-- 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" } 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 -- 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 -- Chatcommands -- 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 frequency = tonumber(ticker[2]) or yl_ticker.settings.frequency or 3600 local runtime = tonumber(ticker[3]) or 7257600 -- 12 weeks local owner = name local success, ticker_id = yl_ticker.set(message, frequency, runtime, owner) return success, "Ticker ID " .. tostring(ticker_id) end function yl_ticker.chatcommand_ticker_copy(name, param) -- param is a numerical a_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 a_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 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 /ticker_list 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", "owner", "runtime", "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 runtime = tostring(ticker.runtime) or "N/A" local frequency = tostring(ticker.frequency) or "N/A" local t = {id, message, created, owner, runtime, 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 a_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.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 /ticker_say 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 a_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 /ticker_help without parameters." end local success, message = yl_ticker.display_help() if (success == false) then return false, message end return true, message end