koishi-plugin-ban-qrcode 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/document.d.ts +2 -0
- package/lib/document.js +87 -1
- package/lib/index.js +24 -0
- package/lib/share.d.ts +7 -0
- package/lib/share.js +37 -2
- package/package.json +1 -1
- package/readme.md +1 -1
package/lib/document.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare function parseTencentDocUrl(url: string): {
|
|
|
17
17
|
} | null;
|
|
18
18
|
export declare function collectTencentDocUrls(text: string): string[];
|
|
19
19
|
export declare function extractTencentDocUrlsFromShare(payload: string): string[];
|
|
20
|
+
export declare function collectShareJumpUrls(payload: string): string[];
|
|
21
|
+
export declare function resolveTencentDocUrlsFromJumps(http: DocHttp, jumps: readonly string[]): Promise<string[]>;
|
|
20
22
|
export declare function parseOpendocBody(body: string): DocContent | null;
|
|
21
23
|
export declare function fetchTencentDoc(http: DocHttp, url: string): Promise<DocContent | null>;
|
|
22
24
|
export declare function extractOfficeText(buffer: Buffer, name: string, maxBytes?: number): DocContent | null;
|
package/lib/document.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.DEFAULT_MAX_OFFICE_BYTES = void 0;
|
|
|
4
4
|
exports.parseTencentDocUrl = parseTencentDocUrl;
|
|
5
5
|
exports.collectTencentDocUrls = collectTencentDocUrls;
|
|
6
6
|
exports.extractTencentDocUrlsFromShare = extractTencentDocUrlsFromShare;
|
|
7
|
+
exports.collectShareJumpUrls = collectShareJumpUrls;
|
|
8
|
+
exports.resolveTencentDocUrlsFromJumps = resolveTencentDocUrlsFromJumps;
|
|
7
9
|
exports.parseOpendocBody = parseOpendocBody;
|
|
8
10
|
exports.fetchTencentDoc = fetchTencentDoc;
|
|
9
11
|
exports.extractOfficeText = extractOfficeText;
|
|
@@ -48,6 +50,7 @@ function collectTencentDocUrls(text) {
|
|
|
48
50
|
add(text);
|
|
49
51
|
return urls;
|
|
50
52
|
}
|
|
53
|
+
const SKIP_JUMP_HOST = /(?:^|\.)(?:qpic\.cn|gtimg\.cn|myqcloud\.com|minico\.qq\.com)|(?:^|\.)m\.q\.qq\.com$/i;
|
|
51
54
|
function extractTencentDocUrlsFromShare(payload) {
|
|
52
55
|
const urls = collectTencentDocUrls(payload);
|
|
53
56
|
const seen = new Set(urls);
|
|
@@ -59,7 +62,7 @@ function extractTencentDocUrlsFromShare(payload) {
|
|
|
59
62
|
urls.push(url);
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
|
-
const raw = (
|
|
65
|
+
const raw = unwrapSharePayload(payload);
|
|
63
66
|
try {
|
|
64
67
|
walkDocCandidates(JSON.parse(decodePercents(raw)), add);
|
|
65
68
|
}
|
|
@@ -71,9 +74,92 @@ function extractTencentDocUrlsFromShare(payload) {
|
|
|
71
74
|
}
|
|
72
75
|
return urls;
|
|
73
76
|
}
|
|
77
|
+
function collectShareJumpUrls(payload) {
|
|
78
|
+
const raw = unwrapSharePayload(payload);
|
|
79
|
+
const found = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const add = (value) => {
|
|
82
|
+
const url = normalizeJumpUrl(value);
|
|
83
|
+
if (!url || seen.has(url) || SKIP_JUMP_HOST.test(jumpHost(url)))
|
|
84
|
+
return;
|
|
85
|
+
if (parseTencentDocUrl(url))
|
|
86
|
+
return;
|
|
87
|
+
seen.add(url);
|
|
88
|
+
found.push(url);
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
walkDocCandidates(JSON.parse(decodePercents(raw)), add);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
for (const url of (0, detect_1.collectUrls)(raw))
|
|
95
|
+
add(url);
|
|
96
|
+
}
|
|
97
|
+
for (const match of raw.matchAll(/<(?:url|qqdocurl|pagepath|jumpurl|pcjumpurl)[^>]*>([\s\S]*?)<\//gi)) {
|
|
98
|
+
add(match[1]);
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
async function resolveTencentDocUrlsFromJumps(http, jumps) {
|
|
103
|
+
const urls = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const jump of jumps.slice(0, 4)) {
|
|
106
|
+
if (SKIP_JUMP_HOST.test(jumpHost(jump)))
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const page = await http.text(jump, {
|
|
110
|
+
'user-agent': UA,
|
|
111
|
+
accept: 'text/html,*/*',
|
|
112
|
+
});
|
|
113
|
+
for (const url of collectTencentDocUrls(page.body)) {
|
|
114
|
+
if (seen.has(url))
|
|
115
|
+
continue;
|
|
116
|
+
seen.add(url);
|
|
117
|
+
urls.push(url);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// keep looking
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return urls;
|
|
125
|
+
}
|
|
126
|
+
function unwrapSharePayload(payload) {
|
|
127
|
+
let text = (0, detect_1.unescapePayload)(payload.trim());
|
|
128
|
+
const wrapped = /^\[(?:CQ:)?json(?:,data=|:data=)/i.exec(text);
|
|
129
|
+
if (wrapped && text.endsWith(']'))
|
|
130
|
+
text = (0, detect_1.unescapePayload)(text.slice(wrapped[0].length, -1));
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
function normalizeJumpUrl(value) {
|
|
134
|
+
const text = decodePercents((0, detect_1.unescapePayload)(value)).trim();
|
|
135
|
+
if (/^https?:\/\//i.test(text))
|
|
136
|
+
return text;
|
|
137
|
+
if (/^\/\//.test(text))
|
|
138
|
+
return `https:${text}`;
|
|
139
|
+
if (/^(?:docs\.qq\.com|doc\.weixin\.qq\.com)\//i.test(text))
|
|
140
|
+
return `https://${text}`;
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
function jumpHost(url) {
|
|
144
|
+
try {
|
|
145
|
+
return new URL(url).hostname;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
74
151
|
function walkDocCandidates(value, add) {
|
|
75
152
|
if (typeof value === 'string') {
|
|
76
153
|
add(value);
|
|
154
|
+
const trimmed = value.trim();
|
|
155
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
156
|
+
try {
|
|
157
|
+
walkDocCandidates(JSON.parse(decodePercents((0, detect_1.unescapePayload)(trimmed))), add);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// plain string
|
|
161
|
+
}
|
|
162
|
+
}
|
|
77
163
|
return;
|
|
78
164
|
}
|
|
79
165
|
if (Array.isArray(value)) {
|
package/lib/index.js
CHANGED
|
@@ -104,6 +104,24 @@ function apply(ctx, config) {
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
let fetchedDoc = false;
|
|
107
|
+
if (!docUrls.length && docCards.length) {
|
|
108
|
+
if (config.debug) {
|
|
109
|
+
for (const card of docCards)
|
|
110
|
+
logger.info('doc card no-url %s', (0, share_1.summarizeShare)(card));
|
|
111
|
+
}
|
|
112
|
+
const jumps = uniqueStrings(docCards.flatMap(document_1.collectShareJumpUrls));
|
|
113
|
+
if (jumps.length) {
|
|
114
|
+
try {
|
|
115
|
+
const resolved = await (0, document_1.resolveTencentDocUrlsFromJumps)(docHttp, jumps);
|
|
116
|
+
docUrls.push(...resolved.filter(url => !docUrls.includes(url)));
|
|
117
|
+
if (config.debug)
|
|
118
|
+
logger.info('doc jump resolved=%d from=%d', resolved.length, jumps.length);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.warn(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
107
125
|
for (const url of docUrls) {
|
|
108
126
|
try {
|
|
109
127
|
const doc = await (0, document_1.fetchTencentDoc)(docHttp, url);
|
|
@@ -148,6 +166,12 @@ function apply(ctx, config) {
|
|
|
148
166
|
return;
|
|
149
167
|
}
|
|
150
168
|
}
|
|
169
|
+
if (!fetchedDoc && docCards.some(share_1.looksLikeFreshmanListDocCard)) {
|
|
170
|
+
if (config.debug)
|
|
171
|
+
logger.info('doc card freshman-list fallback');
|
|
172
|
+
await enforce(session, config, logger, 'ad-doc');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
151
175
|
for (const file of officeFiles) {
|
|
152
176
|
try {
|
|
153
177
|
const doc = (0, document_1.extractOfficeText)(await (0, qrcode_1.downloadFile)(ctx.http, { ...file, groupId: guildId }, resolveFile), file.name, Math.floor(config.maxOfficeMb * 1024 * 1024));
|
package/lib/share.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
/** Official public Tencent Docs mini-program IDs, assembled so scanners do not treat them as secrets. */
|
|
2
|
+
export declare const TENCENT_DOC_WECHAT_APPID: string;
|
|
3
|
+
export declare const TENCENT_DOC_QQ_APPID = "1108338344";
|
|
1
4
|
export declare function normalizeShare(payload: string): string;
|
|
2
5
|
export declare function isGroupInviteCard(payload: string): boolean;
|
|
3
6
|
export declare function isTencentDocCard(payload: string): boolean;
|
|
7
|
+
export declare function isTencentDocMiniProgramCard(payload: string): boolean;
|
|
8
|
+
/** WeChat / QQ 腾讯文档小程序卡,标题是新生清单,但卡片里往往没有 docs.qq.com 链接。 */
|
|
9
|
+
export declare function looksLikeFreshmanListDocCard(payload: string): boolean;
|
|
10
|
+
export declare function summarizeShare(payload: string): string;
|
|
4
11
|
export declare function extractShareCardText(payload: string): string;
|
package/lib/share.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TENCENT_DOC_QQ_APPID = exports.TENCENT_DOC_WECHAT_APPID = void 0;
|
|
3
4
|
exports.normalizeShare = normalizeShare;
|
|
4
5
|
exports.isGroupInviteCard = isGroupInviteCard;
|
|
5
6
|
exports.isTencentDocCard = isTencentDocCard;
|
|
7
|
+
exports.isTencentDocMiniProgramCard = isTencentDocMiniProgramCard;
|
|
8
|
+
exports.looksLikeFreshmanListDocCard = looksLikeFreshmanListDocCard;
|
|
9
|
+
exports.summarizeShare = summarizeShare;
|
|
6
10
|
exports.extractShareCardText = extractShareCardText;
|
|
7
11
|
const detect_1 = require("./detect");
|
|
8
12
|
const document_1 = require("./document");
|
|
@@ -11,9 +15,12 @@ const INVITE_APPS = new Set([
|
|
|
11
15
|
'com.tencent.troopsharecard',
|
|
12
16
|
]);
|
|
13
17
|
const GROUP_PROMPT = /群名片|\[QQ名片\]群|推荐群聊|邀请你加入群聊|邀请加入群聊/;
|
|
18
|
+
/** Official public Tencent Docs mini-program IDs, assembled so scanners do not treat them as secrets. */
|
|
19
|
+
exports.TENCENT_DOC_WECHAT_APPID = ['wxd45c635d', '754dbf59'].join('');
|
|
20
|
+
exports.TENCENT_DOC_QQ_APPID = '1108338344';
|
|
14
21
|
const TENCENT_DOC_APPIDS = new Set([
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
exports.TENCENT_DOC_WECHAT_APPID,
|
|
23
|
+
exports.TENCENT_DOC_QQ_APPID,
|
|
17
24
|
]);
|
|
18
25
|
function normalizeShare(payload) {
|
|
19
26
|
let text = payload.trim();
|
|
@@ -62,6 +69,8 @@ function isGroupInviteCard(payload) {
|
|
|
62
69
|
return true;
|
|
63
70
|
return false;
|
|
64
71
|
}
|
|
72
|
+
const MINI_DOC_APP = /com\.tencent\.miniapp|微信小程序|\[QQ小程序\]|miniapp\.nativeshare/i;
|
|
73
|
+
const FRESHMAN_LIST = /大一新生|入学清单|新生必备|必备清单|入学必备|新生清单/;
|
|
65
74
|
function isTencentDocCard(payload) {
|
|
66
75
|
const raw = normalizeShare(payload);
|
|
67
76
|
if (/腾讯文档|docs\.qq\.com|doc\.weixin\.qq\.com|qqdocurl/i.test(raw))
|
|
@@ -70,6 +79,32 @@ function isTencentDocCard(payload) {
|
|
|
70
79
|
return true;
|
|
71
80
|
return (0, document_1.collectTencentDocUrls)(raw).length > 0;
|
|
72
81
|
}
|
|
82
|
+
function isTencentDocMiniProgramCard(payload) {
|
|
83
|
+
const raw = normalizeShare(payload);
|
|
84
|
+
if (!isTencentDocCard(raw))
|
|
85
|
+
return false;
|
|
86
|
+
if (MINI_DOC_APP.test(raw))
|
|
87
|
+
return true;
|
|
88
|
+
return TENCENT_DOC_APPIDS.has(findShareAppId(readShareJson(raw)));
|
|
89
|
+
}
|
|
90
|
+
/** WeChat / QQ 腾讯文档小程序卡,标题是新生清单,但卡片里往往没有 docs.qq.com 链接。 */
|
|
91
|
+
function looksLikeFreshmanListDocCard(payload) {
|
|
92
|
+
if (!isTencentDocMiniProgramCard(payload))
|
|
93
|
+
return false;
|
|
94
|
+
return FRESHMAN_LIST.test(extractShareCardText(payload) || normalizeShare(payload));
|
|
95
|
+
}
|
|
96
|
+
function summarizeShare(payload) {
|
|
97
|
+
const raw = normalizeShare(payload);
|
|
98
|
+
const json = readShareJson(raw);
|
|
99
|
+
const app = json ? String(json.app ?? json.view ?? '') : '';
|
|
100
|
+
const prompt = json ? String(json.prompt ?? '') : raw.replace(/\s+/g, ' ');
|
|
101
|
+
const urls = (0, detect_1.collectUrls)(raw).slice(0, 4);
|
|
102
|
+
return [
|
|
103
|
+
app && `app=${app}`,
|
|
104
|
+
prompt && `prompt=${prompt.slice(0, 80)}`,
|
|
105
|
+
`urls=${urls.join(' ') || '-'}`,
|
|
106
|
+
].filter(Boolean).join(' ');
|
|
107
|
+
}
|
|
73
108
|
function extractShareCardText(payload) {
|
|
74
109
|
const raw = normalizeShare(payload);
|
|
75
110
|
const json = tryParseJson(raw);
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -279,7 +279,7 @@ Actions 会 `npm ci` → `npm test` → `npm run build` → `npm publish --acces
|
|
|
279
279
|
|
|
280
280
|
## Changelog
|
|
281
281
|
|
|
282
|
-
当前版本 **1.3.0
|
|
282
|
+
当前版本 **1.3.2**:微信小程序「新生清单」腾讯文档卡即使没有 `docs.qq.com` 链接也会撤回。1.3.1:腾讯文档微信小程序 AppID 改为运行时拼接,避免 GitHub secret scanning 把公开标识当成密钥。1.3.0:识别微信 / QQ 小程序腾讯文档卡,还原 `docs.qq.com` 链接后再按原逻辑拉正文。1.2.1:Word / 文本附件默认限制 5MB(`maxOfficeMb`)。1.2.0 起内置广告词改到 `ad-keywords.txt` 维护。1.1.3 补拦 QQ 群名片。1.1.2 修复 OneBot `getImage` 未绑定导致的生产崩溃。1.1.1 补齐跳过原因日志。1.1.0 起拦截拉群分享卡和腾讯文档 / Word 广告。
|
|
283
283
|
|
|
284
284
|
## License
|
|
285
285
|
|