@follenfang/fupload 0.0.0-bootstrap.0 → 0.0.1
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 +208 -3
- package/fupload/SKILL.md +129 -0
- package/fupload/agents/openai.yaml +4 -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/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 +266 -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 +539 -0
- package/fupload/scripts/fupload_cli/transport.py +125 -0
- package/fupload/scripts/fupload_cli/trust.py +207 -0
- package/npm/bin/fupload.mjs +90 -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 +99 -0
- package/npm/lib/versions.mjs +63 -0
- package/npm/postinstall.mjs +18 -0
- package/npm/skill-manifest.json +164 -0
- package/package.json +50 -6
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
"""NewBeeBox Creator provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import http.client
|
|
7
|
+
import json
|
|
8
|
+
import mimetypes
|
|
9
|
+
import os
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
15
|
+
|
|
16
|
+
from .errors import FuploadError, ValidationError
|
|
17
|
+
from .transport import json_request, multipart_request
|
|
18
|
+
from .newbee_auth import API_BASE, API_ORIGIN, auth_store_dir, creator_headers
|
|
19
|
+
from .trust import NEWBEE_ORIGINS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
METADATA_URL = NEWBEE_ORIGINS["metadata"] + "/modconfig.json"
|
|
23
|
+
UPLOAD_SERVER = NEWBEE_ORIGINS["upload"] + "/uploadserver"
|
|
24
|
+
NEXT_API_BASE = NEWBEE_ORIGINS["next"]
|
|
25
|
+
NEXT_ORIGIN = "next"
|
|
26
|
+
METADATA_ORIGIN = "metadata"
|
|
27
|
+
UPLOAD_ORIGIN = "upload"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Creator Center uses different numeric namespaces for each main record type.
|
|
31
|
+
RELATION_TYPES = {
|
|
32
|
+
"plugin": {"co_authors": 1, "references": 1},
|
|
33
|
+
"config": {"co_authors": 4, "references": 3},
|
|
34
|
+
"wa": {"co_authors": 3, "references": 2},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _first_object(value: Any) -> Dict[str, Any]:
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
return value
|
|
41
|
+
if isinstance(value, list) and value and isinstance(value[0], dict):
|
|
42
|
+
return value[0]
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _decode(value: Any) -> Any:
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(value)
|
|
50
|
+
except ValueError:
|
|
51
|
+
return value
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pick(value: Mapping[str, Any], *names: str, default: Any = None) -> Any:
|
|
56
|
+
for name in names:
|
|
57
|
+
if name in value and value[name] is not None:
|
|
58
|
+
return value[name]
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _list(value: Any) -> List[Any]:
|
|
63
|
+
value = _decode(value)
|
|
64
|
+
return list(value) if isinstance(value, list) else []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _urls(value: Any) -> List[str]:
|
|
68
|
+
result = []
|
|
69
|
+
for item in _list(value):
|
|
70
|
+
if isinstance(item, str):
|
|
71
|
+
result.append(item)
|
|
72
|
+
elif isinstance(item, dict):
|
|
73
|
+
url = _pick(item, "media_url", "url", "name", default="")
|
|
74
|
+
if url:
|
|
75
|
+
result.append(str(url))
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _selected_ids(value: Any) -> List[int]:
|
|
80
|
+
"""Project selected option rows to their top-level numeric wire IDs."""
|
|
81
|
+
decoded = _decode(value)
|
|
82
|
+
if decoded is None:
|
|
83
|
+
return []
|
|
84
|
+
if not isinstance(decoded, list):
|
|
85
|
+
raise FuploadError("remote selected-ID field was not an array", kind="platform_data_error")
|
|
86
|
+
found: List[int] = []
|
|
87
|
+
for item in decoded:
|
|
88
|
+
candidate = _pick(item, "id", "t_id", "category_id", "t_category_id", "value", default=None) if isinstance(item, dict) else item
|
|
89
|
+
if isinstance(candidate, bool):
|
|
90
|
+
raise FuploadError("remote selected-ID item was not a positive integer", kind="platform_data_error")
|
|
91
|
+
if isinstance(candidate, int) and candidate > 0:
|
|
92
|
+
found.append(candidate)
|
|
93
|
+
elif isinstance(candidate, str) and candidate.isdigit() and int(candidate) > 0:
|
|
94
|
+
found.append(int(candidate))
|
|
95
|
+
else:
|
|
96
|
+
raise FuploadError("remote selected-ID item was not a positive integer", kind="platform_data_error")
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _selected_names(value: Any) -> List[str]:
|
|
101
|
+
"""Project remote option rows to the string values accepted by mutations."""
|
|
102
|
+
decoded = _decode(value)
|
|
103
|
+
if decoded is None:
|
|
104
|
+
return []
|
|
105
|
+
if not isinstance(decoded, list):
|
|
106
|
+
raise FuploadError("remote selected-name field was not an array", kind="platform_data_error")
|
|
107
|
+
found: List[str] = []
|
|
108
|
+
for item in decoded:
|
|
109
|
+
candidate = _pick(item, "name", "display_name", "label", "value", default="") if isinstance(item, dict) else item
|
|
110
|
+
if isinstance(candidate, str) and candidate:
|
|
111
|
+
found.append(candidate)
|
|
112
|
+
else:
|
|
113
|
+
raise FuploadError("remote selected-name item was not a nonempty string", kind="platform_data_error")
|
|
114
|
+
return found
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _version_greater(candidate: Any, current: Any) -> bool:
|
|
118
|
+
left = str(candidate or "").strip()
|
|
119
|
+
right = str(current or "").strip()
|
|
120
|
+
if not left or not right:
|
|
121
|
+
return False
|
|
122
|
+
if left.isdigit() and right.isdigit():
|
|
123
|
+
return int(left) > int(right)
|
|
124
|
+
def parts(value: str) -> List[int]:
|
|
125
|
+
return [int(part) for part in value.split(".") if part.isdigit()]
|
|
126
|
+
left_parts, right_parts = parts(left), parts(right)
|
|
127
|
+
return bool(left_parts and right_parts and left_parts > right_parts)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _redact_wa(value: Any) -> Any:
|
|
131
|
+
if isinstance(value, dict):
|
|
132
|
+
result = {}
|
|
133
|
+
for key, item in value.items():
|
|
134
|
+
if key.lower() in ("wa_str", "t_wa_str") and isinstance(item, str):
|
|
135
|
+
result[key + "_summary"] = {
|
|
136
|
+
"length": len(item), "sha256": hashlib.sha256(item.encode("utf-8")).hexdigest()
|
|
137
|
+
}
|
|
138
|
+
else:
|
|
139
|
+
result[key] = _redact_wa(item)
|
|
140
|
+
return result
|
|
141
|
+
if isinstance(value, list):
|
|
142
|
+
return [_redact_wa(item) for item in value]
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _option_rows(value: Any) -> List[Dict[str, Any]]:
|
|
147
|
+
"""Read only documented top-level option containers."""
|
|
148
|
+
if isinstance(value, list):
|
|
149
|
+
return [item for item in value if isinstance(item, dict)]
|
|
150
|
+
if not isinstance(value, dict):
|
|
151
|
+
return []
|
|
152
|
+
for key in ("list", "items", "rows", "options"):
|
|
153
|
+
if isinstance(value.get(key), list):
|
|
154
|
+
return [item for item in value[key] if isinstance(item, dict)]
|
|
155
|
+
data = value.get("data")
|
|
156
|
+
if isinstance(data, list):
|
|
157
|
+
return [item for item in data if isinstance(item, dict)]
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _same_value(expected: Any, actual: Any) -> bool:
|
|
162
|
+
if isinstance(expected, bool):
|
|
163
|
+
return actual is expected
|
|
164
|
+
if isinstance(expected, (int, float)) and not isinstance(expected, bool):
|
|
165
|
+
try:
|
|
166
|
+
return float(actual) == float(expected)
|
|
167
|
+
except (TypeError, ValueError):
|
|
168
|
+
return False
|
|
169
|
+
if isinstance(expected, list):
|
|
170
|
+
if not isinstance(actual, list):
|
|
171
|
+
return False
|
|
172
|
+
unmatched = list(actual)
|
|
173
|
+
for wanted in expected:
|
|
174
|
+
match = next((index for index, candidate in enumerate(unmatched) if _same_value(wanted, candidate)), None)
|
|
175
|
+
if match is None:
|
|
176
|
+
return False
|
|
177
|
+
unmatched.pop(match)
|
|
178
|
+
return len(unmatched) == 0
|
|
179
|
+
if isinstance(expected, dict):
|
|
180
|
+
if not isinstance(actual, dict):
|
|
181
|
+
return False
|
|
182
|
+
for name, wanted in expected.items():
|
|
183
|
+
actual_name = "updateType" if name == "update_type" and "updateType" in actual else name
|
|
184
|
+
if actual_name not in actual or not _same_value(wanted, actual[actual_name]):
|
|
185
|
+
return False
|
|
186
|
+
return True
|
|
187
|
+
if isinstance(expected, str) and isinstance(actual, str):
|
|
188
|
+
if expected.startswith(("http://", "https://")) or actual.startswith(("http://", "https://")):
|
|
189
|
+
expected_path = urllib.parse.urlsplit(expected).path.lstrip("/")
|
|
190
|
+
actual_path = urllib.parse.urlsplit(actual).path.lstrip("/")
|
|
191
|
+
return bool(expected_path and expected_path == actual_path)
|
|
192
|
+
return actual == expected
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _require_readback(expected: Mapping[str, Any], actual: Mapping[str, Any], endpoint: str) -> None:
|
|
196
|
+
mismatches = []
|
|
197
|
+
for name, wanted in expected.items():
|
|
198
|
+
if name not in actual or not _same_value(wanted, actual[name]):
|
|
199
|
+
mismatches.append(name)
|
|
200
|
+
if mismatches:
|
|
201
|
+
raise FuploadError(
|
|
202
|
+
"write readback did not match field(s): %s" % ", ".join(sorted(mismatches)),
|
|
203
|
+
kind="verification_required", endpoint=endpoint, verification_required=True,
|
|
204
|
+
details={"fields": sorted(mismatches)},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _paged_items(value: Any) -> Tuple[int, List[Dict[str, Any]], Dict[str, Any]]:
|
|
209
|
+
obj = _first_object(value)
|
|
210
|
+
raw_items = _list(_pick(obj, "list", "items", default=[]))
|
|
211
|
+
return int(_pick(obj, "total", "count", default=len(raw_items)) or len(raw_items)), [x for x in raw_items if isinstance(x, dict)], obj
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _plugin_summary(item: Mapping[str, Any]) -> Dict[str, Any]:
|
|
215
|
+
versions = []
|
|
216
|
+
for version in _list(item.get("game_versions")):
|
|
217
|
+
if isinstance(version, dict):
|
|
218
|
+
versions.append({
|
|
219
|
+
"id": version.get("id"), "name": version.get("name"),
|
|
220
|
+
"support_version": version.get("support_version"), "tag": version.get("tag"),
|
|
221
|
+
})
|
|
222
|
+
return {
|
|
223
|
+
"id": int(_pick(item, "t_id", "id", default=0) or 0),
|
|
224
|
+
"name": str(_pick(item, "t_name", "name", default="")),
|
|
225
|
+
"public": int(_pick(item, "t_share", "share_state", default=0) or 0) == 1,
|
|
226
|
+
"review_status": _pick(item, "t_check", "review_status"),
|
|
227
|
+
"content_format": _pick(item, "t_content_format", "content_format"),
|
|
228
|
+
"content_origin": _pick(item, "t_original", "content_origin"),
|
|
229
|
+
"logo": _pick(item, "t_logo", "logo"),
|
|
230
|
+
"screenshots": _urls(item.get("screenshots")),
|
|
231
|
+
"game_versions": versions,
|
|
232
|
+
"subscribe_plan_level": _pick(item, "t_subscribe_plan_level", "subscribe_plan_level"),
|
|
233
|
+
"link_to_channel": bool(_pick(item, "t_link_to_channel", "link_to_channel", default=False)),
|
|
234
|
+
"updated_at": _pick(item, "t_last_update", "updated_at"),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _wa_summary(item: Mapping[str, Any]) -> Dict[str, Any]:
|
|
239
|
+
categories = []
|
|
240
|
+
for category in _list(item.get("category_list")):
|
|
241
|
+
if isinstance(category, dict):
|
|
242
|
+
categories.append({"id": category.get("t_id"), "name": category.get("t_show_name") or category.get("t_name")})
|
|
243
|
+
return {
|
|
244
|
+
"id": int(_pick(item, "t_id", "id", default=0) or 0),
|
|
245
|
+
"name": str(_pick(item, "t_name", "name", default="")),
|
|
246
|
+
"version": str(_pick(item, "t_version", "version", default="")),
|
|
247
|
+
"public": int(_pick(item, "t_share_state", "share_state", default=2) or 2) == 1,
|
|
248
|
+
"review_status": _pick(item, "t_check_status", "review_status"),
|
|
249
|
+
"game_version_id": _pick(item, "t_game_version_id", "game_version_id"),
|
|
250
|
+
"thumbnail": _pick(item, "t_thumbnail", "thumbnail"),
|
|
251
|
+
"images": _urls(_pick(item, "t_images", "images", default=[])),
|
|
252
|
+
"categories": categories,
|
|
253
|
+
"attachments": _list(_pick(item, "t_attachments", "attachments", default=[])),
|
|
254
|
+
"content_format": _pick(item, "t_content_format", "content_format"),
|
|
255
|
+
"content_origin": _pick(item, "t_content_origin", "content_origin"),
|
|
256
|
+
"subscribe_plan_level": _pick(item, "t_subscribe_plan_level", "subscribe_plan_level"),
|
|
257
|
+
"price": _pick(item, "price", "t_price"),
|
|
258
|
+
"time_range": _pick(item, "t_time_range", "time_range", default=""),
|
|
259
|
+
"link_to_channel": bool(_pick(item, "t_link_to_channel", "link_to_channel", default=False)),
|
|
260
|
+
"updated_at": _pick(item, "t_update_time", "updated_at"),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class NewBee:
|
|
265
|
+
platform = "newbee"
|
|
266
|
+
|
|
267
|
+
def __init__(self) -> None:
|
|
268
|
+
self.base = API_BASE.rstrip("/")
|
|
269
|
+
self._headers: Optional[Dict[str, str]] = None
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def headers(self) -> Dict[str, str]:
|
|
273
|
+
if self._headers is None:
|
|
274
|
+
self._headers = creator_headers()
|
|
275
|
+
return self._headers
|
|
276
|
+
|
|
277
|
+
def post(self, endpoint: str, body: Mapping[str, Any]) -> Any:
|
|
278
|
+
envelope = json_request(
|
|
279
|
+
self.base + endpoint, method="POST", headers=self.headers, body=body,
|
|
280
|
+
trusted_service=API_ORIGIN,
|
|
281
|
+
)
|
|
282
|
+
if not isinstance(envelope, dict) or envelope.get("code") != 1:
|
|
283
|
+
raise FuploadError(
|
|
284
|
+
str((envelope or {}).get("message") or "NewBeeBox request failed"),
|
|
285
|
+
endpoint=endpoint,
|
|
286
|
+
business_code=(envelope or {}).get("code"),
|
|
287
|
+
)
|
|
288
|
+
return envelope.get("data")
|
|
289
|
+
|
|
290
|
+
def post_next(self, endpoint: str, body: Mapping[str, Any]) -> Any:
|
|
291
|
+
envelope = json_request(
|
|
292
|
+
NEXT_API_BASE + endpoint, method="POST", headers=self.headers, body=body,
|
|
293
|
+
trusted_service=NEXT_ORIGIN,
|
|
294
|
+
)
|
|
295
|
+
if not isinstance(envelope, dict) or envelope.get("code") != 1:
|
|
296
|
+
raise FuploadError(
|
|
297
|
+
str((envelope or {}).get("message") or "NewBeeBox next request failed"),
|
|
298
|
+
endpoint=endpoint,
|
|
299
|
+
business_code=(envelope or {}).get("code"),
|
|
300
|
+
)
|
|
301
|
+
return envelope.get("data")
|
|
302
|
+
|
|
303
|
+
def upload(self, endpoint: str, path: str, fields: Optional[Mapping[str, str]] = None) -> Any:
|
|
304
|
+
envelope = multipart_request(
|
|
305
|
+
self.base + endpoint, path, headers=self.headers, fields=fields,
|
|
306
|
+
trusted_service=API_ORIGIN,
|
|
307
|
+
)
|
|
308
|
+
if not isinstance(envelope, dict) or envelope.get("code") != 1:
|
|
309
|
+
raise FuploadError(
|
|
310
|
+
str((envelope or {}).get("message") or "NewBeeBox upload failed"),
|
|
311
|
+
endpoint=endpoint,
|
|
312
|
+
business_code=(envelope or {}).get("code"),
|
|
313
|
+
)
|
|
314
|
+
return envelope.get("data")
|
|
315
|
+
|
|
316
|
+
def metadata(self) -> Dict[str, Any]:
|
|
317
|
+
value = json_request(METADATA_URL, trusted_service=METADATA_ORIGIN)
|
|
318
|
+
if not isinstance(value, dict):
|
|
319
|
+
raise FuploadError("NewBeeBox metadata had an unexpected shape", endpoint=METADATA_URL)
|
|
320
|
+
return value
|
|
321
|
+
|
|
322
|
+
def categories(self) -> Dict[str, Any]:
|
|
323
|
+
items = []
|
|
324
|
+
for item in _list(self.metadata().get("mod_category")):
|
|
325
|
+
if isinstance(item, dict):
|
|
326
|
+
items.append({
|
|
327
|
+
"id": int(item.get("t_id") or 0), "name": str(item.get("t_name") or ""),
|
|
328
|
+
"parent_id": int(item.get("t_parent_category_id") or 0),
|
|
329
|
+
"sort_index": int(item.get("t_show_index") or 0),
|
|
330
|
+
})
|
|
331
|
+
return {"total": len(items), "items": items}
|
|
332
|
+
|
|
333
|
+
def game_versions(self) -> Dict[str, Any]:
|
|
334
|
+
items = []
|
|
335
|
+
for item in _list(self.metadata().get("game_version")):
|
|
336
|
+
if isinstance(item, dict):
|
|
337
|
+
items.append({
|
|
338
|
+
"id": int(item.get("id") or 0), "name": str(item.get("name") or ""),
|
|
339
|
+
"search_enabled": bool(item.get("search_enable")), "versions": _list(item.get("version")),
|
|
340
|
+
})
|
|
341
|
+
return {"total": len(items), "items": items}
|
|
342
|
+
|
|
343
|
+
def list_plugins(self, keyword: str, page: int, size: int) -> Any:
|
|
344
|
+
raw = self.post("/creator/wow/mod/publish_list", {
|
|
345
|
+
"keyword": keyword, "game_version_id": 0, "sort_by": "t_last_update",
|
|
346
|
+
"sort_order": "DESC", "pagenum": page, "pagesize": size,
|
|
347
|
+
})
|
|
348
|
+
total, items, obj = _paged_items(raw)
|
|
349
|
+
return {"total": total, "items": [_plugin_summary(item) for item in items], "page": obj.get("pagenum", page), "page_size": obj.get("pagesize", size)}
|
|
350
|
+
|
|
351
|
+
def get_plugin_raw(self, ident: int) -> Dict[str, Any]:
|
|
352
|
+
return _first_object(self.post("/creator/wow/mod/publish_detail", {"id": ident}))
|
|
353
|
+
|
|
354
|
+
def get_plugin(self, ident: int) -> Dict[str, Any]:
|
|
355
|
+
raw = self.get_plugin_raw(ident)
|
|
356
|
+
summary = _plugin_summary(raw)
|
|
357
|
+
summary.update({
|
|
358
|
+
"intro": str(_pick(raw, "t_description", "intro", default="")),
|
|
359
|
+
"description": str(_pick(raw, "t_description_v2", "description", default="")),
|
|
360
|
+
"category_ids": _list(_pick(raw, "category_ids", "mod_categories", default=[])),
|
|
361
|
+
})
|
|
362
|
+
return summary
|
|
363
|
+
|
|
364
|
+
def plugin_versions(self, ident: int, page: int = 1, size: int = 100) -> Any:
|
|
365
|
+
return self.post("/creator/wow/mod_file/mod_file_list", {
|
|
366
|
+
"mod_id": ident, "game_version_id": 0, "pagenum": page, "pagesize": size,
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
def list_backups_raw(self) -> List[Dict[str, Any]]:
|
|
370
|
+
raw = self.post("/creator/wow/share/list", {})
|
|
371
|
+
result: List[Dict[str, Any]] = []
|
|
372
|
+
def walk(value: Any) -> None:
|
|
373
|
+
if isinstance(value, list):
|
|
374
|
+
for item in value:
|
|
375
|
+
walk(item)
|
|
376
|
+
elif isinstance(value, dict):
|
|
377
|
+
result.append(value)
|
|
378
|
+
walk(raw)
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
def list_backups(self) -> Dict[str, Any]:
|
|
382
|
+
items = []
|
|
383
|
+
for item in self.list_backups_raw():
|
|
384
|
+
if "t_id" not in item:
|
|
385
|
+
continue
|
|
386
|
+
items.append({
|
|
387
|
+
"cloud_id": int(item.get("t_id") or 0), "name": item.get("t_name"),
|
|
388
|
+
"game_version_id": item.get("t_Versionid"), "created_at": item.get("t_create_time"),
|
|
389
|
+
"known_plugins": len(_list(item.get("t_Known_plug"))),
|
|
390
|
+
"unknown_plugins": len(_list(item.get("t_unKnown_list"))),
|
|
391
|
+
"materials": len(_list(item.get("t_material_list"))),
|
|
392
|
+
"fonts": len(_list(item.get("t_font_list"))),
|
|
393
|
+
})
|
|
394
|
+
return {"total": len(items), "items": items}
|
|
395
|
+
|
|
396
|
+
def get_backup(self, cloud_id: int) -> Dict[str, Any]:
|
|
397
|
+
for item in self.list_backups_raw():
|
|
398
|
+
if int(item.get("t_id") or 0) == cloud_id:
|
|
399
|
+
linked = []
|
|
400
|
+
for mod in _list(item.get("t_Known_plug")):
|
|
401
|
+
if isinstance(mod, dict):
|
|
402
|
+
linked.append({
|
|
403
|
+
"mod_id": int(mod.get("id") or 0), "mod_name": str(mod.get("name") or ""),
|
|
404
|
+
"mod_file_id": mod.get("mod_file_id"), "mod_version": mod.get("mod_version"),
|
|
405
|
+
"display_name": mod.get("display_name"),
|
|
406
|
+
"update_type": int(mod.get("updateType") or 1),
|
|
407
|
+
})
|
|
408
|
+
roles = []
|
|
409
|
+
for account in _list(item.get("wtflist")):
|
|
410
|
+
for server in _list((account or {}).get("server")):
|
|
411
|
+
for role in _list((server or {}).get("roleList")):
|
|
412
|
+
roles.append({
|
|
413
|
+
"account": (account or {}).get("account"), "server": (server or {}).get("serverName"),
|
|
414
|
+
"name": (role or {}).get("name"), "role_id": (role or {}).get("role_id"),
|
|
415
|
+
})
|
|
416
|
+
return {
|
|
417
|
+
"cloud_id": cloud_id, "name": item.get("t_name"), "linked_mods": linked,
|
|
418
|
+
"unknown_plugins": [str(x.get("name") or "") for x in _list(item.get("t_unKnown_list")) if isinstance(x, dict)],
|
|
419
|
+
"materials": [str(x.get("name") or "") for x in _list(item.get("t_material_list")) if isinstance(x, dict)],
|
|
420
|
+
"fonts": [str(x.get("name") or "") if isinstance(x, dict) else str(x) for x in _list(item.get("t_font_list"))],
|
|
421
|
+
"roles": roles,
|
|
422
|
+
}
|
|
423
|
+
raise FuploadError("cloud backup %d was not found" % cloud_id, kind="not_found")
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _validate_backup_selection(backup: Mapping[str, Any], doc: Mapping[str, Any]) -> None:
|
|
427
|
+
linked = {str(item.get("mod_id")) for item in _list(backup.get("linked_mods")) if isinstance(item, dict)}
|
|
428
|
+
selected = {str(item.get("mod_id")) for item in _list(doc.get("linked_mods")) if isinstance(item, dict)}
|
|
429
|
+
if selected - linked:
|
|
430
|
+
raise ValidationError("linked_mods contains an item absent from the selected cloud backup", path="$.linked_mods")
|
|
431
|
+
for field, source in (("ignored_unknown_mods", "unknown_plugins"), ("ignored_materials", "materials"), ("ignored_fronts", "fonts")):
|
|
432
|
+
available = {str(item) for item in _list(backup.get(source))}
|
|
433
|
+
selected = {str(item) for item in _list(doc.get(field))}
|
|
434
|
+
if selected - available:
|
|
435
|
+
raise ValidationError("%s contains an item absent from the selected cloud backup" % field, path="$.%s" % field)
|
|
436
|
+
roles = {str(item.get("role_id")) for item in _list(backup.get("roles")) if isinstance(item, dict)}
|
|
437
|
+
roleid = str(doc.get("roleid") or "")
|
|
438
|
+
if roleid and roleid not in roles:
|
|
439
|
+
raise ValidationError("roleid is absent from the selected cloud backup", path="$.roleid")
|
|
440
|
+
|
|
441
|
+
@staticmethod
|
|
442
|
+
def _created_id(value: Any, title: str, secondary: Optional[Any] = None) -> int:
|
|
443
|
+
direct = _pick(_first_object(value), "id", "t_id", "mod_id", "wa_id", default=0)
|
|
444
|
+
try:
|
|
445
|
+
if int(direct or 0) > 0:
|
|
446
|
+
return int(direct)
|
|
447
|
+
except (TypeError, ValueError):
|
|
448
|
+
pass
|
|
449
|
+
matches: List[int] = []
|
|
450
|
+
def walk(node: Any) -> None:
|
|
451
|
+
if isinstance(node, dict):
|
|
452
|
+
name = _pick(node, "name", "title", "t_name", "t_title", default=None)
|
|
453
|
+
if name == title:
|
|
454
|
+
candidate = _pick(node, "id", "t_id", "mod_id", "wa_id", default=0)
|
|
455
|
+
try:
|
|
456
|
+
candidate_id = int(candidate or 0)
|
|
457
|
+
except (TypeError, ValueError):
|
|
458
|
+
candidate_id = 0
|
|
459
|
+
if candidate_id > 0 and (secondary is None or str(_pick(node, "cloud_id", "t_cloudblackid", default="")) == str(secondary)):
|
|
460
|
+
matches.append(candidate_id)
|
|
461
|
+
for child in node.values():
|
|
462
|
+
if isinstance(child, (dict, list)):
|
|
463
|
+
walk(child)
|
|
464
|
+
elif isinstance(node, list):
|
|
465
|
+
for child in node:
|
|
466
|
+
walk(child)
|
|
467
|
+
walk(value)
|
|
468
|
+
return matches[0] if len(set(matches)) == 1 else 0
|
|
469
|
+
|
|
470
|
+
def list_configs(self, keyword: str, offset: int, size: int) -> Any:
|
|
471
|
+
return self.post("/creator/wow/share_config/publish_list", {
|
|
472
|
+
"keyword": keyword, "game_version_id": 0, "sort": 3,
|
|
473
|
+
"offset": offset, "pagesize": size,
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
def get_config_raw(self, ident: int) -> Dict[str, Any]:
|
|
477
|
+
return _first_object(self.post("/creator/wow/share_config/details_aps", {"id": ident}))
|
|
478
|
+
|
|
479
|
+
def get_config(self, ident: int) -> Dict[str, Any]:
|
|
480
|
+
detail = self.get_config_raw(ident)
|
|
481
|
+
return {
|
|
482
|
+
"id": int(_pick(detail, "t_id", "id", default=ident) or ident),
|
|
483
|
+
"title": _pick(detail, "t_title", "title", default=""),
|
|
484
|
+
"cloud_id": int(_pick(detail, "t_cloudblackid", "cloud_id", default=0) or 0),
|
|
485
|
+
"public": int(_pick(detail, "t_sharing", "sharing", default=0) or 0) != 0,
|
|
486
|
+
"review_status": _pick(detail, "t_check", "review_status"),
|
|
487
|
+
"content": _pick(detail, "t_content", "content", default=""),
|
|
488
|
+
"content_format": int(_pick(detail, "t_content_format", "content_format", default=0) or 0),
|
|
489
|
+
"intro": _pick(detail, "t_intro", "intro", default=""),
|
|
490
|
+
"picture_urls": _urls(_pick(detail, "pic_url", "picture_urls", "piclist", default=[])),
|
|
491
|
+
"content_origin": int(_pick(detail, "t_content_origin", "content_origin", default=0) or 0),
|
|
492
|
+
"link_to_channel": bool(_pick(detail, "t_link_to_channel", "link_to_channel", default=False)),
|
|
493
|
+
"subscribe_plan_level": int(_pick(detail, "t_subscribe_plan_level", "subscribe_plan_level", default=0) or 0),
|
|
494
|
+
"price": int(_pick(detail, "t_price", "price", default=0) or 0),
|
|
495
|
+
"time_range": _pick(detail, "t_time_range", "time_range", default=""),
|
|
496
|
+
"linked_mods": _list(_pick(detail, "t_linked_mods", "linked_mods", default=[])),
|
|
497
|
+
"ignored_unknown_mods": _list(_pick(detail, "t_ignored_unknown_mods", "ignored_unknown_mods", default=[])),
|
|
498
|
+
"ignored_materials": _list(_pick(detail, "t_ignored_materials", "ignored_materials", default=[])),
|
|
499
|
+
"ignored_fronts": _list(_pick(detail, "t_ignored_fronts", "ignored_fronts", default=[])),
|
|
500
|
+
"roleid": str(_pick(detail, "t_roleid", "roleid", "role_id", default="")),
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
def list_was(self, keyword: str, offset: int, size: int) -> Any:
|
|
504
|
+
raw = self.post("/creator/wow/wa/mtg_uc_publish_list", {
|
|
505
|
+
"keyword": keyword, "game_version_id": 0, "sort": 3,
|
|
506
|
+
"offset": offset, "pagesize": size,
|
|
507
|
+
})
|
|
508
|
+
total, items, obj = _paged_items(raw)
|
|
509
|
+
return {"total": total, "items": [_wa_summary(item) for item in items], "next_offset": obj.get("next_offset"), "offset": offset, "page_size": size}
|
|
510
|
+
|
|
511
|
+
def get_wa_raw(self, ident: int) -> Dict[str, Any]:
|
|
512
|
+
return _first_object(self.post("/creator/wow/wa/detail_aps", {"id": ident}))
|
|
513
|
+
|
|
514
|
+
def get_wa(self, ident: int) -> Dict[str, Any]:
|
|
515
|
+
raw = self.get_wa_raw(ident)
|
|
516
|
+
summary = _wa_summary(raw)
|
|
517
|
+
summary.update({
|
|
518
|
+
"intro": str(_pick(raw, "t_intro", "intro", default="")),
|
|
519
|
+
"description": str(_pick(raw, "t_description", "description", default="")),
|
|
520
|
+
"wa_str_titles": _list(_pick(raw, "t_wa_str_titles", "wa_str_titles", default=[])),
|
|
521
|
+
})
|
|
522
|
+
return _redact_wa(summary)
|
|
523
|
+
|
|
524
|
+
def wa_categories(self, game_version_id: int) -> Any:
|
|
525
|
+
return self.post("/creator/wow/wa/category", {"game_version": game_version_id})
|
|
526
|
+
|
|
527
|
+
def attachment_paths(self) -> Any:
|
|
528
|
+
return self.post("/creator/wow/wa/attachment_install_path_list", {})
|
|
529
|
+
|
|
530
|
+
def content_origins(self) -> Dict[str, Any]:
|
|
531
|
+
rows = _option_rows(self.post("/v3/sys/content_origin_list", {}))
|
|
532
|
+
return {"total": len(rows), "items": [{"label": row.get("label"), "value": row.get("value")} for row in rows if row.get("value") is not None]}
|
|
533
|
+
|
|
534
|
+
def subscribe_plans(self) -> Dict[str, Any]:
|
|
535
|
+
rows = _option_rows(self.post("/creator/author_subscribe/plan_level_preset", {}))
|
|
536
|
+
return {"total": len(rows), "items": [{"label": row.get("label") or row.get("name"), "value": row.get("value")} for row in rows if row.get("value") is not None]}
|
|
537
|
+
|
|
538
|
+
def time_ranges(self) -> Dict[str, Any]:
|
|
539
|
+
rows = _option_rows(self.post_next("/cloudsaveserver/GameCloudSavePublish/GetTimeRangeList", {}))
|
|
540
|
+
return {"total": len(rows), "items": [{"label": row.get("label") or row.get("name"), "value": row.get("value")} for row in rows if row.get("value") is not None]}
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _option_values(payload: Mapping[str, Any], path: str) -> set[str]:
|
|
544
|
+
values = {str(item["value"]) for item in payload.get("items", []) if isinstance(item, dict) and item.get("value") is not None}
|
|
545
|
+
if not values:
|
|
546
|
+
raise FuploadError("live option response contained no selectable values", kind="platform_data_error", details={"path": path})
|
|
547
|
+
return values
|
|
548
|
+
|
|
549
|
+
def _validate_business_options(self, doc: Mapping[str, Any]) -> None:
|
|
550
|
+
if "content_origin" in doc:
|
|
551
|
+
allowed = self._option_values(self.content_origins(), "$.content_origin")
|
|
552
|
+
if str(doc["content_origin"]) not in allowed:
|
|
553
|
+
raise ValidationError("content_origin is unavailable", path="$.content_origin")
|
|
554
|
+
if int(doc.get("subscribe_plan_level") or 0):
|
|
555
|
+
allowed = self._option_values(self.subscribe_plans(), "$.subscribe_plan_level")
|
|
556
|
+
if str(doc["subscribe_plan_level"]) not in allowed:
|
|
557
|
+
raise ValidationError("subscribe_plan_level is unavailable", path="$.subscribe_plan_level")
|
|
558
|
+
if doc.get("time_range"):
|
|
559
|
+
allowed = self._option_values(self.time_ranges(), "$.time_range")
|
|
560
|
+
if str(doc["time_range"]) not in allowed:
|
|
561
|
+
raise ValidationError("time_range is unavailable", path="$.time_range")
|
|
562
|
+
|
|
563
|
+
def _validate_changed_business_options(self, form: Mapping[str, Any], doc: Mapping[str, Any]) -> None:
|
|
564
|
+
fields = ("content_origin", "subscribe_plan_level", "time_range")
|
|
565
|
+
self._validate_business_options({name: form[name] for name in fields if name in doc})
|
|
566
|
+
|
|
567
|
+
@staticmethod
|
|
568
|
+
def _normalize_commercial(form: Dict[str, Any], public: bool) -> None:
|
|
569
|
+
"""Apply the Creator Center's submitted, not intermediate, payment state."""
|
|
570
|
+
subscription = int(form.get("subscribe_plan_level") or 0)
|
|
571
|
+
if subscription < 0:
|
|
572
|
+
raise ValidationError("subscribe_plan_level must not be negative", path="$.subscribe_plan_level")
|
|
573
|
+
form["subscribe_plan_level"] = subscription
|
|
574
|
+
if "price" in form:
|
|
575
|
+
price = int(form.get("price") or 0)
|
|
576
|
+
if price < 0:
|
|
577
|
+
raise ValidationError("price must not be negative", path="$.price")
|
|
578
|
+
form["price"] = price
|
|
579
|
+
# A one-time duration has no wire meaning without a one-time price.
|
|
580
|
+
if price == 0:
|
|
581
|
+
form["time_range"] = ""
|
|
582
|
+
if not public:
|
|
583
|
+
form["link_to_channel"] = False
|
|
584
|
+
|
|
585
|
+
@staticmethod
|
|
586
|
+
def _relation_rows(value: Any, name: str) -> Optional[List[Dict[str, Any]]]:
|
|
587
|
+
if isinstance(value, list):
|
|
588
|
+
return [item for item in value if isinstance(item, dict)]
|
|
589
|
+
if not isinstance(value, dict):
|
|
590
|
+
return None
|
|
591
|
+
for key in (name, "list", "items", "data"):
|
|
592
|
+
candidate = value.get(key)
|
|
593
|
+
if isinstance(candidate, list):
|
|
594
|
+
return [item for item in candidate if isinstance(item, dict)]
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
@staticmethod
|
|
598
|
+
def _require_relation_readback(name: str, expected: Sequence[Mapping[str, Any]], actual: Any, endpoint: str) -> None:
|
|
599
|
+
rows = NewBee._relation_rows(actual, name)
|
|
600
|
+
if rows is None:
|
|
601
|
+
raise FuploadError(
|
|
602
|
+
"relationship write succeeded but its readback had an unexpected shape",
|
|
603
|
+
kind="verification_required", endpoint=endpoint, verification_required=True,
|
|
604
|
+
)
|
|
605
|
+
if name == "co_authors":
|
|
606
|
+
wanted = {(int(item["user_id"]), float(item["share_percent"])) for item in expected}
|
|
607
|
+
observed = {
|
|
608
|
+
(int(_pick(item, "user_id", "t_user_id", "id", default=0) or 0),
|
|
609
|
+
float(_pick(item, "share_percent", "t_share_percent", "ratio", default=-1) or -1))
|
|
610
|
+
for item in rows
|
|
611
|
+
}
|
|
612
|
+
else:
|
|
613
|
+
wanted = {(int(item["type"]), int(item["id"])) for item in expected}
|
|
614
|
+
observed = {
|
|
615
|
+
(int(_pick(item, "type", "content_type", "t_type", default=0) or 0),
|
|
616
|
+
int(_pick(item, "id", "content_id", "t_id", default=0) or 0))
|
|
617
|
+
for item in rows
|
|
618
|
+
}
|
|
619
|
+
if wanted != observed:
|
|
620
|
+
raise FuploadError(
|
|
621
|
+
"relationship write readback did not match the complete replacement",
|
|
622
|
+
kind="verification_required", endpoint=endpoint, verification_required=True,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def _replace_relationships(self, resource: str, ident: int, doc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
626
|
+
types = RELATION_TYPES[resource]
|
|
627
|
+
result: Dict[str, Any] = {}
|
|
628
|
+
if "co_authors" in doc:
|
|
629
|
+
body = {"content_type": types["co_authors"], "content_id": ident, "co_authors": doc["co_authors"]}
|
|
630
|
+
mutation = self.post("/creator/co_author/set", body)
|
|
631
|
+
readback = self.post("/creator/co_author/list", {"content_type": types["co_authors"], "content_id": ident})
|
|
632
|
+
self._require_relation_readback("co_authors", doc["co_authors"], readback, "/creator/co_author/list")
|
|
633
|
+
result["co_authors"] = {"result": mutation, "readback": readback}
|
|
634
|
+
if "references" in doc:
|
|
635
|
+
body = {"source_type": types["references"], "source_id": ident, "references": doc["references"]}
|
|
636
|
+
mutation = self.post("/creator/content_reference/set", body)
|
|
637
|
+
readback = self.post("/creator/content_reference/list", {"content_type": types["references"], "content_id": ident})
|
|
638
|
+
self._require_relation_readback("references", doc["references"], readback, "/creator/content_reference/list")
|
|
639
|
+
result["references"] = {"result": mutation, "readback": readback}
|
|
640
|
+
return result
|
|
641
|
+
|
|
642
|
+
@staticmethod
|
|
643
|
+
def _wa_category_values(payload: Any) -> set[int]:
|
|
644
|
+
values: set[int] = set()
|
|
645
|
+
rows = _option_rows(payload)
|
|
646
|
+
def visit(row: Mapping[str, Any]) -> None:
|
|
647
|
+
raw = _pick(row, "id", "t_id", "category_id", "value", default=None)
|
|
648
|
+
if raw is not None and not isinstance(raw, bool):
|
|
649
|
+
try:
|
|
650
|
+
values.add(int(raw))
|
|
651
|
+
except (TypeError, ValueError):
|
|
652
|
+
pass
|
|
653
|
+
for key in ("children", "items", "options"):
|
|
654
|
+
for child in _list(row.get(key)):
|
|
655
|
+
if isinstance(child, dict):
|
|
656
|
+
visit(child)
|
|
657
|
+
for row in rows:
|
|
658
|
+
visit(row)
|
|
659
|
+
return values
|
|
660
|
+
|
|
661
|
+
def _validate_wa_categories(self, game_version_id: int, selected: Sequence[int]) -> None:
|
|
662
|
+
values = self._wa_category_values(self.wa_categories(game_version_id))
|
|
663
|
+
if not values:
|
|
664
|
+
raise FuploadError("live WA category response contained no selectable values", kind="platform_data_error")
|
|
665
|
+
invalid = sorted(set(map(int, selected)) - values)
|
|
666
|
+
if invalid:
|
|
667
|
+
raise ValidationError("category_id_list contains an unavailable live category", path="$.category_id_list")
|
|
668
|
+
|
|
669
|
+
@staticmethod
|
|
670
|
+
def _media_url(value: Any) -> str:
|
|
671
|
+
if isinstance(value, str):
|
|
672
|
+
return value
|
|
673
|
+
obj = _first_object(value)
|
|
674
|
+
result = _pick(obj, "media_url", "url", "name", default="")
|
|
675
|
+
if not result and isinstance(value, list) and value:
|
|
676
|
+
result = value[0] if isinstance(value[0], str) else ""
|
|
677
|
+
if not result:
|
|
678
|
+
raise FuploadError("media upload response did not contain a reusable URL")
|
|
679
|
+
return str(result)
|
|
680
|
+
|
|
681
|
+
def upload_media(self, endpoint: str, path: str) -> str:
|
|
682
|
+
if not os.path.isfile(path):
|
|
683
|
+
raise ValidationError("media file does not exist", path=path)
|
|
684
|
+
return self._media_url(self.upload(endpoint, path))
|
|
685
|
+
|
|
686
|
+
def upload_attachment(self, path: str) -> Dict[str, Any]:
|
|
687
|
+
if not os.path.isfile(path):
|
|
688
|
+
raise ValidationError("attachment file does not exist", path="$.file")
|
|
689
|
+
data = Path(path).read_bytes()
|
|
690
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
691
|
+
upload_code = hashlib.md5(data, usedforsecurity=False).hexdigest()
|
|
692
|
+
name = Path(path).name
|
|
693
|
+
prepare = json_request(
|
|
694
|
+
UPLOAD_SERVER + "/upload/v3/prepare", method="POST",
|
|
695
|
+
trusted_service=UPLOAD_ORIGIN,
|
|
696
|
+
body={
|
|
697
|
+
"code": upload_code, "indexType": 2, "fileName": name,
|
|
698
|
+
"files": [{"fullHash": upload_code, "totalSize": len(data), "chunks": [{"hash": upload_code, "size": len(data)}]}],
|
|
699
|
+
"checksumAlgorithm": 1,
|
|
700
|
+
},
|
|
701
|
+
)
|
|
702
|
+
if not isinstance(prepare, dict) or prepare.get("code") != 1:
|
|
703
|
+
raise FuploadError(str((prepare or {}).get("message") or "attachment upload preparation failed"), endpoint="/upload/v3/prepare")
|
|
704
|
+
prepared = prepare.get("data") or {}
|
|
705
|
+
item = (prepared.get("items") or {}).get(upload_code) or {}
|
|
706
|
+
if not item.get("exists"):
|
|
707
|
+
if not item.get("url") or not item.get("callback"):
|
|
708
|
+
raise FuploadError("attachment upload preparation omitted object credentials", endpoint="/upload/v3/prepare")
|
|
709
|
+
parsed = urllib.parse.urlsplit(item["url"])
|
|
710
|
+
if parsed.scheme.casefold() != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.fragment:
|
|
711
|
+
raise FuploadError("attachment object URL was not HTTPS", kind="trust_boundary")
|
|
712
|
+
connection = http.client.HTTPSConnection(parsed.hostname, parsed.port, timeout=600)
|
|
713
|
+
try:
|
|
714
|
+
target = parsed.path + (("?" + parsed.query) if parsed.query else "")
|
|
715
|
+
connection.request(
|
|
716
|
+
"PUT", target, body=data,
|
|
717
|
+
headers={"x-oss-callback": item["callback"], "Content-Length": str(len(data))},
|
|
718
|
+
)
|
|
719
|
+
response = connection.getresponse()
|
|
720
|
+
response.read()
|
|
721
|
+
if response.status < 200 or response.status >= 300:
|
|
722
|
+
raise FuploadError(
|
|
723
|
+
"attachment object upload returned HTTP %d" % response.status,
|
|
724
|
+
endpoint="object-storage PUT", http_status=response.status,
|
|
725
|
+
)
|
|
726
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
727
|
+
raise FuploadError("attachment upload result is uncertain", verification_required=True) from exc
|
|
728
|
+
finally:
|
|
729
|
+
connection.close()
|
|
730
|
+
index_code = str(prepared.get("id") or "")
|
|
731
|
+
index = json_request(
|
|
732
|
+
UPLOAD_SERVER + "/upload/v3/index/get", method="POST", body={"code": index_code},
|
|
733
|
+
trusted_service=UPLOAD_ORIGIN,
|
|
734
|
+
)
|
|
735
|
+
index_data = (index or {}).get("data") or {}
|
|
736
|
+
if (index or {}).get("code") != 1 or not index_data.get("code"):
|
|
737
|
+
raise FuploadError("attachment upload index could not be read back", endpoint="/upload/v3/index/get")
|
|
738
|
+
return {
|
|
739
|
+
"file_id": 0, "name": str(index_data.get("fileName") or name),
|
|
740
|
+
"value": str(index_data["code"]), "size": int(index_data.get("totalSize") or len(data)),
|
|
741
|
+
"type": mimetypes.guess_type(name)[0] or "application/zip", "timestamp": 0,
|
|
742
|
+
"sha256": digest,
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
def _resolve_media(self, endpoint: str, urls: Sequence[str], files: Sequence[str]) -> List[str]:
|
|
746
|
+
result = list(urls)
|
|
747
|
+
for path in files:
|
|
748
|
+
result.append(self.upload_media(endpoint, path))
|
|
749
|
+
return result
|
|
750
|
+
|
|
751
|
+
def _validate_ids(self, selected: Iterable[int], available: Iterable[int], path: str) -> None:
|
|
752
|
+
allowed = {int(value) for value in available}
|
|
753
|
+
invalid = sorted({int(value) for value in selected} - allowed)
|
|
754
|
+
if invalid:
|
|
755
|
+
raise ValidationError("unknown or unavailable ID(s): %s" % invalid, path=path)
|
|
756
|
+
|
|
757
|
+
def _validate_game_versions(self, selected: Iterable[str], path: str) -> None:
|
|
758
|
+
available = {
|
|
759
|
+
str(version).strip()
|
|
760
|
+
for item in self.game_versions()["items"]
|
|
761
|
+
for version in _list(item.get("versions"))
|
|
762
|
+
if str(version).strip()
|
|
763
|
+
}
|
|
764
|
+
invalid = sorted({str(value).strip() for value in selected} - available)
|
|
765
|
+
if invalid:
|
|
766
|
+
raise ValidationError("unknown or unavailable game version(s): %s" % invalid, path=path)
|
|
767
|
+
|
|
768
|
+
def create_plugin(self, doc: Dict[str, Any]) -> Any:
|
|
769
|
+
self.post("/creator/wow/mod/permission_check", {})
|
|
770
|
+
self._validate_ids(doc["mod_categories"], [x["id"] for x in self.categories()["items"]], "$.mod_categories")
|
|
771
|
+
self._validate_business_options(doc)
|
|
772
|
+
logo = doc.get("logo", "")
|
|
773
|
+
if doc.get("logo_file"):
|
|
774
|
+
logo = self.upload_media("/creator/wow/mod/upload_media", doc["logo_file"])
|
|
775
|
+
screenshots = self._resolve_media("/creator/wow/mod/upload_media", doc.get("screenshots", []), doc.get("screenshot_files", []))
|
|
776
|
+
if not logo:
|
|
777
|
+
raise ValidationError("logo or logo_file is required", path="$.logo")
|
|
778
|
+
payload = {
|
|
779
|
+
"mod_categories": doc["mod_categories"], "content_origin": doc["content_origin"],
|
|
780
|
+
"content_format": doc["content_format"], "name": doc["name"],
|
|
781
|
+
"description": doc["description"], "intro": doc["intro"], "logo": logo,
|
|
782
|
+
"screenshots": screenshots, "share_state": 0,
|
|
783
|
+
"subscribe_plan_level": doc.get("subscribe_plan_level", 0),
|
|
784
|
+
"link_to_channel": False,
|
|
785
|
+
}
|
|
786
|
+
self._normalize_commercial(payload, False)
|
|
787
|
+
result = self.post("/creator/wow/mod/create", payload)
|
|
788
|
+
ident = self._created_id(result, doc["name"])
|
|
789
|
+
if ident <= 0:
|
|
790
|
+
ident = self._created_id(self.list_plugins(doc["name"], 1, 100), doc["name"])
|
|
791
|
+
if ident <= 0:
|
|
792
|
+
ident = self._created_id(self.list_plugins("", 1, 100), doc["name"])
|
|
793
|
+
if ident <= 0:
|
|
794
|
+
raise FuploadError(
|
|
795
|
+
"plugin was submitted but its ID could not be resolved; read the author list before retrying",
|
|
796
|
+
kind="verification_required", verification_required=True,
|
|
797
|
+
)
|
|
798
|
+
readback = self.get_plugin(ident)
|
|
799
|
+
_require_readback({
|
|
800
|
+
"name": doc["name"], "category_ids": doc["mod_categories"],
|
|
801
|
+
"content_origin": doc["content_origin"], "content_format": doc["content_format"],
|
|
802
|
+
"intro": doc["intro"], "description": doc["description"], "logo": logo,
|
|
803
|
+
"screenshots": screenshots, "public": False,
|
|
804
|
+
"subscribe_plan_level": doc.get("subscribe_plan_level", 0), "link_to_channel": False,
|
|
805
|
+
}, readback, "/creator/wow/mod/publish_detail")
|
|
806
|
+
relationships = self._replace_relationships("plugin", ident, doc)
|
|
807
|
+
return {
|
|
808
|
+
"result": result, "id": ident,
|
|
809
|
+
"review_intent": bool(doc.get("public") and doc.get("submit_for_review")),
|
|
810
|
+
"public_after_first_version": bool(doc.get("public")),
|
|
811
|
+
"readback": readback, "relationships": relationships,
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
def _plugin_form(self, ident: int, detail: Dict[str, Any]) -> Dict[str, Any]:
|
|
815
|
+
return {
|
|
816
|
+
"id": ident,
|
|
817
|
+
"mod_categories": _selected_ids(_pick(detail, "category_ids", "mod_categories", default=[])),
|
|
818
|
+
"content_origin": int(_pick(detail, "t_original", "content_origin", default=0) or 0),
|
|
819
|
+
"content_format": int(_pick(detail, "t_content_format", "content_format", default=0) or 0),
|
|
820
|
+
"name": str(_pick(detail, "t_name", "name", default="")),
|
|
821
|
+
"description": str(_pick(detail, "t_description_v2", "description", "t_description", default="")),
|
|
822
|
+
"intro": str(_pick(detail, "t_description", "intro", default="")),
|
|
823
|
+
"logo": str(_pick(detail, "t_logo", "logo", default="")),
|
|
824
|
+
"screenshots": _urls(_pick(detail, "screenshots", default=[])),
|
|
825
|
+
"share_state": int(_pick(detail, "t_share", "share_state", default=0) or 0),
|
|
826
|
+
"subscribe_plan_level": int(_pick(detail, "t_subscribe_plan_level", "subscribe_plan_level", default=0) or 0),
|
|
827
|
+
"link_to_channel": bool(_pick(detail, "t_link_to_channel", "link_to_channel", default=False)),
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
def edit_plugin(self, doc: Dict[str, Any]) -> Any:
|
|
831
|
+
ident = int(doc["id"])
|
|
832
|
+
form = self._plugin_form(ident, self.get_plugin_raw(ident))
|
|
833
|
+
simple = ("name", "mod_categories", "content_origin", "content_format", "intro", "description", "logo", "screenshots", "subscribe_plan_level", "link_to_channel")
|
|
834
|
+
for name in simple:
|
|
835
|
+
if name in doc:
|
|
836
|
+
form[name] = doc[name]
|
|
837
|
+
if doc.get("logo_file"):
|
|
838
|
+
form["logo"] = self.upload_media("/creator/wow/mod/upload_media", doc["logo_file"])
|
|
839
|
+
if doc.get("screenshot_files"):
|
|
840
|
+
form["screenshots"] = self._resolve_media("/creator/wow/mod/upload_media", form["screenshots"], doc["screenshot_files"])
|
|
841
|
+
if "public" in doc:
|
|
842
|
+
if doc["public"] and not _list(_first_object(self.plugin_versions(ident)).get("list")):
|
|
843
|
+
raise ValidationError("a plugin must have a version before it can be submitted for public review", path="$.public")
|
|
844
|
+
form["share_state"] = 1 if doc["public"] else 0
|
|
845
|
+
if "mod_categories" in doc:
|
|
846
|
+
self._validate_ids(form["mod_categories"], [x["id"] for x in self.categories()["items"]], "$.mod_categories")
|
|
847
|
+
self._normalize_commercial(form, form["share_state"] == 1)
|
|
848
|
+
self._validate_changed_business_options(form, doc)
|
|
849
|
+
result = self.post("/creator/wow/mod/edit", form)
|
|
850
|
+
readback = self.get_plugin(ident)
|
|
851
|
+
mapping = {"mod_categories": "category_ids"}
|
|
852
|
+
expected = {mapping.get(name, name): form[name] for name in doc if name != "public" and name in {
|
|
853
|
+
"name", "mod_categories", "content_origin", "content_format", "intro", "description",
|
|
854
|
+
"logo", "screenshots", "subscribe_plan_level", "link_to_channel", "public",
|
|
855
|
+
}}
|
|
856
|
+
if "public" in doc:
|
|
857
|
+
expected["public"] = form["share_state"] == 1
|
|
858
|
+
_require_readback(expected, readback, "/creator/wow/mod/publish_detail")
|
|
859
|
+
relationships = self._replace_relationships("plugin", ident, doc)
|
|
860
|
+
return {"result": result, "review_intent": bool(doc.get("public") and doc.get("submit_for_review")), "readback": readback, "relationships": relationships}
|
|
861
|
+
|
|
862
|
+
def update_plugin(self, doc: Dict[str, Any]) -> Any:
|
|
863
|
+
ident = int(doc["mod_id"])
|
|
864
|
+
current = self.get_plugin_raw(ident)
|
|
865
|
+
versions = _list(_first_object(self.plugin_versions(ident)).get("list"))
|
|
866
|
+
for item in versions:
|
|
867
|
+
remote = str(_pick(item, "t_display_name", "display_name", "version", "t_version", default="")).strip()
|
|
868
|
+
if remote.lower() == str(doc["version"]).strip().lower():
|
|
869
|
+
raise ValidationError("version already exists; overwrite is not allowed", path="$.version")
|
|
870
|
+
self._validate_game_versions(doc["game_version_list"], "$.game_version_list")
|
|
871
|
+
size = Path(doc["file"]).stat().st_size
|
|
872
|
+
if size > 300 * 1024 * 1024 or Path(doc["file"]).suffix.lower() not in (".zip", ".rar", ".7z"):
|
|
873
|
+
raise ValidationError("plugin package must be .zip/.rar/.7z and no larger than 300 MB", path="$.file")
|
|
874
|
+
fields = {
|
|
875
|
+
"mod_id": str(ident), "version": str(doc["version"]),
|
|
876
|
+
"game_version_list": json.dumps(doc["game_version_list"], separators=(",", ":")),
|
|
877
|
+
"link_to_channel": json.dumps(
|
|
878
|
+
bool(doc["link_to_channel"])
|
|
879
|
+
if "link_to_channel" in doc
|
|
880
|
+
else bool(_pick(current, "t_link_to_channel", "link_to_channel", default=False))
|
|
881
|
+
),
|
|
882
|
+
}
|
|
883
|
+
if "changelog" in doc:
|
|
884
|
+
fields["changelog"] = str(doc["changelog"])
|
|
885
|
+
result = self.upload("/creator/wow/mod_file/upload_mod_file", doc["file"], fields)
|
|
886
|
+
readback = {"plugin": self.get_plugin(ident), "versions": self.plugin_versions(ident)}
|
|
887
|
+
version_items = _list(_first_object(readback["versions"]).get("list"))
|
|
888
|
+
uploaded = next(
|
|
889
|
+
(
|
|
890
|
+
item for item in version_items
|
|
891
|
+
if str(_pick(item, "t_display_name", "display_name", "version", "t_version", default="")).strip().lower()
|
|
892
|
+
== str(doc["version"]).strip().lower()
|
|
893
|
+
),
|
|
894
|
+
None,
|
|
895
|
+
)
|
|
896
|
+
if uploaded is None:
|
|
897
|
+
raise FuploadError(
|
|
898
|
+
"upload returned success but the new plugin version was not present in readback",
|
|
899
|
+
endpoint="/creator/wow/mod_file/mod_file_list",
|
|
900
|
+
)
|
|
901
|
+
bound_versions = _list(_pick(uploaded, "versions", "game_version_list", "t_game_version_list", default=[]))
|
|
902
|
+
bound_values = {
|
|
903
|
+
str(_pick(item, "version", "build", "support_version", "value", default=""))
|
|
904
|
+
if isinstance(item, dict) else str(item)
|
|
905
|
+
for item in bound_versions
|
|
906
|
+
}
|
|
907
|
+
missing_bindings = sorted(set(map(str, doc["game_version_list"])) - bound_values)
|
|
908
|
+
if not bound_versions or missing_bindings:
|
|
909
|
+
raise FuploadError(
|
|
910
|
+
"upload returned success but requested game-version bindings were not recorded",
|
|
911
|
+
endpoint="/creator/wow/mod_file/mod_file_list",
|
|
912
|
+
kind="verification_required", verification_required=True,
|
|
913
|
+
details={"missing_builds": missing_bindings},
|
|
914
|
+
)
|
|
915
|
+
return {
|
|
916
|
+
"result": result, "sha256": hashlib.sha256(Path(doc["file"]).read_bytes()).hexdigest(),
|
|
917
|
+
"readback": readback,
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
@staticmethod
|
|
921
|
+
def _linked_mods(value: Any, *, strict: bool = True) -> List[Dict[str, Any]]:
|
|
922
|
+
decoded = _decode(value)
|
|
923
|
+
if decoded is None:
|
|
924
|
+
return []
|
|
925
|
+
if not isinstance(decoded, list):
|
|
926
|
+
raise FuploadError("linked_mods response was not an array", kind="platform_data_error")
|
|
927
|
+
result = []
|
|
928
|
+
for item in decoded:
|
|
929
|
+
if not isinstance(item, dict):
|
|
930
|
+
raise ValidationError("each linked_mods item must be an object", path="$.linked_mods")
|
|
931
|
+
allowed = {"mod_id", "mod_name", "mod_file_id", "mod_version", "display_name", "update_type", "updateType"}
|
|
932
|
+
unknown = set(item) - allowed
|
|
933
|
+
if strict and unknown:
|
|
934
|
+
raise ValidationError("unknown linked_mods field: %s" % sorted(unknown)[0], path="$.linked_mods")
|
|
935
|
+
mod_id = item.get("mod_id")
|
|
936
|
+
if isinstance(mod_id, bool) or not isinstance(mod_id, int) or mod_id <= 0:
|
|
937
|
+
raise ValidationError("mod_id must be a positive integer", path="$.linked_mods")
|
|
938
|
+
for name in ("mod_name", "mod_version", "display_name"):
|
|
939
|
+
if item.get(name) is not None and not isinstance(item[name], str):
|
|
940
|
+
raise ValidationError("%s must be a string or null" % name, path="$.linked_mods")
|
|
941
|
+
if item.get("mod_file_id") is not None and (isinstance(item["mod_file_id"], bool) or not isinstance(item["mod_file_id"], int)):
|
|
942
|
+
raise ValidationError("mod_file_id must be an integer or null", path="$.linked_mods")
|
|
943
|
+
update_type = item.get("update_type", item.get("updateType", 1))
|
|
944
|
+
if isinstance(update_type, bool) or not isinstance(update_type, int):
|
|
945
|
+
raise ValidationError("update_type must be an integer", path="$.linked_mods")
|
|
946
|
+
result.append({
|
|
947
|
+
"mod_id": mod_id, "mod_name": item.get("mod_name"),
|
|
948
|
+
"mod_file_id": item.get("mod_file_id"), "mod_version": item.get("mod_version") or None,
|
|
949
|
+
"display_name": item.get("display_name") or None,
|
|
950
|
+
"updateType": update_type,
|
|
951
|
+
})
|
|
952
|
+
return result
|
|
953
|
+
|
|
954
|
+
@classmethod
|
|
955
|
+
def _canonical_linked_mods(cls, backup: Mapping[str, Any], value: Any) -> List[Dict[str, Any]]:
|
|
956
|
+
requested = cls._linked_mods(value)
|
|
957
|
+
live = {
|
|
958
|
+
item["mod_id"]: item
|
|
959
|
+
for item in cls._linked_mods(backup.get("linked_mods", []), strict=False)
|
|
960
|
+
}
|
|
961
|
+
result: List[Dict[str, Any]] = []
|
|
962
|
+
seen: set[int] = set()
|
|
963
|
+
for item in requested:
|
|
964
|
+
mod_id = item["mod_id"]
|
|
965
|
+
if mod_id in seen:
|
|
966
|
+
raise ValidationError("linked_mods contains a duplicate mod_id", path="$.linked_mods")
|
|
967
|
+
seen.add(mod_id)
|
|
968
|
+
if mod_id not in live:
|
|
969
|
+
raise ValidationError("linked_mods contains an item absent from the selected cloud backup", path="$.linked_mods")
|
|
970
|
+
result.append(dict(live[mod_id]))
|
|
971
|
+
return result
|
|
972
|
+
|
|
973
|
+
def _config_form(self, ident: int, detail: Dict[str, Any]) -> Dict[str, Any]:
|
|
974
|
+
return {
|
|
975
|
+
"tid": ident, "cloud_id": int(_pick(detail, "t_cloudblackid", "cloud_id", default=0) or 0),
|
|
976
|
+
"title": str(_pick(detail, "t_title", "title", default="")),
|
|
977
|
+
"content": str(_pick(detail, "t_content", "content", default="")),
|
|
978
|
+
"content_format": int(_pick(detail, "t_content_format", "content_format", default=0) or 0),
|
|
979
|
+
"intro": str(_pick(detail, "t_intro", "intro", default="")),
|
|
980
|
+
"pic_url": _urls(_pick(detail, "pic_url", "picture_urls", "piclist", default=[])),
|
|
981
|
+
"content_origin": int(_pick(detail, "t_content_origin", "content_origin", default=0) or 0),
|
|
982
|
+
"sharing": int(_pick(detail, "t_sharing", "sharing", default=0) or 0),
|
|
983
|
+
"link_to_channel": bool(_pick(detail, "t_link_to_channel", "link_to_channel", default=False)),
|
|
984
|
+
"subscribe_plan_level": int(_pick(detail, "t_subscribe_plan_level", "subscribe_plan_level", default=0) or 0),
|
|
985
|
+
"price": int(_pick(detail, "t_price", "price", default=0) or 0),
|
|
986
|
+
"time_range": str(_pick(detail, "t_time_range", "time_range", default="")),
|
|
987
|
+
"linked_mods": self._linked_mods(_pick(detail, "t_linked_mods", "linked_mods", default=[]), strict=False),
|
|
988
|
+
"ignored_unknown_mods": _selected_names(_pick(detail, "t_ignored_unknown_mods", "ignored_unknown_mods", default=[])),
|
|
989
|
+
"ignored_materials": _selected_names(_pick(detail, "t_ignored_materials", "ignored_materials", default=[])),
|
|
990
|
+
"ignored_fronts": _selected_names(_pick(detail, "t_ignored_fronts", "ignored_fronts", default=[])),
|
|
991
|
+
"roleid": str(_pick(detail, "t_roleid", "roleid", "role_id", default="")),
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
def create_config(self, doc: Dict[str, Any]) -> Any:
|
|
995
|
+
backup = self.get_backup(int(doc["cloud_id"]))
|
|
996
|
+
self._validate_backup_selection(backup, doc)
|
|
997
|
+
linked_mods = self._canonical_linked_mods(backup, doc["linked_mods"])
|
|
998
|
+
pictures = self._resolve_media("/creator/wow/share_config/upload", doc.get("picture_urls", []), doc.get("picture_files", []))
|
|
999
|
+
if not pictures:
|
|
1000
|
+
raise ValidationError("picture_urls or picture_files must contain at least one image", path="$.picture_urls")
|
|
1001
|
+
payload = {
|
|
1002
|
+
"cloud_id": doc["cloud_id"], "title": doc["title"], "content": doc["content"],
|
|
1003
|
+
"content_format": doc["content_format"], "intro": doc.get("intro", ""), "pic_url": pictures,
|
|
1004
|
+
"content_origin": doc["content_origin"], "sharing": 1 if doc["public"] else 0,
|
|
1005
|
+
"link_to_channel": bool(doc.get("link_to_channel", False)) if doc["public"] else False,
|
|
1006
|
+
"subscribe_plan_level": doc.get("subscribe_plan_level", 0), "price": doc.get("price", 0),
|
|
1007
|
+
"time_range": doc.get("time_range", ""), "linked_mods": linked_mods,
|
|
1008
|
+
"ignored_unknown_mods": doc["ignored_unknown_mods"], "ignored_materials": doc["ignored_materials"],
|
|
1009
|
+
"ignored_fronts": doc["ignored_fronts"], "roleid": doc["roleid"],
|
|
1010
|
+
}
|
|
1011
|
+
self._normalize_commercial(payload, bool(doc["public"]))
|
|
1012
|
+
self._validate_business_options(payload)
|
|
1013
|
+
result = self.post("/creator/wow/share_config/release", payload)
|
|
1014
|
+
ident = self._created_id(result, doc["title"], doc["cloud_id"])
|
|
1015
|
+
if ident <= 0:
|
|
1016
|
+
ident = self._created_id(self.list_configs(doc["title"], 0, 100), doc["title"], doc["cloud_id"])
|
|
1017
|
+
if ident <= 0:
|
|
1018
|
+
ident = self._created_id(self.list_configs("", 0, 100), doc["title"], doc["cloud_id"])
|
|
1019
|
+
if ident <= 0:
|
|
1020
|
+
raise FuploadError("configuration was submitted but its ID could not be resolved; read the author list before retrying", kind="verification_required", verification_required=True)
|
|
1021
|
+
readback = self.get_config(ident)
|
|
1022
|
+
expected = {name: payload[name] for name in (
|
|
1023
|
+
"title", "content", "content_format", "intro", "content_origin", "subscribe_plan_level", "price", "time_range",
|
|
1024
|
+
"linked_mods", "ignored_unknown_mods", "ignored_materials", "ignored_fronts", "roleid",
|
|
1025
|
+
)}
|
|
1026
|
+
expected["public"] = bool(payload["sharing"])
|
|
1027
|
+
expected["picture_urls"] = pictures
|
|
1028
|
+
expected["link_to_channel"] = payload["link_to_channel"]
|
|
1029
|
+
_require_readback(expected, readback, "/creator/wow/share_config/details_aps")
|
|
1030
|
+
relationships = self._replace_relationships("config", ident, doc)
|
|
1031
|
+
return {"result": result, "id": ident, "review_intent": bool(doc.get("public") and doc.get("submit_for_review")), "readback": readback, "relationships": relationships}
|
|
1032
|
+
|
|
1033
|
+
def update_config(self, doc: Dict[str, Any], metadata_only: bool) -> Any:
|
|
1034
|
+
ident = int(doc["id"])
|
|
1035
|
+
form = self._config_form(ident, self.get_config_raw(ident))
|
|
1036
|
+
if metadata_only:
|
|
1037
|
+
mapping = {
|
|
1038
|
+
"title": "title", "content": "content", "content_format": "content_format", "intro": "intro",
|
|
1039
|
+
"picture_urls": "pic_url", "content_origin": "content_origin", "link_to_channel": "link_to_channel",
|
|
1040
|
+
"subscribe_plan_level": "subscribe_plan_level", "price": "price", "time_range": "time_range",
|
|
1041
|
+
}
|
|
1042
|
+
for source, target in mapping.items():
|
|
1043
|
+
if source in doc:
|
|
1044
|
+
form[target] = doc[source]
|
|
1045
|
+
if doc.get("picture_files"):
|
|
1046
|
+
form["pic_url"] = self._resolve_media("/creator/wow/share_config/upload", form["pic_url"], doc["picture_files"])
|
|
1047
|
+
if "public" in doc:
|
|
1048
|
+
form["sharing"] = 1 if doc["public"] else 0
|
|
1049
|
+
else:
|
|
1050
|
+
mapping = {
|
|
1051
|
+
"cloud_id": "cloud_id", "linked_mods": "linked_mods", "ignored_unknown_mods": "ignored_unknown_mods",
|
|
1052
|
+
"ignored_materials": "ignored_materials", "ignored_fronts": "ignored_fronts", "roleid": "roleid",
|
|
1053
|
+
}
|
|
1054
|
+
for source, target in mapping.items():
|
|
1055
|
+
if source in doc:
|
|
1056
|
+
form[target] = self._linked_mods(doc[source]) if source == "linked_mods" else doc[source]
|
|
1057
|
+
backup = self.get_backup(int(form["cloud_id"]))
|
|
1058
|
+
form["linked_mods"] = self._canonical_linked_mods(backup, form["linked_mods"])
|
|
1059
|
+
selection = {name: form[name] for name in ("linked_mods", "ignored_unknown_mods", "ignored_materials", "ignored_fronts", "roleid")}
|
|
1060
|
+
self._validate_backup_selection(backup, selection)
|
|
1061
|
+
self._normalize_commercial(form, bool(form["sharing"]))
|
|
1062
|
+
self._validate_changed_business_options(form, doc)
|
|
1063
|
+
result = self.post("/creator/wow/share_config/update", form)
|
|
1064
|
+
readback = self.get_config(ident)
|
|
1065
|
+
expected = {}
|
|
1066
|
+
mapping = {"picture_urls": "pic_url", "public": "sharing"}
|
|
1067
|
+
for name in {
|
|
1068
|
+
"cloud_id", "title", "content", "content_format", "intro", "picture_urls", "content_origin",
|
|
1069
|
+
"link_to_channel", "subscribe_plan_level", "price", "time_range", "linked_mods",
|
|
1070
|
+
"ignored_unknown_mods", "ignored_materials", "ignored_fronts", "roleid",
|
|
1071
|
+
}:
|
|
1072
|
+
if name in doc:
|
|
1073
|
+
expected[name] = form[mapping.get(name, name)]
|
|
1074
|
+
if "public" in doc:
|
|
1075
|
+
expected["public"] = bool(form["sharing"])
|
|
1076
|
+
if doc.get("picture_files"):
|
|
1077
|
+
expected["picture_urls"] = form["pic_url"]
|
|
1078
|
+
_require_readback(expected, readback, "/creator/wow/share_config/details_aps")
|
|
1079
|
+
relationships = self._replace_relationships("config", ident, doc)
|
|
1080
|
+
return {"result": result, "review_intent": bool(doc.get("public") and doc.get("submit_for_review")), "readback": readback, "relationships": relationships}
|
|
1081
|
+
|
|
1082
|
+
def _wa_form(self, ident: int, detail: Dict[str, Any]) -> Dict[str, Any]:
|
|
1083
|
+
return {
|
|
1084
|
+
"id": ident,
|
|
1085
|
+
"game_version_id": int(_pick(detail, "game_version_id", "t_game_version_id", default=0) or 0),
|
|
1086
|
+
"name": str(_pick(detail, "name", "t_name", default="")), "intro": str(_pick(detail, "intro", "t_intro", default="")),
|
|
1087
|
+
"description": str(_pick(detail, "description", "t_description", default="")),
|
|
1088
|
+
"content_format": int(_pick(detail, "content_format", "t_content_format", default=0) or 0),
|
|
1089
|
+
"thumbnail": str(_pick(detail, "thumbnail", "t_thumbnail", default="")),
|
|
1090
|
+
"images": _urls(_pick(detail, "images", "t_images", default=[])),
|
|
1091
|
+
"category_id_list": _selected_ids(
|
|
1092
|
+
_pick(detail, "category_id_list", "category_ids", "category_list", default=[])
|
|
1093
|
+
),
|
|
1094
|
+
"content_origin": int(_pick(detail, "content_origin", "t_content_origin", default=0) or 0),
|
|
1095
|
+
"subscribe_plan_level": int(_pick(detail, "subscribe_plan_level", "t_subscribe_plan_level", default=0) or 0),
|
|
1096
|
+
"price": int(_pick(detail, "price", "t_price", default=0) or 0),
|
|
1097
|
+
"time_range": str(_pick(detail, "time_range", "t_time_range", default="")),
|
|
1098
|
+
"share_state": int(_pick(detail, "share_state", "t_share_state", default=2) or 2),
|
|
1099
|
+
"link_to_channel": bool(_pick(detail, "link_to_channel", "t_link_to_channel", default=False)),
|
|
1100
|
+
"attachments": self._attachments(_pick(detail, "attachments", default=[]), strict=False), "wa_log": "",
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
@staticmethod
|
|
1104
|
+
def _attachments(value: Any, *, strict: bool) -> List[Dict[str, Any]]:
|
|
1105
|
+
fields = ("name", "install_type", "install_path", "value", "is_compressed", "timestamp")
|
|
1106
|
+
allowed = set(fields)
|
|
1107
|
+
decoded = _decode(value)
|
|
1108
|
+
if decoded is None:
|
|
1109
|
+
return []
|
|
1110
|
+
if not isinstance(decoded, list):
|
|
1111
|
+
raise FuploadError("attachments response was not an array", kind="platform_data_error")
|
|
1112
|
+
result: List[Dict[str, Any]] = []
|
|
1113
|
+
for index, item in enumerate(decoded):
|
|
1114
|
+
path = "$.attachments[%d]" % index
|
|
1115
|
+
if not isinstance(item, dict):
|
|
1116
|
+
raise ValidationError("attachment must be an object", path=path)
|
|
1117
|
+
unknown = set(item) - allowed
|
|
1118
|
+
if strict and unknown:
|
|
1119
|
+
raise ValidationError("unknown attachment field: %s" % sorted(unknown)[0], path=path)
|
|
1120
|
+
if strict:
|
|
1121
|
+
projected = {name: item[name] for name in fields if name in item}
|
|
1122
|
+
else:
|
|
1123
|
+
projected = {
|
|
1124
|
+
"name": _pick(item, "name", "display_name", "filename", default=""),
|
|
1125
|
+
"install_type": item.get("install_type"),
|
|
1126
|
+
"install_path": item.get("install_path"),
|
|
1127
|
+
"value": _pick(item, "value", "url", default=""),
|
|
1128
|
+
"is_compressed": item.get("is_compressed", True),
|
|
1129
|
+
}
|
|
1130
|
+
projected["timestamp"] = item.get("timestamp", 0)
|
|
1131
|
+
result.append(projected)
|
|
1132
|
+
return result
|
|
1133
|
+
|
|
1134
|
+
def create_wa(self, doc: Dict[str, Any]) -> Any:
|
|
1135
|
+
self._validate_ids([doc["game_version_id"]], [x["id"] for x in self.game_versions()["items"]], "$.game_version_id")
|
|
1136
|
+
self._validate_wa_categories(int(doc["game_version_id"]), doc["category_id_list"])
|
|
1137
|
+
attachments = self._attachments(doc.get("attachments", []), strict=True)
|
|
1138
|
+
self._validate_attachments(attachments)
|
|
1139
|
+
thumbnail = doc.get("thumbnail", "")
|
|
1140
|
+
if doc.get("thumbnail_file"):
|
|
1141
|
+
thumbnail = self.upload_media("/creator/wow/wa/upload_media", doc["thumbnail_file"])
|
|
1142
|
+
if not thumbnail:
|
|
1143
|
+
raise ValidationError("thumbnail or thumbnail_file is required", path="$.thumbnail")
|
|
1144
|
+
images = self._resolve_media("/creator/wow/wa/upload_media", doc.get("images", []), doc.get("image_files", []))
|
|
1145
|
+
payload = {
|
|
1146
|
+
"game_version_id": doc["game_version_id"], "name": doc["name"], "intro": doc.get("intro", ""),
|
|
1147
|
+
"description": doc.get("description", ""), "content_format": doc["content_format"],
|
|
1148
|
+
"thumbnail": thumbnail, "images": images, "category_id_list": doc["category_id_list"],
|
|
1149
|
+
"content_origin": doc["content_origin"], "subscribe_plan_level": doc.get("subscribe_plan_level", 0),
|
|
1150
|
+
"price": doc.get("price", 0), "time_range": doc.get("time_range", ""),
|
|
1151
|
+
"share_state": 1 if doc["public"] else 2,
|
|
1152
|
+
"link_to_channel": bool(doc.get("link_to_channel", False)) if doc["public"] else False,
|
|
1153
|
+
"attachments": attachments, "wa_str": doc["wa_str"],
|
|
1154
|
+
"wa_str_titles": doc.get("wa_str_titles", []), "wa_log": doc["wa_log"],
|
|
1155
|
+
"string_mode": doc["string_mode"],
|
|
1156
|
+
}
|
|
1157
|
+
self._normalize_commercial(payload, bool(doc["public"]))
|
|
1158
|
+
self._validate_business_options(payload)
|
|
1159
|
+
result = self.post("/creator/wow/wa/publish", payload)
|
|
1160
|
+
ident = self._created_id(result, doc["name"])
|
|
1161
|
+
if ident <= 0:
|
|
1162
|
+
ident = self._created_id(self.list_was(doc["name"], 0, 100), doc["name"])
|
|
1163
|
+
if ident <= 0:
|
|
1164
|
+
ident = self._created_id(self.list_was("", 0, 100), doc["name"])
|
|
1165
|
+
if ident <= 0:
|
|
1166
|
+
raise FuploadError("WA was submitted but its ID could not be resolved; read the author list before retrying", kind="verification_required", verification_required=True)
|
|
1167
|
+
readback = self.get_wa(ident)
|
|
1168
|
+
expected = {name: payload[name] for name in (
|
|
1169
|
+
"game_version_id", "name", "intro", "description", "content_format", "content_origin",
|
|
1170
|
+
"subscribe_plan_level", "price", "time_range", "link_to_channel", "attachments",
|
|
1171
|
+
) if name in payload}
|
|
1172
|
+
expected.update({"thumbnail": thumbnail, "images": images, "public": doc["public"], "categories": [{"id": value, "name": None} for value in doc["category_id_list"]]})
|
|
1173
|
+
actual_for_compare = dict(readback)
|
|
1174
|
+
actual_for_compare["categories"] = [{"id": item.get("id"), "name": None} for item in readback.get("categories", [])]
|
|
1175
|
+
_require_readback(expected, actual_for_compare, "/creator/wow/wa/detail_aps")
|
|
1176
|
+
relationships = self._replace_relationships("wa", ident, doc)
|
|
1177
|
+
return {"result": result, "id": ident, "review_intent": bool(doc.get("public") and doc.get("submit_for_review")), "readback": readback, "relationships": relationships}
|
|
1178
|
+
|
|
1179
|
+
def edit_wa(self, doc: Dict[str, Any]) -> Any:
|
|
1180
|
+
ident = int(doc["id"])
|
|
1181
|
+
form = self._wa_form(ident, self.get_wa_raw(ident))
|
|
1182
|
+
mapping = {
|
|
1183
|
+
"game_version_id": "game_version_id", "name": "name", "intro": "intro", "description": "description",
|
|
1184
|
+
"content_format": "content_format", "thumbnail": "thumbnail", "images": "images",
|
|
1185
|
+
"category_id_list": "category_id_list", "content_origin": "content_origin",
|
|
1186
|
+
"subscribe_plan_level": "subscribe_plan_level", "price": "price", "time_range": "time_range",
|
|
1187
|
+
"link_to_channel": "link_to_channel", "attachments": "attachments",
|
|
1188
|
+
}
|
|
1189
|
+
for source, target in mapping.items():
|
|
1190
|
+
if source in doc:
|
|
1191
|
+
form[target] = self._attachments(doc[source], strict=True) if source == "attachments" else doc[source]
|
|
1192
|
+
if doc.get("thumbnail_file"):
|
|
1193
|
+
form["thumbnail"] = self.upload_media("/creator/wow/wa/upload_media", doc["thumbnail_file"])
|
|
1194
|
+
if doc.get("image_files"):
|
|
1195
|
+
form["images"] = self._resolve_media("/creator/wow/wa/upload_media", form["images"], doc["image_files"])
|
|
1196
|
+
if "public" in doc:
|
|
1197
|
+
form["share_state"] = 1 if doc["public"] else 2
|
|
1198
|
+
self._validate_ids([form["game_version_id"]], [x["id"] for x in self.game_versions()["items"]], "$.game_version_id")
|
|
1199
|
+
self._validate_wa_categories(int(form["game_version_id"]), form["category_id_list"])
|
|
1200
|
+
self._normalize_commercial(form, form["share_state"] == 1)
|
|
1201
|
+
self._validate_changed_business_options(form, doc)
|
|
1202
|
+
self._validate_attachments(form.get("attachments", []))
|
|
1203
|
+
result = self.post("/creator/wow/wa/update", form)
|
|
1204
|
+
readback = self.get_wa(ident)
|
|
1205
|
+
mapping = {"category_id_list": "categories"}
|
|
1206
|
+
expected = {mapping.get(name, name): form[name] for name in doc if name in {
|
|
1207
|
+
"game_version_id", "name", "intro", "description", "content_format", "thumbnail", "images",
|
|
1208
|
+
"category_id_list", "content_origin", "subscribe_plan_level", "price", "time_range",
|
|
1209
|
+
"link_to_channel", "attachments",
|
|
1210
|
+
}}
|
|
1211
|
+
if "public" in doc:
|
|
1212
|
+
expected["public"] = form["share_state"] == 1
|
|
1213
|
+
if "categories" in expected:
|
|
1214
|
+
expected["categories"] = [{"id": value, "name": None} for value in expected["categories"]]
|
|
1215
|
+
actual_for_compare = dict(readback)
|
|
1216
|
+
actual_for_compare["categories"] = [{"id": item.get("id"), "name": None} for item in readback.get("categories", [])]
|
|
1217
|
+
_require_readback(expected, actual_for_compare, "/creator/wow/wa/detail_aps")
|
|
1218
|
+
relationships = self._replace_relationships("wa", ident, doc)
|
|
1219
|
+
return {"result": result, "review_intent": bool(doc.get("public") and doc.get("submit_for_review")), "readback": readback, "relationships": relationships}
|
|
1220
|
+
|
|
1221
|
+
def update_wa(self, doc: Dict[str, Any]) -> Any:
|
|
1222
|
+
ident = int(doc["id"])
|
|
1223
|
+
current = self.get_wa_raw(ident)
|
|
1224
|
+
next_value = self.post("/creator/wow/wa/get_next_version", {"id": ident})
|
|
1225
|
+
version = str(doc.get("version") or _pick(_first_object(next_value), "version", "next_version", "t_version", default=next_value if isinstance(next_value, str) else ""))
|
|
1226
|
+
if not version:
|
|
1227
|
+
raise FuploadError("NewBeeBox did not return a next WA version")
|
|
1228
|
+
if not _version_greater(version, _pick(current, "t_version", "version", default="")):
|
|
1229
|
+
raise ValidationError("version must be greater than the current WA version", path="$.version")
|
|
1230
|
+
payload = {
|
|
1231
|
+
"id": ident, "version": version, "wa_str": doc["wa_str"],
|
|
1232
|
+
"wa_str_titles": (
|
|
1233
|
+
doc["wa_str_titles"] if "wa_str_titles" in doc
|
|
1234
|
+
else _selected_names(_pick(current, "t_wa_str_titles", "wa_str_titles", default=[]))
|
|
1235
|
+
),
|
|
1236
|
+
"wa_log": doc["wa_log"],
|
|
1237
|
+
"link_to_channel": (
|
|
1238
|
+
bool(doc["link_to_channel"])
|
|
1239
|
+
if "link_to_channel" in doc
|
|
1240
|
+
else bool(_pick(current, "t_link_to_channel", "link_to_channel", default=False))
|
|
1241
|
+
),
|
|
1242
|
+
}
|
|
1243
|
+
result = self.post("/creator/wow/wa/update_wa_str", payload)
|
|
1244
|
+
readback = self.latest_wa(ident)
|
|
1245
|
+
actual_version = str(_pick(_first_object(readback), "version", "t_version", default=""))
|
|
1246
|
+
if actual_version != version:
|
|
1247
|
+
raise FuploadError("WA update version was not present in readback", kind="verification_required", verification_required=True, endpoint="/creator/wow/wa_log/latest_str_info")
|
|
1248
|
+
return {"result": result, "version": version, "readback": readback}
|
|
1249
|
+
|
|
1250
|
+
def latest_wa(self, ident: int) -> Any:
|
|
1251
|
+
return _redact_wa(self.post("/creator/wow/wa_log/latest_str_info", {"wa_id": ident}))
|
|
1252
|
+
|
|
1253
|
+
def _validate_attachments(self, attachments: Any) -> None:
|
|
1254
|
+
allowed = {"name", "install_type", "install_path", "value", "is_compressed", "timestamp"}
|
|
1255
|
+
paths = self.attachment_paths()
|
|
1256
|
+
candidates = []
|
|
1257
|
+
def walk_rows(rows: Sequence[Any]) -> None:
|
|
1258
|
+
for value in rows:
|
|
1259
|
+
if not isinstance(value, dict):
|
|
1260
|
+
continue
|
|
1261
|
+
if "value" in value and ("extract_base_dir" in value or "install_path" in value):
|
|
1262
|
+
candidates.append(value)
|
|
1263
|
+
for key in ("children", "items", "options"):
|
|
1264
|
+
walk_rows(_list(value.get(key)))
|
|
1265
|
+
walk_rows(_option_rows(paths))
|
|
1266
|
+
if attachments and not candidates:
|
|
1267
|
+
raise FuploadError("live attachment path response contained no selectable values", kind="platform_data_error")
|
|
1268
|
+
for index, item in enumerate(attachments):
|
|
1269
|
+
path = "$.attachments[%d]" % index
|
|
1270
|
+
if not isinstance(item, dict):
|
|
1271
|
+
raise ValidationError("attachment must be an object", path=path)
|
|
1272
|
+
unknown = set(item) - allowed
|
|
1273
|
+
if unknown:
|
|
1274
|
+
raise ValidationError("unknown attachment field: %s" % sorted(unknown)[0], path=path)
|
|
1275
|
+
for name in ("name", "install_type", "install_path", "value", "is_compressed"):
|
|
1276
|
+
if name not in item:
|
|
1277
|
+
raise ValidationError("field is required", path=path + "." + name)
|
|
1278
|
+
if not isinstance(item["name"], str) or not item["name"]:
|
|
1279
|
+
raise ValidationError("expected nonempty string", path=path + ".name")
|
|
1280
|
+
if isinstance(item["install_type"], bool) or not isinstance(item["install_type"], int):
|
|
1281
|
+
raise ValidationError("expected integer", path=path + ".install_type")
|
|
1282
|
+
if not isinstance(item["install_path"], str):
|
|
1283
|
+
raise ValidationError("expected string", path=path + ".install_path")
|
|
1284
|
+
if not isinstance(item["value"], str) or not item["value"]:
|
|
1285
|
+
raise ValidationError("expected nonempty string", path=path + ".value")
|
|
1286
|
+
if not isinstance(item["is_compressed"], bool):
|
|
1287
|
+
raise ValidationError("expected boolean", path=path + ".is_compressed")
|
|
1288
|
+
if "timestamp" in item and (isinstance(item["timestamp"], bool) or not isinstance(item["timestamp"], int)):
|
|
1289
|
+
raise ValidationError("expected integer", path=path + ".timestamp")
|
|
1290
|
+
if candidates and not any(
|
|
1291
|
+
str(option.get("value")) == str(item["install_type"])
|
|
1292
|
+
and str(option.get("extract_base_dir") or item["install_path"]) == str(item["install_path"])
|
|
1293
|
+
for option in candidates
|
|
1294
|
+
):
|
|
1295
|
+
raise ValidationError("install type/path is not in the current platform options", path=path + ".install_path")
|
|
1296
|
+
|
|
1297
|
+
def execute_write(self, resource: str, action: str, doc: Dict[str, Any]) -> Any:
|
|
1298
|
+
if action == "delete" and resource in ("plugin", "config", "wa"):
|
|
1299
|
+
return self.delete(resource, doc)
|
|
1300
|
+
if (resource, action) == ("plugin", "create"): return self.create_plugin(doc)
|
|
1301
|
+
if (resource, action) == ("plugin", "update"): return self.update_plugin(doc)
|
|
1302
|
+
if (resource, action) == ("plugin", "edit"): return self.edit_plugin(doc)
|
|
1303
|
+
if (resource, action) == ("config", "create"): return self.create_config(doc)
|
|
1304
|
+
if (resource, action) == ("config", "update"): return self.update_config(doc, False)
|
|
1305
|
+
if (resource, action) == ("config", "edit"): return self.update_config(doc, True)
|
|
1306
|
+
if (resource, action) == ("wa", "create"): return self.create_wa(doc)
|
|
1307
|
+
if (resource, action) == ("wa", "update"): return self.update_wa(doc)
|
|
1308
|
+
if (resource, action) == ("wa", "edit"): return self.edit_wa(doc)
|
|
1309
|
+
if (resource, action) == ("plugin-changelog", "edit"):
|
|
1310
|
+
result = self.post("/creator/wow/mod_file/edit_changelog", {"file_id": doc["file_id"], "changelog": doc["changelog"] or ""})
|
|
1311
|
+
return {"result": result, "readback": self.post("/creator/wow/mod_file/get_changelog", {"file_id": doc["file_id"]})}
|
|
1312
|
+
if (resource, action) == ("wa-changelog", "edit"):
|
|
1313
|
+
result = self.post("/creator/wow/wa_log/edit", {"wa_log_id": doc["id"], "content": doc["wa_log"] or ""})
|
|
1314
|
+
readback = None
|
|
1315
|
+
if doc.get("wa_id"):
|
|
1316
|
+
readback = self.post("/creator/wow/wa_log/list", {"wa_id": doc["wa_id"], "pagenum": 1, "pagesize": 20})
|
|
1317
|
+
return {"result": result, "readback": readback}
|
|
1318
|
+
if action == "set" and resource.endswith("-co-author"):
|
|
1319
|
+
base = resource[:-len("-co-author")]
|
|
1320
|
+
if base in RELATION_TYPES:
|
|
1321
|
+
relationships = self._replace_relationships(base, int(doc["content_id"]), {"co_authors": doc["co_authors"]})
|
|
1322
|
+
return relationships["co_authors"]
|
|
1323
|
+
if action == "set" and resource.endswith("-reference"):
|
|
1324
|
+
base = resource[:-len("-reference")]
|
|
1325
|
+
if base in RELATION_TYPES:
|
|
1326
|
+
relationships = self._replace_relationships(base, int(doc["source_id"]), {"references": doc["references"]})
|
|
1327
|
+
return relationships["references"]
|
|
1328
|
+
if (resource, action) == ("wa-share-code", "set"):
|
|
1329
|
+
result = self.post_next("/bannerserver/ShareCode/Set", {"gameId": 1, "moduleId": doc["module_id"], "moduleType": 3})
|
|
1330
|
+
return {"result": result, "readback": self.get_wa(int(doc["module_id"]))}
|
|
1331
|
+
if (resource, action) == ("wa-media", "upload"):
|
|
1332
|
+
if doc["kind"] == "attachment":
|
|
1333
|
+
uploaded = self.upload_attachment(doc["file"])
|
|
1334
|
+
attachment = {
|
|
1335
|
+
"name": uploaded["name"], "value": uploaded["value"],
|
|
1336
|
+
"is_compressed": True, "timestamp": uploaded.get("timestamp", 0),
|
|
1337
|
+
}
|
|
1338
|
+
if "install_type" in doc:
|
|
1339
|
+
attachment["install_type"] = doc["install_type"]
|
|
1340
|
+
if "install_path" in doc:
|
|
1341
|
+
attachment["install_path"] = doc["install_path"]
|
|
1342
|
+
return {"upload": uploaded, "attachment": attachment}
|
|
1343
|
+
return {"url": self.upload_media("/creator/wow/wa/upload_media", doc["file"])}
|
|
1344
|
+
raise FuploadError("unsupported NewBeeBox write operation", kind="unsupported_operation")
|
|
1345
|
+
|
|
1346
|
+
def delete(self, resource: str, doc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
1347
|
+
ident = int(doc["id"])
|
|
1348
|
+
getters = {"plugin": self.get_plugin, "config": self.get_config, "wa": self.get_wa}
|
|
1349
|
+
listers = {
|
|
1350
|
+
"plugin": lambda keyword: self.list_plugins(keyword, 1, 100),
|
|
1351
|
+
"config": lambda keyword: self.list_configs(keyword, 0, 100),
|
|
1352
|
+
"wa": lambda keyword: self.list_was(keyword, 0, 100),
|
|
1353
|
+
}
|
|
1354
|
+
endpoints = {
|
|
1355
|
+
"plugin": "/creator/wow/mod/remove",
|
|
1356
|
+
"config": "/creator/wow/share_config/delete",
|
|
1357
|
+
"wa": "/creator/wow/wa/delete",
|
|
1358
|
+
}
|
|
1359
|
+
before = getters[resource](ident)
|
|
1360
|
+
name = str(before.get("name") or before.get("title") or "")
|
|
1361
|
+
response = self.post(endpoints[resource], {"id": ident})
|
|
1362
|
+
listing = listers[resource](name)
|
|
1363
|
+
_total, rows, _obj = _paged_items(listing)
|
|
1364
|
+
if any(int(_pick(row, "id", "t_id", "mod_id", "wa_id", default=0) or 0) == ident for row in rows):
|
|
1365
|
+
raise FuploadError(
|
|
1366
|
+
"delete response succeeded but the target remains in the author list",
|
|
1367
|
+
kind="verification_required", endpoint=endpoints[resource], verification_required=True,
|
|
1368
|
+
)
|
|
1369
|
+
return {"result": response, "deleted": True, "id": ident, "before": before, "readback": {"present": False}}
|
|
1370
|
+
|
|
1371
|
+
def execute_read(self, resource: str, action: str, args: Any) -> Any:
|
|
1372
|
+
if resource == "session" and action == "doctor":
|
|
1373
|
+
self.headers
|
|
1374
|
+
return {
|
|
1375
|
+
"authenticated": True,
|
|
1376
|
+
"source": "NewBeeBox desktop auth-store",
|
|
1377
|
+
"auth_store": str(auth_store_dir()),
|
|
1378
|
+
"auth_store_source": "windows-known-folder",
|
|
1379
|
+
"api_origins": dict(NEWBEE_ORIGINS),
|
|
1380
|
+
"trusted": True,
|
|
1381
|
+
}
|
|
1382
|
+
if resource in RELATION_TYPES:
|
|
1383
|
+
types = RELATION_TYPES[resource]
|
|
1384
|
+
if action == "co-author-search": return self.post("/creator/co_author/search_user", {"keyword": args.keyword})
|
|
1385
|
+
if action == "co-author-list": return self.post("/creator/co_author/list", {"content_type": types["co_authors"], "content_id": args.id})
|
|
1386
|
+
if action == "reference-search": return self.post("/creator/content_reference/search", {"keyword": args.keyword, "limit": 20, "target_types": [types["references"]]})
|
|
1387
|
+
if action == "reference-list": return self.post("/creator/content_reference/list", {"content_type": types["references"], "content_id": args.id})
|
|
1388
|
+
if resource == "plugin":
|
|
1389
|
+
if action == "list": return self.list_plugins(args.keyword, args.page, args.page_size)
|
|
1390
|
+
if action == "get": return self.get_plugin(args.id)
|
|
1391
|
+
if action == "categories": return self.categories()
|
|
1392
|
+
if action == "game-versions": return self.game_versions()
|
|
1393
|
+
if action == "versions": return self.plugin_versions(args.id, args.page, args.page_size)
|
|
1394
|
+
if action == "changelog-list": return self.post("/creator/wow/mod_file/changelog_list", {"mod_id": args.id, "pagenum": args.page, "pagesize": args.page_size})
|
|
1395
|
+
if action == "changelog-get": return self.post("/creator/wow/mod_file/get_changelog", {"file_id": args.id})
|
|
1396
|
+
if resource == "config":
|
|
1397
|
+
if action == "list": return self.list_configs(args.keyword, args.offset, args.page_size)
|
|
1398
|
+
if action == "get": return self.get_config(args.id)
|
|
1399
|
+
if action == "backups": return self.list_backups()
|
|
1400
|
+
if action == "backup-get": return self.get_backup(args.id)
|
|
1401
|
+
if resource == "wa":
|
|
1402
|
+
if action == "list": return self.list_was(args.keyword, args.offset, args.page_size)
|
|
1403
|
+
if action == "get": return self.get_wa(args.id)
|
|
1404
|
+
if action == "categories": return self.wa_categories(args.game_version_id)
|
|
1405
|
+
if action == "attachment-paths": return self.attachment_paths()
|
|
1406
|
+
if action == "changelog-latest": return self.latest_wa(args.id)
|
|
1407
|
+
if action == "changelog-list": return _redact_wa(self.post("/creator/wow/wa_log/list", {"wa_id": args.id, "pagenum": args.page, "pagesize": args.page_size}))
|
|
1408
|
+
if resource == "options":
|
|
1409
|
+
if action == "content-origins": return self.content_origins()
|
|
1410
|
+
if action == "subscribe-plans": return self.subscribe_plans()
|
|
1411
|
+
if action == "time-ranges": return self.time_ranges()
|
|
1412
|
+
raise FuploadError("unsupported NewBeeBox read operation", kind="unsupported_operation")
|