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

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 +74 -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,15 @@ 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
+ trimValues: true,
52
+ });
48
53
  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;
54
+ 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;
55
+ const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]/i;
56
+ const sectionHeadingPattern = /^\s*\[\s*([A-Z][A-Z0-9 &'/-]{1,48})\s*\]\s*$/gim;
51
57
  function apply(ctx, config) {
52
58
  const logger = ctx.logger(loggerName);
53
59
  const statePath = resolveStatePath(ctx, config.stateFile);
@@ -95,22 +101,19 @@ function apply(ctx, config) {
95
101
  }
96
102
  async function fetchNews() {
97
103
  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
- },
104
+ const xml = await ctx.http.get(STEAM_RSS_URL, {
105
+ responseType: 'text',
106
106
  });
107
- const items = response?.appnews?.newsitems;
108
- if (!Array.isArray(items))
109
- return [];
110
- return items.filter((item) => item?.gid && item?.title);
107
+ const parsed = rssParser.parse(xml);
108
+ const rawItems = parsed.rss?.channel?.item;
109
+ const items = Array.isArray(rawItems) ? rawItems : rawItems ? [rawItems] : [];
110
+ return items
111
+ .map(parseRssItem)
112
+ .filter((item) => !!item?.gid && !!item.title)
113
+ .slice(0, config.count);
111
114
  }
112
115
  catch (error) {
113
- logger.error('拉取 Steam CS2 新闻失败:%s', formatError(error));
116
+ logger.error('拉取 Steam CS2 RSS 失败:%s', formatError(error));
114
117
  return [];
115
118
  }
116
119
  }
@@ -155,7 +158,7 @@ function apply(ctx, config) {
155
158
  }
156
159
  async function pushNews(news) {
157
160
  const link = getNewsLink(news.item);
158
- const baseMarkdown = steamBbcodeToMarkdown(news.item.contents);
161
+ const baseMarkdown = steamContentToMarkdown(news.item.content);
159
162
  const translated = await maybeTranslate(news.item.title, baseMarkdown);
160
163
  const displayTitle = translated.title || news.item.title;
161
164
  const displayMarkdown = translated.markdown || baseMarkdown;
@@ -308,7 +311,7 @@ function apply(ctx, config) {
308
311
  }, Math.max(5, config.interval) * 1000);
309
312
  }
310
313
  function classifyNews(item) {
311
- const body = decodeHtmlEntities(item.contents || '');
314
+ const body = decodeHtmlEntities(item.content || '');
312
315
  const isUpdate = updateTitlePattern.test(item.title)
313
316
  || updateSectionPattern.test(body)
314
317
  || updateSectionInlinePattern.test(body);
@@ -318,13 +321,61 @@ function classifyNews(item) {
318
321
  category: isUpdate ? 'update' : 'announcement',
319
322
  };
320
323
  }
321
- function steamBbcodeToMarkdown(input) {
324
+ function parseRssItem(item) {
325
+ const title = readXmlText(item.title);
326
+ const url = readXmlText(item.link);
327
+ const guid = readXmlText(item.guid);
328
+ const gid = extractNewsGid(guid) || extractNewsGid(url);
329
+ if (!gid || !title)
330
+ return null;
331
+ const pubDate = readXmlText(item.pubDate);
332
+ const dateValue = Date.parse(pubDate);
333
+ return {
334
+ gid,
335
+ title,
336
+ url,
337
+ author: 'Valve',
338
+ content: readXmlText(item.description),
339
+ date: Number.isFinite(dateValue) ? Math.floor(dateValue / 1000) : 0,
340
+ };
341
+ }
342
+ function readXmlText(value) {
343
+ if (value == null)
344
+ return '';
345
+ if (typeof value === 'string' || typeof value === 'number')
346
+ return String(value).trim();
347
+ return String(value['#text'] ?? '').trim();
348
+ }
349
+ function extractNewsGid(value) {
350
+ return value.match(/\/view\/(\d+)/)?.[1] || '';
351
+ }
352
+ function steamContentToMarkdown(input) {
322
353
  let output = decodeHtmlEntities(input || '')
323
- .replace(/\r\n?/g, '\n');
354
+ .replace(/\r\n?/g, '\n')
355
+ .replace(/\\([\[\]])/g, '$1');
324
356
  output = output
357
+ .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)')
358
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h1[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n# ${text.trim()}\n`)
359
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h2[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n## ${text.trim()}\n`)
360
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h3[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
361
+ .replace(/<div\b[^>]*class=["'][^"']*bb_h[45][^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
362
+ .replace(/<p\b[^>]*class=["'][^"']*bb_paragraph[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, (_, text) => `\n${text.trim()}\n`)
363
+ .replace(/<li\b[^>]*>\s*/gi, '\n- ')
364
+ .replace(/<\/li>/gi, '\n')
365
+ .replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n')
366
+ .replace(/<br\s*\/?>/gi, '\n')
367
+ .replace(/<img\b[^>]*>/gi, '')
368
+ .replace(/<strong\b[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**')
369
+ .replace(/<b\b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**')
370
+ .replace(/<em\b[^>]*>([\s\S]*?)<\/em>/gi, '*$1*')
371
+ .replace(/<i\b[^>]*>([\s\S]*?)<\/i>/gi, '*$1*')
372
+ .replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
373
+ .replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${code.trim()}\n\`\`\`\n`)
374
+ .replace(/<[^>]+>/g, '')
325
375
  .replace(/\[h1\]([\s\S]*?)\[\/h1\]/gi, (_, text) => `\n# ${text.trim()}\n`)
326
376
  .replace(/\[h2\]([\s\S]*?)\[\/h2\]/gi, (_, text) => `\n## ${text.trim()}\n`)
327
377
  .replace(/\[h3\]([\s\S]*?)\[\/h3\]/gi, (_, text) => `\n### ${text.trim()}\n`)
378
+ .replace(/\[h[45]\]([\s\S]*?)\[\/h[45]\]/gi, (_, text) => `\n### ${text.trim()}\n`)
328
379
  .replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '**$1**')
329
380
  .replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, '**$1**')
330
381
  .replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '*$1*')
@@ -342,8 +393,8 @@ function steamBbcodeToMarkdown(input) {
342
393
  .replace(/<br\s*\/?>/gi, '\n')
343
394
  .replace(/<\/p>\s*<p>/gi, '\n\n')
344
395
  .replace(/<\/?p>/gi, '\n');
345
- output = output.replace(updateSectionPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
346
- updateSectionPattern.lastIndex = 0;
396
+ output = output.replace(sectionHeadingPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
397
+ sectionHeadingPattern.lastIndex = 0;
347
398
  return output
348
399
  .replace(/\n{3,}/g, '\n\n')
349
400
  .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.0",
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": {