epidermis/skindb.lua

243 lines
6.8 KiB
Lua

-- SkinDB (https://bitbucket.org/kingarthursteam/mt-skin-db/src/master/) support
--[[
Assumptions:
- Skins are usually added
- Skins are rarely removed by the admin / hoster
- Skins are never changed
- `GROUP BY` works like `ORDER BY` (otherwise no ordering is guaranteed)
]]
-- Load offline copy
local texture_path = epidermis.paths.dynamic_textures.skindb
epidermis.skins = {}
local function on_local_copy_loaded() end
local function load_local_copy()
local ids = {}
for _, filename in ipairs(minetest.get_dir_list(texture_path, false)) do
local id = filename:match"^epidermis_skindb_(%d+)%.png$"
if id then
table.insert(ids, tonumber(id))
end
end
table.sort(ids)
for index, id in ipairs(ids) do
local filename = ("epidermis_skindb_%d.png"):format(id)
local path = modlib.file.concat_path{texture_path, filename}
local metafile = assert(io.open(modlib.file.concat_path{texture_path, filename .. ".json"}))
local meta = modlib.json:read_file(metafile)
metafile:close()
meta.texture = "blank.png" -- dynamic media isn't available yet
epidermis.skins[index] = meta
epidermis.dynamic_add_media(path, function()
meta.texture = filename
end, false) -- Enable caching for SkinDB skins
end
on_local_copy_loaded()
end
-- HACK wait a globalstep before loading the local copy to prevent the dynamic media join race condition in singleplayer
minetest.after(0, function()
minetest.after(0, load_local_copy)
end)
-- HTTP-requiring code
local http = ...
if not http then
function on_local_copy_loaded()
if #epidermis.skins == 0 then -- empty local copy...
epidermis.skins = nil -- ... disable skin picking entirely
end
end
return -- disable entirely
end
local base_url = "http://minetest.fensta.bplaced.net"
-- Uploading
epidermis.upload_licenses = {
"CC BY-SA 3.0",
"CC BY-NC-SA 3.0",
"CC BY 3.0",
"CC BY 4.0",
"CC BY-SA 4.0",
"CC BY-NC-SA 4.0",
"CC 0 (1.0)"
}
function epidermis.upload(params)
http.fetch({
url = base_url .. "/api/v2/upload.php",
timeout = 10,
method = "POST",
data = {
name = assert(params.name),
author = assert(params.author),
license = assert(params.license),
img = "data:image/png;base64," .. minetest.encode_base64(params.raw_png_data)
},
extra_headers = { "Accept: application/json", "Accept-Charset: utf-8" },
}, function(res)
if res.timeout then
params.on_complete"Timeout"
return
end
if not res.succeeded then
params.on_complete("HTTP status code: " .. res.code)
return
end
local status, data_or_err = pcall(modlib.json.read_string, modlib.json, res.data)
if not status then
local err = data_or_err
params.on_complete("JSON error: " .. err)
return
end
local data = data_or_err
if not data.success then
local message = data.status_msg
if #message > 100 then -- trim to 100 characters
message = message:sub(1, 100) .. "..."
end
params.on_complete(("SkinDB error message: %q"):format(message))
end
params.on_complete() -- success
end)
end
minetest.register_privilege("epidermis_upload", {
description = "Can upload skins",
give_to_singleplayer = false,
give_to_admin = false,
})
-- "Downloading" / Updating
local timeout = 10
local html_unescape = modlib.web.html.unescape
local function fetch_page(num, per_page, func, retry_time)
local function on_fail()
if retry_time then
modlib.minetest.after(retry_time, fetch_page, num, per_page, func, retry_time)
return
end
func()
end
http.fetch({
url = ("%s/api/v2/get.json.php?getlist&outformat=base64&page=%d&per_page=%d"):format(base_url, num, per_page),
timeout = timeout,
method = "GET",
extra_headers = { "Accept-Charset: utf-8" },
}, function(res)
if not res.succeeded then
return on_fail()
end
local status, data = pcall(modlib.json.read_string, modlib.json, res.data)
if not status then
return on_fail()
end
local skins = data.skins
-- Check sortedness of skins
for i = 2, #skins do
assert(skins[i - 1].id < skins[i].id)
end
func(data.pages, skins)
end)
end
local function add_skin(skin, index)
assert(skin.type == "image/png")
assert(type(skin.id) == "number" and skin.id % 1 == 0)
assert(type(skin.uploaded) == "string" and #skin.uploaded < 100)
-- These fields may have been incorrectly & automatically casted to numbers by SkinDB (PHP)
local name = html_unescape(tostring(skin.name))
local author = html_unescape(tostring(skin.author))
local license = tostring(skin.license)
local uploaded = skin.uploaded == "0000-00-00 00:00:00" and "Before 2013-08-11" or skin.uploaded
local data = assert(minetest.decode_base64(skin.img))
local meta = {
id = skin.id,
name = name,
author = author,
license = license,
uploaded = uploaded
}
local path, texture = epidermis.write_skindb_skin(skin.id, data, meta)
meta.texture = "blank.png"
if index then -- replace at index
epidermis.skins[index] = meta
else
table.insert(epidermis.skins, meta)
end
epidermis.dynamic_add_media(path, function()
meta.texture = texture
end, false)
end
local function page(pagenum, per_page, on_complete)
fetch_page(pagenum, per_page, function(pages, skins)
pagenum = math.min(pagenum, pages)
local start = math.min(1 + #epidermis.skins - (pagenum - 1) * per_page, per_page + 1)
for i = start - 1, 1, -1 do
local index = i + (pagenum - 1) * per_page
if skins[i].id > epidermis.skins[index].id then -- Deletion
epidermis.remove_skindb_skin(epidermis.skins[index])
add_skin(skins[i], index)
end
end
for i = start, #skins do
add_skin(skins[i])
end
if pagenum < pages then
return page(pagenum + 1, per_page, on_complete)
end
-- Last page reached, delete leftover skins
for i = (pagenum - 1) * per_page + #skins + 1, #epidermis.skins do
epidermis.skins[i] = nil
end
(on_complete or modlib.func.no_op)()
end, timeout)
end
minetest.register_chatcommand("epidermis_fetch_skindb", {
params = "<per_page>",
privs = {server = true},
description = "Start fully fetching SkinDB",
func = function(name, per_page)
per_page = modlib.text.trim_spacing(per_page)
if per_page == "" then
per_page = "50"
end
if not per_page:match"^%d+$" then
return false, "per_page must be an integer"
end
per_page = tonumber(per_page)
if per_page < 10 or per_page > 100 then
return false, "per_page must be between 10 and 100, both inclusive."
end
page(1, per_page, function()
minetest.chat_send_player(name, minetest.colorize("yellow", "[epidermis]") .. " SkinDB fetching complete.")
end)
return true, minetest.colorize("yellow", "[epidermis]") .. " SkinDB fetching started..."
end
})
if not epidermis.conf.skindb.autosync then return end
local function last_page(per_page, on_complete)
page(1 + math.floor(#epidermis.skins / per_page), per_page, on_complete)
end
function on_local_copy_loaded()
last_page(50, function()
-- Fetch 10 skins every 10s
modlib.minetest.register_globalstep(10, function()
last_page(10)
end)
end)
end