koishi-plugin-cs2-update-log 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +75 -23
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Koishi QQ 机器人插件,用于轮询 CS2 官方 Steam 公告流,按 `gid`
6
6
 
7
7
  ## 功能
8
8
 
9
- - 使用 Steam `ISteamNews/GetNewsForApp/v2` 拉取 AppID `730` 的官方公告流。
9
+ - 使用 Steam Store RSS `https://store.steampowered.com/feeds/news/app/730/` 拉取 AppID `730` 的官方公告流。
10
10
  - 默认每 30 秒轮询一次。
11
11
  - 首次启动默认只记录历史 `gid`,不推送历史内容。
12
12
  - 自动分类:
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ exports.Config = exports.inject = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const markdown_it_1 = __importDefault(require("markdown-it"));
10
+ const fast_xml_parser_1 = require("fast-xml-parser");
10
11
  const node_fs_1 = require("node:fs");
11
12
  const node_path_1 = __importDefault(require("node:path"));
12
13
  exports.name = 'cs2-update-log';
@@ -15,8 +16,7 @@ exports.inject = {
15
16
  optional: ['puppeteer'],
16
17
  };
17
18
  const APP_ID = 730;
18
- const FEED_NAME = 'steam_community_announcements';
19
- const STEAM_NEWS_API = 'https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/';
19
+ const STEAM_RSS_URL = `https://store.steampowered.com/feeds/news/app/${APP_ID}/`;
20
20
  const STATE_LIMIT = 1000;
21
21
  const loggerName = 'cs2-update-log';
