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("__(.+)__", "%1") :gsub("%*%*(.+)%*%*", "%1") :gsub("%^%^(.+)%^%^", "%1") :gsub("``(.+)``", "%1") 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 "", 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 "", 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;;Error: "..args.err.."]" 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)..";;"..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 `base64 -i ` (e.g. `base64 -i ~/Desktop/skins/my_skin.png`) and copying the output. Alternatively, you can use a web tool (search 'base64-encode an image' 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;;There are no pending skin requests.]" 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;;No matches found.]" 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;;Searching...]" 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;;No skins have been uploaded yet.]" 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 = "", 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 = "", 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 = " ", description = "Set the maximum concurrent skin requests allowed for player to .", 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 | grant | revoke | privs | alter =[,=]*", 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 : 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 = ""} 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 = ""} end return res[string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] or {n = "Unnamed", d = "Nondescript", c = ""} 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 = "", 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) --]]