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 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 = (0, detect_1.unescapePayload)(payload);
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
- 'wxd45c635d754dbf59',
16
- '1108338344',
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ban-qrcode",
3
3
  "description": "检测到群内二维码、拉群卡片或文档广告后自动撤回并禁言",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
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**:识别微信 / 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 广告。
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