├── src ├── requirements.txt ├── uss_config.py ├── static │ ├── style.css │ ├── ajaxfileupload.js │ ├── index.html │ └── scripts.js ├── uss_runtime.py ├── uss_database.py └── UniSkinServer.py ├── .gitignore ├── README.md └── doc ├── UniSkinAPI_zh-CN.md ├── UniSkinServerAPI.txt └── UniSkinAPI_en.md /src/requirements.txt: -------------------------------------------------------------------------------- 1 | bsddb3==6.1.1 2 | tornado==4.3 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | test.py 3 | src/runtime 4 | __pycache__ 5 | src/*.db 6 | src/*.json 7 | src/textures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Skin API 计划 2 | 相关描述请见MCBBS[Universal Skin API 计划](http://www.mcbbs.net/thread-366248-1-1.html) 3 | 4 | ### Universal Skin API 5 | 具体文档请见`doc`目录 6 | 7 | Documents under `doc` folder 8 | 9 | **如对API有意见,请先发Issue,尽量避免直接Pull Request** 10 | 11 | **Open an issue first if you have any concerns about the API** 12 | 13 | ### Universal Skin Server 14 | 位于`src`目录下 15 | 16 | 以GPLv2协议发布 17 | 18 | 网页API请见`doc/UniSkinServerAPI.txt` 19 | 20 | Source under `src` folder. Licensed under GPLv2 21 | 22 | For the website API, see `doc/UniSkinServerAPI.txt` 23 | 24 | **注意:密码现在是明文传输,你可能需要HTTPS保证安全** 25 | 26 | **Warning: Passwords are transmitted in plain-text. You may need HTTPS to secure the connection.** 27 | 28 | ### Universal Skin Mod 29 | 请见[UniSkinMod](https://github.com/RecursiveG/UniSkinMod) 30 | 31 | -------------------------------------------------------------------------------- /src/uss_config.py: -------------------------------------------------------------------------------- 1 | DEFAULT_CONFIG='''{ 2 | "port": "12345", 3 | "allow-reg": true, 4 | "texture-folder": "textures/", 5 | "database-path": "data.db", 6 | "admin-passphrase": "", 7 | "session-time": 600, 8 | "max-file-size": 1048576 9 | } 10 | ''' 11 | 12 | def get_config(file_path="server_config.json"): 13 | try: 14 | import os 15 | import json 16 | if not os.path.exists(file_path): 17 | f=open(file_path,"w") 18 | f.write(DEFAULT_CONFIG) 19 | f.close() 20 | 21 | f=open(file_path) 22 | config=json.loads(f.read()) 23 | if not os.path.exists(config["texture-folder"]): 24 | os.mkdir(config["texture-folder"]) 25 | except Exception as e: 26 | print(e) 27 | return None 28 | return config 29 | -------------------------------------------------------------------------------- /src/static/style.css: -------------------------------------------------------------------------------- 1 | #main-div{ 2 | width: 50%; 3 | margin-right: auto; 4 | margin-left: auto; 5 | } 6 | 7 | .panel{ 8 | border: 1px solid transparent; 9 | border-radius: 6px; 10 | border-color: #ddd; 11 | margin-bottom: 20px; 12 | box-shadow: 0 1px 1px rgba(0,0,0,0.05); 13 | width: 30em; 14 | } 15 | 16 | .panel-head{ 17 | padding: 10px 15px; 18 | border-bottom: 1px solid transparent; 19 | border-color: #ddd; 20 | background-color: #F5F5F5; 21 | } 22 | 23 | .panel-title{ 24 | padding: 0; 25 | margin: 0; 26 | font-size: 16px; 27 | } 28 | 29 | .panel-body{ 30 | padding: 15px; 31 | padding-bottom: 5px; 32 | } 33 | 34 | 35 | .panel-btn{ 36 | color: #333; 37 | background-color: #fff; 38 | display: inline-block; 39 | margin-right: 15px; 40 | padding: 6px 12px; 41 | margin-bottom: 0; 42 | font-size: 14px; 43 | font-weight: 400; 44 | line-height: 1; 45 | text-align: center; 46 | white-space: nowrap; 47 | vertical-align: middle; 48 | cursor: pointer; 49 | background-image: none; 50 | border: 1px solid transparent; 51 | border-color: #ccc; 52 | border-radius: 4px; 53 | } 54 | 55 | .panel-line{ 56 | margin-bottom: 10px; 57 | display: table; 58 | } 59 | 60 | .line-start{ 61 | white-space: nowrap; 62 | margin-right: 0; 63 | padding: 6px 12px; 64 | font-size: 14px; 65 | font-weight: 400; 66 | line-height: 1; 67 | color: #555; 68 | text-align: center; 69 | background-color: #eee; 70 | border: 1px solid #ccc; 71 | border-right: 0; 72 | border-radius: 4px 0 0 4px; 73 | display: table-cell; 74 | width: 1%; 75 | } 76 | 77 | .line-mid{ 78 | margin-right: 0; 79 | padding: 6px 12px; 80 | font-size: 14px; 81 | font-weight: 400; 82 | line-height: 1; 83 | border: 1px solid #ccc; 84 | border-right: 0; 85 | display: table-cell; 86 | width: 100%; 87 | } 88 | 89 | .line-end{ 90 | padding: 6px 12px; 91 | font-size: 14px; 92 | font-weight: 400; 93 | line-height: 1; 94 | border: 1px solid #ccc; 95 | border-radius: 0 4px 4px 0; 96 | display: table-cell; 97 | width: 100%; 98 | } 99 | 100 | .error-msg{ 101 | color: red; 102 | } 103 | 104 | #preview-img{ 105 | max-width: 100%; 106 | margin-bottom: 10px; 107 | } 108 | -------------------------------------------------------------------------------- /doc/UniSkinAPI_zh-CN.md: -------------------------------------------------------------------------------- 1 | ## 根地址 2 | 根地址是每个URL请求的开始部分,每个请求的URL均由 3 | 根地址与Endpoint连接而成,根地址也唯一地标识了一台服务器 4 | 所有请求都是不带参数的GET请求 5 | 假设一个服务器的根地址是 6 | 7 | http://127.0.0.1:8000/skinserver/ 8 | 9 | 获取玩家Profile的Endpoint是 10 | 11 | /{玩家名}.json 12 | 13 | 那么,从该服务器获取玩家`XiaoMing`的皮肤的请求即为 14 | 15 | http://127.0.0.1:8000/skinserver/XiaoMing.json 16 | 17 | 一个实现了UniSkinAPI的服务端应当实现所有Endpoint 18 | 19 | ## 材质文件链接 20 | Endpoint: 21 | 22 | /textures/{材质文件唯一标识符} 23 | 24 | 返回值: 25 | 26 | - 200: 返回材质文件 27 | - 404: 材质未找到 28 | 29 | 唯一标识符应当与文件一一对应 30 | 我推荐使用文件的SHA-256作为唯一标识符 31 | 32 | ## 获得玩家信息: 33 | Endpoint: 34 | 35 | /{玩家名}.json 36 | 37 | 该处玩家名大小写可随意 38 | 如果玩家名包含非ASCII字符,请使用UTF-8编码的[URL编码](https://en.wikipedia.org/wiki/Percent-encoding) 39 | 例如:`%E5%B0%8F%E6%98%8E.json` 40 | 41 | 返回值: 42 | 43 | - 200: 返回玩家信息(UserProfile) 44 | - 400: 不可接受的字符编码 45 | - 404: 该玩家不存在 46 | 47 | ## UserProfile: 48 | UserProfile代表了一个玩家的信息 49 | 50 | { 51 | "player_name": {字符串,大小写正确的玩家名}, 52 | "last_update": {整数,玩家最后一次修改个人信息的时间,UNIX时间戳, 秒}, 53 | "model_preference": {字符串数组,按顺序存储玩家需要加载的模型名称}, 54 | "skins": {模型名称到对应材质UID的字典} 55 | "cape": {披风的UID} (已弃用) 56 | } 57 | 58 | 一个完整的样例是: 59 | 60 | { 61 | "player_name": "XiaoMing", 62 | "last_update": 1416300800, 63 | "model_preference": ["slim","default","cape","default_dynamic"], 64 | "skins": { 65 | "slim": "67cbc70720c4666e9a12384d041313c7bb9154630d7647eb1e8fba0c461275c6", 66 | "default": "6d342582972c5465b5771033ccc19f847a340b76d6131129666299eb2d6ce66e", 67 | "cape": "970a71c6a4fc81e83ae22c181703558d9346e0566390f06fb93d09fcc9783799" 68 | "default_dynamic": "1000,67cbc70720c4666e9a12384d041313c7bb9154630d7647eb1e8fba0c461275c6,6d342582972c5465b5771033ccc19f847a340b76d6131129666299eb2d6ce66e" 69 | } 70 | "cape": "970a71c6a4fc81e83ae22c181703558d9346e0566390f06fb93d09fcc9783799" 71 | } 72 | 73 | 所有的成员都是可选的,一个支持UniSkinAPI的皮肤mod不应因缺少任何部分而崩溃。 74 | 75 | 推荐启用压缩。 76 | 77 | ## 模型名称约定 78 | 79 | 模型名称是大小写敏感的 80 | 81 | - `slim`: 细手臂/女性玩家模型 82 | - `default`: 经典玩家模型 83 | - `cape`: 为披风材质使用 84 | - `elytron`: 为滑翔翅材质使用 85 | 86 | ## 加载顺序约定 87 | 88 | - 对于`slim`和`default`,在前的皮肤被加载 89 | - 对于`cape`和`elytron`,若存在则加载 90 | - 对于不支持`slim`/`cape`/`elytron`模型的客户端,对应材质被忽略 91 | - 当存在两个`cape`字段时,以`skins`字典中的为准 92 | 93 | ## UniSkinMod 扩展模型 94 | 95 | UniSkinMod正考虑加入动态皮肤功能,占用以下四个模型: 96 | 97 | - slim_dynamic 98 | - default_dynamic 99 | - cape_dynamic 100 | - elytron_dynamic 101 | 102 | 依然是在前面的被加载,若不存在或不支持则忽略。 103 | texture字符串为由`,`分隔的多个代表每帧图片的UID,第一项是例外。是一个正整数,表示多长时间循环一遍,单位millisecond。 104 | 所有图片等间隔播放。 105 | 106 | ## 错误回应: 107 | 当某个请求出错时(返回值不为200时),服务器可以返回空,也可以返回如下JSON 108 | 109 | { 110 | "errno": {整数,错误代号}, 111 | "msg": {字符串,人类可读的信息} 112 | } 113 | 114 | 目前定义的错误代号有: 115 | - 0: 没有错误发生 116 | - 1: 玩家不存在 117 | -------------------------------------------------------------------------------- /doc/UniSkinServerAPI.txt: -------------------------------------------------------------------------------- 1 | # UniSkinServer API Document 2 | This file is not a part of `UniSkinAPI` 3 | 4 | ## Register: `/register` 5 | POST: LOGIN={username}&PASSWD={password} 6 | RETN: {"errno":0,"msg":"success"} 7 | 1: already registered 8 | 2: passwd not satisfied request(too short) 9 | 3: invalid name or passwd 10 | 11 | ## Login: `/login` 12 | POST: same as register 13 | RETN: {"errno":0,"msg":"{sessionToken}"} 14 | 1: invalid login 15 | 2: attempt blocked 16 | 17 | ## Logout: `/logout` 18 | POST: token={} 19 | RETN: {"errno":0,"msg":"{sessionToken}"} 20 | 21 | ## Get Full User Data: `/userdata` 22 | GET :token={sessionToken} 23 | RETN :Same as UniSkinAPI 24 | 25 | ## Delete Account: `/delete_account` 26 | POST: token={} 27 | current_passwd={} 28 | login={} 29 | RETN: {"errno":0,"msg":"success"} 30 | 31 | ## Change Password: `/chpwd` 32 | POST: token={} 33 | current_passwd={} 34 | new_passwd={} 35 | login={} 36 | RETN: {"errno":0,"msg":"success"} 37 | 38 | ## Set Skin Model preference: `/model_preference` 39 | POST: token={} 40 | prefered_model={slim|default} 41 | RETN: {"errno":0,"msg":"success"} 42 | 43 | ## Set Static/Dynamic preference: `/type_preference` 44 | POST: token={} 45 | type={skin|cape|elytra} 46 | preference={dynamic|static|off} 47 | RETN: {"errno":0,"msg":"success"} 48 | 49 | ## Set Dynamic Interval: `/dynamic_interval` 50 | POST: token={} 51 | type={skin_slim|skin_default|cape|elytra} 52 | interval={positive integer} 53 | RETN: {"errno":0,"msg":"success"} 54 | 55 | ## Upload Texture: `/upload_texture` 56 | POST: token={} 57 | model={skin_slim|skin_default|cape|elytra} 58 | type={static|dynamic} 59 | file={} 60 | RETN: {"errno":0,"msg":"success"} 61 | 62 | ## Delete Texture: `/delete_texture` 63 | POST: token={} 64 | model={skin_slim|skin_default|cape|elytra} 65 | type={static|dynamic} 66 | RETN: {"errno":0,"msg":"success"} 67 | 68 | ## `errno` definitions: 69 | 0: success 70 | 1: invalid credential 71 | 2: bad password 72 | 3: bad username 73 | 4: attempt blocked 74 | 5: already registered 75 | 6: argument not acceptable 76 | 7: register not allowed 77 | 8: internal server error 78 | 9: file too large 79 | 80 | # Database: `bsddb3` 81 | 82 | { 83 | "username": "{string}", 84 | "password": "{string}", 85 | "last_update": "{unix timestamp in s}", 86 | "textures": { 87 | "skin_default_static": {string, hash}, 88 | "skin_default_dynamic": { 89 | "interval": "{int, ms}", 90 | "hashes": ["{ordered list of string}"] 91 | }, 92 | "skin_slim_static": {}, 93 | "skin_slim_dynamic": {}, 94 | "cape_static": {}, 95 | "cape_dynamic": {}, 96 | "elytra_static": {}, 97 | "elytra_dynamic": {} 98 | }, 99 | "type_preference": { 100 | "skin": "{string, static|dynamic|off}", 101 | "cape": "{string, static|dynamic|off}", 102 | "elytra": "{string, static|dynamic|off}", 103 | }, 104 | "model_preference": {string,slim|default} 105 | } 106 | -------------------------------------------------------------------------------- /doc/UniSkinAPI_en.md: -------------------------------------------------------------------------------- 1 | ## "The Root" 2 | "The Root" is the start part of every request. 3 | Every Request URL is by connecting The Root and the Endpoint 4 | For example, The Root of a server is 5 | 6 | http://127.0.0.1:8000/skinserver/ 7 | 8 | and the Endpoint for getting the profile is 9 | 10 | /{PlayerName}.json 11 | 12 | if a client want the profile for player `John`, the request will be 13 | 14 | http://127.0.0.1:8000/skinserver/John.json 15 | 16 | A compatible server should implements all the endpoints. 17 | 18 | ## Texture Files Link: 19 | Endpoint: 20 | 21 | /textures/{unique identifier to the file} 22 | 23 | Response: 24 | 25 | - 200: Return the texture 26 | - 404: Not found 27 | 28 | UID should be always different for different file 29 | Even they belongs to the same player or even in different servers 30 | I recommend using SHA-256 of the file as the UID 31 | 32 | ## Get user profile: 33 | Endpoint: 34 | 35 | /{PlayerName}.json 36 | 37 | The `{PlayerName}` here is not case-sensitive 38 | If non-ASCII character is needed. They should be encoded as UTF-8 and use [Precent-Encoding](https://en.wikipedia.org/wiki/Percent-encoding) notation. 39 | e.g. `%E5%B0%8F%E6%98%8E.json` 40 | 41 | Response: 42 | 43 | - 200: Return the UserProfile json 44 | - 400: If the encoding is not acceptable 45 | - 404: PlayerName not registered 46 | 47 | ## UserProfile: 48 | UserProfile is a JSON string, the format is: 49 | 50 | { 51 | "player_name": {string, Case-Corrected player name}, 52 | "last_update": {int, the time the player made the last change, in unix timestamp, seconds}, 53 | "model_preference": {array of string, the name of the models are listed in order of preference}, 54 | "skins": {Map, map of model name to texture hash} 55 | "cape": {hash of cape texture}(Deprecated) 56 | } 57 | 58 | And An Example Could Be: 59 | 60 | { 61 | "player_name": "John", 62 | "last_update": 1416300800, 63 | "model_preference": ["slim","default","cape","default_dynamic"], 64 | "skins": { 65 | "slim": "67cbc70720c4666e9a12384d041313c7bb9154630d7647eb1e8fba0c461275c6", 66 | "default": "6d342582972c5465b5771033ccc19f847a340b76d6131129666299eb2d6ce66e", 67 | "cape": "970a71c6a4fc81e83ae22c181703558d9346e0566390f06fb93d09fcc9783799" 68 | "default_dynamic": "1000,67cbc70720c4666e9a12384d041313c7bb9154630d7647eb1e8fba0c461275c6,6d342582972c5465b5771033ccc19f847a340b76d6131129666299eb2d6ce66e" 69 | } 70 | "cape": "970a71c6a4fc81e83ae22c181703558d9346e0566390f06fb93d09fcc9783799" 71 | } 72 | 73 | All the fields are optional, and a compatible client should automatically skip 74 | any missing information. 75 | 76 | Compression is recommended. 77 | 78 | ## Model Name Convention 79 | 80 | Model names are case-sensitive. 81 | 82 | - `slim`: The slim-arm or female player model 83 | - `default`: The triditional player model 84 | - `cape`: Reserved for cape texture use 85 | - `elytron`: Reserved for elytron texture use 86 | 87 | ## Model load order 88 | 89 | - For `slim` and default, the front one is used 90 | - For `cape` and elytron, if exists then use 91 | - For clients which not support `slim`/`cape`/`elytron`, they are ignored 92 | - When `cape` exists both inside and outside, the one *in* the `skins` dictonary is used. 93 | 94 | ## Entended Model for UniSkinMod 95 | 96 | UniSkinMod may support dynamic skins in the future. 97 | So these four model names are reserved. 98 | 99 | - slim_dynamic 100 | - default_dynamic 101 | - cape_dynamic 102 | - elytron_dynamic 103 | 104 | Still, the ones in front are loaded. If not supported then ignore. 105 | The `texture` string for dynamic skins is a comma-seperated string consists of all UID of every frame. 106 | Except the first one, which is a positive integer represents how many milliseconds it plays for one time. 107 | All the frames are played in equal time interval. 108 | 109 | ## Response for Errors: 110 | For any response other than 2** & 3**, 111 | the body could be either empty or a json. 112 | Returning an empty body would be fine in most case 113 | 114 | { 115 | "errno": {int}, 116 | "msg": {human readable string and can be anything you like} 117 | } 118 | 119 | Defined errnos are: 120 | - 0: No Error Occurred 121 | - 1: User not registered 122 | No such player in the database at all. 123 | -------------------------------------------------------------------------------- /src/uss_runtime.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class SessionManager(): 4 | '''track the accessToken, no interaction with database 5 | session: { 6 | 'token1': { 7 | 'time': expire time 8 | 'name': playername 9 | }, 10 | ... 11 | } 12 | ''' 13 | 14 | def __init__(self, expire_time: int): 15 | self.__sessions=dict() 16 | self.__lastcheck=time.time() 17 | self.expire=expire_time 18 | def valid(self,token): 19 | if token in self.__sessions: 20 | if self.__sessions[token]['time']>time.time(): 21 | return True 22 | else: 23 | del(self.__sessions[token]) 24 | return False 25 | else: 26 | return False 27 | def get_name(self,token): 28 | return self.__sessions[token]['name'] if self.valid(token) else None 29 | def login(self,name): 30 | if self.__lastcheck + 3600 < time.time(): 31 | self.__lastcheck=time.time() 32 | for t in [x for x in self.__sessions if self.__sessions[x]['time']0 and x["interval"]>0) 117 | join = (lambda x: str(x["interval"])+","+",".join(x["hashes"])) 118 | s = record["textures"]["skin_default_dynamic"] 119 | if good(s): skins["default_dynamic"]=join(s) 120 | s = record["textures"]["skin_slim_dynamic"] 121 | if good(s): skins["slim_dynamic"]=join(s) 122 | s = record["textures"]["cape_dynamic"] 123 | if good(s): skins["cape_dynamic"]=join(s) 124 | s = record["textures"]["elytra_dynamic"] 125 | if good(s): skins["elytra_dynamic"]=join(s) 126 | 127 | data["skins"] = skins 128 | return json.dumps(data) 129 | 130 | def WebDataFormatter(database_record): 131 | import json 132 | del database_record["password"] 133 | return json.dumps(database_record) 134 | -------------------------------------------------------------------------------- /src/uss_database.py: -------------------------------------------------------------------------------- 1 | from bsddb3 import dbshelve 2 | import time as time_library 3 | time=(lambda: int(time_library.time())) 4 | 5 | def pwdhash(name,pwd): 6 | import hashlib 7 | m1=hashlib.sha1() 8 | m2=hashlib.sha1() 9 | m3=hashlib.sha512() 10 | m1.update(name.encode("utf8")) 11 | m2.update(pwd.encode("utf8")) 12 | m3.update(m1.digest()) 13 | m3.update(m2.digest()) 14 | return m3.hexdigest() 15 | 16 | class uss_database: 17 | ''' this class will do what is instructed to do 18 | the caller is responsible for data validation ''' 19 | db = None # dbshelve.DBShelf 20 | 21 | def __init__(self, database_path: str): 22 | self.db = dbshelve.open(database_path) 23 | def close(self): 24 | self.db.close() 25 | self.db = None 26 | 27 | def _get_user(self, username: str): 28 | try: 29 | return self.db[username.lower().encode("UTF-8")] 30 | except: 31 | return None 32 | def _set_user(self, username: str, data): 33 | self.db[username.lower().encode("UTF-8")] = data 34 | self.db.sync() 35 | 36 | def has_user(self, username: str): 37 | return self._get_user(username) is not None 38 | 39 | @staticmethod 40 | def _get_dynmic_default(): return {"interval": -1, "hashes": []} 41 | def create_user(self, username: str, passwd: str): 42 | hashed_passwd=pwdhash(username, passwd) 43 | if (self.has_user(username)): return 44 | gdd=self._get_dynmic_default 45 | data = dict(username=username, password=hashed_passwd, last_update=time(), 46 | type_preference=dict(skin="off", cape="off", elytra="off"), 47 | model_preference="default", 48 | textures=dict(skin_default_static="", skin_default_dynamic=gdd(), 49 | skin_slim_static="", skin_slim_dynamic=gdd(), 50 | cape_static="", cape_dynamic=gdd(), 51 | elytra_static="", elytra_dynamic=gdd())) 52 | self._set_user(username, data) 53 | 54 | def delete_user(self, username:str): 55 | if (not self.has_user(username)): return 56 | del self.db[username.encode("UTF-8")] 57 | self.db.sync(); 58 | 59 | def change_pwd(self, username: str, passwd: str): 60 | hashed_passwd=pwdhash(username, passwd) 61 | data = self._get_user(username) 62 | if data is None: return 63 | data["password"]=hashed_passwd 64 | self._set_user(username, data) 65 | 66 | def is_passwd_match(self, username: str, passwd: str): 67 | data = self._get_user(username) 68 | if data is None: return False 69 | hashed_passwd=pwdhash(username, passwd) 70 | return data["password"]==hashed_passwd 71 | 72 | def set_skin_model_preference(self, username: str, model: str): 73 | data = self._get_user(username) 74 | if data is None: return 75 | data["model_preference"]=model 76 | self._set_user(username, data) 77 | 78 | def set_type_preference(self, username: str, type: str, preference: str): 79 | data = self._get_user(username) 80 | if data is None: return 81 | data["type_preference"][type]=preference 82 | self._set_user(username, data) 83 | 84 | def set_dynamic_interval(self, username:str, type:str,interval:int): 85 | data = self._get_user(username) 86 | if data is None: return 87 | data["textures"][type+"_dynamic"]["interval"]=interval 88 | self._set_user(username, data) 89 | 90 | def set_texture_hash(self, username: str, type: str, is_dynamic: bool, hash: str): 91 | data = self._get_user(username) 92 | if data is None: return 93 | key=type + ("_dynamic" if is_dynamic else "_static") 94 | if is_dynamic: 95 | data['textures'][key]['hashes'].append(hash) 96 | else: 97 | data['textures'][key]=hash 98 | self._set_user(username, data) 99 | 100 | def del_texture_hash(self, username: str, type: str, is_dynamic: bool, hash_delete_callback): 101 | data = self._get_user(username) 102 | if data is None: return 103 | key=type + ("_dynamic" if is_dynamic else "_static") 104 | if is_dynamic: 105 | for hash in data['textures'][key]['hashes']: hash_delete_callback(hash) 106 | data['textures'][key]['hashes']=[] 107 | data['textures'][key]['interval']=-1 108 | else: 109 | hash_delete_callback(data['textures'][key]) 110 | data['textures'][key]="" 111 | self._set_user(username, data) 112 | 113 | def scan_hashes(self, hash_callback): 114 | '''used for TextureManager on startup''' 115 | for key in self.db: 116 | rec = self.db[key] 117 | hash_callback(rec["textures"]["skin_default_static"]) 118 | hash_callback(rec["textures"]["skin_slim_static"]) 119 | hash_callback(rec["textures"]["cape_static"]) 120 | hash_callback(rec["textures"]["elytra_static"]) 121 | for hash in rec["textures"]["skin_default_dynamic"]["hashes"]: hash_callback(hash) 122 | for hash in rec["textures"]["skin_slim_dynamic"]["hashes"]: hash_callback(hash) 123 | for hash in rec["textures"]["cape_dynamic"]["hashes"]: hash_callback(hash) 124 | for hash in rec["textures"]["elytra_dynamic"]["hashes"]: hash_callback(hash) 125 | 126 | def scan_user_hash(self, username: str, hash_callback): 127 | rec = self._get_user(username) 128 | if rec is None: return 129 | hash_callback(rec["textures"]["skin_default_static"]) 130 | hash_callback(rec["textures"]["skin_slim_static"]) 131 | hash_callback(rec["textures"]["cape_static"]) 132 | hash_callback(rec["textures"]["elytra_static"]) 133 | for hash in rec["textures"]["skin_default_dynamic"]["hashes"]: hash_callback(hash) 134 | for hash in rec["textures"]["skin_slim_dynamic"]["hashes"]: hash_callback(hash) 135 | for hash in rec["textures"]["cape_dynamic"]["hashes"]: hash_callback(hash) 136 | for hash in rec["textures"]["elytra_dynamic"]["hashes"]: hash_callback(hash) 137 | 138 | def get_formatted(self, username, formatter): 139 | return formatter(self._get_user(username)) 140 | -------------------------------------------------------------------------------- /src/static/ajaxfileupload.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery.extend({ 3 | 4 | 5 | createUploadIframe: function(id, uri) 6 | { 7 | //create frame 8 | var frameId = 'jUploadFrame' + id; 9 | var iframeHtml = '