@sovovs/bycli 2.1.17 → 2.1.18
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/cli-manifest.json +198 -0
- package/clis/weixin/_wechat/collections.js +429 -0
- package/clis/weixin/_wechat/fixtures/collection-detail.json +55 -0
- package/clis/weixin/_wechat/fixtures/collections-list.json +38 -0
- package/clis/weixin/_wechat/fixtures/published-page.json +6 -0
- package/clis/weixin/_wechat/publish-download.js +165 -0
- package/clis/weixin/_wechat/publish-records.js +304 -0
- package/clis/weixin/collection-detail.js +75 -0
- package/clis/weixin/collections.js +66 -0
- package/clis/weixin/download-publish-data.js +66 -0
- package/clis/weixin/published.js +72 -0
- package/dist/src/browser/base-page.d.ts +2 -1
- package/dist/src/browser/base-page.js +14 -3
- package/dist/src/browser/page.d.ts +2 -2
- package/dist/src/browser/page.js +3 -1
- package/dist/src/observation/redaction.js +3 -1
- package/dist/src/types.d.ts +6 -1
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -27907,6 +27907,90 @@
|
|
|
27907
27907
|
"sourceFile": "weixin/articles.js",
|
|
27908
27908
|
"navigateBefore": "https://mp.weixin.qq.com"
|
|
27909
27909
|
},
|
|
27910
|
+
{
|
|
27911
|
+
"site": "weixin",
|
|
27912
|
+
"name": "collection-detail",
|
|
27913
|
+
"description": "Show one WeChat content collection with its settings and items",
|
|
27914
|
+
"access": "read",
|
|
27915
|
+
"domain": "mp.weixin.qq.com",
|
|
27916
|
+
"strategy": "cookie",
|
|
27917
|
+
"browser": true,
|
|
27918
|
+
"args": [
|
|
27919
|
+
{
|
|
27920
|
+
"name": "collectionId",
|
|
27921
|
+
"type": "str",
|
|
27922
|
+
"required": true,
|
|
27923
|
+
"positional": true,
|
|
27924
|
+
"help": "Collection ID returned by weixin collections"
|
|
27925
|
+
},
|
|
27926
|
+
{
|
|
27927
|
+
"name": "max-pages",
|
|
27928
|
+
"type": "int",
|
|
27929
|
+
"default": 5,
|
|
27930
|
+
"required": false,
|
|
27931
|
+
"help": "Maximum number of collection pages to scan"
|
|
27932
|
+
}
|
|
27933
|
+
],
|
|
27934
|
+
"columns": [
|
|
27935
|
+
"collectionId",
|
|
27936
|
+
"title",
|
|
27937
|
+
"description",
|
|
27938
|
+
"collectionType",
|
|
27939
|
+
"coverUrl",
|
|
27940
|
+
"itemCount",
|
|
27941
|
+
"createdAt",
|
|
27942
|
+
"updatedAt",
|
|
27943
|
+
"settingsJson",
|
|
27944
|
+
"itemsJson"
|
|
27945
|
+
],
|
|
27946
|
+
"type": "js",
|
|
27947
|
+
"modulePath": "weixin/collection-detail.js",
|
|
27948
|
+
"sourceFile": "weixin/collection-detail.js",
|
|
27949
|
+
"navigateBefore": false
|
|
27950
|
+
},
|
|
27951
|
+
{
|
|
27952
|
+
"site": "weixin",
|
|
27953
|
+
"name": "collections",
|
|
27954
|
+
"description": "List WeChat official-account content collections",
|
|
27955
|
+
"access": "read",
|
|
27956
|
+
"domain": "mp.weixin.qq.com",
|
|
27957
|
+
"strategy": "cookie",
|
|
27958
|
+
"browser": true,
|
|
27959
|
+
"args": [
|
|
27960
|
+
{
|
|
27961
|
+
"name": "limit",
|
|
27962
|
+
"type": "int",
|
|
27963
|
+
"default": 20,
|
|
27964
|
+
"required": false,
|
|
27965
|
+
"help": "Maximum number of collections to return"
|
|
27966
|
+
},
|
|
27967
|
+
{
|
|
27968
|
+
"name": "max-pages",
|
|
27969
|
+
"type": "int",
|
|
27970
|
+
"default": 5,
|
|
27971
|
+
"required": false,
|
|
27972
|
+
"help": "Maximum number of collection pages to scan"
|
|
27973
|
+
}
|
|
27974
|
+
],
|
|
27975
|
+
"columns": [
|
|
27976
|
+
"collectionId",
|
|
27977
|
+
"title",
|
|
27978
|
+
"collectionType",
|
|
27979
|
+
"itemCount",
|
|
27980
|
+
"views",
|
|
27981
|
+
"continuousRead",
|
|
27982
|
+
"isUpdating",
|
|
27983
|
+
"isBanned",
|
|
27984
|
+
"isPaid",
|
|
27985
|
+
"createdAt",
|
|
27986
|
+
"updatedAt",
|
|
27987
|
+
"coverUrl"
|
|
27988
|
+
],
|
|
27989
|
+
"type": "js",
|
|
27990
|
+
"modulePath": "weixin/collections.js",
|
|
27991
|
+
"sourceFile": "weixin/collections.js",
|
|
27992
|
+
"navigateBefore": false
|
|
27993
|
+
},
|
|
27910
27994
|
{
|
|
27911
27995
|
"site": "weixin",
|
|
27912
27996
|
"name": "create-draft",
|
|
@@ -28007,6 +28091,63 @@
|
|
|
28007
28091
|
"sourceFile": "weixin/download.js",
|
|
28008
28092
|
"navigateBefore": "https://mp.weixin.qq.com"
|
|
28009
28093
|
},
|
|
28094
|
+
{
|
|
28095
|
+
"site": "weixin",
|
|
28096
|
+
"name": "download-publish-data",
|
|
28097
|
+
"description": "Match a Weixin published article and download its detail spreadsheet",
|
|
28098
|
+
"access": "write",
|
|
28099
|
+
"domain": "mp.weixin.qq.com",
|
|
28100
|
+
"strategy": "intercept",
|
|
28101
|
+
"browser": true,
|
|
28102
|
+
"args": [
|
|
28103
|
+
{
|
|
28104
|
+
"name": "query",
|
|
28105
|
+
"type": "str",
|
|
28106
|
+
"required": true,
|
|
28107
|
+
"positional": true,
|
|
28108
|
+
"help": "Exact article URL or title text"
|
|
28109
|
+
},
|
|
28110
|
+
{
|
|
28111
|
+
"name": "date",
|
|
28112
|
+
"type": "str",
|
|
28113
|
+
"required": false,
|
|
28114
|
+
"help": "Optional publication date in YYYY-MM-DD"
|
|
28115
|
+
},
|
|
28116
|
+
{
|
|
28117
|
+
"name": "output",
|
|
28118
|
+
"type": "str",
|
|
28119
|
+
"default": "./weixin-publish-data",
|
|
28120
|
+
"required": false,
|
|
28121
|
+
"help": "Directory for downloaded spreadsheets"
|
|
28122
|
+
},
|
|
28123
|
+
{
|
|
28124
|
+
"name": "max-pages",
|
|
28125
|
+
"type": "int",
|
|
28126
|
+
"default": 5,
|
|
28127
|
+
"required": false,
|
|
28128
|
+
"help": "Maximum published-record pages to scan"
|
|
28129
|
+
},
|
|
28130
|
+
{
|
|
28131
|
+
"name": "timeout",
|
|
28132
|
+
"type": "int",
|
|
28133
|
+
"default": 60,
|
|
28134
|
+
"required": false,
|
|
28135
|
+
"help": "Maximum seconds for capture and download"
|
|
28136
|
+
}
|
|
28137
|
+
],
|
|
28138
|
+
"columns": [
|
|
28139
|
+
"title",
|
|
28140
|
+
"published_at",
|
|
28141
|
+
"url",
|
|
28142
|
+
"status",
|
|
28143
|
+
"path",
|
|
28144
|
+
"size"
|
|
28145
|
+
],
|
|
28146
|
+
"type": "js",
|
|
28147
|
+
"modulePath": "weixin/download-publish-data.js",
|
|
28148
|
+
"sourceFile": "weixin/download-publish-data.js",
|
|
28149
|
+
"navigateBefore": false
|
|
28150
|
+
},
|
|
28010
28151
|
{
|
|
28011
28152
|
"site": "weixin",
|
|
28012
28153
|
"name": "drafts",
|
|
@@ -28041,6 +28182,63 @@
|
|
|
28041
28182
|
"sourceFile": "weixin/drafts.js",
|
|
28042
28183
|
"navigateBefore": false
|
|
28043
28184
|
},
|
|
28185
|
+
{
|
|
28186
|
+
"site": "weixin",
|
|
28187
|
+
"name": "published",
|
|
28188
|
+
"description": "List Weixin published records and engagement metrics",
|
|
28189
|
+
"access": "read",
|
|
28190
|
+
"domain": "mp.weixin.qq.com",
|
|
28191
|
+
"strategy": "intercept",
|
|
28192
|
+
"browser": true,
|
|
28193
|
+
"args": [
|
|
28194
|
+
{
|
|
28195
|
+
"name": "query",
|
|
28196
|
+
"type": "str",
|
|
28197
|
+
"required": false,
|
|
28198
|
+
"positional": true,
|
|
28199
|
+
"help": "Optional article title or URL filter"
|
|
28200
|
+
},
|
|
28201
|
+
{
|
|
28202
|
+
"name": "limit",
|
|
28203
|
+
"type": "int",
|
|
28204
|
+
"default": 10,
|
|
28205
|
+
"required": false,
|
|
28206
|
+
"help": "Maximum articles to return"
|
|
28207
|
+
},
|
|
28208
|
+
{
|
|
28209
|
+
"name": "max-pages",
|
|
28210
|
+
"type": "int",
|
|
28211
|
+
"default": 5,
|
|
28212
|
+
"required": false,
|
|
28213
|
+
"help": "Maximum published-record pages to scan"
|
|
28214
|
+
},
|
|
28215
|
+
{
|
|
28216
|
+
"name": "timeout",
|
|
28217
|
+
"type": "int",
|
|
28218
|
+
"default": 30,
|
|
28219
|
+
"required": false,
|
|
28220
|
+
"help": "Maximum seconds for request capture"
|
|
28221
|
+
}
|
|
28222
|
+
],
|
|
28223
|
+
"columns": [
|
|
28224
|
+
"title",
|
|
28225
|
+
"published_at",
|
|
28226
|
+
"url",
|
|
28227
|
+
"notified",
|
|
28228
|
+
"failed",
|
|
28229
|
+
"reads",
|
|
28230
|
+
"likes",
|
|
28231
|
+
"shares",
|
|
28232
|
+
"recommends",
|
|
28233
|
+
"comments",
|
|
28234
|
+
"underlines",
|
|
28235
|
+
"reprints"
|
|
28236
|
+
],
|
|
28237
|
+
"type": "js",
|
|
28238
|
+
"modulePath": "weixin/published.js",
|
|
28239
|
+
"sourceFile": "weixin/published.js",
|
|
28240
|
+
"navigateBefore": false
|
|
28241
|
+
},
|
|
28044
28242
|
{
|
|
28045
28243
|
"site": "weixin",
|
|
28046
28244
|
"name": "save-articles",
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgumentError,
|
|
3
|
+
AuthRequiredError,
|
|
4
|
+
CliError,
|
|
5
|
+
CommandExecutionError,
|
|
6
|
+
} from '@sovovs/bycli/errors';
|
|
7
|
+
import { buildSecretSet, redactText } from './redact.js';
|
|
8
|
+
|
|
9
|
+
const DOMAIN = 'mp.weixin.qq.com';
|
|
10
|
+
const ENDPOINT = `https://${DOMAIN}/cgi-bin/appmsgalbummgr`;
|
|
11
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
12
|
+
|
|
13
|
+
function argument(condition, message) {
|
|
14
|
+
if (!condition) throw new ArgumentError(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function positiveInteger(value, fallback, name) {
|
|
18
|
+
const raw = value === undefined ? fallback : value;
|
|
19
|
+
argument(Number.isSafeInteger(raw) && raw > 0, `${name} must be a positive safe integer`);
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nonnegativeInteger(value, name) {
|
|
24
|
+
argument(Number.isSafeInteger(value) && value >= 0, `${name} must be a non-negative safe integer`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nonemptyString(value, name) {
|
|
29
|
+
const normalized = typeof value === 'number' && Number.isSafeInteger(value)
|
|
30
|
+
? String(value) : value;
|
|
31
|
+
argument(typeof normalized === 'string' && normalized.trim().length > 0, `${name} is required`);
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isWellFormedString(value) {
|
|
36
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
37
|
+
const code = value.charCodeAt(index);
|
|
38
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
39
|
+
const next = value.charCodeAt(index + 1);
|
|
40
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
41
|
+
index += 1;
|
|
42
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requiredToken(value) {
|
|
50
|
+
argument(typeof value === 'string' && value.trim().length > 0 && isWellFormedString(value), 'token must be a non-empty well-formed string');
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function numericType(value) {
|
|
55
|
+
const normalized = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value;
|
|
56
|
+
argument(Number.isSafeInteger(normalized) && normalized >= 0, 'collectionType must be a non-negative safe integer');
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildCollectionsUrl({ token, begin, count }) {
|
|
61
|
+
const validToken = requiredToken(token);
|
|
62
|
+
const validBegin = nonnegativeInteger(begin, 'begin');
|
|
63
|
+
const validCount = positiveInteger(count, undefined, 'count');
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
action: 'list', begin: String(validBegin), count: String(validCount), latest: '1', type: '',
|
|
66
|
+
token: validToken, lang: 'zh_CN', f: 'json', ajax: '1',
|
|
67
|
+
});
|
|
68
|
+
return `${ENDPOINT}?${params}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildCollectionDetailUrl({ token, collectionId, collectionType, begin, count }) {
|
|
72
|
+
const validToken = requiredToken(token);
|
|
73
|
+
const validId = nonemptyString(collectionId, 'collectionId');
|
|
74
|
+
const validType = numericType(collectionType);
|
|
75
|
+
const validBegin = nonnegativeInteger(begin, 'begin');
|
|
76
|
+
const validCount = positiveInteger(count, undefined, 'count');
|
|
77
|
+
const params = new URLSearchParams({
|
|
78
|
+
action: 'edit', type: String(validType), id: validId, begin: String(validBegin), count: String(validCount),
|
|
79
|
+
token: validToken, lang: 'zh_CN', f: 'json', ajax: '1',
|
|
80
|
+
});
|
|
81
|
+
return `${ENDPOINT}?${params}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function collectionTypeName(value) {
|
|
85
|
+
return ({ 0: 'article', 5: 'video', 7: 'audio', 8: 'image' })[value] ?? `unknown:${String(value)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function object(value) {
|
|
89
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function execution(condition, message) {
|
|
93
|
+
if (!condition) throw new CommandExecutionError(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkResponse(payload, label) {
|
|
97
|
+
execution(object(payload), `WeChat ${label} returned an unreadable response`);
|
|
98
|
+
const ret = payload.base_resp?.ret;
|
|
99
|
+
if (ret === 200040 || ret === 200003) {
|
|
100
|
+
throw new AuthRequiredError(DOMAIN, 'WeChat collection credentials have expired');
|
|
101
|
+
}
|
|
102
|
+
execution(ret === 0, `WeChat ${label} failed (ret=${String(ret ?? 'unknown')})`);
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requiredString(value, message, { allowEmpty = false } = {}) {
|
|
107
|
+
execution(typeof value === 'string' && (allowEmpty || value.trim().length > 0), message);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requiredId(value, message) {
|
|
112
|
+
execution((typeof value === 'string' && value.trim().length > 0)
|
|
113
|
+
|| (typeof value === 'number' && Number.isSafeInteger(value)), message);
|
|
114
|
+
return String(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function requiredNumber(value, message, { integer = false, nonnegative = false } = {}) {
|
|
118
|
+
execution(typeof value === 'number' && Number.isFinite(value), message);
|
|
119
|
+
if (integer) execution(Number.isSafeInteger(value), message);
|
|
120
|
+
if (nonnegative) execution(value >= 0, message);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function optionalNumber(value, message, options) {
|
|
125
|
+
if (value === undefined || value === null) return null;
|
|
126
|
+
return requiredNumber(value, message, options);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isoFromUnix(value, message, optional = false) {
|
|
130
|
+
if (optional && (value === undefined || value === null)) return null;
|
|
131
|
+
const seconds = requiredNumber(value, message, { integer: true, nonnegative: true });
|
|
132
|
+
const milliseconds = seconds * 1000;
|
|
133
|
+
execution(Number.isSafeInteger(milliseconds), message);
|
|
134
|
+
const date = new Date(milliseconds);
|
|
135
|
+
execution(Number.isFinite(date.getTime()), message);
|
|
136
|
+
return date.toISOString();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function updateFrequency(value, message) {
|
|
140
|
+
if (value === undefined || value === null) return null;
|
|
141
|
+
execution(object(value), message);
|
|
142
|
+
return {
|
|
143
|
+
month: requiredNumber(value.month, message, { integer: true, nonnegative: true }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function flag(value, message, optional = false) {
|
|
148
|
+
if (optional && (value === undefined || value === null)) return null;
|
|
149
|
+
execution(value === 0 || value === 1 || typeof value === 'boolean', message);
|
|
150
|
+
return value === true || value === 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function optionalString(value, message) {
|
|
154
|
+
if (value === undefined || value === null) return null;
|
|
155
|
+
return requiredString(value, message, { allowEmpty: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mapListItem(item, index) {
|
|
159
|
+
execution(object(item), `WeChat collections returned an invalid item at index ${index}`);
|
|
160
|
+
const prefix = `WeChat collections returned an invalid item at index ${index}`;
|
|
161
|
+
const collectionTypeRaw = requiredNumber(item.type, prefix, { integer: true, nonnegative: true });
|
|
162
|
+
return {
|
|
163
|
+
row: {
|
|
164
|
+
collectionId: requiredId(item.id, prefix),
|
|
165
|
+
title: requiredString(item.title, prefix),
|
|
166
|
+
collectionType: collectionTypeName(collectionTypeRaw),
|
|
167
|
+
itemCount: requiredNumber(item.total, prefix, { integer: true, nonnegative: true }),
|
|
168
|
+
views: requiredNumber(item.uv, prefix, { integer: true, nonnegative: true }),
|
|
169
|
+
continuousRead: flag(item.continous_read_on, prefix),
|
|
170
|
+
isUpdating: flag(item.is_updating, prefix),
|
|
171
|
+
isBanned: flag(item.is_ban, prefix),
|
|
172
|
+
isPaid: flag(item.need_pay, prefix),
|
|
173
|
+
createdAt: isoFromUnix(item.create_time, prefix),
|
|
174
|
+
updatedAt: isoFromUnix(item.update_time, prefix),
|
|
175
|
+
coverUrl: optionalString(item.cover_url, prefix),
|
|
176
|
+
},
|
|
177
|
+
collectionTypeRaw,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mapCollectionsPayload(payload) {
|
|
182
|
+
const response = checkResponse(payload, 'collections');
|
|
183
|
+
execution(object(response.list_resp), 'WeChat collections returned an invalid list response');
|
|
184
|
+
const total = requiredNumber(response.list_resp.total, 'WeChat collections returned an invalid total', {
|
|
185
|
+
integer: true, nonnegative: true,
|
|
186
|
+
});
|
|
187
|
+
execution(Array.isArray(response.list_resp.items), 'WeChat collections returned an invalid item list');
|
|
188
|
+
return { rows: response.list_resp.items.map((item, index) => mapListItem(item, index).row), total };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detailContext(context) {
|
|
192
|
+
execution(object(context), 'WeChat collection detail requires request context');
|
|
193
|
+
const collectionId = requiredId(context.collectionId, 'WeChat collection detail requires a collection ID');
|
|
194
|
+
const collectionType = requiredNumber(context.collectionType, 'WeChat collection detail requires a numeric type', {
|
|
195
|
+
integer: true, nonnegative: true,
|
|
196
|
+
});
|
|
197
|
+
const begin = nonnegativeInteger(context.begin === undefined ? 0 : context.begin, 'begin');
|
|
198
|
+
const positionOffset = nonnegativeInteger(
|
|
199
|
+
context.positionOffset === undefined ? 0 : context.positionOffset,
|
|
200
|
+
'positionOffset',
|
|
201
|
+
);
|
|
202
|
+
return { collectionId, collectionType, begin, positionOffset };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function mapDetailItem(item, index, positionOffset) {
|
|
206
|
+
const prefix = `WeChat collection detail returned an invalid item at index ${index}`;
|
|
207
|
+
execution(object(item), prefix);
|
|
208
|
+
return {
|
|
209
|
+
appmsgId: requiredId(item.appmsgid, prefix),
|
|
210
|
+
itemIndex: requiredNumber(item.itemidx, prefix, { integer: true, nonnegative: true }),
|
|
211
|
+
position: positionOffset + index + 1,
|
|
212
|
+
title: requiredString(item.title, prefix),
|
|
213
|
+
link: requiredString(item.link, prefix),
|
|
214
|
+
coverUrl: optionalString(item.cover, prefix),
|
|
215
|
+
createdAt: isoFromUnix(item.create_time, prefix, true),
|
|
216
|
+
type: optionalNumber(item.type, prefix, { integer: true, nonnegative: true }),
|
|
217
|
+
status: optionalNumber(item.status, prefix, { integer: true, nonnegative: true }),
|
|
218
|
+
failReason: optionalString(item.fail_reason, prefix),
|
|
219
|
+
sharePageType: optionalNumber(item.share_page_type, prefix, { integer: true, nonnegative: true }),
|
|
220
|
+
isPaid: flag(item.is_pay_subscribe, prefix, true),
|
|
221
|
+
payAlbumId: item.pay_album_id === undefined || item.pay_album_id === null
|
|
222
|
+
? null : requiredId(item.pay_album_id, prefix),
|
|
223
|
+
wecoinCount: optionalNumber(item.wecoin_count, prefix, { integer: true, nonnegative: true }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseCollectionDetailPayload(payload, context) {
|
|
228
|
+
const response = checkResponse(payload, 'collection detail');
|
|
229
|
+
const expected = detailContext(context);
|
|
230
|
+
execution(object(response.edit_resp), 'WeChat collection detail returned an invalid edit response');
|
|
231
|
+
const edit = response.edit_resp;
|
|
232
|
+
const actualId = requiredId(edit.id, 'WeChat collection detail returned an invalid collection ID');
|
|
233
|
+
execution(actualId === expected.collectionId, 'WeChat collection detail returned a mismatched collection ID');
|
|
234
|
+
const rawType = requiredNumber(edit.type, 'WeChat collection detail returned an invalid collection type', {
|
|
235
|
+
integer: true, nonnegative: true,
|
|
236
|
+
});
|
|
237
|
+
execution(rawType === expected.collectionType, 'WeChat collection detail returned a mismatched collection type');
|
|
238
|
+
execution(Array.isArray(edit.appmsg_infos), 'WeChat collection detail returned an invalid item list');
|
|
239
|
+
const continueFlag = edit.continue_flag;
|
|
240
|
+
execution(Number.isSafeInteger(continueFlag), 'WeChat collection detail returned an invalid continue flag');
|
|
241
|
+
const prefix = 'WeChat collection detail returned invalid metadata';
|
|
242
|
+
const responseBegin = requiredNumber(edit.begin, prefix, { integer: true, nonnegative: true });
|
|
243
|
+
execution(responseBegin === expected.begin, 'WeChat collection detail returned a mismatched begin offset');
|
|
244
|
+
const detail = {
|
|
245
|
+
collectionId: actualId,
|
|
246
|
+
title: requiredString(edit.title, prefix),
|
|
247
|
+
description: optionalString(edit.desc, prefix),
|
|
248
|
+
collectionType: collectionTypeName(rawType),
|
|
249
|
+
coverUrl: optionalString(edit.cover_url, prefix),
|
|
250
|
+
itemCount: requiredNumber(edit.total, prefix, { integer: true, nonnegative: true }),
|
|
251
|
+
createdAt: isoFromUnix(edit.create_time, prefix),
|
|
252
|
+
updatedAt: isoFromUnix(edit.update_time, prefix),
|
|
253
|
+
settings: {
|
|
254
|
+
continuousRead: flag(edit.continous_read_on, prefix),
|
|
255
|
+
isUpdating: flag(edit.is_updating, prefix),
|
|
256
|
+
isReverse: flag(edit.is_reverse, prefix),
|
|
257
|
+
isNumbered: flag(edit.is_numbered, prefix),
|
|
258
|
+
isPaid: flag(edit.need_pay, prefix),
|
|
259
|
+
fee: optionalNumber(edit.fee, prefix),
|
|
260
|
+
isBanned: flag(edit.is_ban, prefix),
|
|
261
|
+
canModifyTitle: flag(edit.can_modify_title, prefix),
|
|
262
|
+
sendQuota: optionalNumber(edit.send_quota, prefix, { integer: true, nonnegative: true }),
|
|
263
|
+
subtype: optionalNumber(edit.subtype, prefix, { integer: true, nonnegative: true }),
|
|
264
|
+
themeColor: optionalString(edit.theme_color, prefix),
|
|
265
|
+
updateFrequency: updateFrequency(edit.update_frequence, prefix),
|
|
266
|
+
},
|
|
267
|
+
items: edit.appmsg_infos.map((item, index) => mapDetailItem(item, index, expected.positionOffset)),
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
detail,
|
|
271
|
+
continueFlag: continueFlag === 0 ? 0 : 1,
|
|
272
|
+
rawItemCount: edit.appmsg_infos.length,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function mapCollectionDetailPayload(payload, context) {
|
|
277
|
+
return parseCollectionDetailPayload(payload, context).detail;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requestOptions(safeReferer) {
|
|
281
|
+
argument(typeof safeReferer === 'string' && safeReferer.length > 0, 'safeReferer is required');
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = new URL(safeReferer);
|
|
285
|
+
} catch {
|
|
286
|
+
throw new ArgumentError('safeReferer must be a safe mp.weixin.qq.com HTTPS URL without a token');
|
|
287
|
+
}
|
|
288
|
+
const hasToken = [...parsed.searchParams.keys()].some(key => key.toLowerCase() === 'token');
|
|
289
|
+
argument(
|
|
290
|
+
parsed.origin === `https://${DOMAIN}` && parsed.username === '' && parsed.password === '' && !hasToken,
|
|
291
|
+
'safeReferer must be a safe mp.weixin.qq.com HTTPS URL without a token',
|
|
292
|
+
);
|
|
293
|
+
return { referrer: safeReferer, headers: { 'X-Requested-With': 'XMLHttpRequest' } };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function transportError(error, token) {
|
|
297
|
+
const redactableToken = typeof token === 'string' && isWellFormedString(token) ? token : '';
|
|
298
|
+
const secrets = buildSecretSet({ token: redactableToken, cookie: '' });
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
const hint = object(error) && typeof error.hint === 'string' ? error.hint : undefined;
|
|
301
|
+
const tokenRepresentations = [];
|
|
302
|
+
if (typeof token === 'string' && token.length > 0) {
|
|
303
|
+
tokenRepresentations.push(token);
|
|
304
|
+
try {
|
|
305
|
+
tokenRepresentations.push(encodeURIComponent(token));
|
|
306
|
+
} catch {
|
|
307
|
+
// Invalid UTF-16 is rejected by requiredToken; redaction must not mask that error.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const sanitize = value => {
|
|
311
|
+
let sanitized = redactText(value, secrets);
|
|
312
|
+
for (const representation of tokenRepresentations) {
|
|
313
|
+
sanitized = sanitized.replaceAll(representation, '[REDACTED]');
|
|
314
|
+
}
|
|
315
|
+
return sanitized.replace(/https?:\/\/mp\.weixin\.qq\.com\/cgi-bin\/appmsgalbummgr\S*/giu, '[REDACTED]');
|
|
316
|
+
};
|
|
317
|
+
const safeMessage = sanitize(message);
|
|
318
|
+
const safeHint = hint === undefined ? undefined : sanitize(hint);
|
|
319
|
+
if (error instanceof CliError && safeMessage === message && safeHint === hint) return error;
|
|
320
|
+
return new CommandExecutionError(`WeChat collections request failed: ${safeMessage}`, safeHint);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function pagination({ limit, pageSize, maxPages }) {
|
|
324
|
+
return {
|
|
325
|
+
limit: positiveInteger(limit, undefined, 'limit'),
|
|
326
|
+
pageSize: positiveInteger(pageSize, DEFAULT_PAGE_SIZE, 'pageSize'),
|
|
327
|
+
maxPages: positiveInteger(maxPages, undefined, 'maxPages'),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function collectCollections({ page, token, safeReferer, limit, pageSize, maxPages }) {
|
|
332
|
+
const options = pagination({ limit, pageSize, maxPages });
|
|
333
|
+
const requestInit = requestOptions(safeReferer);
|
|
334
|
+
const rows = [];
|
|
335
|
+
const seenCollectionIds = new Set();
|
|
336
|
+
let begin = 0;
|
|
337
|
+
try {
|
|
338
|
+
for (let pageNumber = 0; pageNumber < options.maxPages && rows.length < options.limit; pageNumber += 1) {
|
|
339
|
+
const url = buildCollectionsUrl({ token, begin, count: options.pageSize });
|
|
340
|
+
const payload = await page.fetchJson(url, requestInit);
|
|
341
|
+
const mapped = mapCollectionsPayload(payload);
|
|
342
|
+
const rawCount = payload.list_resp.items.length;
|
|
343
|
+
for (const row of mapped.rows) {
|
|
344
|
+
execution(!seenCollectionIds.has(row.collectionId), 'WeChat collections returned a duplicate collection ID');
|
|
345
|
+
seenCollectionIds.add(row.collectionId);
|
|
346
|
+
}
|
|
347
|
+
rows.push(...mapped.rows.slice(0, options.limit - rows.length));
|
|
348
|
+
begin += rawCount;
|
|
349
|
+
if (rawCount === 0 || begin >= mapped.total) break;
|
|
350
|
+
}
|
|
351
|
+
return rows;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw transportError(error, token);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function fetchCollectionDetail({
|
|
358
|
+
page, token, safeReferer, collectionId, collectionType, limit, pageSize, maxPages,
|
|
359
|
+
}) {
|
|
360
|
+
const options = pagination({ limit, pageSize, maxPages });
|
|
361
|
+
const requestInit = requestOptions(safeReferer);
|
|
362
|
+
const validId = nonemptyString(collectionId, 'collectionId');
|
|
363
|
+
const validType = numericType(collectionType);
|
|
364
|
+
let begin = 0;
|
|
365
|
+
let result = null;
|
|
366
|
+
const seenItemIdentities = new Set();
|
|
367
|
+
try {
|
|
368
|
+
for (let pageNumber = 0; pageNumber < options.maxPages && begin < options.limit; pageNumber += 1) {
|
|
369
|
+
const url = buildCollectionDetailUrl({
|
|
370
|
+
token, collectionId: validId, collectionType: validType, begin, count: options.pageSize,
|
|
371
|
+
});
|
|
372
|
+
const payload = await page.fetchJson(url, requestInit);
|
|
373
|
+
const parsed = parseCollectionDetailPayload(payload, {
|
|
374
|
+
collectionId: validId, collectionType: validType, begin, positionOffset: begin,
|
|
375
|
+
});
|
|
376
|
+
const { detail: mapped, continueFlag, rawItemCount: rawCount } = parsed;
|
|
377
|
+
if (continueFlag !== 0 && rawCount === 0) {
|
|
378
|
+
throw new CommandExecutionError('WeChat collection detail returned an empty continuing page');
|
|
379
|
+
}
|
|
380
|
+
for (const item of mapped.items) {
|
|
381
|
+
const identity = JSON.stringify([item.appmsgId, item.itemIndex]);
|
|
382
|
+
execution(!seenItemIdentities.has(identity), 'WeChat collection detail returned a duplicate item identity');
|
|
383
|
+
seenItemIdentities.add(identity);
|
|
384
|
+
}
|
|
385
|
+
if (result === null) result = { ...mapped, items: [] };
|
|
386
|
+
result.items.push(...mapped.items.slice(0, options.limit - result.items.length));
|
|
387
|
+
begin += rawCount;
|
|
388
|
+
if (continueFlag === 0 || rawCount === 0 || result.items.length >= options.limit) break;
|
|
389
|
+
}
|
|
390
|
+
execution(result !== null, 'WeChat collection detail returned no pages');
|
|
391
|
+
return result;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw transportError(error, token);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function findCollectionById({ page, token, safeReferer, collectionId, pageSize, maxPages }) {
|
|
398
|
+
const validId = nonemptyString(collectionId, 'collectionId');
|
|
399
|
+
const size = positiveInteger(pageSize, DEFAULT_PAGE_SIZE, 'pageSize');
|
|
400
|
+
const pages = positiveInteger(maxPages, undefined, 'maxPages');
|
|
401
|
+
const requestInit = requestOptions(safeReferer);
|
|
402
|
+
let begin = 0;
|
|
403
|
+
const seenCollectionIds = new Set();
|
|
404
|
+
try {
|
|
405
|
+
for (let pageNumber = 0; pageNumber < pages; pageNumber += 1) {
|
|
406
|
+
const url = buildCollectionsUrl({ token, begin, count: size });
|
|
407
|
+
const payload = await page.fetchJson(url, requestInit);
|
|
408
|
+
const mapped = mapCollectionsPayload(payload);
|
|
409
|
+
const rawItems = payload.list_resp.items;
|
|
410
|
+
for (let index = 0; index < mapped.rows.length; index += 1) {
|
|
411
|
+
execution(!seenCollectionIds.has(mapped.rows[index].collectionId), 'WeChat collections returned a duplicate collection ID');
|
|
412
|
+
seenCollectionIds.add(mapped.rows[index].collectionId);
|
|
413
|
+
if (mapped.rows[index].collectionId === validId) {
|
|
414
|
+
const collectionTypeRaw = requiredNumber(
|
|
415
|
+
rawItems[index].type,
|
|
416
|
+
'WeChat collections returned an invalid collection type',
|
|
417
|
+
{ integer: true, nonnegative: true },
|
|
418
|
+
);
|
|
419
|
+
return { row: mapped.rows[index], collectionTypeRaw };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
begin += rawItems.length;
|
|
423
|
+
if (rawItems.length === 0 || begin >= mapped.total) break;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
throw transportError(error, token);
|
|
428
|
+
}
|
|
429
|
+
}
|