libskinupload/init.lua
2025-10-06 14:06:55 -04:00

1283 lines
No EOL
55 KiB
Lua

libskinupload = {
notifications = {},
reviewers = {},
enabled = {}
}
local mcl = not not minetest.get_modpath("mcl_skins")
local it = not not minetest.get_modpath("itbase")
if mcl then
--Override mcl_skins to degrade gracefully instead of giving an unknown item
local _get_node_id_by_player = mcl_skins.get_node_id_by_player
function mcl_skins.get_node_id_by_player(p)
local out = _get_node_id_by_player(p)
if not minetest.registered_items["mcl_meshhand:"..out] then
return "mcl_skins_character_1_female_surv"
else
return out
end
end
end
-- Shim for 3d_armor
if minetest.get_modpath("3d_armor") then
local _get_player_skin = armor.get_player_skin
function armor.get_player_skin(self, name)
local id = libskinupload.get_skin_id(name)
if id == "" then
return _get_player_skin(self, name)
end
return "libskinupload_uploaded_skin_"..id..".png"
end
end
-- Shim for u_skins
if minetest.get_modpath("u_skins") then
local gt = u_skins.get_type
u_skins.get_type = function(tx)
local out = gt(tx)
if out then return out else return u_skins.type.MODEL end
end
end
-- Shim for Nodecore
if minetest.global_exists("nodecore") then
local _player_skin = nodecore.player_skin
function nodecore.player_skin(player, options, ...)
local pname = options and options.playername or player and player:get_player_name()
local skin = pname and libskinupload.get_skin(pname)
return skin or _player_skin(player, options, ...)
end
end
-- Polyfill for older versions
if not minetest.hypertext_escape then
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
function core.hypertext_escape(text)
return text and text:gsub("[\\<>]", hypertext_escapes)
end
minetest.hypertext_escape = core.hypertext_escape
end
local function player_model(p, tx, slim_arms)
return p:get_properties().mesh..";"..tx..",blank.png,blank.png,blank.png"
end
local db = minetest.get_mod_storage()
local world_dir = minetest.get_worldpath()
local meta_file = world_dir.."/libskinupload_meta.json"
local storage_dir = world_dir.."/libskinupload_skins/"
local storage_dir_meta = world_dir.."/libskinupload_meta/"
local optimize_media = minetest.settings:get_bool("libskinupload.optimize_media", true)
local use_fragmented_meta = minetest.settings:get_bool("libskinupload.fragmented_meta", false)
local allow_search = minetest.settings:get_bool("libskinupload.allow_search", true) and not use_fragmented_meta
minetest.mkdir(storage_dir)
if use_fragmented_meta then
minetest.mkdir(storage_dir_meta)
else
local m = io.open(meta_file)
local dl = minetest.get_dir_list(storage_dir_meta)
if m then
m:close()
elseif #dl > 0 then
minetest.log("action", "[ libskinupload ] Fragmented meta is disabled, but no master meta file was detected and a fragmented meta file is present. Automatically migrating to consolidated meta (this will not remove the libskinupload_meta folder)...")
local out = "{"
local i = 0
for _, x in pairs(dl) do
minetest.log("action", "Merging file "..x.."...")
local id = x:match "libskinupload_uploaded_skin_(%d*).json"
if id then
local file = io.open(storage_dir_meta..x)
local data = file:read("a")
file:close()
out = out..(i == 0 and ("\""..id.."\":") or (",\""..id.."\":"))..data
i = i +1
end
end
local f = io.open(meta_file, "w+")
f:write(out.."}")
f:flush()
f:close()
end
end
function libskinupload.add_skin_media(id, fname, cb)
local x
if not fname then
id = tostring(id)
x = "libskinupload_uploaded_skin_"..id..".png"
else
x = id
id = id:match "libskinupload_uploaded_skin_([^.]+).png"
end
if libskinupload.enabled[id] then return end
libskinupload.enabled[id] = true
if fname then
minetest.dynamic_add_media{
filepath = storage_dir..x,
filename = x
}
else
minetest.dynamic_add_media({
filepath = storage_dir..x,
filename = x
}, cb or function() end)
end
if mcl then
local meta = {}
if use_fragmented_meta then
local f = io.open(storage_dir_meta..string.gsub(x, ".png", ".json"))
if f then
meta = minetest.parse_json(f:read("a"))
f:close()
end
else
meta = libskinupload.get_skin_meta(x)
end
if not meta.p then
mcl_skins.register_simple_skin{texture = x, slim_arms = meta.msa or false}
end
end
end
if not optimize_media then
for _, x in ipairs(minetest.get_dir_list(storage_dir, false)) do
libskinupload.add_skin_media(x, true)
end
end
minetest.register_privilege("skin_review", {
on_grant = function(name)
libskinupload.reviewers[name] = true
db:set_string("reviewers", minetest.serialize(libskinupload.reviewers))
end,
on_revoke = function(name)
libskinupload.reviewers[name] = nil
db:set_string("reviewers", minetest.serialize(libskinupload.reviewers))
end,
give_to_singleplayer = false,
give_to_admin = false
})
libskinupload.notifications = minetest.deserialize(db:get_string("notifications")) or {}
libskinupload.reviewers = minetest.deserialize(db:get_string("reviewers")) or {}
libskinupload.requests = minetest.deserialize(db:get_string("requests")) or {}
local function markdown(str)
local out = minetest.hypertext_escape(str)
:gsub("__(.+)__", "<i>%1</i>")
:gsub("%*%*(.+)%*%*", "<b>%1</b>")
:gsub("%^%^(.+)%^%^", "<u>%1</u>")
:gsub("``(.+)``", "<mono>%1</mono>")
return out
end
local function listify(name)
local out = {}
for _, x in pairs(libskinupload.requests[name]) do
out[#out +1] = x
end
libskinupload.requests[name] = out
end
function libskinupload.notify(name, msg)
if name == ":reviewers" then
for n, _ in pairs(libskinupload.reviewers) do
if minetest.get_player_by_name(n) then
minetest.chat_send_player(n, msg)
end
end
else
if minetest.get_player_by_name(name) then
minetest.chat_send_player(name, msg)
else
libskinupload.notifications[name] = msg
db:set_string("notifications", minetest.serialize(libskinupload.notifications))
end
end
end
function libskinupload.get_skin_data(fname)
local f = io.open(storage_dir..fname)
local out = minetest.encode_base64(f:read("a"))
f:close()
return out
end
minetest.register_on_joinplayer(function(p)
if db:contains("skin:"..p:get_player_name()) then
local skin = db:get_string("skin:"..p:get_player_name())
libskinupload.set_skin(p, skin)
end
local msg = libskinupload.notifications[p:get_player_name()]
if msg then
minetest.chat_send_player(p:get_player_name(), msg)
libskinupload.notifications[p:get_player_name()] = nil
db:set_string("notifications", minetest.serialize(libskinupload.notifications))
end
if minetest.check_player_privs(p, {skin_review = true}) then
local newskins = db:get_int("newrequests")
if newskins > 0 then minetest.chat_send_player(p:get_player_name(), minetest.colorize("#579a1e", "[libskinupload] "..newskins.." new skin request"..(newskins == 1 and " has" or "s have").." been submitted. Run /skinreview to see "..(newskins == 1 and "it" or "them")..".")) end
elseif p:get_player_name() == "singleplayer" then
minetest.change_player_privs(p:get_player_name(), {skin_review = true})
end
end)
function libskinupload.can_player_upload_skins(p)
if db:get("upload_permitted:"..p:get_player_name()) == "false" then
return false
end
return true
end
function libskinupload.queue(p, req)
local data = req.data
if not libskinupload.can_player_upload_skins(p) then
minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.")
return "No permission."
end
if not minetest.decode_base64(data) then
return "Invalid base64 data."
end
if #data < 5 or string.sub(data, 1, 5) ~= "iVBOR" then
return "Invalid PNG file."
end
local def = {name = req.name or "", desc = req.desc or "", data = data, uploader = p:get_player_name(), tags = req.tags}
if mcl then
def.mcl_slim_arms = req.mcl_slim_arms
end
def.private = req.private
local name = p:get_player_name()
local rq = libskinupload.requests[name]
if not rq then
libskinupload.requests[name] = {}
rq = libskinupload.requests[name]
end
local max = db:get_int("reqmax:"..name)
local num = #rq +1
if max < 1 then max = 1 end
if num <= max then
rq[num] = "skinreq:"..name..num
db:set_string("requests", minetest.serialize(libskinupload.requests))
db:set_string("skinreq:"..name..num, minetest.serialize(def))
db:set_int("newrequests", db:get_int("newrequests") +1)
else
num = max
rq[num] = "skinreq:"..name..num
db:set_string("requests", minetest.serialize(libskinupload.requests))
db:set_string("skinreq:"..name..num, minetest.serialize(def))
end
if name == "singleplayer" then
libskinupload.accept(p, name.."@"..num)
return false
end
libskinupload.notify(":reviewers", minetest.colorize("#579a1e", "[libskinupload] A new skin request has been submitted. Run /skinreview to see it."))
return false
end
function libskinupload.deny(by, rq, msg)
if not minetest.check_player_privs(by, {skin_review = true}) then
minetest.chat_send_player(by:get_player_name(), "Insufficient permissions.")
return true
end
local num = rq:match "@(.+)"
rq = rq:gsub("@", "")
local name = minetest.deserialize(db:get_string("skinreq:"..rq)).uploader
minetest.log("action", by:get_player_name().." denied a skin upload request from "..name..". Reason: "..(msg or ""))
db:set_string("skinreq:"..rq, "")
libskinupload.requests[name][num] = nil
db:set_string("requests", minetest.serialize(libskinupload.requests))
listify(name)
db:set_int("newrequests", db:get_int("newrequests") -1)
libskinupload.notify(name, minetest.colorize("#dc4207", "[libskinupload] Your skin request was denied. "..(msg and msg ~= "" and "Reason: "..msg or "No reason was provided.")))
end
local function get_next_id()
local f, err = io.open(world_dir.."/libskinupload_nextid.txt", "r")
local out = 0
if f then
out = tonumber(f:read("a"))
f:close()
end
f, err = io.open(world_dir.."/libskinupload_nextid.txt", "w")
local out2 = db:get_int("_nextid")
if out2 > out then out = out2 end
db:set_int("_nextid", out +1)
if f then
f:write(out +1)
f:flush()
f:close()
else
minetest.log("Notice: libskinupload failed to access nextid file: "..err.." Falling back to mod storage.")
end
return out
end
function libskinupload.accept(by, rq)
if not minetest.check_player_privs(by, {skin_review = true}) then
minetest.chat_send_player(by:get_player_name(), "Insufficient permissions.")
return true
end
local num = rq:match "@(.+)"
rq = rq:gsub("@", "")
local req = minetest.deserialize(db:get_string("skinreq:"..rq))
local name = req.uploader
if name then
minetest.log("action", by:get_player_name().." accepted a skin upload request from "..name..".")
libskinupload.requests[name][num] = nil
db:set_string("requests", minetest.serialize(libskinupload.requests))
listify(name)
end
local id = get_next_id()
local fp = storage_dir.."/libskinupload_uploaded_skin_"..id..".png"
if not minetest.safe_file_write(fp, minetest.decode_base64(req.data)) then minetest.log("error", "Failed to write skin file.") end
if use_fragmented_meta then
local fpm = storage_dir_meta.."/libskinupload_uploaded_skin_"..id..".json"
local fm = io.open(fpm, "w+")
if not fm:write(minetest.write_json{n = req.name, d = req.desc, c = req.uploader or "<Unknown>", p = req.private or false, t = req.tags, msa = req.mcl_slim_arms or false}) then minetest.log("error", "Failed to write skin meta.") end
fm:flush()
fm:close()
else
local fm = io.open(meta_file, "r+")
if not fm then
io.open(meta_file, "w+"):close()
fm = io.open(meta_file, "r+")
end
if not fm:seek("end", -1) then
fm:write("{")
else
fm:write(",")
end
if not fm:write("\""..id.."\":"..minetest.write_json{n = req.name, d = req.desc, c = req.uploader or "<Unknown>", p = req.private or false, t = req.tags, msa = req.mcl_slim_arms or false}.."}") then minetest.log("error", "Failed to write skin meta.") end
fm:flush()
fm:close()
end
db:set_string("skinreq:"..rq, "")
db:set_int("newrequests", db:get_int("newrequests") -1)
if name then libskinupload.notify(name, minetest.colorize("#579a1e", "[libskinupload] Your skin request was accepted.")) end
end
local upload_state = {}
function libskinupload.show_upload_dialog(name, args)
if not args or type(args) == "string" then args = {err = ""} end
local p = minetest.get_player_by_name(name)
if not libskinupload.can_player_upload_skins(p) then
minetest.chat_send_player(name, "Insufficient privileges.")
return
end
if not upload_state[name] then upload_state[name] = {} end
local m = upload_state[name]
local size = (minetest.get_player_window_information(name) or {}).max_formspec_size or {x = 20, y = 11.5}
local w = size.x /2 -1
local w2 = size.x /2 -1.5
local tags = ""
local x = 0
local y = 0
if m.tags then
for _, tag in ipairs(m.tags) do
local width = math.max(#tag *0.2, 1.5)
if x +width > w then
x = 0
y = y +0.6
end
tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\
"
x = x +width +0.05
end
end
local start = math.max(y +1.35, (size.y -7) /2)
minetest.show_formspec(name, "libskinupload:upload", "formspec_version[7]\
size["..size.x..","..size.y.."]\
\
"..((args.err and args.err ~= "") and "hypertext[0.5,"..(size.y -2)..";"..(size.x /3)..",3;;<tag name=clr color=#b31706><clr>Error: "..args.err.."</clr>]" or "").."\
container[0.5,0.5]\
style[add_tag;bgcolor=#0000;border=false;bgimg=]\
style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\
style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\
"..tags.."\
"..(args.add_tag and "set_focus[new_tag,true]\
field_close_on_enter[new_tag;false]\
field["..x..","..y..";2,0.5;new_tag;;]" or "button["..x..","..y..";0.5,0.5;add_tag;+]").."\
container_end[]\
container[0.5,"..start.."]\
field[0.5,0;"..w..",0.5;skin;Data;"..minetest.formspec_escape(m.data or "").."]\
field[0.5,1;"..w..",0.5;skinname;Name;"..minetest.formspec_escape(m.name or "").."]\
textarea[0.5,2;"..w..",2;skindesc;Description;"..minetest.formspec_escape(m.desc or "").."]\
checkbox[0.5,4.5;private;Personal Skin;"..(not not m.private and "true" or "false").."]\
"..
(mcl and "checkbox[0.5,5;mcl_slim_arms;Slim Arms;"..(m.mcl_slim_arms and "true" or "false").."]" or "")
.."\
button[0.5,6;"..w..",1;confirm;Upload Skin]\
container_end[]\
"..(m.data and m.data ~= "" and #m.data > 5 and string.sub(m.data, 1, 5) == "iVBOR" and minetest.decode_base64(m.data) and "model["..(size.x /2 +1)..",0.5;"..w2..","..(size.y -2)..";preview;"..player_model(p, m.data and minetest.formspec_escape("[png:"..m.data) or "blank.png", m.mcl_slim_arms)..";0,210;;true;;]" or "button["..(size.x /2 +1 +(w2 /4))..","..(size.y /2)..";"..(w2 /2)..",1;eh;Show Preview]\
").."\
button["..(size.x /2 +1)..","..(size.y -1.5)..";"..w2..",1;help;Show Help]\
"..(m.help and "box[0,0;"..size.x..","..size.y..";#000a]\
style[help;bgcolor=#0000;border=false;bgimg=]\
style[help:hovered;bgcolor=#0000;border=false;bgimg=]\
style[help:pressed;bgcolor=#0000;border=false;bgimg=]\
button[0,0;"..size.x..","..size.y..";help;]\
hypertext["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2)..";;<tag name=clr color=#ffbc5b>"..minetest.formspec_escape("To upload a skin, paste the base64-encoded image into the Data field below. This can be obtained from a terminal by running <clr><b>`base64 -i <filepath>`</b></clr> (e.g. <clr><b>`base64 -i ~/Desktop/skins/my_skin.png`</b></clr>) and copying the output. Alternatively, you can use a web tool (search <clr>'base64-encode an image'</clr> or similar). Once a request is sbmitted, it will need to be reviewed. You will be notified when your request is accepted or denied. Note that you can only have one skin request pending approval at a time; if you send another request before the previous one is approved or rejected, the later request will overwrite the previous one.\nIf the preview for the skin you're uploading looks wrong, the image was probably in a layout that won't work with the player model. Usually, this is because you're trying to upload a Minecraft skin, which is incompatible with most Minetest player models. In that case, you can fix it by simply cropping off the bottom 32 pixels of the original 64x64 image and trying again.").."]" or ""))
end
minetest.register_chatcommand("skinupload", {
description = "Show the skin upload dialog.",
func = libskinupload.show_upload_dialog
})
local review_state = {}
function libskinupload.show_review_dialog(name, args)
local p = minetest.get_player_by_name(name)
local fs = ""
local fsp = [=[formspec_version[7]
size[12,10]]=]
if args == "" then
fs = "scroll_container[0.5,0.5;11,9;na;vertical;]"
local i = 0
for n, l in pairs(libskinupload.requests) do
for k, v in ipairs(l) do
local req = minetest.deserialize(db:get_string(v))
if not req then goto continue end
if req.data and #req.data > 10923 then req.data = "" end
local pn = n.."@"..k
fs = fs.."style[view"..pn..";bgcolor=#0000;border=false]\
style[view"..pn..":hovered;bgcolor=#0001;border=false]\
style[view"..pn..":focused;bgcolor=#0001;border=false]\
style[view"..pn..":pressed;bgcolor=#0001;border=false]\
model["..((i %4) *2)..","..4 *math.floor(i /4)..";2,4;pre;"..player_model(p, "\\[png:"..minetest.formspec_escape(req.data), req.mcl_slim_arms or false)..";0,210;;false;0,0;]\
button["..((i %4) *2)..","..4 *math.floor(i /4)..";2,4;view"..pn..";]"
i = i + 1
::continue::
end
end
if i == 0 then
db:set_int("newrequests", 0)
fs = fs.."\
hypertext[-0.5,-0.5;12,10;;<global valign=middle halign=center size=32 color=#888><b>There are no pending skin requests.</b>]"
end
fs = fsp..(i > 8 and "\
scrollbaroptions[min=0;max="..(50 *(math.ceil(i /4) - 2))..";thumbsize=10]\
scrollbar[10.8,0.5;0.2,9;vertical;na;0]\
" or "")..fs..[=[
scroll_container_end[]
]=]
else
local m = review_state[name]
local req = m.req
if not req then
m.req = minetest.deserialize(db:get_string(libskinupload.requests[args:match "^([^@]+)@"][tonumber(args:match "@(.+)")]))
req = m.req
end
local tags = "style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\
style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\
"
local x = 0
local y = 0
if req.tags then
for _, tag in ipairs(req.tags) do
local width = math.max(#tag *0.2, 1.5)
-- if x +width > 10 then
-- x = 0
-- y = y +0.6
-- end
tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\
"
x = x +width +0.05
end
end
fs = "formspec_version[7]\
size[12,12]\
model[0.5,0.5;8,7;pre;"..player_model(p, "\\[png:"..minetest.formspec_escape(req.data), req.mcl_slim_arms or false)..";0,210;;true;0,0;]\
button[8,1;3,1;accept;Accept]\
button[8,2;3,1;showdeny;Deny]\
label[8,3.5;Creator\\: "..(req.uploader or "N/A").."]\
hypertext[8,4;3,2;;Name\\: "..minetest.formspec_escape(minetest.hypertext_escape(req.name or "N/A")).."]\
hypertext[0.5,8.5;8,3;;"..minetest.formspec_escape(markdown(req.desc) or "N/A").."]\
button[8,6;3,1;back;Back]\
scroll_container[0.5,7.5;11,1;tagscroll;horizontal;;0]\
scrollbar[0,-800;8,0;horizontal;tagscroll;0]\
"..tags.."\
"..(m.add_tag and "set_focus[new_tag,true]\
field_close_on_enter[new_tag;false]\
field["..x..",0;2,0.5;new_tag;;]" or "button["..x..",0;0.5,0.5;add_tag;+]").."\
scroll_container_end[]"
if m.reason then
fs = fs.."\
box[0,0;12,12;#000a]\
style[closedeny;bgcolor=#0000;border=false;bgimg=]\
style[closedeny:hovered;bgcolor=#0000;border=false;bgimg=]\
style[closedeny:pressed;bgcolor=#0000;border=false;bgimg=]\
button[0,0;12,12;closedeny;]\
textarea[3,2.5;6,5;reason;Reason;"..m.reason.."]\
button[3,7.5;3,1;closedeny2;Cancel]\
button[6,7.5;3,1;deny;Deny]\
"
end
end
minetest.show_formspec(name, "libskinupload:review", fs)
end
minetest.register_chatcommand("skinreview", {
description = "Show the skin review dialog.",
func = function(name, args)
review_state[name] = {}
libskinupload.show_review_dialog(name, "")
end,
privs = {skin_review = true}
})
local function pattern_escape(str)
return str:gsub("([]%)([.^$*+-])", "%%%1")
end
local choose_state = {}
function libskinupload.show_choose_dialog(name)
local p = minetest.get_player_by_name(name)
local m = choose_state[name]
local target = m.current
local search = m.search
local fs = ""
local fsp = [=[formspec_version[7]
size[12,10]]=]
if not choose_state["@dirlist"] or not #choose_state["@dirlist"] then
choose_state["@dirlist"] = minetest.get_dir_list(storage_dir, false)
choose_state["@max_pages"] = math.floor(#choose_state["@dirlist"] /8)
end
if not choose_state["@index"] then
local f = io.open(meta_file)
if not f then
choose_state["@index"] = {}
else
local res = f:read("a")
choose_state["@index"] = minetest.parse_json(res)
f:close()
end
end
if target == "" or not target then
fs = fs.."container[2,0.5]"
if search and allow_search then
local out = "\
"
local tags = {}
for x in search:gmatch("%[([^%]]+)%]") do
tags[x] = 1
end
search = search:gsub("%[[^%]]+%]", ""):gsub("%s+$", "")
--minetest.chat_send_player(name, minetest.serialize{search, tags})
local i = 0
local j = 0
for id, meta in pairs(choose_state["@index"]) do
if not (next(tags) and search == "") and not meta.n:lower():find(search:lower(), nil, true) or meta.p and meta.c ~= name then
goto continue
else
if next(tags) then
if not meta.t then goto continue end
for _, x in pairs(meta.t) do
if tags[x] then
tags[x] = 2
end
end
for _, x in pairs(tags) do
if x < 2 then goto continue end
end
end
if j < m.page *8 or j >= m.page *8 +8 then
j = j +1
goto continue
end
end
local pn = "libskinupload_uploaded_skin_"..id..".png"
local private = meta.p and meta.c ~= name
out = out.."style[view"..pn..";bgcolor=#0000;border=false]\
style[view"..pn..":hovered;bgcolor=#0001;border=false]\
style[view"..pn..":focused;bgcolor=#0001;border=false]\
style[view"..pn..":pressed;bgcolor=#0001;border=false]\
"..(private and
"model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, "[fill:1x1:#777", not not meta.msa)..";0,210;;false;0,0;]\
image["..((i %4) *2 +0.75)..","..(4 *math.floor(i / 4) +1.75)..";0.5,0.5;libskinupload_locked.png;]" or
"model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(pn) or pn, not not meta.msa)..";0,210;;false;0,0;]\
button["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;view"..pn..";]")
i = i +1
j = j +1
::continue::
end
if i == 0 then
out = out.."\
hypertext[-2,-0.5;12,8;;<global valign=middle halign=center size=32 color=#888><b>No matches found.</b>]"
end
m.max_pages = math.ceil(j /8) -1
fs = fsp..fs..out.."\
container_end[]\
label[0.5,8.5;Page "..m.page.."]\
"..(allow_search and "style[searchfor;noclip=true]\
style[search;noclip=true;border=false]\
image_button[1,-1;1,1;libskinupload_search.png;search;]\
field[2,-0.75;8,0.5;searchfor;;"..minetest.formspec_escape(m.search or "").."]\
tooltip[search;Search;#444;#aaa]\
field_close_on_enter[searchfor;false]\
" or "").."\
button[0.5,9;1,0.5;prev;<<]\
scrollbaroptions[min=0;max="..m.max_pages..";smallstep=1;largestep=10;arrows=hide]\
scrollbar[1.5,9;9,0.5;horizontal;page;"..m.page.."]\
button[10.5,9;1,0.5;next;>>]\
"
minetest.show_formspec(name, "libskinupload:choose", fs)
return
-- fs = fs.."\
-- hypertext[-2,-0.5;12,8;;<global valign=middle halign=center size=32 color=#888><b>Searching...</b>]"
else
local i = 0
while true do
local pn = choose_state["@dirlist"][i +(m.page *8) +1]
if not pn then break end
local meta = libskinupload.get_skin_meta(pn) or {p = false, msa = false, c = ""}
--if not meta then break end
local private = meta.p and meta.c ~= name
fs = fs.."style[view"..pn..";bgcolor=#0000;border=false]\
style[view"..pn..":hovered;bgcolor=#0001;border=false]\
style[view"..pn..":focused;bgcolor=#0001;border=false]\
style[view"..pn..":pressed;bgcolor=#0001;border=false]\
"..(private and
"model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, "[fill:1x1:#777", not not meta.msa)..";0,210;;false;0,0;]\
image["..((i %4) *2 +0.75)..","..(4 *math.floor(i / 4) +1.75)..";0.5,0.5;libskinupload_locked.png;]" or
"model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(pn) or pn, not not meta.msa)..";0,210;;false;0,0;]\
button["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;view"..pn..";]")
i = i +1
if i >= 8 then break end
end
if i == 0 then
fs = fs.."\
hypertext[-2,-0.5;12,8;;<global valign=middle halign=center size=32 color=#888><b>No skins have been uploaded yet.</b>]"
end
end
fs = fsp..fs.."\
container_end[]\
label[0.5,8.5;Page "..m.page.."]\
"..(allow_search and "style[searchfor;noclip=true]\
style[search;noclip=true;border=false]\
image_button[1,-1;1,1;libskinupload_search.png;search;]\
field[2,-0.75;8,0.5;searchfor;;"..minetest.formspec_escape(m.search or "").."]\
tooltip[search;Search;#444;#aaa]\
field_close_on_enter[searchfor;false]\
" or "").."\
button[0.5,9;1,0.5;prev;<<]\
scrollbaroptions[min=0;max="..choose_state["@max_pages"]..";smallstep=1;largestep=10;arrows=hide]\
scrollbar[1.5,9;9,0.5;horizontal;page;"..m.page.."]\
button[10.5,9;1,0.5;next;>>]\
"
else
local id = string.gsub(string.sub(target, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")
local meta = choose_state["@index"][id] --libskinupload.get_skin_meta(target)
if not meta then
meta = {p = false}
end
if meta.p and meta.c ~= name then
fs = fs.."label[0.5,0.5;Insufficient privileges.]"
return
end
local tags = "style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\
style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\
"
local x = 0
local y = 0
if meta.t then
for _, tag in ipairs(meta.t) do
local width = math.max(#tag *0.2, 1.5)
-- if x +width > 10 then
-- x = 0
-- y = y +0.6
-- end
tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\
"
x = x +width +0.05
end
end
fs = fsp.."model[0.5,0.5;8,7;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(target) or target, not not meta.msa)..";0,210;;true;0,0;]\
label[8,1;"..minetest.formspec_escape("ID: "..string.gsub(string.sub(target, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")).."]\
button[8,2;3,1;confirm;Set Skin]\
hypertext[8,3.5;3,2;;"..minetest.formspec_escape(minetest.hypertext_escape(meta.n or "N/A")).."]\
hypertext[0.5,8;8,2;;"..minetest.formspec_escape(markdown(meta.d or "N/A")).."]\
scroll_container[0.5,7.5;11,1;tagscroll;horizontal;;0]\
scrollbar[0,-800;8,0;horizontal;tagscroll;0]\
"..tags.."\
scroll_container_end[]\
button[8,6;3,1;back;Back]"
end
minetest.show_formspec(name, "libskinupload:choose", fs)
end
minetest.register_chatcommand("skinchoose", {
description = "Show the skin selection dialog.",
func = function(name, args)
choose_state[name] = {page = 0}
libskinupload.show_choose_dialog(name, "")
end
})
minetest.register_chatcommand("skinchange", {
params = "<id>",
description = "Set your skin directly by ID, bypassing the skin selection dialog.",
func = function(name, args)
local f = io.open(storage_dir.."libskinupload_uploaded_skin_"..args..".png")
if not f then
minetest.chat_send_player(name, "Invalid identifier.")
return
else
f:close()
end
local meta = libskinupload.get_skin_meta("libskinupload_uploaded_skin_"..args..".png")
if meta.p then
minetest.chat_send_player(name, "Insufficient privileges.")
return
end
libskinupload.set_skin(minetest.get_player_by_name(name), args)
end
})
minetest.register_chatcommand("skindelete", {
params = "<id>",
description = "Remove the target skin from the filesystem.",
func = function(name, id)
local meta = {}
if use_fragmented_meta then
local f = io.open(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json")
if not f then
minetest.chat_send_player(name, "Invalid identifier.")
return
end
meta = minetest.parse_json(f:read("a")) or {}
f:close()
os.remove(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json")
else
local f = io.open(meta_file)
res = minetest.parse_json(f:read("a"))
f:close()
meta = res[tostring(id)]
res[tostring(id)] = nil
f = io.open(meta_file, "w+")
f:write(minetest.write_json(res))
f:flush()
f:close()
end
os.remove(storage_dir.."libskinupload_uploaded_skin_"..id..".png")
minetest.chat_send_player(name, "Successfully deleted skin '"..(meta.n or "#"..id).."' from "..(meta.c or "[unknown]")..".")
end,
privs = {skin_review = true}
})
minetest.register_chatcommand("skinlimit", {
privs = {skin_review = true},
params = "<name> <limit>",
description = "Set the maximum concurrent skin requests allowed for player <name> to <limit>.",
func = function(name, args)
local pn, num = args:match "([%w_-]+)%s+(%d+)"
num = tonumber(num)
if pn and pn ~= "" and num and num > 0 then
db:set_int("reqmax:"..pn, num)
minetest.chat_send_player(name, "Set "..pn.."'s max concurrent requests to "..num..".")
else
minetest.chat_send_player(name, "Invalid parameters.")
end
end
})
minetest.register_chatcommand("skinforget", {
description = "Forget your curent skin, to prevent conflicts when you change your skin to one provided by another mod.",
func = function(name, args)
libskinupload.forget_skin(name)
end
})
function libskinupload.print_skin_info(name, skin)
local m = libskinupload.get_skin_meta(skin)
if not m then
minetest.chat_send_player(name, "Skin `"..skin.."` does not have metadata!")
return
end
minetest.chat_send_player(name, "Info for skin "..skin..":\
Name: "..m.n.."\
Description: "..m.d.."\
Created by: "..m.c.."\
Tags: "..minetest.write_json(m.t))
end
minetest.register_chatcommand("skinmanage", {
privs = {skin_review = true},
params = "cull | list | meta <id> | grant <name> | revoke <name> | privs <name> | alter <id> <newkey>=<newvalue>[,<newkey2>=<newvalue2>]*",
func = function(name, args)
if args == "cull" then
local i = 0
for _, x in pairs(minetest.get_dir_list(storage_dir)) do
if not libskinupload.get_skin_meta(x) then
os.remove(storage_dir..x)
minetest.chat_send_player(name, "The skin `"..x.."` was removed because it did not have a matching meta file.")
i = i +1
end
end
elseif args:match "^meta" then
libskinupload.print_skin_info(name, "libskinupload_uploaded_skin_"..args:gsub("^meta%s", "")..".png")
elseif args:match "^alter" then
local id, changes = args:match "^alter%s+(%d+)%s+(.+)"
local fname = "libskinupload_uploaded_skin_"..id..".png"
local m = libskinupload.get_skin_meta(fname)
for k, v in changes:gmatch "(%w+)%s*=%s*([^,=]+)" do
if k == "private" or k == "p" then
k = "p"
v = v == "true"
elseif k == "creator" then
k = "c"
elseif k == "description" then
k = "d"
elseif k == "name" then
k = "n"
elseif k == "t" or k == "tags" then
k = "t"
v = v:split(",")
end
m[k] = v
end
libskinupload.set_skin_meta(fname, m)
elseif args:match "^tag" then
local id, tags = args:match "^tag%s+(%d+)%s+(.+)"
local fname = "libskinupload_uploaded_skin_"..id..".png"
local m = libskinupload.get_skin_meta(fname)
if not m.t then m.t = {} end
tags = tags:split(",")
for _, x in pairs(tags) do
table.insert(m.t, x)
end
libskinupload.set_skin_meta(fname, m)
elseif args:match "^untag" then
local id, tags = args:match "^untag%s+(%d+)%s+(.+)"
local fname = "libskinupload_uploaded_skin_"..id..".png"
local m = libskinupload.get_skin_meta(fname)
if not m.t then return end
tags = tags:split(",")
local itags = {}
for _, x in pairs(tags) do
itags[x] = true
end
local rm = {}
for i, x in pairs(m.t) do
if itags[x] then rm[i] = true end
end
for k in pairs(rm) do
table.remove(m.t, k)
end
libskinupload.set_skin_meta(fname, m)
elseif args:match "^list" then
for _, x in pairs(minetest.get_dir_list(storage_dir)) do
libskinupload.print_skin_info(name, x)
end
elseif args:match "^remove_fragmented_meta" then
minetest.rmdir(storage_dir_meta, true)
minetest.chat_send_player(name, "Removed all fragmented meta.")
elseif args:match "^grant" then
local pname = args:match "grant%s+([%w_-]+)"
db:set_string("upload_permitted:"..pname, "")
minetest.chat_send_player(name, "Granted skin upload privileges to player "..pname..".")
elseif args:match "^revoke" then
local pname = args:match "revoke%s+([%w_-]+)"
db:set_string("upload_permitted:"..pname, "false")
minetest.chat_send_player(name, "Revoked skin upload privileges from player "..pname..".")
elseif args:match "^privs" then
local pname = args:match "privs%s+([%w_-]+)"
minetest.chat_send_player(name, "Player "..pname.." is "..(db:get("upload_permitted:"..pname) == "false" and "not " or "").."allowed to upload skins.")
else
minetest.chat_send_player(name, "Invalid arguments, see /help skinmanage for accepted subcommands.")
end
end,
description = "Print debug information about libskinupload. /skinmanage meta <id>: Print metadata for the specified skin. Does not validate the provided ID! /skinmanage alter: Change the metadata of a skin. /skinmanage cull: Remove unmatched skin or meta files."
})
local function apply_skin(p, tx, id)
p:set_properties({
textures = tx,
})
if minetest.get_modpath("u_skins") then
u_skins.u_skins[p:get_player_name()] = "libskinupload_uploaded_skin_"..id
u_skins.save()
elseif mcl then
mcl_player.player_set_skin(p, "libskinupload_uploaded_skin_"..id..".png")
mcl_skins.save(p)
end
if minetest.get_modpath("3d_armor") then
armor.textures[p:get_player_name()].skin = "libskinupload_uploaded_skin_"..id..".png"
end
end
function libskinupload.set_skin(p, id)
local fname = "libskinupload_uploaded_skin_"..id..".png"
local meta = libskinupload.get_skin_meta(fname)
if not meta then
libskinupload.forget_skin(p)
minetest.chat_send_player(p:get_player_name(), "Your skin does not exist anymore.")
return
end
if meta.p and meta.c ~= p:get_player_name() then
--minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.")
return
end
local m = p:get_meta()
local tx = p:get_properties().textures
tx[1] = fname..m:get_string("texmod")
if not libskinupload.enabled[id] then
libskinupload.add_skin_media(id, false, function(name)
-- 5s delay to dodge a potential race condition that happens for unknown reasons.
minetest.after(5, function()
apply_skin(p, tx, id)
end)
end)
else
minetest.after(0, function()
apply_skin(p, tx, id)
end)
end
db:set_string("skin:"..p:get_player_name(), id)
end
function libskinupload.forget_skin(p)
db:set_string("skin:"..(type(p) == "string" and p or p:get_player_name()), "")
end
function libskinupload.get_skin_id(name)
return db:get_string("skin:"..name)
end
function libskinupload.get_skin(name)
local id = db:get("skin:"..name)
return id and "libskinupload_uploaded_skin_"..id..".png"
end
function libskinupload.get_skin_meta(skin)
if use_fragmented_meta then
local f = io.open(storage_dir_meta..string.gsub(skin, ".png", ".json"))
local meta
if f then
meta = minetest.parse_json(f:read("a"))
f:close()
return meta
else
return false
end
else
if choose_state["@index"] then
return choose_state["@index"][string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] or {n = "Unnamed", d = "Nondescript", c = "<Unknown>"}
end
local f = io.open(meta_file)
if not f then
return false
else
local res = minetest.parse_json(f:read("a"))
f:close()
if not res then return {n = "Unnamed", d = "Nondescript", c = "<Unknown>"} end
return res[string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] or {n = "Unnamed", d = "Nondescript", c = "<Unknown>"}
end
end
end
function libskinupload.set_skin_meta(skin, meta)
if use_fragmented_meta then
local f = io.open(storage_dir_meta..string.gsub(skin, ".png", ".json"), "w")
f:write(minetest.write_json(meta))
f:close()
else
local f = io.open(meta_file)
if not f then
return false
else
local res = minetest.parse_json(f:read("a"))
f:close()
res[string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] = meta
f = io.open(meta_file, "w+")
f:write(minetest.write_json(res))
f:flush()
f:close()
end
end
end
minetest.register_chatcommand("skinget", {
params = "<name>",
description = "Print the ID of the skin currently worn by the target player.",
func = function(name, args)
if args == "" then args = name end
local p = minetest.get_player_by_name(args)
if p then
local id = libskinupload.get_skin_id(args)
if id == "" then id = "an external skin." else id = "skin #"..id.."." end
minetest.chat_send_player(name, "Player `"..args.."` is wearing "..id)
end
end
})
--Compat
if minetest.get_modpath("unified_inventory") then
unified_inventory.register_page("libskinupload", {
get_formspec = function(p)
local name = p:get_player_name()
local tx = p:get_properties().textures[1]
local fs = (unified_inventory.style_full and unified_inventory.style_full.standard_inv_bg or "background[0.06,0.99;7.92,7.52;ui_misc_form.png]").."\
model[0.5,0.75;1,2;pre;character.b3d;"..tx..";0,210;;false;0,0;]"
local meta = {}
if string.find(tx, "libskinupload_uploaded_skin_", 1, true) then
if use_fragmented_media then
local id = string.gsub(string.sub(tx, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")
local f = io.open(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json")
if f then
meta = minetest.parse_json(f:read("a"))
f:close()
end
else
meta = libskinupload.get_skin_meta(tx)
end
end
fs = fs .. "label[2,.5;Name: "..minetest.formspec_escape(meta.n or "N/A").."]\
textarea[2,1;6,2;;;"..minetest.formspec_escape(meta.d or "N/A").."]"
fs = fs.."button[.75,3;4.5,.5;libskinupload_chooser;Change]\
button[5.25,3;2,.5;libskinupload_upload_trigger;Upload Skin]"
return {formspec = fs}
end,
})
unified_inventory.register_button("libskinupload", {
type = "image",
image = "libskinupload_icon.png",
tooltip = "Choose Player-Submitted Skin",
})
end
minetest.register_on_player_receive_fields(function(p, name, data)
if name == "libskinupload:upload" then
if not libskinupload.can_player_upload_skins(p) then
minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.")
minetest.close_formspec(p:get_player_name(), name)
return true
end
local m = upload_state[p:get_player_name()]
if data.skin then
if #data.skin >= 10923 then data.skin = string.sub(data.skin, 1, 10923) end
m.data = data.skin:gsub("^data:image/png;base64,", ""):gsub("[^A-Za-z0-9+/=]", "")
end
if data.key_enter_field == "new_tag" then
if not m.tags then m.tags = {} end
if #m.tags >= 50 then
libskinupload.show_upload_dialog(p:get_player_name(), {err = "Skins may not have more than 50 tags."})
return true
end
if #data.new_tag >= 51 then data.new_tag = string.sub(data.new_tag, 1, 51) end
m.tags[#m.tags +1] = data.new_tag:lower():gsub("[^a-z_0-9$-]", "")
libskinupload.show_upload_dialog(p:get_player_name())
return true
end
if data.remove_tag and m.tags then
local i = 0
for k, v in pairs(m.tags) do
if v == data.remove_tag then
i = k
break
end
end
table.remove(m.tags, i)
libskinupload.show_upload_dialog(p:get_player_name())
return true
end
if data.key_enter_field or data.confirm then
if mcl then
data.mcl_slim_arms = m.mcl_slim_arms or false
end
data.private = m.private or false
local x = libskinupload.queue(p, m)
if x then
libskinupload.show_upload_dialog(p:get_player_name(), {err = x})
return true
end
upload_state[p:get_player_name()] = nil
minetest.close_formspec(p:get_player_name(), name)
return true
end
if data.quit then
upload_state[p:get_player_name()] = nil
return true
end
if data.private then
m.private = data.private
end
if data.mcl_slim_arms then
m.mcl_slim_arms = data.mcl_slim_arms == "true"
end
if data.help then
m.help = not m.help
end
m.name = data.skinname
m.desc = data.skindesc
if data.add_tag then
libskinupload.show_upload_dialog(p:get_player_name(), {add_tag = true})
return true
end
libskinupload.show_upload_dialog(p:get_player_name())
return true
elseif name == "libskinupload:review" then
local name = p:get_player_name()
if not minetest.check_player_privs(p, {skin_review = true}) then
minetest.chat_send_player(name, "Insufficient permissions.")
return true
end
if data.quit then
review_state[name] = nil
return true
end
local m = review_state[name]
if data.back then
m.req = nil
m.current = nil
m.add_tag = nil
libskinupload.show_review_dialog(name, "")
return true
elseif data.showdeny then
review_state[name].reason = ""
libskinupload.show_review_dialog(name, review_state[name].current)
elseif data.closedeny or data.closedeny2 then
review_state[name].reason = nil
libskinupload.show_review_dialog(name, review_state[name].current)
elseif data.deny then
libskinupload.deny(p, review_state[name].current, data.reason)
libskinupload.show_review_dialog(name, "")
return true
elseif data.accept then
db:set_string("skinreq:"..m.current:gsub("@", ""), minetest.serialize(m.req))
libskinupload.accept(p, review_state[name].current)
libskinupload.show_review_dialog(name, "")
return true
end
if data.key_enter_field == "new_tag" then
if not m.req.tags then m.req.tags = {} end
if #m.req.tags >= 50 then
return true
end
if #data.new_tag >= 51 then data.new_tag = string.sub(data.new_tag, 1, 51) end
m.req.tags[#m.req.tags +1] = data.new_tag:lower():gsub("[^a-z_0-9$-]", "")
m.add_tag = nil
libskinupload.show_review_dialog(name, review_state[name].current)
return true
end
if data.remove_tag then
if not m.req.tags then m.req.tags = {} end
if #m.req.tags < 1 then
return true
end
local idx = 0
for i, x in pairs(m.req.tags) do
if x == data.remove_tag then
idx = i
break
end
end
table.remove(m.req.tags, idx)
m.add_tag = nil
libskinupload.show_review_dialog(name, review_state[name].current)
return true
end
if data.add_tag then
m.add_tag = true
libskinupload.show_review_dialog(name, review_state[name].current)
return true
end
for k, v in pairs(data) do
if string.find(k, "view", 1, true) then
review_state[p:get_player_name()].current = string.gsub(k, "^view", "")
libskinupload.show_review_dialog(name, review_state[name].current)
return true
end
end
return true
elseif name == "libskinupload:choose" then
if data.quit then
choose_state[p:get_player_name()] = nil
choose_state["@dirlist"] = nil
choose_state["@index"] = nil
return true
end
local m = choose_state[p:get_player_name()]
if data.back then
m.current = ""
libskinupload.show_choose_dialog(p:get_player_name())
return true
elseif data.confirm then
libskinupload.set_skin(p, string.gsub(string.sub(m.current, string.len("libskinupload_uploaded_skin_."), -1), ".png", ""))
minetest.close_formspec(p:get_player_name(), name)
return true
end
if allow_search and (data.search or data.key_enter_field == "searchfor") then
if #data.searchfor > 2 then
m.search = data.searchfor
else
m.search = nil
m.max_pages = nil
end
m.page = 0
libskinupload.show_choose_dialog(p:get_player_name())
return true
end
for k, v in pairs(data) do
if string.find(k, "view", 1, true) then
m.current = string.gsub(k, "^view", "")
libskinupload.show_choose_dialog(p:get_player_name())
return true
end
end
if data.page then
local ev = minetest.explode_scrollbar_event(data.page)
if ev.type == "CHG" then
m.page = ev.value
libskinupload.show_choose_dialog(p:get_player_name())
end
end
if data.prev then
m.page = m.page -1
if m.page < 0 then
m.page = m.max_pages or choose_state["@max_pages"]
end
libskinupload.show_choose_dialog(p:get_player_name())
elseif data.next then
m.page = m.page +1
if m.page > (m.max_pages or choose_state["@max_pages"]) then
m.page = 0
end
libskinupload.show_choose_dialog(p:get_player_name())
end
return true
elseif data.libskinupload_chooser then
choose_state[p:get_player_name()] = {page = 0}
libskinupload.show_choose_dialog(p:get_player_name())
elseif data.libskinupload_upload_trigger then
libskinupload.show_upload_dialog(p:get_player_name())
end
end)
--[[
minetest.register_on_joinplayer(function(p)
for i = 1, 100 do
local rd = ""
for i = 1,10 do
rd = rd..string.char(math.floor(math.random() *2) +97)
end
db:set_string("skinreq:a"..i, minetest.serialize{name = "",desc = rd, data = "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"})
libskinupload.accept(p, "a"..i)
end
end)
--]]