@follenfang/fupload 0.0.11 → 0.0.12
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/fupload/SKILL.md +1 -1
- package/fupload/references/modus.md +85 -4
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/cli.py +46 -10
- package/fupload/scripts/fupload_cli/modus.py +299 -11
- package/fupload/scripts/fupload_cli/schema.py +53 -1
- package/npm/skill-manifest.json +13 -13
- package/package.json +1 -1
package/fupload/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: fupload
|
|
3
3
|
description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, CurseForge, Heybox Workshop, and ModUs.Creator, including local Creator login reuse and plugin ZIP publishing. Use only when the user explicitly invokes `$fupload`, explicitly asks to use the Fupload Skill, or loads this Skill by path. Do not trigger from ordinary mentions of publishing, NewBeeBox, DD, CurseForge, Heybox, ModUs, plugins, configurations, or WA.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.0.
|
|
5
|
+
version: "0.0.12"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
@@ -19,6 +19,11 @@ game/data/config/detail` with a JSON `{"keys": [...]}` body. Repeat `--key`
|
|
|
19
19
|
to request selected server-defined configuration keys; at least one key is
|
|
20
20
|
required because the service rejects an empty list.
|
|
21
21
|
|
|
22
|
+
Main-client form choices are available through `modus options config-tags`,
|
|
23
|
+
`modus options wa-tags`, and `modus options wa-support-addons`. These commands
|
|
24
|
+
read the official ModUs static client-data origin without sending the local
|
|
25
|
+
login token.
|
|
26
|
+
|
|
22
27
|
## Project writes
|
|
23
28
|
|
|
24
29
|
`modus project create|edit|delete` uses versioned JSON through `--input`.
|
|
@@ -41,7 +46,7 @@ The complete project model includes `project_id`, `name`, `alt_name`, `summary`,
|
|
|
41
46
|
| `categories` | comma-delimited `categories` | 1-5 positive integer IDs | required; no default | service `categories` enum; `basic_info`; ID 998 requires BigFoot only | comma-delimited `categories` |
|
|
42
47
|
| `publish_platforms` | derived `synchronizationType` | non-empty unique enum array | required; no default | `basic_info`; values `modus`, `bigfoot`, or both | integer `1`, `2`, or `3` |
|
|
43
48
|
| `synchronization_type` | `synchronizationType` | integer | derived; caller value is replaced | derived from platform toggles in `basic_info` | `synchronizationType` |
|
|
44
|
-
| `required_tier_id` | `requiredTierId` |
|
|
49
|
+
| `required_tier_id` | `requiredTierId` | nullable positive integer input | omitted means no tier | service `subscription-tiers` enum; current result is `[]`; BigFoot requires no tier | omitted from the wire for no tier |
|
|
45
50
|
| `repo_url` | `repoUrl` | string/null | optional; omitted preserves value | `basic_info`; create empty is omitted; edit clear uses `<null>` | `repoUrl` |
|
|
46
51
|
| `logo_base64` | `screenshotBase64sReqs.screenshotBase64s` | base64 string | optional; default empty string | create-only `basic_info`; sent as `logo.webp` | server-managed `logo` path |
|
|
47
52
|
| `screenshot_base64s` | same create logo payload | string array | compatibility alias; default empty array | create-only `basic_info`; first value is used when `logo_base64` is absent | server-managed `logo` path |
|
|
@@ -63,7 +68,7 @@ before changing state.
|
|
|
63
68
|
`publish_platforms` is a non-empty array containing `modus`,
|
|
64
69
|
`bigfoot`, or both; the two values are not mutually exclusive. Creator derives
|
|
65
70
|
wire `synchronizationType` as 1 for ModUs, 2 for BigFoot, and 3 for both.
|
|
66
|
-
When BigFoot is selected, `required_tier_id` must be null. Category ID 998 is
|
|
71
|
+
When BigFoot is selected, `required_tier_id` must be null or omitted. Category ID 998 is
|
|
67
72
|
BigFoot-exclusive and forces BigFoot as the only platform. `categories` contains
|
|
68
73
|
one to five positive integer IDs.
|
|
69
74
|
|
|
@@ -84,8 +89,9 @@ local form state, not project update keys.
|
|
|
84
89
|
`project_state` is a resumable `choose_game` -> `basic_info` -> `license` ->
|
|
85
90
|
`complete` snapshot and is required for both create and edit; incomplete or
|
|
86
91
|
missing snapshots are rejected before dry-run or network access. Each step preserves
|
|
87
|
-
its state on validation failure. `required_tier_id` is null for the
|
|
88
|
-
no-tier branch
|
|
92
|
+
its state on validation failure. `required_tier_id` is null or omitted for the
|
|
93
|
+
no-tier branch and is omitted from the HTTP payload; a selected tier is a
|
|
94
|
+
positive dropdown ID.
|
|
89
95
|
The write document also accepts `confirm` for destructive operations.
|
|
90
96
|
Deletion requires `confirm: "DELETE"`.
|
|
91
97
|
|
|
@@ -111,3 +117,78 @@ never substituted for the binary upload. Release deletion requires
|
|
|
111
117
|
|
|
112
118
|
All write commands support `--dry-run`, which validates the versioned JSON and
|
|
113
119
|
local files without constructing an authenticated provider or making a request.
|
|
120
|
+
|
|
121
|
+
## Main ModUs client: configuration shares
|
|
122
|
+
|
|
123
|
+
The installed main client stores its Chromium session under `%APPDATA%\modus\Local Storage\leveldb`.
|
|
124
|
+
Fupload reads the persisted `token`/`deviceId` pair and calls the fixed
|
|
125
|
+
`https://app.modus.cool/api` origin with the raw `Authorization` value and
|
|
126
|
+
`X-Device-Id`; values are never printed. The Creator DPAPI store remains a
|
|
127
|
+
separate authentication source for `project` and `plugin` commands.
|
|
128
|
+
|
|
129
|
+
The configuration surface uses:
|
|
130
|
+
|
|
131
|
+
* `GET /system/user/backup/list`, `GET /system/user/backup/detail/{id}`
|
|
132
|
+
* `POST /system/user/backup/update`, `DELETE /system/user/backup/delete/{id}`
|
|
133
|
+
* `POST /system/user/share/list`, `POST /system/user/share/detail`
|
|
134
|
+
* `POST /system/user/share/create`, `PUT /system/user/share/update`,
|
|
135
|
+
`DELETE /system/user/share/delete/{id}`
|
|
136
|
+
|
|
137
|
+
Share write fields are `share_id`, `addons_id`, `account_name`, `backup_id`,
|
|
138
|
+
`content`, `content_text`, `image_url`, `is_paid`, `is_public`, `price`,
|
|
139
|
+
`share_type`, `tags`, `title`, `exclude_wtf`, `role_name`,
|
|
140
|
+
`required_tier_id`, `sub_type`, optional low-level `platform` /
|
|
141
|
+
`synchronization_type`, and
|
|
142
|
+
`server_type`. `exclude_wtf` is a binary state: when it is `1`, account and
|
|
143
|
+
role are empty; when it is `0`, an account is required and a selected role is
|
|
144
|
+
optional. `sub_type=0` is the normal subscription mode; `sub_type=1` is the
|
|
145
|
+
season mode. Both modes allow the "none" tier represented by null or omitted
|
|
146
|
+
`required_tier_id`; Fupload omits `requiredTierId` from the wire in that branch.
|
|
147
|
+
A selected tier must be a positive ID returned by the service. The official
|
|
148
|
+
configuration form's submit object omits `platform` and `synchronizationType`.
|
|
149
|
+
Fupload's low-level API surface nevertheless exposes the backend fields: list
|
|
150
|
+
defaults `platform=0`, create defaults `platform=1` and
|
|
151
|
+
`synchronizationType=1`, and explicit low-level values are passed through.
|
|
152
|
+
`backup_id`, content, cover, title, and tags are required for create;
|
|
153
|
+
update is presence-aware and uses `share_id`. `addons_id` must be non-empty,
|
|
154
|
+
the trimmed title must contain at least 6 characters, tags contain 1 to 3
|
|
155
|
+
comma-delimited IDs, and plain `content_text` must contain more than 20
|
|
156
|
+
characters.
|
|
157
|
+
|
|
158
|
+
Backup rename uses `backup_id`, `backup_name`, and `server_type`; backup
|
|
159
|
+
deletion uses `backup_id`, `confirm`, and `server_type`.
|
|
160
|
+
|
|
161
|
+
## Main ModUs client: strings
|
|
162
|
+
|
|
163
|
+
String articles use:
|
|
164
|
+
|
|
165
|
+
* `POST /system/user/import/list`, `POST /system/user/import/detail`
|
|
166
|
+
* `POST /system/user/import/create`, `POST /system/user/import/update`
|
|
167
|
+
* `DELETE /system/user/import/delete/{id}`
|
|
168
|
+
* `POST /system/user/import/version/publish`
|
|
169
|
+
* `DELETE /system/user/import/version/delete?versionId={id}`
|
|
170
|
+
|
|
171
|
+
String fields are `import_id`, `code_text`, `content`, `addons_id`,
|
|
172
|
+
`content_text`, `file_path`, `image_url`, `is_paid`, `is_public`, `price`,
|
|
173
|
+
`share_type`, `support_addon`, `tags`, `title`, `version`, `required_tier_id`,
|
|
174
|
+
`sub_type`, and `server_type`. The client requires a supported addon,
|
|
175
|
+
non-empty rendered/plain article content, cover, title, tags, and version.
|
|
176
|
+
`sub_type` and `required_tier_id` follow the same normal/season state machine.
|
|
177
|
+
The current account uses the no-tier branch, so `requiredTierId` is absent from
|
|
178
|
+
the wire. The official string form's submit object omits `platform` and
|
|
179
|
+
`synchronizationType`; Fupload's low-level API surface defaults list
|
|
180
|
+
`platform=0` and create/update `platform=1` / `synchronizationType=1`, while
|
|
181
|
+
preserving explicit values.
|
|
182
|
+
The trimmed title must contain at least 6 characters, tags contain 1 to 3
|
|
183
|
+
comma-delimited IDs, plain `content_text` must contain more than 20 characters,
|
|
184
|
+
and `file_path` is the client's fixed empty string. Version history is separate
|
|
185
|
+
from article metadata:
|
|
186
|
+
`version-publish` takes `import_id`, `version`, `code_text`, and optional
|
|
187
|
+
`changelog`; `version-delete` takes `version_id` and `confirm: "DELETE"`.
|
|
188
|
+
|
|
189
|
+
Both main-client modules support explicit `confirm: "DELETE"` on destructive
|
|
190
|
+
commands. List operations send only server fields (`pageNum`, `pageSize`,
|
|
191
|
+
`keyword`, status/filter values), not the local CLI envelope or `server_type`.
|
|
192
|
+
### Main-client resource IDs and deletion
|
|
193
|
+
|
|
194
|
+
Configuration-share and string/import IDs are opaque decimal strings (for example, 16-digit IDs), not the numeric backup/project IDs. Fupload preserves them as strings. ModUs delete is a soft-delete: a successful `DELETE` returns code `200`, and a subsequent detail may return the record with `status=4` and `isPublic=0`; regression treats that state as deleted.
|
|
@@ -30,11 +30,11 @@ schema exposes them. The calling Skill must show the complete plan and obtain co
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _modus_provider(*, authenticate: bool = True) -> Any:
|
|
33
|
+
def _modus_provider(*, authenticate: bool = True, main_session: bool = False) -> Any:
|
|
34
34
|
# Keep ModUs optional at import time so existing platform commands remain
|
|
35
35
|
# usable while the provider is installed or upgraded independently.
|
|
36
36
|
from .modus import Modus
|
|
37
|
-
return Modus(authenticate=authenticate)
|
|
37
|
+
return Modus(authenticate=authenticate, main_session=main_session)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _parser(**kwargs: Any) -> argparse.ArgumentParser:
|
|
@@ -88,6 +88,19 @@ def _read_leaf(parent: argparse._SubParsersAction, name: str, summary: str, **de
|
|
|
88
88
|
return leaf
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def _modus_server_flag(parser: argparse.ArgumentParser) -> None:
|
|
92
|
+
"""Select the WoW Build sent as ``server`` and ``X-Server-Type``.
|
|
93
|
+
|
|
94
|
+
``None`` deliberately means "use the main client's current Build". Build
|
|
95
|
+
zero is valid, so this is an integer flag rather than ``_positive``.
|
|
96
|
+
``--build`` is a concise alias; both names write the same input field.
|
|
97
|
+
"""
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--server-type", "--build", dest="server_type", type=int, default=None,
|
|
100
|
+
metavar="BUILD_ID", help="WoW Build id; omitted follows the local ModUs current Build (0-4).",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
91
104
|
def _newbee_relationship_tree(parent: argparse.ArgumentParser, resource: str, label: str) -> None:
|
|
92
105
|
authors = parent.add_parser("co-author", help="Search, list, or replace %s co-authors" % label).add_subparsers(dest="author_action", required=True)
|
|
93
106
|
leaf = _read_leaf(authors, "search", "Search current co-author candidates.", platform="newbee", resource=resource, action="co-author-search"); leaf.add_argument("--keyword", required=True)
|
|
@@ -236,6 +249,7 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
|
|
|
236
249
|
groups = root.add_subparsers(dest="resource_command", required=True)
|
|
237
250
|
session = groups.add_parser("session", help="Local authentication diagnostics").add_subparsers(dest="action_command", required=True)
|
|
238
251
|
_read_leaf(session, "doctor", "Check the local ModUs.Creator token store, DPAPI decryption, and authenticated API readiness without exposing credentials.", platform="modus", resource="session", action="doctor")
|
|
252
|
+
_read_leaf(groups, "builds", "List the fixed ModUs WoW Build ids, codes, names, and the locally selected current Build.", platform="modus", resource="builds", action="list")
|
|
239
253
|
|
|
240
254
|
account = groups.add_parser("account", help="ModUs author account and statistics").add_subparsers(dest="action_command", required=True)
|
|
241
255
|
_read_leaf(account, "info", "Read the current ModUs account capability flags.", platform="modus", resource="account", action="info")
|
|
@@ -245,13 +259,13 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
|
|
|
245
259
|
addon = groups.add_parser("addon", help="ModUs addon discovery and history APIs").add_subparsers(dest="action_command", required=True)
|
|
246
260
|
leaf = _read_leaf(addon, "info", "Resolve addon directories to project records.", platform="modus", resource="addon", action="info")
|
|
247
261
|
leaf.add_argument("--directory", dest="directories", action="append", required=True)
|
|
248
|
-
leaf
|
|
262
|
+
_modus_server_flag(leaf)
|
|
249
263
|
leaf = _read_leaf(addon, "project-info", "Resolve addon project IDs to names.", platform="modus", resource="addon", action="project-info")
|
|
250
264
|
leaf.add_argument("--project-id", dest="project_ids", action="append", type=_positive, required=True)
|
|
251
|
-
leaf
|
|
265
|
+
_modus_server_flag(leaf)
|
|
252
266
|
leaf = _read_leaf(addon, "history", "Read addon project version history.", platform="modus", resource="addon", action="history")
|
|
253
267
|
leaf.add_argument("--project-id", type=_positive, required=True)
|
|
254
|
-
leaf
|
|
268
|
+
_modus_server_flag(leaf)
|
|
255
269
|
_page_flags(leaf)
|
|
256
270
|
|
|
257
271
|
options = groups.add_parser("options", help="Read ModUs dynamic choices").add_subparsers(dest="option_action", required=True)
|
|
@@ -259,6 +273,9 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
|
|
|
259
273
|
game_versions = _read_leaf(options, "game-versions", "List supported ModUs game versions.", platform="modus", resource="options", action="game-versions")
|
|
260
274
|
game_versions.add_argument("--key", dest="keys", action="append", required=True, help="Request one game config key; repeatable.")
|
|
261
275
|
_read_leaf(options, "subscription-tiers", "List author subscription tiers returned by ModUs.", platform="modus", resource="options", action="subscription-tiers")
|
|
276
|
+
_read_leaf(options, "config-tags", "List configuration-share tag choices from the official ModUs client data.", platform="modus", resource="options", action="config-tags")
|
|
277
|
+
_read_leaf(options, "wa-tags", "List string tag choices from the official ModUs client data.", platform="modus", resource="options", action="wa-tags")
|
|
278
|
+
_read_leaf(options, "wa-support-addons", "List applicable-addon choices from the official ModUs client data.", platform="modus", resource="options", action="wa-support-addons")
|
|
262
279
|
|
|
263
280
|
project = groups.add_parser("project", help="ModUs plugin project records").add_subparsers(dest="action_command", required=True)
|
|
264
281
|
for action, text in (
|
|
@@ -282,9 +299,23 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
|
|
|
282
299
|
("delete", "Delete one explicitly confirmed ModUs plugin release."),
|
|
283
300
|
):
|
|
284
301
|
_write_leaf(plugin, "modus", "plugin", action, text)
|
|
285
|
-
leaf = _read_leaf(plugin, "list", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="list"); leaf.add_argument("--project-id", type=_positive, required=True); _page_flags(leaf)
|
|
286
|
-
leaf = _read_leaf(plugin, "get", "Read one ModUs plugin release detail.", platform="modus", resource="plugin", action="get"); leaf.add_argument("--project-id", type=_positive, required=True); leaf.add_argument("--file-id", type=_positive, required=True)
|
|
287
|
-
leaf = _read_leaf(plugin, "versions", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="versions"); leaf.add_argument("--project-id", type=_positive, required=True); _page_flags(leaf)
|
|
302
|
+
leaf = _read_leaf(plugin, "list", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="list"); leaf.add_argument("--project-id", type=_positive, required=True); _modus_server_flag(leaf); _page_flags(leaf)
|
|
303
|
+
leaf = _read_leaf(plugin, "get", "Read one ModUs plugin release detail.", platform="modus", resource="plugin", action="get"); leaf.add_argument("--project-id", type=_positive, required=True); leaf.add_argument("--file-id", type=_positive, required=True); _modus_server_flag(leaf)
|
|
304
|
+
leaf = _read_leaf(plugin, "versions", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="versions"); leaf.add_argument("--project-id", type=_positive, required=True); _modus_server_flag(leaf); _page_flags(leaf)
|
|
305
|
+
|
|
306
|
+
config = groups.add_parser("config", help="ModUs main-client configuration shares and backups").add_subparsers(dest="action_command", required=True)
|
|
307
|
+
for action, text in (("create", "Create a configuration share from an existing ModUs cloud backup."), ("update", "Update a configuration share."), ("edit", "Edit a configuration share."), ("delete", "Delete one explicitly confirmed configuration share."), ("backup-edit", "Rename an existing ModUs cloud backup."), ("backup-delete", "Delete one explicitly confirmed ModUs cloud backup.")):
|
|
308
|
+
_write_leaf(config, "modus", "config", action, text)
|
|
309
|
+
leaf = _read_leaf(config, "list", "List ModUs configuration shares.", platform="modus", resource="config", action="list"); _modus_server_flag(leaf); leaf.add_argument("--page-num", type=int); leaf.add_argument("--page-size", type=int); leaf.add_argument("--keyword"); leaf.add_argument("--status", type=int); leaf.add_argument("--share-type", type=int); leaf.add_argument("--order-by"); leaf.add_argument("--mine", action=argparse.BooleanOptionalAction); leaf.add_argument("--is-public", type=int, choices=(0, 1)); leaf.add_argument("--is-paid", type=int, choices=(0, 1)); leaf.add_argument("--tags")
|
|
310
|
+
leaf = _read_leaf(config, "get", "Read a ModUs configuration share detail.", platform="modus", resource="config", action="get"); leaf.add_argument("--share-id", required=True); _modus_server_flag(leaf)
|
|
311
|
+
leaf = _read_leaf(config, "backups", "List existing ModUs cloud backups.", platform="modus", resource="config", action="backups"); _modus_server_flag(leaf)
|
|
312
|
+
leaf = _read_leaf(config, "backup-get", "Read one ModUs cloud backup detail.", platform="modus", resource="config", action="backup-get"); leaf.add_argument("--backup-id", type=_positive, required=True); _modus_server_flag(leaf)
|
|
313
|
+
|
|
314
|
+
wa = groups.add_parser("wa", help="ModUs main-client string articles").add_subparsers(dest="action_command", required=True)
|
|
315
|
+
for action, text in (("create", "Create a ModUs string article."), ("update", "Update a ModUs string article."), ("edit", "Edit a ModUs string article."), ("delete", "Delete one explicitly confirmed string article."), ("version-publish", "Publish a new string article version."), ("version-delete", "Delete one explicitly confirmed historical string version.")):
|
|
316
|
+
_write_leaf(wa, "modus", "wa", action, text)
|
|
317
|
+
leaf = _read_leaf(wa, "list", "List ModUs string articles.", platform="modus", resource="wa", action="list"); _modus_server_flag(leaf); leaf.add_argument("--page-num", type=int); leaf.add_argument("--page-size", type=int); leaf.add_argument("--keyword"); leaf.add_argument("--status", type=int); leaf.add_argument("--order-by"); leaf.add_argument("--mine", action=argparse.BooleanOptionalAction); leaf.add_argument("--support-addon"); leaf.add_argument("--tags"); leaf.add_argument("--is-paid", type=int, choices=(0, 1))
|
|
318
|
+
leaf = _read_leaf(wa, "get", "Read one ModUs string article detail.", platform="modus", resource="wa", action="get"); leaf.add_argument("--import-id", required=True); _modus_server_flag(leaf)
|
|
288
319
|
|
|
289
320
|
|
|
290
321
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -347,7 +378,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
347
378
|
if args.dry_run:
|
|
348
379
|
write_output(platform, operation, _dry_run_data(doc, schema.name), dry_run=True)
|
|
349
380
|
return 0
|
|
350
|
-
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider())))
|
|
381
|
+
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(main_session=resource in ("config", "wa")))))
|
|
351
382
|
try:
|
|
352
383
|
if platform == "dd":
|
|
353
384
|
data = provider.execute_write(resource, action, doc, getattr(args, "session", None))
|
|
@@ -360,10 +391,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
360
391
|
write_output(platform, operation, data)
|
|
361
392
|
return 0
|
|
362
393
|
modus_doctor = platform == "modus" and resource == "session" and action == "doctor"
|
|
363
|
-
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(authenticate=not modus_doctor))))
|
|
394
|
+
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(authenticate=not modus_doctor, main_session=resource in ("addon", "plugin", "config", "wa", "builds")))))
|
|
364
395
|
try:
|
|
365
396
|
if platform == "dd":
|
|
366
397
|
data = provider.execute_read(resource, action, args, getattr(args, "session", None))
|
|
398
|
+
elif platform == "modus" and resource == "builds" and action == "list":
|
|
399
|
+
# Build discovery is local and deterministic; it must not
|
|
400
|
+
# accidentally hit a platform endpoint or confuse platform
|
|
401
|
+
# selection with the Build id sent on CRUD requests.
|
|
402
|
+
data = provider.builds()
|
|
367
403
|
else:
|
|
368
404
|
data = provider.execute_read(resource, action, args)
|
|
369
405
|
finally:
|
|
@@ -25,12 +25,22 @@ from typing import Any, Dict, Mapping, Optional
|
|
|
25
25
|
from .errors import FuploadError, ValidationError, redact
|
|
26
26
|
from .state_machine import COMPLETE, ProjectStateMachine
|
|
27
27
|
from .modus_zip import parse_modus_zip
|
|
28
|
+
from .transport import json_request
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
API_BASE = "https://app.modus.cool/api/"
|
|
32
|
+
STATIC_API_BASE = "https://cdn.modus.cool/modus/client_static_api/"
|
|
31
33
|
TOKEN_PATH = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local")) / "ModUs.Creator" / "auth" / "token.dat"
|
|
32
34
|
TOKEN_ENTROPY = b"ModUs.Creator.TokenStore.v1"
|
|
35
|
+
MODUS_APPDATA = Path(os.environ.get("APPDATA", Path.home() / "AppData/Roaming")) / "modus"
|
|
33
36
|
MAX_PACKAGE_BYTES = 200 * 1024 * 1024
|
|
37
|
+
MODUS_BUILDS = (
|
|
38
|
+
{"id": 0, "code": "retail", "name": "至暗之夜", "label": "正式服-至暗之夜"},
|
|
39
|
+
{"id": 1, "code": "classic_era", "name": "经典旧世", "label": "怀旧服-经典旧世"},
|
|
40
|
+
{"id": 2, "code": "classic", "name": "熊猫人之谜", "label": "怀旧服-熊猫人之谜"},
|
|
41
|
+
{"id": 3, "code": "classic_titan", "name": "泰坦重铸", "label": "时光服-泰坦重铸"},
|
|
42
|
+
{"id": 4, "code": "anniversary", "name": "燃烧的远征", "label": "周年服-燃烧的远征"},
|
|
43
|
+
)
|
|
34
44
|
_SECRET_KEYS = {"token", "access_token", "authorization", "cookie", "signedurl", "signed_url", "upload_url"}
|
|
35
45
|
_WIRE_NAMES = {
|
|
36
46
|
"project_id": "projectId", "file_id": "fileId", "alt_name": "altName",
|
|
@@ -158,6 +168,16 @@ def _positive_id(value: Any, *, field: str) -> int:
|
|
|
158
168
|
return result
|
|
159
169
|
|
|
160
170
|
|
|
171
|
+
def _resource_id(value: Any, *, field: str) -> str:
|
|
172
|
+
"""ModUs share/import IDs are opaque positive decimal strings."""
|
|
173
|
+
if isinstance(value, bool) or value is None:
|
|
174
|
+
raise ValidationError("%s must be a non-empty identifier" % field, path=field)
|
|
175
|
+
text = str(value).strip()
|
|
176
|
+
if not text or (text.isdigit() and int(text) <= 0):
|
|
177
|
+
raise ValidationError("%s must be a non-empty identifier" % field, path=field)
|
|
178
|
+
return text
|
|
179
|
+
|
|
180
|
+
|
|
161
181
|
def _supported_game_versions(value: Any) -> list[Dict[str, str]]:
|
|
162
182
|
"""Map CLI aliases to Creator's anonymous {gameVersion, server} objects.
|
|
163
183
|
|
|
@@ -407,6 +427,94 @@ def load_token(path: Optional[Path] = None) -> str:
|
|
|
407
427
|
return token
|
|
408
428
|
|
|
409
429
|
|
|
430
|
+
def load_main_session(root: Optional[Path] = None) -> Dict[str, str]:
|
|
431
|
+
"""Recover the main ModUs renderer session from Chromium local storage.
|
|
432
|
+
|
|
433
|
+
Chromium LevelDB values are length-prefixed and may be locked by the
|
|
434
|
+
running client. We therefore scan shared-read bytes for JSON fragments,
|
|
435
|
+
accepting only the known persisted token/device keys and never returning
|
|
436
|
+
unrelated storage values.
|
|
437
|
+
"""
|
|
438
|
+
base = root or MODUS_APPDATA
|
|
439
|
+
leveldb = base / "Local Storage" / "leveldb"
|
|
440
|
+
if not leveldb.is_dir():
|
|
441
|
+
raise FuploadError("ModUs main-client local storage was not found", kind="authentication_error")
|
|
442
|
+
token = ""
|
|
443
|
+
device = ""
|
|
444
|
+
for path in sorted(leveldb.iterdir()):
|
|
445
|
+
if not path.is_file() or path.name in {"LOCK", "LOG", "LOG.old", "CURRENT", "MANIFEST-000001"}:
|
|
446
|
+
continue
|
|
447
|
+
try:
|
|
448
|
+
with path.open("rb", buffering=0) as handle:
|
|
449
|
+
raw = handle.read()
|
|
450
|
+
except OSError:
|
|
451
|
+
continue
|
|
452
|
+
text = raw.decode("utf-8", errors="ignore")
|
|
453
|
+
# Pinia persistence is JSON, but the LevelDB record can contain a
|
|
454
|
+
# prefix/suffix. Try JSON objects first, then bounded key/value pairs.
|
|
455
|
+
candidates = [text]
|
|
456
|
+
# Persisted Pinia user JSON contains nested wallet objects, so a
|
|
457
|
+
# shallow-brace regex is insufficient. Extract only scalar credential
|
|
458
|
+
# fields from the record instead of attempting to parse the whole log.
|
|
459
|
+
import re
|
|
460
|
+
token_match = re.search(r'"(?:token|accessToken|access_token)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"', text)
|
|
461
|
+
device_match = re.search(r'"(?:deviceId|device_id)"\s*:\s*"([^"\\]*)"', text)
|
|
462
|
+
if token_match and not token:
|
|
463
|
+
token = bytes(token_match.group(1), "utf-8").decode("unicode_escape")
|
|
464
|
+
if device_match and not device:
|
|
465
|
+
device = device_match.group(1)
|
|
466
|
+
for candidate in candidates:
|
|
467
|
+
try:
|
|
468
|
+
parsed = json.loads(candidate)
|
|
469
|
+
except (TypeError, ValueError):
|
|
470
|
+
continue
|
|
471
|
+
stack = [parsed]
|
|
472
|
+
while stack:
|
|
473
|
+
item = stack.pop()
|
|
474
|
+
if isinstance(item, Mapping):
|
|
475
|
+
for key, value in item.items():
|
|
476
|
+
normalized = str(key).lower()
|
|
477
|
+
if isinstance(value, str) and value.strip():
|
|
478
|
+
if normalized in {"token", "accesstoken", "access_token"} and not token:
|
|
479
|
+
token = value.strip()
|
|
480
|
+
elif normalized in {"deviceid", "device_id"} and not device:
|
|
481
|
+
device = value.strip()
|
|
482
|
+
elif isinstance(value, (Mapping, list)):
|
|
483
|
+
stack.append(value)
|
|
484
|
+
elif isinstance(item, list):
|
|
485
|
+
stack.extend(item)
|
|
486
|
+
if token and device:
|
|
487
|
+
break
|
|
488
|
+
if not token:
|
|
489
|
+
raise FuploadError("ModUs main-client login token was not found", kind="authentication_error")
|
|
490
|
+
return {"token": token, **({"device_id": device} if device else {})}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def load_current_build(root: Optional[Path] = None) -> Optional[int]:
|
|
494
|
+
"""Read the persisted main-client currentGameWow id when available."""
|
|
495
|
+
base = root or MODUS_APPDATA
|
|
496
|
+
leveldb = base / "Local Storage" / "leveldb"
|
|
497
|
+
if not leveldb.is_dir():
|
|
498
|
+
return None
|
|
499
|
+
import re
|
|
500
|
+
for path in sorted(leveldb.iterdir()):
|
|
501
|
+
if not path.is_file() or path.name in {"LOCK", "LOG", "LOG.old", "CURRENT", "MANIFEST-000001"}:
|
|
502
|
+
continue
|
|
503
|
+
try:
|
|
504
|
+
with path.open("rb", buffering=0) as handle:
|
|
505
|
+
text = handle.read().decode("utf-8", errors="ignore")
|
|
506
|
+
except OSError:
|
|
507
|
+
continue
|
|
508
|
+
# Pinia persistence may be split across records; accept only the scalar id.
|
|
509
|
+
for pattern in (r'currentGameWow[^{}]{0,120}?"id"\s*:\s*(\d+)', r'"currentGameWowId"\s*:\s*(\d+)'):
|
|
510
|
+
match = re.search(pattern, text)
|
|
511
|
+
if match:
|
|
512
|
+
value = int(match.group(1))
|
|
513
|
+
if 0 <= value <= 4:
|
|
514
|
+
return value
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
|
|
410
518
|
def _unwrap(payload: Any) -> Any:
|
|
411
519
|
if isinstance(payload, Mapping) and "data" in payload:
|
|
412
520
|
return payload["data"]
|
|
@@ -423,12 +531,28 @@ class ModUs:
|
|
|
423
531
|
base_url: str = API_BASE,
|
|
424
532
|
timeout: int = 60,
|
|
425
533
|
token_path: Optional[Path] = None,
|
|
534
|
+
device_id: Optional[str] = None,
|
|
535
|
+
main_session: bool = False,
|
|
426
536
|
authenticate: bool = True,
|
|
427
537
|
) -> None:
|
|
428
538
|
self.base_url = base_url.rstrip("/") + "/"
|
|
429
539
|
self.timeout = timeout
|
|
430
540
|
self.token_path = token_path or TOKEN_PATH
|
|
431
|
-
self.
|
|
541
|
+
self.device_id = device_id
|
|
542
|
+
self.main_session = main_session
|
|
543
|
+
self.current_build = load_current_build() if main_session else None
|
|
544
|
+
self.authorization_scheme = ""
|
|
545
|
+
if token is not None:
|
|
546
|
+
self.token = token
|
|
547
|
+
elif authenticate:
|
|
548
|
+
if main_session:
|
|
549
|
+
session = load_main_session()
|
|
550
|
+
self.token, self.device_id = session["token"], session.get("device_id")
|
|
551
|
+
self.authorization_scheme = ""
|
|
552
|
+
else:
|
|
553
|
+
self.token = load_token(self.token_path)
|
|
554
|
+
else:
|
|
555
|
+
self.token = ""
|
|
432
556
|
|
|
433
557
|
def _url(self, path: str) -> str:
|
|
434
558
|
return urllib.parse.urljoin(self.base_url, path.lstrip("/"))
|
|
@@ -454,7 +578,10 @@ class ModUs:
|
|
|
454
578
|
def _request(self, method: str, path: str, body: Any = None, *, headers: Optional[Mapping[str, str]] = None) -> Any:
|
|
455
579
|
url = self._url(path)
|
|
456
580
|
stage = self._request_stage(method, path)
|
|
457
|
-
|
|
581
|
+
authorization = self.token if self.authorization_scheme == "" else self.authorization_scheme + self.token
|
|
582
|
+
request_headers = {"Accept": "application/json", "Authorization": authorization}
|
|
583
|
+
if self.device_id:
|
|
584
|
+
request_headers["X-Device-Id"] = self.device_id
|
|
458
585
|
if headers:
|
|
459
586
|
request_headers.update(headers)
|
|
460
587
|
data = None
|
|
@@ -576,31 +703,57 @@ class ModUs:
|
|
|
576
703
|
def user_info(self) -> Any:
|
|
577
704
|
return _safe(_unwrap(self._request("GET", "system/user/getInfo")))
|
|
578
705
|
|
|
706
|
+
def builds(self) -> Any:
|
|
707
|
+
"""Return the fixed WoW Build choices used by the ModUs main client."""
|
|
708
|
+
current = self.current_build
|
|
709
|
+
return {"current": current, "builds": [dict(item) for item in MODUS_BUILDS]}
|
|
710
|
+
|
|
711
|
+
def _build(self, value: Optional[int]) -> int:
|
|
712
|
+
selected = self.current_build if value is None else value
|
|
713
|
+
if selected is None:
|
|
714
|
+
# The desktop wrapper uses getCurrentWow?.id || 0.
|
|
715
|
+
selected = 0
|
|
716
|
+
selected = int(selected)
|
|
717
|
+
if selected < 0 or selected > 4:
|
|
718
|
+
raise ValidationError("server_type must be a known ModUs Build id", path="server_type")
|
|
719
|
+
return selected
|
|
720
|
+
|
|
579
721
|
def active_subscription_count(self) -> Any:
|
|
580
722
|
return _safe(_unwrap(self._request("GET", "user/author/subscription/active/count")))
|
|
581
723
|
|
|
582
724
|
def project_statistics(self) -> Any:
|
|
583
725
|
return _safe(_unwrap(self._request("GET", "game/data/author/project/statistics")))
|
|
584
726
|
|
|
585
|
-
def addon_info(self, directories: Any, *, server_type: int =
|
|
727
|
+
def addon_info(self, directories: Any, *, server_type: Optional[int] = None) -> Any:
|
|
586
728
|
values = directories if isinstance(directories, (list, tuple)) else [directories]
|
|
587
729
|
body = {"pluginList": [str(value) for value in values]}
|
|
588
|
-
return _safe(_unwrap(self._request("POST", "plugin/list/info", body, headers={"X-Server-Type": str(
|
|
730
|
+
return _safe(_unwrap(self._request("POST", "plugin/list/info", body, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
589
731
|
|
|
590
|
-
def addon_project_info(self, project_ids: Any, *, server_type: int =
|
|
732
|
+
def addon_project_info(self, project_ids: Any, *, server_type: Optional[int] = None) -> Any:
|
|
591
733
|
values = project_ids if isinstance(project_ids, (list, tuple)) else [project_ids]
|
|
592
734
|
body = {"projectIds": [int(value) for value in values]}
|
|
593
|
-
return _safe(_unwrap(self._request("POST", "plugin/list/detail", body, headers={"X-Server-Type": str(
|
|
735
|
+
return _safe(_unwrap(self._request("POST", "plugin/list/detail", body, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
594
736
|
|
|
595
|
-
def addon_history(self, project_id: int, *, page_num: int = 1, page_size: int = 5, server_type: int =
|
|
737
|
+
def addon_history(self, project_id: int, *, page_num: int = 1, page_size: int = 5, server_type: Optional[int] = None) -> Any:
|
|
596
738
|
body = {"projectIds": [int(project_id)], "pageNum": int(page_num), "pageSize": int(page_size)}
|
|
597
|
-
return _safe(_unwrap(self._request("POST", "plugin/project/history", body, headers={"X-Server-Type": str(
|
|
739
|
+
return _safe(_unwrap(self._request("POST", "plugin/project/history", body, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
598
740
|
|
|
599
741
|
def project_dependencies(self, query: Any) -> Any:
|
|
600
742
|
body = _dependency_query_wire(query)
|
|
601
743
|
return _safe(_unwrap(self._request("POST", "game/data/author/project/dependency/query", body)))
|
|
602
744
|
|
|
603
745
|
def options(self, action: str, *, keys: Optional[Any] = None) -> Any:
|
|
746
|
+
static_files = {
|
|
747
|
+
"config-tags": "share_tags.json",
|
|
748
|
+
"wa-tags": "imports_tags.json",
|
|
749
|
+
"wa-support-addons": "imports_support_addons.json",
|
|
750
|
+
}
|
|
751
|
+
if action in static_files:
|
|
752
|
+
url = urllib.parse.urljoin(STATIC_API_BASE, static_files[action])
|
|
753
|
+
payload = json_request(url)
|
|
754
|
+
if isinstance(payload, Mapping) and "rows" in payload:
|
|
755
|
+
payload = payload["rows"]
|
|
756
|
+
return _safe(_unwrap(payload))
|
|
604
757
|
routes = {
|
|
605
758
|
# These paths are the routes used by ModUs.Creator's ApiService.
|
|
606
759
|
"categories": "plugin/list/Categories",
|
|
@@ -618,6 +771,123 @@ class ModUs:
|
|
|
618
771
|
raise ValidationError("unsupported ModUs options operation")
|
|
619
772
|
return _safe(_unwrap(self._request("GET", routes[action])))
|
|
620
773
|
|
|
774
|
+
# Main ModUs client: configuration shares and string articles.
|
|
775
|
+
def cloud_backups(self, *, server_type: Optional[int] = None) -> Any:
|
|
776
|
+
return _safe(_unwrap(self._request("GET", "system/user/backup/list", headers={"X-Server-Type": str(self._build(server_type))})))
|
|
777
|
+
|
|
778
|
+
def cloud_backup_detail(self, backup_id: int, *, server_type: Optional[int] = None) -> Any:
|
|
779
|
+
return _safe(_unwrap(self._request("GET", "system/user/backup/detail/%s" % _positive_id(backup_id, field="backup_id"), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
780
|
+
|
|
781
|
+
def cloud_backup_update(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
782
|
+
body = {"id": _positive_id(doc["backup_id"], field="backup_id"), "backupName": doc["backup_name"]}
|
|
783
|
+
return _safe(_unwrap(self._request("POST", "system/user/backup/update", body, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
784
|
+
|
|
785
|
+
def cloud_backup_delete(self, backup_id: int, *, server_type: Optional[int] = None) -> Any:
|
|
786
|
+
return _safe(_unwrap(self._request("DELETE", "system/user/backup/delete/%s" % _positive_id(backup_id, field="backup_id"), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
787
|
+
|
|
788
|
+
@staticmethod
|
|
789
|
+
def _share_wire(doc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
790
|
+
aliases = {"share_id": "id", "addons_id": "addonsId", "backup_id": "backupId", "account_name": "accountName", "content_text": "contentText", "image_url": "imageUrl", "is_paid": "isPaid", "is_public": "isPublic", "share_type": "shareType", "exclude_wtf": "excludeWtf", "role_name": "roleName", "required_tier_id": "requiredTierId", "sub_type": "subType", "synchronization_type": "synchronizationType"}
|
|
791
|
+
allowed = {"id", "addonsId", "backupId", "accountName", "content", "contentText", "imageUrl", "isPaid", "isPublic", "price", "shareType", "tags", "title", "excludeWtf", "roleName", "requiredTierId", "subType", "platform", "synchronizationType"}
|
|
792
|
+
result = {}
|
|
793
|
+
for key, value in doc.items():
|
|
794
|
+
wire = aliases.get(key, key)
|
|
795
|
+
if wire in allowed and value is not None:
|
|
796
|
+
result[wire] = value
|
|
797
|
+
result.setdefault("platform", 1)
|
|
798
|
+
exclude = result.get("excludeWtf", 0)
|
|
799
|
+
result["excludeWtf"] = 1 if str(exclude).strip().lower() in {"1", "true"} else 0
|
|
800
|
+
return result
|
|
801
|
+
|
|
802
|
+
def share_list(self, body: Optional[Mapping[str, Any]] = None, *, server_type: Optional[int] = None) -> Any:
|
|
803
|
+
source = body or {}
|
|
804
|
+
selected_build = self._build(source.get("server", server_type))
|
|
805
|
+
mine = source.get("mine")
|
|
806
|
+
share_type = source.get("share_type")
|
|
807
|
+
wire = {
|
|
808
|
+
"pageNum": int(source.get("page_num") or 1),
|
|
809
|
+
"pageSize": int(source.get("page_size") or 20),
|
|
810
|
+
"server": selected_build,
|
|
811
|
+
"mine": False if mine is None else bool(mine),
|
|
812
|
+
"shareType": 0 if share_type is None else share_type,
|
|
813
|
+
}
|
|
814
|
+
platform = source.get("platform")
|
|
815
|
+
wire["platform"] = int(platform) if isinstance(platform, (int, float)) and not isinstance(platform, bool) else 0
|
|
816
|
+
for key in ("keyword", "status", "tags", "order_by", "is_public", "is_paid"):
|
|
817
|
+
if key in source and source[key] is not None:
|
|
818
|
+
wire[{"order_by": "orderBy", "is_public": "isPublic", "is_paid": "isPaid"}.get(key, key)] = source[key]
|
|
819
|
+
return _safe(_unwrap(self._request("POST", "system/user/share/list", wire, headers={"X-Server-Type": str(selected_build)})))
|
|
820
|
+
|
|
821
|
+
def share_detail(self, share_ids: Any, *, server_type: Optional[int] = None) -> Any:
|
|
822
|
+
ids = share_ids if isinstance(share_ids, list) else [share_ids]
|
|
823
|
+
return _safe(_unwrap(self._request("POST", "system/user/share/detail", {"shareIds": [_resource_id(x, field="share_id") for x in ids]}, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
824
|
+
|
|
825
|
+
def share_create(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
826
|
+
wire = self._share_wire(doc)
|
|
827
|
+
wire.setdefault("synchronizationType", 3 if wire["platform"] == 3 else 1)
|
|
828
|
+
return _safe(_unwrap(self._request("POST", "system/user/share/create", wire, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
829
|
+
|
|
830
|
+
def share_update(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
831
|
+
return _safe(_unwrap(self._request("PUT", "system/user/share/update", self._share_wire(doc), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
832
|
+
|
|
833
|
+
def share_delete(self, share_id: Any, *, server_type: Optional[int] = None) -> Any:
|
|
834
|
+
return _safe(_unwrap(self._request("DELETE", "system/user/share/delete/%s" % urllib.parse.quote(_resource_id(share_id, field="share_id"), safe=""), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
835
|
+
|
|
836
|
+
@staticmethod
|
|
837
|
+
def _import_wire(doc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
838
|
+
aliases = {"import_id": "id", "code_text": "codeText", "addons_id": "addonsId", "content_text": "contentText", "file_path": "filePath", "image_url": "imageUrl", "is_paid": "isPaid", "is_public": "isPublic", "share_type": "shareType", "support_addon": "supportAddon", "required_tier_id": "requiredTierId", "sub_type": "subType", "synchronization_type": "synchronizationType"}
|
|
839
|
+
allowed = {"id", "codeText", "content", "addonsId", "contentText", "filePath", "imageUrl", "isPaid", "isPublic", "price", "shareType", "supportAddon", "tags", "title", "version", "requiredTierId", "subType", "platform", "synchronizationType"}
|
|
840
|
+
result = {aliases.get(key, key): value for key, value in doc.items() if aliases.get(key, key) in allowed and value is not None}
|
|
841
|
+
result.setdefault("platform", 1)
|
|
842
|
+
result.setdefault("synchronizationType", 3 if result["platform"] == 3 else 1)
|
|
843
|
+
return result
|
|
844
|
+
|
|
845
|
+
def import_list(self, body: Optional[Mapping[str, Any]] = None, *, server_type: Optional[int] = None) -> Any:
|
|
846
|
+
source = body or {}
|
|
847
|
+
selected_build = self._build(source.get("server", server_type))
|
|
848
|
+
mine = source.get("mine")
|
|
849
|
+
status = source.get("status")
|
|
850
|
+
wire = {
|
|
851
|
+
"pageNum": int(source.get("page_num") or 1),
|
|
852
|
+
"pageSize": int(source.get("page_size") or 10),
|
|
853
|
+
"server": selected_build,
|
|
854
|
+
"mine": False if mine is None else bool(mine),
|
|
855
|
+
"status": 1 if status is None else status,
|
|
856
|
+
}
|
|
857
|
+
platform = source.get("platform")
|
|
858
|
+
wire["platform"] = int(platform) if isinstance(platform, (int, float)) and not isinstance(platform, bool) else 0
|
|
859
|
+
for key in ("keyword", "support_addon", "tags", "is_paid", "order_by"):
|
|
860
|
+
if key in source and source[key] is not None:
|
|
861
|
+
wire[{"support_addon": "supportAddon", "is_paid": "isPaid", "order_by": "orderBy"}.get(key, key)] = source[key]
|
|
862
|
+
return _safe(_unwrap(self._request("POST", "system/user/import/list", wire, headers={"X-Server-Type": str(selected_build)})))
|
|
863
|
+
|
|
864
|
+
def import_detail(self, import_ids: Any, *, server_type: Optional[int] = None) -> Any:
|
|
865
|
+
ids = import_ids if isinstance(import_ids, list) else [import_ids]
|
|
866
|
+
return _safe(_unwrap(self._request("POST", "system/user/import/detail", {"importIds": [_resource_id(x, field="import_id") for x in ids]}, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
867
|
+
|
|
868
|
+
def import_create(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
869
|
+
return _safe(_unwrap(self._request("POST", "system/user/import/create", self._import_wire(doc), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
870
|
+
|
|
871
|
+
def import_update(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
872
|
+
return _safe(_unwrap(self._request("POST", "system/user/import/update", self._import_wire(doc), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
873
|
+
|
|
874
|
+
def import_delete(self, import_id: Any, *, server_type: Optional[int] = None) -> Any:
|
|
875
|
+
return _safe(_unwrap(self._request("DELETE", "system/user/import/delete/%s" % urllib.parse.quote(_resource_id(import_id, field="import_id"), safe=""), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
876
|
+
|
|
877
|
+
def import_version_publish(self, doc: Mapping[str, Any], *, server_type: Optional[int] = None) -> Any:
|
|
878
|
+
body = {
|
|
879
|
+
"importId": _resource_id(doc["import_id"], field="import_id"),
|
|
880
|
+
"version": str(doc["version"]).strip(),
|
|
881
|
+
"codeText": str(doc["code_text"]).strip(),
|
|
882
|
+
}
|
|
883
|
+
changelog = str(doc.get("changelog") or "").strip()
|
|
884
|
+
if changelog:
|
|
885
|
+
body["changelog"] = changelog
|
|
886
|
+
return _safe(_unwrap(self._request("POST", "system/user/import/version/publish", body, headers={"X-Server-Type": str(self._build(server_type))})))
|
|
887
|
+
|
|
888
|
+
def import_version_delete(self, version_id: Any, *, server_type: Optional[int] = None) -> Any:
|
|
889
|
+
return _safe(_unwrap(self._request("DELETE", "system/user/import/version/delete?versionId=%s" % urllib.parse.quote(str(version_id), safe=""), headers={"X-Server-Type": str(self._build(server_type))})))
|
|
890
|
+
|
|
621
891
|
def release_signature(self, project_id: int, file_id: int) -> str:
|
|
622
892
|
result = _unwrap(self._request("GET", "game/data/author/project/file/upload/signature/%s/%s" % (_positive_id(project_id, field="project_id"), _positive_id(file_id, field="file_id"))))
|
|
623
893
|
url = result.get("signedUrl") if isinstance(result, Mapping) else result
|
|
@@ -819,15 +1089,23 @@ class ModUs:
|
|
|
819
1089
|
|
|
820
1090
|
def execute_read(self, resource: str, action: str, args: Any = None) -> Any:
|
|
821
1091
|
if resource == "session" and action == "doctor": return self.doctor()
|
|
1092
|
+
if resource == "options" and action == "builds": return self.builds()
|
|
1093
|
+
if resource == "builds" and action == "list": return self.builds()
|
|
822
1094
|
doc = vars(args) if args is not None and hasattr(args, "__dict__") else (args or {})
|
|
823
1095
|
if resource == "account" and action == "info": return self.user_info()
|
|
824
1096
|
if resource == "account" and action == "subscription-count": return self.active_subscription_count()
|
|
825
1097
|
if resource == "account" and action == "statistics": return self.project_statistics()
|
|
826
|
-
if resource == "addon" and action == "info": return self.addon_info(doc.get("directories", []), server_type=doc.get("server_type"
|
|
827
|
-
if resource == "addon" and action == "project-info": return self.addon_project_info(doc.get("project_ids", []), server_type=doc.get("server_type"
|
|
828
|
-
if resource == "addon" and action == "history": return self.addon_history(doc["project_id"], page_num=doc.get("page_num", 1), page_size=doc.get("page_size", 5), server_type=doc.get("server_type"
|
|
1098
|
+
if resource == "addon" and action == "info": return self.addon_info(doc.get("directories", []), server_type=doc.get("server_type"))
|
|
1099
|
+
if resource == "addon" and action == "project-info": return self.addon_project_info(doc.get("project_ids", []), server_type=doc.get("server_type"))
|
|
1100
|
+
if resource == "addon" and action == "history": return self.addon_history(doc["project_id"], page_num=doc.get("page_num", 1), page_size=doc.get("page_size", 5), server_type=doc.get("server_type"))
|
|
829
1101
|
if resource == "project" and action == "dependencies": return self.project_dependencies(doc.get("query") or doc.get("project_ids") or doc.get("project_id"))
|
|
830
1102
|
if resource == "options": return self.options(action, keys=doc.get("keys"))
|
|
1103
|
+
if resource == "config" and action == "backups": return self.cloud_backups(server_type=doc.get("server_type"))
|
|
1104
|
+
if resource == "config" and action == "backup-get": return self.cloud_backup_detail(doc["backup_id"], server_type=doc.get("server_type"))
|
|
1105
|
+
if resource == "config" and action == "list": return self.share_list(doc, server_type=doc.get("server_type"))
|
|
1106
|
+
if resource == "config" and action == "get": return self.share_detail(doc["share_id"], server_type=doc.get("server_type"))
|
|
1107
|
+
if resource == "wa" and action == "list": return self.import_list(doc, server_type=doc.get("server_type"))
|
|
1108
|
+
if resource == "wa" and action == "get": return self.import_detail(doc["import_id"], server_type=doc.get("server_type"))
|
|
831
1109
|
if resource == "project" and action == "list": return self.project_list(**doc)
|
|
832
1110
|
if resource == "project" and action in ("get", "detail"): return self.project_detail(doc["project_id"])
|
|
833
1111
|
if resource in ("plugin", "release") and action in ("list", "versions"): return self.release_list(doc["project_id"], page_num=doc.get("page_num", 1), page_size=doc.get("page_size", 50))
|
|
@@ -835,6 +1113,16 @@ class ModUs:
|
|
|
835
1113
|
raise ValidationError("unsupported ModUs read operation")
|
|
836
1114
|
|
|
837
1115
|
def execute_write(self, resource: str, action: str, doc: Mapping[str, Any]) -> Any:
|
|
1116
|
+
if resource == "config" and action == "create": return self.share_create(doc, server_type=doc.get("server_type"))
|
|
1117
|
+
if resource == "config" and action in ("update", "edit"): return self.share_update(doc, server_type=doc.get("server_type"))
|
|
1118
|
+
if resource == "config" and action == "delete": return self.share_delete(doc["share_id"], server_type=doc.get("server_type"))
|
|
1119
|
+
if resource == "config" and action == "backup-edit": return self.cloud_backup_update(doc, server_type=doc.get("server_type"))
|
|
1120
|
+
if resource == "config" and action == "backup-delete": return self.cloud_backup_delete(doc["backup_id"], server_type=doc.get("server_type"))
|
|
1121
|
+
if resource == "wa" and action == "create": return self.import_create(doc, server_type=doc.get("server_type"))
|
|
1122
|
+
if resource == "wa" and action in ("update", "edit"): return self.import_update(doc, server_type=doc.get("server_type"))
|
|
1123
|
+
if resource == "wa" and action == "delete": return self.import_delete(doc["import_id"], server_type=doc.get("server_type"))
|
|
1124
|
+
if resource == "wa" and action == "version-publish": return self.import_version_publish(doc, server_type=doc.get("server_type"))
|
|
1125
|
+
if resource == "wa" and action == "version-delete": return self.import_version_delete(doc["version_id"], server_type=doc.get("server_type"))
|
|
838
1126
|
if resource == "project" and action in ("create", "release"): return self.project_create(doc)
|
|
839
1127
|
if resource == "project" and action in ("update", "edit"): return self.project_update(doc)
|
|
840
1128
|
if resource == "project" and action == "delete": return self.project_delete(int(doc["project_id"]))
|
|
@@ -17,6 +17,7 @@ JSON_TYPES = {
|
|
|
17
17
|
"boolean": (bool,),
|
|
18
18
|
"array": (list,),
|
|
19
19
|
"object": (dict,),
|
|
20
|
+
"identifier": (int, str),
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -98,6 +99,21 @@ class Schema:
|
|
|
98
99
|
return checked
|
|
99
100
|
|
|
100
101
|
def _validate_conditionals(self, value: Dict[str, Any]) -> None:
|
|
102
|
+
if self.name.startswith("fupload.v1.modus.config"):
|
|
103
|
+
exclude = value.get("exclude_wtf")
|
|
104
|
+
if exclude == 1 and (value.get("account_name") or value.get("role_name")):
|
|
105
|
+
raise ValidationError("account_name and role_name must be empty when exclude_wtf=1", path="$.account_name")
|
|
106
|
+
if exclude == 0 and not str(value.get("account_name") or "").strip():
|
|
107
|
+
raise ValidationError("account_name is required when exclude_wtf=0", path="$.account_name")
|
|
108
|
+
if self.name.startswith(("fupload.v1.modus.config", "fupload.v1.modus.wa")):
|
|
109
|
+
if "title" in value and len(str(value["title"]).strip()) < 6:
|
|
110
|
+
raise ValidationError("must contain at least 6 characters", path="$.title")
|
|
111
|
+
if "content_text" in value and len(str(value["content_text"]).strip()) <= 20:
|
|
112
|
+
raise ValidationError("must contain more than 20 characters", path="$.content_text")
|
|
113
|
+
if "tags" in value:
|
|
114
|
+
tags = [item.strip() for item in str(value["tags"]).split(",")]
|
|
115
|
+
if any(not item for item in tags) or not 1 <= len(tags) <= 3:
|
|
116
|
+
raise ValidationError("must contain 1 to 3 comma-delimited tag IDs", path="$.tags")
|
|
101
117
|
if self.name in ("fupload.v1.modus.project.create", "fupload.v1.modus.project.edit"):
|
|
102
118
|
snapshot = value.get("project_state")
|
|
103
119
|
if snapshot is None:
|
|
@@ -693,6 +709,31 @@ MODUS_RELEASE = {
|
|
|
693
709
|
"file": f("string", local_file=True),
|
|
694
710
|
"transaction_log": f("string", max_length=1000),
|
|
695
711
|
}
|
|
712
|
+
MODUS_BUILD_ID = f("integer", minimum=0, maximum=4)
|
|
713
|
+
MODUS_SHARE = {
|
|
714
|
+
"share_id": f("identifier", nonempty=True), "addons_id": f("string", nonempty=True, max_length=10000),
|
|
715
|
+
"account_name": f("string", nullable=True, max_length=120), "backup_id": f("integer", minimum=1),
|
|
716
|
+
"content": f("string", nonempty=True), "content_text": f("string", nonempty=True),
|
|
717
|
+
"image_url": f("string", nonempty=True, max_length=1000), "is_paid": f("integer", choices=(0, 1)),
|
|
718
|
+
"is_public": f("integer", choices=(0, 1)), "price": f("number", minimum=0),
|
|
719
|
+
"share_type": f("integer", minimum=0), "tags": f("string", nonempty=True, max_length=1000),
|
|
720
|
+
"title": f("string", nonempty=True, max_length=120), "exclude_wtf": f("integer", choices=(0, 1)),
|
|
721
|
+
"role_name": f("string", nullable=True, max_length=120), "required_tier_id": f("integer", nullable=True, minimum=1),
|
|
722
|
+
"sub_type": f("integer", choices=(0, 1)), "platform": f("integer", choices=(1, 3)),
|
|
723
|
+
"synchronization_type": f("integer", choices=(1, 3)), "server_type": MODUS_BUILD_ID,
|
|
724
|
+
}
|
|
725
|
+
MODUS_IMPORT = {
|
|
726
|
+
"import_id": f("identifier", nonempty=True), "code_text": f("string", nonempty=True),
|
|
727
|
+
"content": f("string", nonempty=True), "addons_id": f("string", nonempty=True, max_length=10000),
|
|
728
|
+
"content_text": f("string", nonempty=True), "file_path": f("string", choices=("",)),
|
|
729
|
+
"image_url": f("string", nonempty=True, max_length=1000), "is_paid": f("integer", choices=(0, 1)),
|
|
730
|
+
"is_public": f("integer", choices=(0, 1)), "price": f("number", minimum=0),
|
|
731
|
+
"share_type": f("integer", minimum=0), "support_addon": f("string", nonempty=True, max_length=120),
|
|
732
|
+
"tags": f("string", nonempty=True, max_length=1000), "title": f("string", nonempty=True, max_length=120),
|
|
733
|
+
"version": f("string", nonempty=True, max_length=120), "required_tier_id": f("integer", nullable=True, minimum=1),
|
|
734
|
+
"sub_type": f("integer", choices=(0, 1)), "platform": f("integer", choices=(1, 3)),
|
|
735
|
+
"synchronization_type": f("integer", choices=(1, 3)), "server_type": MODUS_BUILD_ID,
|
|
736
|
+
}
|
|
696
737
|
register("modus", "project", "create", required(MODUS_PROJECT_CREATE, ("project_state",)))
|
|
697
738
|
register("modus", "project", "edit", required(with_id(MODUS_PROJECT_EDIT, "project_id"), ("project_id", "project_state")))
|
|
698
739
|
register("modus", "project", "delete", required({"project_id": f("integer", minimum=1), "confirm": f("string", choices=("DELETE",))}, ("project_id", "confirm")))
|
|
@@ -701,7 +742,18 @@ register("modus", "plugin", "upload", required(MODUS_RELEASE, ("project_id", "fi
|
|
|
701
742
|
register("modus", "plugin", "update", required(MODUS_RELEASE, ("project_id", "file_id")))
|
|
702
743
|
register("modus", "plugin", "edit", required(MODUS_RELEASE, ("project_id", "file_id")))
|
|
703
744
|
register("modus", "plugin", "delete", required({"project_id": f("integer", minimum=1), "file_id": f("integer", minimum=1), "confirm": f("string", choices=("DELETE",))}, ("project_id", "file_id", "confirm")))
|
|
704
|
-
|
|
745
|
+
register("modus", "config", "create", required(MODUS_SHARE, ("addons_id", "backup_id", "content", "content_text", "image_url", "tags", "title", "exclude_wtf")))
|
|
746
|
+
register("modus", "config", "update", required(MODUS_SHARE, ("share_id",)))
|
|
747
|
+
register("modus", "config", "edit", required(MODUS_SHARE, ("share_id",)))
|
|
748
|
+
register("modus", "config", "delete", required({"share_id": f("identifier", nonempty=True), "confirm": f("string", choices=("DELETE",)), "server_type": MODUS_BUILD_ID}, ("share_id", "confirm")))
|
|
749
|
+
register("modus", "config", "backup-edit", required({"backup_id": f("integer", minimum=1), "backup_name": f("string", nonempty=True, max_length=200), "server_type": MODUS_BUILD_ID}, ("backup_id", "backup_name")))
|
|
750
|
+
register("modus", "config", "backup-delete", required({"backup_id": f("integer", minimum=1), "confirm": f("string", choices=("DELETE",)), "server_type": MODUS_BUILD_ID}, ("backup_id", "confirm")))
|
|
751
|
+
register("modus", "wa", "create", required(MODUS_IMPORT, ("code_text", "content", "addons_id", "content_text", "image_url", "support_addon", "tags", "title", "version")))
|
|
752
|
+
register("modus", "wa", "update", required(MODUS_IMPORT, ("import_id",)))
|
|
753
|
+
register("modus", "wa", "edit", required(MODUS_IMPORT, ("import_id",)))
|
|
754
|
+
register("modus", "wa", "delete", required({"import_id": f("identifier", nonempty=True), "confirm": f("string", choices=("DELETE",)), "server_type": MODUS_BUILD_ID}, ("import_id", "confirm")))
|
|
755
|
+
register("modus", "wa", "version-publish", required({"import_id": f("identifier", nonempty=True), "version": f("string", nonempty=True, max_length=120), "code_text": f("string", nonempty=True), "changelog": f("string", max_length=10000), "server_type": MODUS_BUILD_ID}, ("import_id", "version", "code_text")))
|
|
756
|
+
register("modus", "wa", "version-delete", required({"version_id": f("identifier", nonempty=True), "confirm": f("string", choices=("DELETE",)), "server_type": MODUS_BUILD_ID}, ("version_id", "confirm")))
|
|
705
757
|
register("blackbox", "plugin", "edit", required({
|
|
706
758
|
"id": f("integer"), "name": f("string"), "logo_url": f("string"), "category_ids": f("array"),
|
|
707
759
|
"type": f("integer", choices=(1, 9)), "desc": f("string"), "official": f("string"),
|
package/npm/skill-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "fupload.npm-skill-manifest.v1",
|
|
3
3
|
"package_name": "@follenfang/fupload",
|
|
4
|
-
"package_version": "0.0.
|
|
5
|
-
"skill_version": "0.0.
|
|
6
|
-
"tree_sha256": "
|
|
4
|
+
"package_version": "0.0.12",
|
|
5
|
+
"skill_version": "0.0.12",
|
|
6
|
+
"tree_sha256": "f1e559aef665537c4b2b24f15dab01b9e4140378bd3a70a00331d56358d2071a",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"path": "references/modus.md",
|
|
95
|
-
"bytes":
|
|
96
|
-
"sha256": "
|
|
95
|
+
"bytes": 13492,
|
|
96
|
+
"sha256": "919b8b097fd1ec14d38583e84f4d33a5320d24af03703dc1c06a072dbab33e68"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"path": "references/newbee-official-cli.md",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
{
|
|
114
114
|
"path": "scripts/fupload_cli/__init__.py",
|
|
115
115
|
"bytes": 50,
|
|
116
|
-
"sha256": "
|
|
116
|
+
"sha256": "cad1ed5cd2ed3020e1fc98c2036d22b47e1138772e9829f5dd61c39cc3bd810f"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"path": "scripts/fupload_cli/blackbox_web.py",
|
|
@@ -127,8 +127,8 @@
|
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"path": "scripts/fupload_cli/cli.py",
|
|
130
|
-
"bytes":
|
|
131
|
-
"sha256": "
|
|
130
|
+
"bytes": 36811,
|
|
131
|
+
"sha256": "bdd159c7d86c009685417e7b4a25194d98a49547e036606b920ff44090b1619e"
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
"path": "scripts/fupload_cli/curseforge.py",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "scripts/fupload_cli/modus.py",
|
|
170
|
-
"bytes":
|
|
171
|
-
"sha256": "
|
|
170
|
+
"bytes": 60792,
|
|
171
|
+
"sha256": "9cf3d8127e2b7583f1760a74f88512b81ab272ea116bf9ca185c7531eba960d2"
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
"path": "scripts/fupload_cli/newbee_auth.py",
|
|
@@ -182,8 +182,8 @@
|
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
"path": "scripts/fupload_cli/schema.py",
|
|
185
|
-
"bytes":
|
|
186
|
-
"sha256": "
|
|
185
|
+
"bytes": 51541,
|
|
186
|
+
"sha256": "aca30dc7997f47645b3da154fa3656fefa1179733e1d61e7ff0de326734f2575"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "scripts/fupload_cli/state_machine.py",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
{
|
|
209
209
|
"path": "SKILL.md",
|
|
210
210
|
"bytes": 25720,
|
|
211
|
-
"sha256": "
|
|
211
|
+
"sha256": "fe14cba8f02167dce36034ce8b73784793c92ed3de37dda1ab982a5a8a9cc8de"
|
|
212
212
|
}
|
|
213
213
|
]
|
|
214
214
|
}
|