22
22
  exports.Config = koishi_1.Schema.object({
@@ -45,9 +45,16 @@ const markdown = new markdown_it_1.default({
45
45
  linkify: true,
46
46
  breaks: true,
47
47
  });
48
+ const rssParser = new fast_xml_parser_1.XMLParser({
49
+ ignoreAttributes: false,
50
+ parseTagValue: false,
51
+ processEntities: false,
52
+ trimValues: true,
53
+ });
48
54
  const updateTitlePattern = /\b(?:Counter-Strike 2 Update|Release Notes)\b/i;
49
- const updateSectionPattern = /^\s*\[\s*(MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING)\s*\]\s*$/gim;
50
- const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING)\s*\]/i;
55
+ const updateSectionPattern = /^\s*\[\s*(MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]\s*$/gim;
56
+ const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]/i;
57
+ const sectionHeadingPattern = /^\s*\[\s*([A-Z][A-Z0-9 &'/-]{1,48})\s*\]\s*$/gim;
51
58
  function apply(ctx, config) {
52
59
  const logger = ctx.logger(loggerName);
53
60
  const statePath = resolveStatePath(ctx, config.stateFile);
@@ -95,22 +102,19 @@ function apply(ctx, config) {
95
102
  }
96
103
  async function fetchNews() {
97
104
  try {
98
- const response = await ctx.http.get(STEAM_NEWS_API, {
99
- params: {
100
- appid: APP_ID,
101
- count: config.count,
102
- maxlength: 0,
103
- format: 'json',
104
- feeds: FEED_NAME,
105
- },
105
+ const xml = await ctx.http.get(STEAM_RSS_URL, {
106
+ responseType: 'text',
106
107
  });
107
- const items = response?.appnews?.newsitems;
108
- if (!Array.isArray(items))
109
- return [];
110
- return items.filter((item) => item?.gid && item?.title);
108
+ const parsed = rssParser.parse(xml);
109
+ const rawItems = parsed.rss?.channel?.item;
110
+ const items = Array.isArray(rawItems) ? rawItems : rawItems ? [rawItems] : [];
111
+ return items
112
+ .map(parseRssItem)
113
+ .filter((item) => !!item?.gid && !!item.title)
114
+ .slice(0, config.count);
111
115
  }
112
116
  catch (error) {
113
- logger.error('拉取 Steam CS2 新闻失败:%s', formatError(error));
117
+ logger.error('拉取 Steam CS2 RSS 失败:%s', formatError(error));
114
118
  return [];
115
119
  }
116
120
  }
@@ -155,7 +159,7 @@ function apply(ctx, config) {
155
159
  }
156
160
  async function pushNews(news) {
157
161
  const link = getNewsLink(news.item);
158
- const baseMarkdown = steamBbcodeToMarkdown(news.item.contents);
162
+ const baseMarkdown = steamContentToMarkdown(news.item.content);
159
163
  const translated = await maybeTranslate(news.item.title, baseMarkdown);
160
164
  const displayTitle = translated.title || news.item.title;
161
165
  const displayMarkdown = translated.markdown || baseMarkdown;
@@ -308,7 +312,7 @@ function apply(ctx, config) {
308
312
  }, Math.max(5, config.interval) * 1000);
309
313
  }
310
314
  function classifyNews(item) {
311
- const body = decodeHtmlEntities(item.contents || '');
315
+ const body = decodeHtmlEntities(item.content || '');
312
316
  const isUpdate = updateTitlePattern.test(item.title)
313
317
  || updateSectionPattern.test(body)
314
318
  || updateSectionInlinePattern.test(body);
@@ -318,13 +322,61 @@ function classifyNews(item) {
318
322
  category: isUpdate ? 'update' : 'announcement',
319
323
  };
320
324
  }
321
- function steamBbcodeToMarkdown(input) {
325
+ function parseRssItem(item) {
326
+ const title = decodeHtmlEntities(readXmlText(item.title));
327
+ const url = readXmlText(item.link);
328
+ const guid = readXmlText(item.guid);
329
+ const gid = extractNewsGid(guid) || extractNewsGid(url);
330
+ if (!gid || !title)
331
+ return null;
332
+ const pubDate = readXmlText(item.pubDate);
333
+ const dateValue = Date.parse(pubDate);
334
+ return {
335
+ gid,
336
+ title,
337
+ url,
338
+ author: 'Valve',
339
+ content: readXmlText(item.description),
340
+ date: Number.isFinite(dateValue) ? Math.floor(dateValue / 1000) : 0,
341
+ };
342
+ }
343
+ function readXmlText(value) {
344
+ if (value == null)
345
+ return '';
346
+ if (typeof value === 'string' || typeof value === 'number')
347
+ return String(value).trim();
348
+ return String(value['#text'] ?? '').trim();
349
+ }
350
+ function extractNewsGid(value) {
351
+ return value.match(/\/view\/(\d+)/)?.[1] || '';
352
+ }
353
+ function steamContentToMarkdown(input) {
322
354
  let output = decodeHtmlEntities(input || '')
323
- .replace(/\r\n?/g, '\n');
355
+ .replace(/\r\n?/g, '\n')
356
+ .replace(/\\([\[\]])/g, '$1');
324
357
  output = output
358
+ .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)')
359
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h1[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n# ${text.trim()}\n`)
360
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h2[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n## ${text.trim()}\n`)
361
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h3[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
362
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h[45][^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
363
+ .replace(/<p\b[^>]*class=["'][^"']*bb_paragraph[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, (_, text) => `\n${text.trim()}\n`)
364
+ .replace(/<li\b[^>]*>\s*/gi, '\n- ')
365
+ .replace(/<\/li>/gi, '\n')
366
+ .replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n')
367
+ .replace(/<br\s*\/?>/gi, '\n')
368
+ .replace(/<img\b[^>]*>/gi, '')
369
+ .replace(/<strong\b[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**')
370
+ .replace(/<b\b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**')
371
+ .replace(/<em\b[^>]*>([\s\S]*?)<\/em>/gi, '*$1*')
372
+ .replace(/<i\b[^>]*>([\s\S]*?)<\/i>/gi, '*$1*')
373
+ .replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
374
+ .replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${code.trim()}\n\`\`\`\n`)
375
+ .replace(/<[^>]+>/g, '')
325
376
  .replace(/\[h1\]([\s\S]*?)\[\/h1\]/gi, (_, text) => `\n# ${text.trim()}\n`)
326
377
  .replace(/\[h2\]([\s\S]*?)\[\/h2\]/gi, (_, text) => `\n## ${text.trim()}\n`)
327
378
  .replace(/\[h3\]([\s\S]*?)\[\/h3\]/gi, (_, text) => `\n### ${text.trim()}\n`)
379
+ .replace(/\[h[45]\]([\s\S]*?)\[\/h[45]\]/gi, (_, text) => `\n### ${text.trim()}\n`)
328
380
  .replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '**$1**')
329
381
  .replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, '**$1**')
330
382
  .replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '*$1*')
@@ -342,8 +394,8 @@ function steamBbcodeToMarkdown(input) {
342
394
  .replace(/<br\s*\/?>/gi, '\n')
343
395
  .replace(/<\/p>\s*<p>/gi, '\n\n')
344
396
  .replace(/<\/?p>/gi, '\n');
345
- output = output.replace(updateSectionPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
346
- updateSectionPattern.lastIndex = 0;
397
+ output = output.replace(sectionHeadingPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
398
+ sectionHeadingPattern.lastIndex = 0;
347
399
  return output
348
400
  .replace(/\n{3,}/g, '\n\n')
349
401
  .trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-cs2-update-log",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Koishi plugin for monitoring CS2 official Steam announcements and update logs.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -32,6 +32,7 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
+ "fast-xml-parser": "^4.5.3",
35
36
  "markdown-it": "^14.1.0"
36
37
  },
37
38
  "devDependencies": {