@follenfang/fupload 0.0.0-bootstrap.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -3
- package/fupload/SKILL.md +142 -0
- package/fupload/agents/openai.yaml +4 -0
- package/fupload/examples/curseforge-plugin-upload.json +21 -0
- package/fupload/examples/dd-config-delete.json +5 -0
- package/fupload/examples/dd-config-update.json +25 -0
- package/fupload/examples/dd-plugin-delete.json +5 -0
- package/fupload/examples/dd-plugin-update.json +9 -0
- package/fupload/examples/dd-wa-delete.json +5 -0
- package/fupload/examples/dd-wa-edit.json +9 -0
- package/fupload/examples/newbee-config-delete.json +5 -0
- package/fupload/examples/newbee-config-update.json +10 -0
- package/fupload/examples/newbee-plugin-create.json +14 -0
- package/fupload/examples/newbee-plugin-delete.json +5 -0
- package/fupload/examples/newbee-wa-delete.json +5 -0
- package/fupload/examples/newbee-wa-update.json +8 -0
- package/fupload/references/curseforge.md +233 -0
- package/fupload/references/dd.md +105 -0
- package/fupload/references/newbee-official-cli.md +288 -0
- package/fupload/references/newbee.md +80 -0
- package/fupload/references/workflow.md +67 -0
- package/fupload/scripts/fupload.py +17 -0
- package/fupload/scripts/fupload_cli/__init__.py +3 -0
- package/fupload/scripts/fupload_cli/cli.py +281 -0
- package/fupload/scripts/fupload_cli/curseforge.py +186 -0
- package/fupload/scripts/fupload_cli/dd.py +2406 -0
- package/fupload/scripts/fupload_cli/dd_broker.py +634 -0
- package/fupload/scripts/fupload_cli/dd_sidecar.py +860 -0
- package/fupload/scripts/fupload_cli/errors.py +94 -0
- package/fupload/scripts/fupload_cli/io.py +125 -0
- package/fupload/scripts/fupload_cli/newbee.py +1412 -0
- package/fupload/scripts/fupload_cli/newbee_auth.py +135 -0
- package/fupload/scripts/fupload_cli/schema.py +587 -0
- package/fupload/scripts/fupload_cli/transport.py +125 -0
- package/fupload/scripts/fupload_cli/trust.py +207 -0
- package/npm/bin/fupload.mjs +92 -0
- package/npm/lib/curseforge-config.mjs +36 -0
- package/npm/lib/managed-install.mjs +86 -0
- package/npm/lib/options.mjs +38 -0
- package/npm/lib/python.mjs +45 -0
- package/npm/lib/skill-installer.mjs +228 -0
- package/npm/lib/uninstall.mjs +211 -0
- package/npm/lib/update.mjs +102 -0
- package/npm/lib/versions.mjs +63 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/skill-manifest.json +179 -0
- package/package.json +50 -6
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""Versioned write schemas with presence-aware validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import zipfile
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
from .errors import ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
JSON_TYPES = {
|
|
14
|
+
"string": (str,),
|
|
15
|
+
"integer": (int,),
|
|
16
|
+
"number": (int, float),
|
|
17
|
+
"boolean": (bool,),
|
|
18
|
+
"array": (list,),
|
|
19
|
+
"object": (dict,),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Field:
|
|
25
|
+
type: str
|
|
26
|
+
required: bool = False
|
|
27
|
+
nullable: bool = False
|
|
28
|
+
choices: Tuple[Any, ...] = ()
|
|
29
|
+
nonempty: bool = False
|
|
30
|
+
max_length: Optional[int] = None
|
|
31
|
+
max_items: Optional[int] = None
|
|
32
|
+
minimum: Optional[float] = None
|
|
33
|
+
maximum: Optional[float] = None
|
|
34
|
+
local_file: bool = False
|
|
35
|
+
description: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Schema:
|
|
40
|
+
name: str
|
|
41
|
+
fields: Mapping[str, Field]
|
|
42
|
+
conditionals: Tuple[str, ...] = field(default_factory=tuple)
|
|
43
|
+
|
|
44
|
+
def validate(self, value: Mapping[str, Any]) -> Dict[str, Any]:
|
|
45
|
+
if not isinstance(value, dict):
|
|
46
|
+
raise ValidationError("input document must be an object")
|
|
47
|
+
unknown = sorted(set(value) - set(self.fields) - {"schema"})
|
|
48
|
+
if unknown:
|
|
49
|
+
raise ValidationError("unknown field(s): %s" % ", ".join(unknown), path="$.%s" % unknown[0])
|
|
50
|
+
actual = value.get("schema")
|
|
51
|
+
if actual != self.name:
|
|
52
|
+
raise ValidationError("schema must be %s" % self.name, path="$.schema")
|
|
53
|
+
for name, spec in self.fields.items():
|
|
54
|
+
if spec.required and name not in value:
|
|
55
|
+
raise ValidationError("field is required", path="$.%s" % name)
|
|
56
|
+
if name not in value:
|
|
57
|
+
continue
|
|
58
|
+
item = value[name]
|
|
59
|
+
if item is None:
|
|
60
|
+
if not spec.nullable:
|
|
61
|
+
raise ValidationError("null is not allowed", path="$.%s" % name)
|
|
62
|
+
continue
|
|
63
|
+
expected = JSON_TYPES[spec.type]
|
|
64
|
+
if spec.type in ("integer", "number") and isinstance(item, bool):
|
|
65
|
+
raise ValidationError("expected %s" % spec.type, path="$.%s" % name)
|
|
66
|
+
if not isinstance(item, expected):
|
|
67
|
+
raise ValidationError("expected %s" % spec.type, path="$.%s" % name)
|
|
68
|
+
if spec.nonempty and item in ("", []):
|
|
69
|
+
raise ValidationError("must not be empty", path="$.%s" % name)
|
|
70
|
+
if spec.max_length is not None and len(item) > spec.max_length:
|
|
71
|
+
raise ValidationError(
|
|
72
|
+
"must contain at most %d characters" % spec.max_length,
|
|
73
|
+
path="$.%s" % name,
|
|
74
|
+
)
|
|
75
|
+
if spec.max_items is not None and len(item) > spec.max_items:
|
|
76
|
+
raise ValidationError(
|
|
77
|
+
"must contain at most %d items" % spec.max_items,
|
|
78
|
+
path="$.%s" % name,
|
|
79
|
+
)
|
|
80
|
+
if spec.minimum is not None and item < spec.minimum:
|
|
81
|
+
raise ValidationError("must be at least %s" % spec.minimum, path="$.%s" % name)
|
|
82
|
+
if spec.maximum is not None and item > spec.maximum:
|
|
83
|
+
raise ValidationError("must be at most %s" % spec.maximum, path="$.%s" % name)
|
|
84
|
+
if spec.choices and item not in spec.choices:
|
|
85
|
+
raise ValidationError(
|
|
86
|
+
"must be one of: %s" % ", ".join(map(str, spec.choices)),
|
|
87
|
+
path="$.%s" % name,
|
|
88
|
+
)
|
|
89
|
+
if spec.local_file and (not item or not os.path.isfile(item)):
|
|
90
|
+
raise ValidationError("file does not exist or is not a regular file", path="$.%s" % name)
|
|
91
|
+
checked = dict(value)
|
|
92
|
+
self._validate_conditionals(checked)
|
|
93
|
+
return checked
|
|
94
|
+
|
|
95
|
+
def _validate_conditionals(self, value: Dict[str, Any]) -> None:
|
|
96
|
+
for name in ("id", "mod_id", "file_id", "content_id", "source_id", "module_id", "game_version_id", "cloud_id"):
|
|
97
|
+
if name in value and isinstance(value[name], int) and value[name] <= 0:
|
|
98
|
+
raise ValidationError("must be greater than zero", path="$.%s" % name)
|
|
99
|
+
if value.get("public") is True and value.get("submit_for_review") is not True:
|
|
100
|
+
raise ValidationError(
|
|
101
|
+
"public=true requires submit_for_review=true",
|
|
102
|
+
path="$.submit_for_review",
|
|
103
|
+
)
|
|
104
|
+
if value.get("submit_for_review") is True and value.get("public") is not True:
|
|
105
|
+
raise ValidationError(
|
|
106
|
+
"submit_for_review=true requires public=true",
|
|
107
|
+
path="$.public",
|
|
108
|
+
)
|
|
109
|
+
if value.get("need_buy") is True:
|
|
110
|
+
for name in ("price_fen", "buy_life_type"):
|
|
111
|
+
if name not in value or value[name] in (None, ""):
|
|
112
|
+
raise ValidationError("%s is required when need_buy=true" % name, path="$.%s" % name)
|
|
113
|
+
if value.get("jump_room") is True:
|
|
114
|
+
if not value.get("room_id"):
|
|
115
|
+
raise ValidationError("room_id is required when jump_room=true", path="$.room_id")
|
|
116
|
+
has_channel_id = bool(value.get("channel_id"))
|
|
117
|
+
has_channel_type = bool(value.get("channel_type"))
|
|
118
|
+
if has_channel_id != has_channel_type:
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
"channel_id and channel_type must both be empty or both be nonempty",
|
|
121
|
+
path="$.channel_id" if not has_channel_id else "$.channel_type",
|
|
122
|
+
)
|
|
123
|
+
if value.get("sync_room") is True:
|
|
124
|
+
if value.get("jump_room") is False:
|
|
125
|
+
raise ValidationError("sync_room=true requires jump_room=true", path="$.jump_room")
|
|
126
|
+
if value.get("scope") == "private":
|
|
127
|
+
raise ValidationError("sync_room=true requires public scope", path="$.scope")
|
|
128
|
+
if value.get("with_associate") is True and not value.get("associated_acts"):
|
|
129
|
+
raise ValidationError("associated_acts must contain at least one item when with_associate=true", path="$.associated_acts")
|
|
130
|
+
if value.get("need_anchor_vip") is True and value.get("scope") == "private":
|
|
131
|
+
raise ValidationError("need_anchor_vip=true requires public scope", path="$.scope")
|
|
132
|
+
if self.name.startswith("fupload.v1.dd.") and self.name.endswith(".create"):
|
|
133
|
+
paid_private_config = self.name == "fupload.v1.dd.config.create" and value.get("need_buy") is True
|
|
134
|
+
if value.get("scope") == "private" and not value.get("share_code_life_type") and not paid_private_config:
|
|
135
|
+
raise ValidationError("share_code_life_type is required when scope=private", path="$.share_code_life_type")
|
|
136
|
+
if self.name in ("fupload.v1.dd.wa.create", "fupload.v1.dd.wa.update") and "version" in value:
|
|
137
|
+
if not value["version"].isdigit():
|
|
138
|
+
raise ValidationError("WA version must contain digits only", path="$.version")
|
|
139
|
+
if value.get("with_file") is True:
|
|
140
|
+
if self.name == "fupload.v1.dd.wa.create" and not value.get("file"):
|
|
141
|
+
raise ValidationError("file is required when with_file=true on create", path="$.file")
|
|
142
|
+
if self.name == "fupload.v1.dd.wa.create" and not value.get("file_install_path"):
|
|
143
|
+
raise ValidationError("required when with_file=true", path="$.file_install_path")
|
|
144
|
+
if value.get("string_mode") == "collection":
|
|
145
|
+
if not value.get("wa_str_titles"):
|
|
146
|
+
raise ValidationError("required for collection mode", path="$.wa_str_titles")
|
|
147
|
+
one_of = []
|
|
148
|
+
if self.name == "fupload.v1.newbee.plugin.create":
|
|
149
|
+
one_of.append(("logo", "logo_file"))
|
|
150
|
+
if not value.get("screenshots") and not value.get("screenshot_files"):
|
|
151
|
+
raise ValidationError("screenshots or screenshot_files must contain at least one image", path="$.screenshots")
|
|
152
|
+
if self.name == "fupload.v1.newbee.config.create":
|
|
153
|
+
one_of.append(("picture_urls", "picture_files"))
|
|
154
|
+
if self.name == "fupload.v1.newbee.wa.create":
|
|
155
|
+
one_of.append(("thumbnail", "thumbnail_file"))
|
|
156
|
+
if self.name == "fupload.v1.dd.plugin.create":
|
|
157
|
+
one_of.extend((("logo", "logo_file"), ("detail_imgs", "detail_img_files"), ("detail_url", "file")))
|
|
158
|
+
if self.name == "fupload.v1.dd.config.create":
|
|
159
|
+
one_of.append(("display_imgs", "display_img_files"))
|
|
160
|
+
if self.name == "fupload.v1.dd.wa.create":
|
|
161
|
+
one_of.append(("display_imgs", "display_img_files"))
|
|
162
|
+
for choices in one_of:
|
|
163
|
+
if not any(value.get(name) for name in choices):
|
|
164
|
+
raise ValidationError("one of %s is required" % ", ".join(choices), path="$.%s" % choices[0])
|
|
165
|
+
if "cloud_id" in value and value.get("cloud_id") is not None and self.name == "fupload.v1.newbee.config.update":
|
|
166
|
+
required = ("linked_mods", "ignored_unknown_mods", "ignored_materials", "ignored_fronts", "roleid")
|
|
167
|
+
for name in required:
|
|
168
|
+
if name not in value:
|
|
169
|
+
raise ValidationError("%s is required when cloud_id is changed" % name, path="$.%s" % name)
|
|
170
|
+
if self.name == "fupload.v1.dd.config.update" and "backup_sn" in value:
|
|
171
|
+
if not value.get("update_desc"):
|
|
172
|
+
raise ValidationError("update_desc is required for DD config update", path="$.update_desc")
|
|
173
|
+
self._validate_nested(value)
|
|
174
|
+
|
|
175
|
+
def _validate_nested(self, value: Mapping[str, Any]) -> None:
|
|
176
|
+
def scalar_array(name: str, expected: Tuple[type, ...], message: str) -> None:
|
|
177
|
+
if name not in value:
|
|
178
|
+
return
|
|
179
|
+
items = value[name]
|
|
180
|
+
if not isinstance(items, list):
|
|
181
|
+
raise ValidationError("expected array", path="$.%s" % name)
|
|
182
|
+
for index, item in enumerate(items):
|
|
183
|
+
if isinstance(item, bool) or not isinstance(item, expected) or (isinstance(item, str) and not item):
|
|
184
|
+
raise ValidationError(message, path="$.%s[%d]" % (name, index))
|
|
185
|
+
|
|
186
|
+
def object_array(name: str, allowed: set[str], required_names: Tuple[str, ...] = ()) -> None:
|
|
187
|
+
if name not in value:
|
|
188
|
+
return
|
|
189
|
+
items = value[name]
|
|
190
|
+
if not isinstance(items, list):
|
|
191
|
+
raise ValidationError("expected array", path="$.%s" % name)
|
|
192
|
+
for index, item in enumerate(items):
|
|
193
|
+
path = "$.%s[%d]" % (name, index)
|
|
194
|
+
if not isinstance(item, dict):
|
|
195
|
+
raise ValidationError("expected object", path=path)
|
|
196
|
+
unknown = sorted(set(item) - allowed)
|
|
197
|
+
if unknown:
|
|
198
|
+
raise ValidationError("unknown field(s): %s" % ", ".join(unknown), path=path + "." + unknown[0])
|
|
199
|
+
for required_name in required_names:
|
|
200
|
+
if required_name not in item:
|
|
201
|
+
raise ValidationError("field is required", path=path + "." + required_name)
|
|
202
|
+
|
|
203
|
+
if self.name.startswith("fupload.v1.newbee"):
|
|
204
|
+
object_array(
|
|
205
|
+
"linked_mods",
|
|
206
|
+
{"mod_id", "mod_name", "mod_file_id", "mod_version", "display_name", "update_type", "updateType"},
|
|
207
|
+
("mod_id",),
|
|
208
|
+
)
|
|
209
|
+
object_array("attachments", {"name", "install_type", "install_path", "value", "is_compressed", "timestamp"}, ("name", "install_type", "install_path", "value", "is_compressed"))
|
|
210
|
+
object_array("co_authors", {"user_id", "share_percent"}, ("user_id", "share_percent"))
|
|
211
|
+
object_array("references", {"type", "id"}, ("type", "id"))
|
|
212
|
+
if "co_authors" in value:
|
|
213
|
+
total = 0.0
|
|
214
|
+
for index, item in enumerate(value["co_authors"]):
|
|
215
|
+
share = item["share_percent"]
|
|
216
|
+
if isinstance(share, bool) or not isinstance(share, (int, float)) or share <= 0 or share > 1:
|
|
217
|
+
raise ValidationError("share_percent must be in (0,1]", path="$.co_authors[%d].share_percent" % index)
|
|
218
|
+
if isinstance(item["user_id"], bool) or not isinstance(item["user_id"], int) or item["user_id"] <= 0:
|
|
219
|
+
raise ValidationError("user_id must be greater than zero", path="$.co_authors[%d].user_id" % index)
|
|
220
|
+
total += float(share)
|
|
221
|
+
if total > 1.000001:
|
|
222
|
+
raise ValidationError("co_authors share_percent total may not exceed 1", path="$.co_authors")
|
|
223
|
+
if "references" in value:
|
|
224
|
+
for index, item in enumerate(value["references"]):
|
|
225
|
+
for name in ("type", "id"):
|
|
226
|
+
candidate = item[name]
|
|
227
|
+
if isinstance(candidate, bool) or not isinstance(candidate, int) or candidate <= 0:
|
|
228
|
+
raise ValidationError("%s must be a positive integer" % name, path="$.references[%d].%s" % (index, name))
|
|
229
|
+
for name in ("mod_categories", "category_id_list"):
|
|
230
|
+
if name in value and any(isinstance(item, bool) or not isinstance(item, int) or item <= 0 for item in value[name]):
|
|
231
|
+
raise ValidationError("array must contain positive integer IDs", path="$.%s" % name)
|
|
232
|
+
scalar_array("game_version_list", (str,), "array must contain nonempty game-version strings")
|
|
233
|
+
if "game_version_list" in value and any(not item.strip() for item in value["game_version_list"]):
|
|
234
|
+
raise ValidationError("array must contain nonempty game-version strings", path="$.game_version_list")
|
|
235
|
+
for name in (
|
|
236
|
+
"screenshots", "picture_urls", "images", "screenshot_files", "picture_files",
|
|
237
|
+
"image_files", "ignored_unknown_mods", "ignored_materials", "ignored_fronts",
|
|
238
|
+
"wa_str_titles",
|
|
239
|
+
):
|
|
240
|
+
scalar_array(name, (str,), "array must contain nonempty strings")
|
|
241
|
+
if self.name.startswith("fupload.v1.dd"):
|
|
242
|
+
object_array("associated_acts", {"sn", "act_type"}, ("sn", "act_type"))
|
|
243
|
+
for name in ("second_category_ids", "vip_levels"):
|
|
244
|
+
if name in value and any(isinstance(item, bool) or not isinstance(item, int) for item in value[name]):
|
|
245
|
+
raise ValidationError("array must contain integer IDs", path="$.%s" % name)
|
|
246
|
+
for name in ("game_type", "primary_category_id"):
|
|
247
|
+
if name in value and value[name] <= 0:
|
|
248
|
+
raise ValidationError("must be greater than zero", path="$.%s" % name)
|
|
249
|
+
for existing, local in (("detail_imgs", "detail_img_files"), ("display_imgs", "display_img_files")):
|
|
250
|
+
if len(value.get(existing) or []) + len(value.get(local) or []) > 8:
|
|
251
|
+
raise ValidationError("combined existing and local images may contain at most 8 items", path="$.%s" % local)
|
|
252
|
+
for index, item in enumerate(value.get("associated_acts") or []):
|
|
253
|
+
if not isinstance(item.get("sn"), str) or not item["sn"]:
|
|
254
|
+
raise ValidationError("expected nonempty string", path="$.associated_acts[%d].sn" % index)
|
|
255
|
+
if item.get("act_type") not in ("addon", "share", "wa"):
|
|
256
|
+
raise ValidationError("act_type must be addon, share, or wa", path="$.associated_acts[%d].act_type" % index)
|
|
257
|
+
scalar_array("game_versions", (str,), "array must contain nonempty version strings")
|
|
258
|
+
scalar_array("category_ids", (str, int), "array must contain nonempty category IDs")
|
|
259
|
+
for name in ("detail_imgs", "display_imgs", "detail_img_files", "display_img_files"):
|
|
260
|
+
scalar_array(name, (str,), "array must contain nonempty strings")
|
|
261
|
+
for name in ("detail_img_files", "display_img_files"):
|
|
262
|
+
for index, path in enumerate(value.get(name) or []):
|
|
263
|
+
if not os.path.isfile(path):
|
|
264
|
+
raise ValidationError(
|
|
265
|
+
"file does not exist or is not a regular file",
|
|
266
|
+
path="$.%s[%d]" % (name, index),
|
|
267
|
+
)
|
|
268
|
+
for name in ("known_addon_ids", "known_addon_update_ids"):
|
|
269
|
+
scalar_array(name, (int,), "array must contain integer addon IDs")
|
|
270
|
+
for name in (
|
|
271
|
+
"unknown_addon_ids", "unknown_addon_update_ids", "wtf_role_ids",
|
|
272
|
+
"material_names", "material_update_names", "font_names", "font_update_names",
|
|
273
|
+
"known_wa_ids", "known_wa_update_ids", "unknown_wa_ids", "unknown_wa_update_ids",
|
|
274
|
+
):
|
|
275
|
+
scalar_array(name, (str,), "array must contain nonempty strings")
|
|
276
|
+
if len(value.get("wtf_role_ids") or []) > 1:
|
|
277
|
+
raise ValidationError("at most one WTF role may be selected", path="$.wtf_role_ids")
|
|
278
|
+
if "price_fen" in value:
|
|
279
|
+
price = value["price_fen"]
|
|
280
|
+
if price != 0 and not 10 <= price <= 20000:
|
|
281
|
+
raise ValidationError("price_fen must be 0 or between 10 and 20000", path="$.price_fen")
|
|
282
|
+
if self.name.startswith("fupload.v1.dd.wa") and "version" in value and not value["version"].isdigit():
|
|
283
|
+
raise ValidationError("version must contain digits only", path="$.version")
|
|
284
|
+
if self.name.startswith("fupload.v1.dd.config") and value.get("retail_ui_config") is not None:
|
|
285
|
+
retail = value["retail_ui_config"]
|
|
286
|
+
allowed = {
|
|
287
|
+
"edit_mode_selectors", "default_edit_mode_selector",
|
|
288
|
+
"cool_down_selectors", "enable_dd_setup_wizard",
|
|
289
|
+
}
|
|
290
|
+
unknown = sorted(set(retail) - allowed)
|
|
291
|
+
if unknown:
|
|
292
|
+
raise ValidationError(
|
|
293
|
+
"unknown field(s): %s" % ", ".join(unknown),
|
|
294
|
+
path="$.retail_ui_config.%s" % unknown[0],
|
|
295
|
+
)
|
|
296
|
+
for name in ("edit_mode_selectors", "cool_down_selectors"):
|
|
297
|
+
if name not in retail:
|
|
298
|
+
continue
|
|
299
|
+
if not isinstance(retail[name], list):
|
|
300
|
+
raise ValidationError("expected array", path="$.retail_ui_config.%s" % name)
|
|
301
|
+
for index, item in enumerate(retail[name]):
|
|
302
|
+
if not isinstance(item, str) or not item:
|
|
303
|
+
raise ValidationError("array must contain nonempty selector strings", path="$.retail_ui_config.%s[%d]" % (name, index))
|
|
304
|
+
if "default_edit_mode_selector" in retail:
|
|
305
|
+
selector = retail["default_edit_mode_selector"]
|
|
306
|
+
if selector is not None and (not isinstance(selector, str) or not selector):
|
|
307
|
+
raise ValidationError("expected nonempty string or null", path="$.retail_ui_config.default_edit_mode_selector")
|
|
308
|
+
if "enable_dd_setup_wizard" in retail and not isinstance(retail["enable_dd_setup_wizard"], bool):
|
|
309
|
+
raise ValidationError("expected boolean", path="$.retail_ui_config.enable_dd_setup_wizard")
|
|
310
|
+
if self.name == "fupload.v1.curseforge.plugin.upload":
|
|
311
|
+
if not zipfile.is_zipfile(value["file"]):
|
|
312
|
+
raise ValidationError("file must be a valid ZIP archive", path="$.file")
|
|
313
|
+
if "game_versions" in value and any(isinstance(item, bool) or not isinstance(item, int) or item <= 0 for item in value["game_versions"]):
|
|
314
|
+
raise ValidationError("array must contain positive integer IDs", path="$.game_versions")
|
|
315
|
+
if "game_version_names" in value and any(not isinstance(item, str) or not item.strip() for item in value["game_version_names"]):
|
|
316
|
+
raise ValidationError("array must contain nonempty strings", path="$.game_version_names")
|
|
317
|
+
relations = value.get("relations")
|
|
318
|
+
if relations is not None:
|
|
319
|
+
if set(relations) != {"projects"}:
|
|
320
|
+
unknown = sorted(set(relations) - {"projects"})
|
|
321
|
+
message = "unknown field(s): %s" % ", ".join(unknown) if unknown else "projects is required"
|
|
322
|
+
raise ValidationError(message, path="$.relations")
|
|
323
|
+
if not isinstance(relations["projects"], list):
|
|
324
|
+
raise ValidationError("expected array", path="$.relations.projects")
|
|
325
|
+
for index, relation in enumerate((relations or {}).get("projects") or []):
|
|
326
|
+
if not isinstance(relation, dict):
|
|
327
|
+
raise ValidationError("expected object", path="$.relations.projects[%d]" % index)
|
|
328
|
+
unknown = sorted(set(relation) - {"slug", "type", "project_id"})
|
|
329
|
+
if unknown:
|
|
330
|
+
raise ValidationError("unknown field(s): %s" % ", ".join(unknown), path="$.relations.projects[%d].%s" % (index, unknown[0]))
|
|
331
|
+
if not {"slug", "type"}.issubset(relation):
|
|
332
|
+
raise ValidationError("slug and type are required", path="$.relations.projects[%d]" % index)
|
|
333
|
+
if not isinstance(relation["slug"], str) or not relation["slug"].strip():
|
|
334
|
+
raise ValidationError("expected nonempty string", path="$.relations.projects[%d].slug" % index)
|
|
335
|
+
if relation["type"] not in ("embeddedLibrary", "incompatible", "optionalDependency", "requiredDependency", "tool"):
|
|
336
|
+
raise ValidationError("unsupported relation type", path="$.relations.projects[%d].type" % index)
|
|
337
|
+
if "project_id" in relation and (isinstance(relation["project_id"], bool) or not isinstance(relation["project_id"], int) or relation["project_id"] <= 0):
|
|
338
|
+
raise ValidationError("must be a positive integer", path="$.relations.projects[%d].project_id" % index)
|
|
339
|
+
if "parent_file_id" in value:
|
|
340
|
+
for field_name in ("game_versions", "game_version_names"):
|
|
341
|
+
if field_name in value:
|
|
342
|
+
raise ValidationError("must be omitted when parent_file_id is set", path="$.%s" % field_name)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def f(type_name: str, **kwargs: Any) -> Field:
|
|
346
|
+
return Field(type_name, **kwargs)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
NB_PLUGIN_META = {
|
|
350
|
+
"name": f("string", nonempty=True),
|
|
351
|
+
"mod_categories": f("array", nonempty=True, max_items=5),
|
|
352
|
+
"content_origin": f("integer"),
|
|
353
|
+
"content_format": f("integer"),
|
|
354
|
+
"intro": f("string"),
|
|
355
|
+
"description": f("string"),
|
|
356
|
+
"logo": f("string"),
|
|
357
|
+
"logo_file": f("string", local_file=True),
|
|
358
|
+
"screenshots": f("array"),
|
|
359
|
+
"screenshot_files": f("array"),
|
|
360
|
+
"public": f("boolean"),
|
|
361
|
+
"submit_for_review": f("boolean"),
|
|
362
|
+
"subscribe_plan_level": f("integer", minimum=0),
|
|
363
|
+
"link_to_channel": f("boolean"),
|
|
364
|
+
"co_authors": f("array"),
|
|
365
|
+
"references": f("array"),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
NB_CONFIG_META = {
|
|
369
|
+
"title": f("string", nonempty=True), "content": f("string", nonempty=True),
|
|
370
|
+
"content_format": f("integer"), "intro": f("string"),
|
|
371
|
+
"picture_urls": f("array"), "picture_files": f("array"),
|
|
372
|
+
"content_origin": f("integer"), "public": f("boolean"),
|
|
373
|
+
"submit_for_review": f("boolean"), "link_to_channel": f("boolean"),
|
|
374
|
+
"subscribe_plan_level": f("integer", minimum=0), "price": f("integer", minimum=0),
|
|
375
|
+
"time_range": f("string"),
|
|
376
|
+
"co_authors": f("array"), "references": f("array"),
|
|
377
|
+
}
|
|
378
|
+
NB_CONFIG_BACKUP = {
|
|
379
|
+
"cloud_id": f("integer"), "linked_mods": f("array"),
|
|
380
|
+
"ignored_unknown_mods": f("array"), "ignored_materials": f("array"),
|
|
381
|
+
"ignored_fronts": f("array"), "roleid": f("string"),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
NB_WA_META = {
|
|
385
|
+
"game_version_id": f("integer"), "name": f("string", nonempty=True),
|
|
386
|
+
"intro": f("string"), "description": f("string", nonempty=True), "content_format": f("integer"),
|
|
387
|
+
"thumbnail": f("string"), "thumbnail_file": f("string", local_file=True),
|
|
388
|
+
"images": f("array"), "image_files": f("array"),
|
|
389
|
+
"category_id_list": f("array", nonempty=True, max_items=5), "content_origin": f("integer"),
|
|
390
|
+
"subscribe_plan_level": f("integer", minimum=0), "price": f("integer", minimum=0), "time_range": f("string"),
|
|
391
|
+
"public": f("boolean"), "submit_for_review": f("boolean"),
|
|
392
|
+
"link_to_channel": f("boolean"), "attachments": f("array"),
|
|
393
|
+
"co_authors": f("array"), "references": f("array"),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
DD_COMMERCIAL = {
|
|
397
|
+
"scope": f("string", choices=("public", "private")),
|
|
398
|
+
"share_code_life_type": f("string"), "need_buy": f("boolean"),
|
|
399
|
+
"price_fen": f("integer"), "buy_life_type": f("string"),
|
|
400
|
+
"jump_room": f("boolean"), "room_id": f("string"), "channel_id": f("string"),
|
|
401
|
+
"channel_type": f("string"), "sync_room": f("boolean"),
|
|
402
|
+
"creation_statement": f("string", choices=("original", "chinesize", "renovate", "second")), "with_associate": f("boolean"),
|
|
403
|
+
"associated_acts": f("array"), "need_anchor_vip": f("boolean"),
|
|
404
|
+
"vip_levels": f("array"),
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
DD_PLUGIN_META = {
|
|
408
|
+
"game_type": f("integer"), "scope": DD_COMMERCIAL["scope"], "addon_type": f("integer", choices=(0, 1)),
|
|
409
|
+
"name": f("string", nonempty=True, max_length=80),
|
|
410
|
+
"description": f("string", nonempty=True, max_length=80),
|
|
411
|
+
"logo": f("string"), "logo_file": f("string", local_file=True),
|
|
412
|
+
"detail_imgs": f("array", max_items=8), "detail_img_files": f("array", max_items=8),
|
|
413
|
+
"primary_category_id": f("integer"), "second_category_ids": f("array"),
|
|
414
|
+
"html_desc": f("string", nonempty=True),
|
|
415
|
+
**DD_COMMERCIAL,
|
|
416
|
+
}
|
|
417
|
+
DD_PLUGIN_EDIT_META = {
|
|
418
|
+
name: DD_COMMERCIAL[name]
|
|
419
|
+
for name in DD_COMMERCIAL
|
|
420
|
+
}
|
|
421
|
+
DD_PLUGIN_VERSION = {
|
|
422
|
+
"game_versions": f("array", nonempty=True), "detail_url": f("string"),
|
|
423
|
+
"file": f("string", local_file=True), "release_type": f("integer", choices=(1, 2, 3)),
|
|
424
|
+
"version": f("string", nonempty=True, max_length=80),
|
|
425
|
+
"update_desc": f("string", nonempty=True, max_length=1000),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
DD_CONFIG_META = {
|
|
429
|
+
"scope": DD_COMMERCIAL["scope"], "title": f("string", nonempty=True, max_length=40),
|
|
430
|
+
"brief_desc": f("string", nonempty=True, max_length=50), "desc": f("string", nonempty=True),
|
|
431
|
+
"display_imgs": f("array", max_items=8), "display_img_files": f("array", max_items=8),
|
|
432
|
+
**DD_COMMERCIAL,
|
|
433
|
+
}
|
|
434
|
+
DD_CONFIG_CONTENT = {
|
|
435
|
+
"backup_sn": f("string", nonempty=True), "update_desc": f("string", max_length=1000),
|
|
436
|
+
"known_addon_ids": f("array"), "known_addon_update_ids": f("array"),
|
|
437
|
+
"unknown_addon_ids": f("array"), "unknown_addon_update_ids": f("array"),
|
|
438
|
+
"wtf_role_ids": f("array"), "material_names": f("array"), "material_update_names": f("array"),
|
|
439
|
+
"font_names": f("array"), "font_update_names": f("array"),
|
|
440
|
+
"known_wa_ids": f("array"), "known_wa_update_ids": f("array"),
|
|
441
|
+
"unknown_wa_ids": f("array"), "unknown_wa_update_ids": f("array"),
|
|
442
|
+
"retail_ui_config": f(
|
|
443
|
+
"object", nullable=True,
|
|
444
|
+
description="safe selector object: edit_mode_selectors, default_edit_mode_selector, cool_down_selectors, enable_dd_setup_wizard",
|
|
445
|
+
),
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
DD_WA_META = {
|
|
449
|
+
"game_type": f("integer"), "scope": DD_COMMERCIAL["scope"],
|
|
450
|
+
"name": f("string", nonempty=True, max_length=40), "game_version": f("string", nonempty=True),
|
|
451
|
+
"brief_desc": f("string", nonempty=True, max_length=50), "display_imgs": f("array", max_items=8),
|
|
452
|
+
"display_img_files": f("array", max_items=8), "category_ids": f("array", nonempty=True, max_items=5),
|
|
453
|
+
"desc": f("string", nonempty=True), **DD_COMMERCIAL,
|
|
454
|
+
}
|
|
455
|
+
DD_WA_CONTENT = {
|
|
456
|
+
"content": f("string", nonempty=True), "update_desc": f("string", nonempty=True, max_length=1000),
|
|
457
|
+
"version": f("string", nonempty=True, max_length=80), "with_file": f("boolean"),
|
|
458
|
+
"file": f("string", local_file=True),
|
|
459
|
+
"file_install_path": f("string", choices=("", "Interface", "Interface/Addons")),
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def required(fields: Mapping[str, Field], names: Iterable[str]) -> Dict[str, Field]:
|
|
464
|
+
result = dict(fields)
|
|
465
|
+
for name in names:
|
|
466
|
+
old = result[name]
|
|
467
|
+
result[name] = Field(
|
|
468
|
+
old.type, required=True, nullable=old.nullable, choices=old.choices,
|
|
469
|
+
nonempty=old.nonempty, max_length=old.max_length, max_items=old.max_items,
|
|
470
|
+
minimum=old.minimum, maximum=old.maximum,
|
|
471
|
+
local_file=old.local_file, description=old.description,
|
|
472
|
+
)
|
|
473
|
+
return result
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def with_id(fields: Mapping[str, Field], name: str = "id") -> Dict[str, Field]:
|
|
477
|
+
return {name: f("string" if name in ("sn", "share_sn") else "integer", required=True, nonempty=True), **fields}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
SCHEMAS: Dict[Tuple[str, str, str], Schema] = {}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def register(platform: str, resource: str, action: str, fields: Mapping[str, Field]) -> None:
|
|
484
|
+
name = "fupload.v1.%s.%s.%s" % (platform, resource, action)
|
|
485
|
+
SCHEMAS[(platform, resource, action)] = Schema(name, fields)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
register("newbee", "plugin", "create", required(NB_PLUGIN_META, ("name", "mod_categories", "content_origin", "content_format", "intro", "description", "public")))
|
|
489
|
+
register("newbee", "plugin", "update", required({"mod_id": f("integer"), "version": f("string", nonempty=True), "game_version_list": f("array", nonempty=True), "file": f("string", local_file=True), "changelog": f("string"), "link_to_channel": f("boolean")}, ("mod_id", "version", "game_version_list", "file")))
|
|
490
|
+
register("newbee", "plugin", "edit", with_id(NB_PLUGIN_META))
|
|
491
|
+
register("newbee", "plugin-changelog", "edit", required({"file_id": f("integer"), "changelog": f("string", nullable=True)}, ("file_id", "changelog")))
|
|
492
|
+
|
|
493
|
+
register("newbee", "config", "create", required({**NB_CONFIG_META, **NB_CONFIG_BACKUP}, ("cloud_id", "title", "content", "content_format", "content_origin", "public", "linked_mods", "ignored_unknown_mods", "ignored_materials", "ignored_fronts", "roleid")))
|
|
494
|
+
register("newbee", "config", "update", with_id(NB_CONFIG_BACKUP))
|
|
495
|
+
register("newbee", "config", "edit", with_id(NB_CONFIG_META))
|
|
496
|
+
|
|
497
|
+
register("newbee", "wa", "create", required({**NB_WA_META, "wa_str": f("string", nonempty=True), "wa_str_titles": f("array"), "wa_log": f("string", nonempty=True), "string_mode": f("string", choices=("single", "collection"))}, ("game_version_id", "name", "description", "content_format", "category_id_list", "content_origin", "public", "wa_str", "wa_log", "string_mode")))
|
|
498
|
+
register("newbee", "wa", "update", required({"id": f("integer"), "version": f("string", nonempty=True), "wa_str": f("string", nonempty=True), "wa_str_titles": f("array"), "wa_log": f("string", nonempty=True), "link_to_channel": f("boolean")}, ("id", "wa_str", "wa_log")))
|
|
499
|
+
register("newbee", "wa", "edit", with_id(NB_WA_META))
|
|
500
|
+
register("newbee", "wa-media", "upload", required({"file": f("string", local_file=True), "kind": f("string", choices=("image", "attachment")), "install_type": f("integer"), "install_path": f("string")}, ("file", "kind")))
|
|
501
|
+
register("newbee", "wa-changelog", "edit", required({"id": f("integer"), "wa_id": f("integer"), "wa_log": f("string", nullable=True)}, ("id", "wa_log")))
|
|
502
|
+
for _resource in ("plugin", "config", "wa"):
|
|
503
|
+
register("newbee", _resource + "-co-author", "set", required({"content_id": f("integer"), "co_authors": f("array")}, ("content_id", "co_authors")))
|
|
504
|
+
register("newbee", _resource + "-reference", "set", required({"source_id": f("integer"), "references": f("array")}, ("source_id", "references")))
|
|
505
|
+
register("newbee", "wa-share-code", "set", required({"module_id": f("integer")}, ("module_id",)))
|
|
506
|
+
for _resource in ("plugin", "config", "wa"):
|
|
507
|
+
register("newbee", _resource, "delete", required({"id": f("integer"), "confirm": f("string", choices=("DELETE",))}, ("id", "confirm")))
|
|
508
|
+
|
|
509
|
+
register("dd", "plugin", "create", required(
|
|
510
|
+
{**DD_PLUGIN_META, **DD_PLUGIN_VERSION},
|
|
511
|
+
("game_type", "scope", "addon_type", "name", "description",
|
|
512
|
+
"primary_category_id", "game_versions", "release_type", "version",
|
|
513
|
+
"html_desc", "update_desc", "creation_statement", "need_buy", "jump_room",
|
|
514
|
+
"with_associate", "need_anchor_vip"),
|
|
515
|
+
))
|
|
516
|
+
register("dd", "plugin", "update", with_id(required(DD_PLUGIN_VERSION, ("game_versions", "version", "update_desc")), "sn"))
|
|
517
|
+
register("dd", "plugin", "edit", with_id(DD_PLUGIN_EDIT_META, "sn"))
|
|
518
|
+
register("dd", "config", "create", required(
|
|
519
|
+
{**DD_CONFIG_META, **DD_CONFIG_CONTENT},
|
|
520
|
+
("scope", "backup_sn", "title", "brief_desc", "desc",
|
|
521
|
+
"creation_statement", "known_addon_ids", "unknown_addon_ids", "wtf_role_ids",
|
|
522
|
+
"material_names", "font_names", "known_wa_ids", "unknown_wa_ids", "need_buy",
|
|
523
|
+
"jump_room", "with_associate", "need_anchor_vip"),
|
|
524
|
+
))
|
|
525
|
+
register("dd", "config", "update", with_id(required(DD_CONFIG_CONTENT, ("backup_sn", "update_desc")), "share_sn"))
|
|
526
|
+
register("dd", "config", "edit", with_id(DD_CONFIG_META, "share_sn"))
|
|
527
|
+
register("dd", "wa", "create", required(
|
|
528
|
+
{**DD_WA_META, **DD_WA_CONTENT},
|
|
529
|
+
("game_type", "scope", "name", "game_version", "brief_desc",
|
|
530
|
+
"category_ids", "content", "desc", "update_desc", "version", "creation_statement",
|
|
531
|
+
"with_file", "need_buy", "jump_room", "with_associate", "need_anchor_vip"),
|
|
532
|
+
))
|
|
533
|
+
register("dd", "wa", "update", with_id(required(DD_WA_CONTENT, ("content", "update_desc", "version", "with_file")), "sn"))
|
|
534
|
+
register("dd", "wa", "edit", with_id(DD_WA_META, "sn"))
|
|
535
|
+
for _resource in ("plugin", "config", "wa"):
|
|
536
|
+
register("dd", _resource, "delete", required({
|
|
537
|
+
"sn": f("string", nonempty=True),
|
|
538
|
+
"confirm_delete": f("boolean", choices=(True,)),
|
|
539
|
+
}, ("sn", "confirm_delete")))
|
|
540
|
+
|
|
541
|
+
register("curseforge", "plugin", "upload", required({
|
|
542
|
+
"project_id": f("integer", minimum=1),
|
|
543
|
+
"file": f("string", local_file=True),
|
|
544
|
+
"changelog": f("string"),
|
|
545
|
+
"changelog_type": f("string", choices=("text", "html", "markdown")),
|
|
546
|
+
"display_name": f("string", nonempty=True),
|
|
547
|
+
"game_versions": f("array", nonempty=True),
|
|
548
|
+
"game_version_names": f("array"),
|
|
549
|
+
"release_type": f("string", choices=("alpha", "beta", "release")),
|
|
550
|
+
"parent_file_id": f("integer", minimum=1),
|
|
551
|
+
"relations": f("object"),
|
|
552
|
+
"is_marked_for_manual_release": f("boolean"),
|
|
553
|
+
}, ("project_id", "file", "changelog", "release_type")))
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def get_schema(platform: str, resource: str, action: str) -> Schema:
|
|
557
|
+
try:
|
|
558
|
+
return SCHEMAS[(platform, resource, action)]
|
|
559
|
+
except KeyError as exc:
|
|
560
|
+
raise ValidationError("no write schema for %s %s %s" % (platform, resource, action)) from exc
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def schema_help(platform: str, resource: str, action: str) -> str:
|
|
564
|
+
schema = get_schema(platform, resource, action)
|
|
565
|
+
rows = ["Input schema: %s" % schema.name, "Fields:"]
|
|
566
|
+
for name, spec in schema.fields.items():
|
|
567
|
+
flags = [spec.type, "required" if spec.required else "optional"]
|
|
568
|
+
if spec.nullable:
|
|
569
|
+
flags.append("nullable/explicit clear")
|
|
570
|
+
if spec.choices:
|
|
571
|
+
flags.append("choices=" + "|".join(map(str, spec.choices)))
|
|
572
|
+
if spec.max_length is not None:
|
|
573
|
+
flags.append("max-length=" + str(spec.max_length))
|
|
574
|
+
if spec.max_items is not None:
|
|
575
|
+
flags.append("max-items=" + str(spec.max_items))
|
|
576
|
+
if spec.local_file:
|
|
577
|
+
flags.append("local file")
|
|
578
|
+
detail = ", ".join(flags)
|
|
579
|
+
if spec.description:
|
|
580
|
+
detail += "; " + spec.description
|
|
581
|
+
rows.append(" %-28s %s" % (name, detail))
|
|
582
|
+
rows.extend([
|
|
583
|
+
"Unknown fields are rejected. On edit/update, omission preserves the remote value.",
|
|
584
|
+
"Frontend preselected values are never used as business defaults.",
|
|
585
|
+
"Use --dry-run for local validation only; it does not validate remote IDs or permissions.",
|
|
586
|
+
])
|
|
587
|
+
return "\n".join(rows)
|