rsshub 1.0.0-master.f71451d → 1.0.0-master.f75997f

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 (50) hide show
  1. package/lib/config.ts +14 -0
  2. package/lib/errors/index.test.ts +2 -2
  3. package/lib/middleware/template.tsx +12 -3
  4. package/lib/routes/0x80/index.ts +87 -0
  5. package/lib/routes/0x80/namespace.ts +7 -0
  6. package/lib/routes/aljazeera/index.ts +17 -14
  7. package/lib/routes/apple/podcast.ts +64 -0
  8. package/lib/routes/bilibili/cache.ts +1 -1
  9. package/lib/routes/bing/daily-wallpaper.ts +9 -8
  10. package/lib/routes/byau/namespace.ts +6 -0
  11. package/lib/routes/byau/xinwen/index.ts +72 -0
  12. package/lib/routes/cpcaauto/index.ts +255 -0
  13. package/lib/routes/cpcaauto/namespace.ts +8 -0
  14. package/lib/routes/dehenglaw/index.ts +128 -0
  15. package/lib/routes/dehenglaw/namespace.ts +8 -0
  16. package/lib/routes/dehenglaw/templates/description.art +7 -0
  17. package/lib/routes/gov/stats/index.ts +25 -22
  18. package/lib/routes/gxmzu/ai.ts +1 -1
  19. package/lib/routes/gxmzu/lib.ts +9 -26
  20. package/lib/routes/gxmzu/utils/index.ts +31 -13
  21. package/lib/routes/gxmzu/yjs.ts +1 -1
  22. package/lib/routes/jou/utils/index.ts +35 -25
  23. package/lib/routes/lofter/tag.ts +3 -3
  24. package/lib/routes/lofter/user.ts +3 -3
  25. package/lib/routes/njxzc/utils/index.ts +31 -13
  26. package/lib/routes/qingting/podcast.ts +61 -39
  27. package/lib/routes/reuters/common.ts +2 -2
  28. package/lib/routes/sara/index.ts +66 -0
  29. package/lib/routes/sara/namespace.ts +6 -0
  30. package/lib/routes/tencent/news/author.ts +13 -11
  31. package/lib/routes/test/index.ts +11 -1
  32. package/lib/routes/twitter/api/mobile-api/login.ts +29 -28
  33. package/lib/routes/twitter/namespace.ts +2 -2
  34. package/lib/routes/twitter/user.ts +5 -0
  35. package/lib/routes/u3c3/index.ts +1 -1
  36. package/lib/routes/u3c3/namespace.ts +1 -1
  37. package/lib/routes/u9a9/index.ts +2 -2
  38. package/lib/routes/u9a9/namespace.ts +1 -1
  39. package/lib/routes/zsxq/group.ts +63 -0
  40. package/lib/routes/zsxq/namespace.ts +6 -0
  41. package/lib/routes/zsxq/types.ts +149 -0
  42. package/lib/routes/zsxq/user.ts +58 -0
  43. package/lib/routes/zsxq/utils.ts +70 -0
  44. package/lib/setup.test.ts +183 -12
  45. package/lib/utils/render.ts +1 -1
  46. package/lib/utils/request-rewriter/get.ts +8 -1
  47. package/lib/utils/wechat-mp.test.ts +411 -32
  48. package/lib/utils/wechat-mp.ts +447 -76
  49. package/lib/views/{rss3-ums.ts → rss3.ts} +2 -2
  50. package/package.json +14 -14
@@ -1,26 +1,22 @@
1
1
  import cache from '@/utils/cache';
2
- // 导入got库,该库用来请求网页数据
3
- import got from '@/utils/got';
4
- // 导入cheerio库,该库用来解析网页数据
2
+ import ofetch from '@/utils/ofetch'; // 使用ofetch库
5
3
  import { load } from 'cheerio';
6
- // 导入parseDate函数,该函数用于日期处理
7
4
  import { parseDate } from '@/utils/parse-date';
8
- // 导入timezone库,该库用于时区处理
9
5
  import timezone from '@/utils/timezone';
10
6
 
11
7
  async function getItems(ctx, url, host, tableClass, timeStyleClass1, titleStyleClass, timeStyleClass2) {
12
- // 发起Http请求,获取网页数据
13
- const response = await got({ url, https: { rejectUnauthorized: false } });
14
- // 解析网页数据
15
- const $ = load(response.data);
8
+ const response = await ofetch(url).catch(() => null);
9
+ if (!response) {
10
+ return [];
11
+ }
12
+ const $ = load(response);
16
13
 
17
- // 通知公告的items的标题、url链接、发布日期
18
14
  const list = $(`table.${tableClass} > tbody > tr[height=20]`)
19
15
  .toArray()
20
16
  .map((item) => {
21
- const currentItem = $(item); // 获取当前tr元素的jQuery对象
22
- const item1 = currentItem.find('td:eq(1)'); // 获取当前tr元素下的第二个td
23
- const item2 = currentItem.find('td:eq(2)'); // 获取当前tr元素下的第三个td
17
+ const currentItem = $(item);
18
+ const item1 = currentItem.find('td:eq(1)');
19
+ const item2 = currentItem.find('td:eq(2)');
24
20
  const link = new URL(item1.find('a').attr('href'), host).href;
25
21
 
26
22
  return {
@@ -32,22 +28,36 @@ async function getItems(ctx, url, host, tableClass, timeStyleClass1, titleStyleC
32
28
 
33
29
  const out = await Promise.all(
34
30
  list.map((item) =>
35
- // 使用缓存
36
31
  cache.tryGet(item.link, async () => {
37
- const response = await got({ url: item.link, https: { rejectUnauthorized: false } });
38
- if (response.redirectUrls.length) {
39
- item.link = response.redirectUrls[0];
32
+ const response = await ofetch(item.link).catch(() => null);
33
+ if (!response || (response.status >= 300 && response.status < 400)) {
34
+ // 响应为空或状态码表明发生了重定向
35
+ return {
36
+ ...item,
37
+ description: '该通知无法直接预览,请点击原文链接↑查看',
38
+ };
39
+ }
40
+ const $ = load(response);
41
+
42
+ item.title = $(`.${titleStyleClass}`).text();
43
+ const hasEmbeddedPDFScript = $('script:contains("showVsbpdfIframe")').length > 0;
44
+
45
+ if (hasEmbeddedPDFScript) {
40
46
  item.description = '该通知无法直接预览,请点击原文链接↑查看';
41
47
  } else {
42
- const $ = load(response.data);
43
- item.title = $(`.${titleStyleClass}`).text();
44
- item.description = $('.v_news_content')
45
- .html()
46
- .replaceAll('src="/', `src="${new URL('.', host).href}`)
47
- .replaceAll('href="/', `href="${new URL('.', host).href}`)
48
- .trim();
49
- item.pubDate = timezone(parseDate($(`.${timeStyleClass2}`).text().replace('发布时间:', '')), +8);
48
+ const contentHtml = $('.v_news_content').html();
49
+ const $content = load(contentHtml);
50
+ $content('a').each(function () {
51
+ const a = $(this);
52
+ const href = a.attr('href');
53
+ if (href && !href.startsWith('http')) {
54
+ a.attr('href', new URL(href, host).href);
55
+ }
56
+ });
57
+ item.description = $content.html();
50
58
  }
59
+ item.pubDate = timezone(parseDate($(`.${timeStyleClass2}`).text().replace('发布时间:', '')), +8);
60
+
51
61
  return item;
52
62
  })
53
63
  )
@@ -18,7 +18,7 @@ export const route: Route = {
18
18
  supportScihub: false,
19
19
  },
20
20
  name: 'Tag',
21
- maintainers: ['hoilc', 'nczitzk'],
21
+ maintainers: ['hoilc', 'nczitzk', 'LucunJi'],
22
22
  handler,
23
23
  description: `| new | date | week | month | total |
24
24
  | ---- | ---- | ---- | ----- | ----- |
@@ -38,7 +38,7 @@ async function handler(ctx) {
38
38
  const response = await got({
39
39
  method: 'post',
40
40
  url: apiUrl,
41
- form: {
41
+ body: new URLSearchParams({
42
42
  callCount: 1,
43
43
  scriptSessionId: '${scriptSessionId}187',
44
44
  httpSessionId: '',
@@ -55,7 +55,7 @@ async function handler(ctx) {
55
55
  'c0-param7': `number:${startingIndex}`,
56
56
  'c0-param8': 'number:0',
57
57
  batchId: 493053,
58
- },
58
+ }),
59
59
  });
60
60
 
61
61
  const dom = new JSDOM(
@@ -18,7 +18,7 @@ export const route: Route = {
18
18
  supportScihub: false,
19
19
  },
20
20
  name: 'User',
21
- maintainers: ['hondajojo', 'nczitzk'],
21
+ maintainers: ['hondajojo', 'nczitzk', 'LucunJi'],
22
22
  handler,
23
23
  };
24
24
 
@@ -34,7 +34,7 @@ async function handler(ctx) {
34
34
  const response = await got({
35
35
  method: 'post',
36
36
  url: `http://api.lofter.com/v2.0/blogHomePage.api?product=lofter-iphone-10.0.0`,
37
- form: {
37
+ body: new URLSearchParams({
38
38
  blogdomain: rootUrl,
39
39
  checkpwd: '1',
40
40
  following: '0',
@@ -44,7 +44,7 @@ async function handler(ctx) {
44
44
  offset: '0',
45
45
  postdigestnew: '1',
46
46
  supportposttypes: '1,2,3,4,5,6',
47
- },
47
+ }),
48
48
  });
49
49
 
50
50
  if (!response.data.response || response.data.response.posts.length === 0) {
@@ -1,12 +1,15 @@
1
1
  import cache from '@/utils/cache';
2
- import got from '@/utils/got';
3
2
  import { load } from 'cheerio';
4
3
  import { parseDate } from '@/utils/parse-date';
5
4
  import timezone from '@/utils/timezone';
5
+ import ofetch from '@/utils/ofetch'; // 使用默认导出的方式导入ofetch
6
6
 
7
7
  async function getNoticeList(ctx, url, host, titleSelector, dateSelector, contentSelector, listSelector) {
8
- const response = await got(url);
9
- const $ = load(response.data);
8
+ const response = await ofetch(url).catch(() => null);
9
+ if (!response) {
10
+ return [];
11
+ }
12
+ const $ = load(response);
10
13
 
11
14
  const list = $(listSelector)
12
15
  .toArray()
@@ -22,20 +25,35 @@ async function getNoticeList(ctx, url, host, titleSelector, dateSelector, conten
22
25
  const out = await Promise.all(
23
26
  list.map((item) =>
24
27
  cache.tryGet(item.link, async () => {
25
- const response = await got(item.link);
26
- if (response.redirectUrls.length) {
27
- item.link = response.redirectUrls[0];
28
+ const response = await ofetch(item.link).catch(() => null);
29
+ if (!response || (response.status >= 300 && response.status < 400)) {
30
+ return {
31
+ ...item,
32
+ description: '该通知无法直接预览,请点击原文链接↑查看',
33
+ };
34
+ }
35
+ const $ = load(response);
36
+
37
+ if ($('.wp_error_msg').length > 0) {
38
+ item.description = '您当前ip并非校内地址,该信息仅允许校内地址访问';
39
+ } else if ($('.wp_pdf_player').length > 0) {
28
40
  item.description = '该通知无法直接预览,请点击原文链接↑查看';
29
41
  } else {
30
- const $ = load(response.data);
42
+ const contentHtml = $(contentSelector.content).html();
43
+ const $content = load(contentHtml);
44
+ $content('a').each(function () {
45
+ const a = $(this);
46
+ const href = a.attr('href');
47
+ if (href && !href.startsWith('http')) {
48
+ a.attr('href', new URL(href, host).href);
49
+ }
50
+ });
51
+ item.description = $content.html();
31
52
  item.title = $(contentSelector.title).text();
32
- item.description = $(contentSelector.content)
33
- .html()
34
- .replaceAll('src="/', `src="${new URL('.', host).href}`)
35
- .replaceAll('href="/', `href="${new URL('.', host).href}`)
36
- .trim();
37
- item.pubDate = timezone(parseDate($(contentSelector.date).text().replace('编辑:', '').replace('发布日期:', '').replace('发布时间:', '')), +8);
53
+ const dateText = $(contentSelector.date).text().replace('编辑:', '').replace('发布日期:', '').replace('发布时间:', '');
54
+ item.pubDate = timezone(parseDate(dateText, 'YYYY-MM-DD'), +8);
38
55
  }
56
+
39
57
  return item;
40
58
  })
41
59
  )
@@ -1,9 +1,12 @@
1
- import { Route } from '@/types';
1
+ import type { DataItem, Route } from '@/types';
2
2
  import cache from '@/utils/cache';
3
3
  import crypto from 'crypto';
4
4
  import got from '@/utils/got';
5
5
  import timezone from '@/utils/timezone';
6
6
  import { parseDate } from '@/utils/parse-date';
7
+ import { config } from '@/config';
8
+
9
+ const qingtingId = config.qingting.id ?? '';
7
10
 
8
11
  export const route: Route = {
9
12
  path: '/podcast/:id',
@@ -11,12 +14,14 @@ export const route: Route = {
11
14
  example: '/qingting/podcast/293411',
12
15
  parameters: { id: '专辑id, 可在专辑页 URL 中找到' },
13
16
  features: {
14
- requireConfig: false,
15
- requirePuppeteer: false,
16
- antiCrawler: false,
17
- supportBT: false,
18
17
  supportPodcast: true,
19
- supportScihub: false,
18
+ requireConfig: [
19
+ {
20
+ name: 'QINGTING_ID',
21
+ optional: true,
22
+ description: '用户id, 部分专辑需要会员身份,用户id可以通过从网页端登录蜻蜓fm后使用开发者工具,在控制台中运行JSON.parse(localStorage.getItem("user")).qingting_id获取',
23
+ },
24
+ ],
20
25
  },
21
26
  radar: [
22
27
  {
@@ -29,21 +34,35 @@ export const route: Route = {
29
34
  description: `获取的播放 URL 有效期只有 1 天,需要开启播客 APP 的自动下载功能。`,
30
35
  };
31
36
 
37
+ function getMediaUrl(channelId: string, mediaId: string) {
38
+ const path = `/audiostream/redirect/${channelId}/${mediaId}?access_token=&device_id=MOBILESITE&qingting_id=${qingtingId}&t=${Date.now()}`;
39
+ const sign = crypto.createHmac('md5', 'fpMn12&38f_2e').update(path).digest('hex').toString();
40
+ return `https://audio.qingting.fm${path}&sign=${sign}`;
41
+ }
42
+
32
43
  async function handler(ctx) {
33
- const channelUrl = `https://i.qingting.fm/capi/v3/channel/${ctx.req.param('id')}`;
34
- let response = await got({
44
+ const channelId = ctx.req.param('id');
45
+
46
+ const channelUrl = `https://i.qingting.fm/capi/v3/channel/${channelId}`;
47
+ const response = await got({
35
48
  method: 'get',
36
49
  url: channelUrl,
37
50
  headers: {
38
51
  Referer: 'https://www.qingting.fm/',
39
52
  },
40
53
  });
54
+
41
55
  const title = response.data.data.title;
42
56
  const channel_img = response.data.data.thumbs['400_thumb'];
43
57
  const authors = response.data.data.podcasters.map((author) => author.nick_name).join(',');
44
58
  const desc = response.data.data.description;
45
- const programUrl = `https://i.qingting.fm/capi/channel/${ctx.req.param('id')}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`;
46
- response = await got({
59
+ const programUrl = `https://i.qingting.fm/capi/channel/${channelId}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`;
60
+
61
+ const {
62
+ data: {
63
+ data: { programs },
64
+ },
65
+ } = await got({
47
66
  method: 'get',
48
67
  url: programUrl,
49
68
  headers: {
@@ -51,45 +70,48 @@ async function handler(ctx) {
51
70
  },
52
71
  });
53
72
 
73
+ const {
74
+ data: { data: channelInfo },
75
+ } = await got(`https://i.qingting.fm/capi/v3/channel/${channelId}?user_id=${qingtingId}`);
76
+
77
+ const isCharged = channelInfo.purchase?.item_type !== 0;
78
+
79
+ const isPaid = channelInfo.user_relevance?.sale_status === 'paid';
80
+
54
81
  const resultItems = await Promise.all(
55
- response.data.data.programs.map((item) =>
56
- cache.tryGet(`qingting:podcast:${ctx.req.param('id')}:${item.id}`, async () => {
57
- const link = `https://www.qingting.fm/channels/${ctx.req.param('id')}/programs/${item.id}/`;
58
-
59
- const path = `/audiostream/redirect/${ctx.req.param('id')}/${item.id}?access_token=&device_id=MOBILESITE&qingting_id=&t=${Date.now()}`;
60
- const sign = crypto.createHmac('md5', 'fpMn12&38f_2e').update(path).digest('hex').toString();
61
-
62
- const [detailRes, mediaRes] = await Promise.all([
63
- got({
64
- method: 'get',
65
- url: link,
66
- headers: {
67
- Referer: 'https://www.qingting.fm/',
68
- },
69
- }),
70
- got({
71
- method: 'get',
72
- url: `https://audio.qingting.fm${path}&sign=${sign}`,
73
- headers: {
74
- Referer: 'https://www.qingting.fm/',
75
- },
76
- }),
77
- ]);
82
+ programs.map(async (item) => {
83
+ const data = (await cache.tryGet(`qingting:podcast:${channelId}:${item.id}`, async () => {
84
+ const link = `https://www.qingting.fm/channels/${channelId}/programs/${item.id}/`;
85
+
86
+ const detailRes = await got({
87
+ method: 'get',
88
+ url: link,
89
+ headers: {
90
+ Referer: 'https://www.qingting.fm/',
91
+ },
92
+ });
78
93
 
79
94
  const detail = JSON.parse(detailRes.data.match(/},"program":(.*?),"plist":/)[1]);
80
95
 
81
- return {
96
+ const rssItem = {
82
97
  title: item.title,
83
98
  link,
84
99
  itunes_item_image: item.cover,
85
100
  itunes_duration: item.duration,
86
101
  pubDate: timezone(parseDate(item.update_time), +8),
87
102
  description: detail.richtext,
88
- enclosure_url: mediaRes.url,
89
- enclosure_type: 'audio/x-m4a',
90
103
  };
91
- })
92
- )
104
+
105
+ return rssItem;
106
+ })) as DataItem;
107
+
108
+ if (!isCharged || isPaid || item.isfree) {
109
+ data.enclosure_url = getMediaUrl(channelId, item.id);
110
+ data.enclosure_type = 'audio/x-m4a';
111
+ }
112
+
113
+ return data;
114
+ })
93
115
  );
94
116
 
95
117
  return {
@@ -97,7 +119,7 @@ async function handler(ctx) {
97
119
  description: desc,
98
120
  itunes_author: authors,
99
121
  image: channel_img,
100
- link: `https://www.qingting.fm/channels/${ctx.req.param('id')}`,
122
+ link: `https://www.qingting.fm/channels/${channelId}`,
101
123
  item: resultItems,
102
124
  };
103
125
  }
@@ -64,13 +64,13 @@ export const route: Route = {
64
64
  };
65
65
 
66
66
  async function handler(ctx) {
67
- const MUST_FETCH_BY_TOPICS = new Set(['authors']);
67
+ const MUST_FETCH_BY_TOPICS = new Set(['authors', 'tags']);
68
68
  const CAN_USE_SOPHI = ['world'];
69
69
 
70
70
  const category = ctx.req.param('category');
71
71
  const topic = ctx.req.param('topic') ?? (category === 'authors' ? 'reuters' : '');
72
72
  const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20;
73
- const useSophi = ctx.req.query('sophi') === 'true' && 'topic' !== '' && CAN_USE_SOPHI.includes(category);
73
+ const useSophi = ctx.req.query('sophi') === 'true' && topic !== '' && CAN_USE_SOPHI.includes(category);
74
74
 
75
75
  const section_id = `/${category}/${topic ? `${topic}/` : ''}`;
76
76
  const { title, description, rootUrl, response } = await (async () => {
@@ -0,0 +1,66 @@
1
+ import { Route, DataItem } from '@/types';
2
+ import cache from '@/utils/cache';
3
+ import { load } from 'cheerio';
4
+ import { ofetch } from 'ofetch';
5
+
6
+ const typeMap = {
7
+ dynamic: '协会动态',
8
+ announcement: '通知公告',
9
+ industry: '行业动态',
10
+ };
11
+
12
+ export const route: Route = {
13
+ path: '/:type',
14
+ categories: ['government'],
15
+ example: '/sara/announcement',
16
+ parameters: { type: 'dynamic | announcement | industry' },
17
+ features: {
18
+ requireConfig: false,
19
+ requirePuppeteer: false,
20
+ antiCrawler: false,
21
+ supportBT: false,
22
+ supportPodcast: false,
23
+ supportScihub: false,
24
+ },
25
+ description: `| 协会动态 | 通知公告 |行业动态 |
26
+ | -------- | ------------ | -------- |
27
+ | dynamic | announcement | industry |`,
28
+
29
+ name: '新闻资讯',
30
+ maintainers: ['HChenZi'],
31
+ handler: async (ctx) => {
32
+ const baseUrl = 'http://www.sara.org.cn';
33
+ const type = ctx.req.param('type');
34
+
35
+ const url = `${baseUrl}/news/${type}.htm`;
36
+ const response = await ofetch(url);
37
+ const $ = load(response);
38
+ const list = $('.newsItem_total > dd')
39
+ .toArray()
40
+ .map((item) => {
41
+ const a = $(item).find('a').first();
42
+ return {
43
+ link: `${baseUrl}${a.attr('href')}`,
44
+ title: a.attr('title'),
45
+ };
46
+ });
47
+ const items = (await Promise.all(list.map(getFeedItem))) as DataItem[];
48
+ return {
49
+ title: typeMap[type],
50
+ link: url,
51
+ item: items,
52
+ };
53
+ },
54
+ };
55
+
56
+ async function getFeedItem(item) {
57
+ return await cache.tryGet(item.link, async () => {
58
+ const response = await ofetch(item.link);
59
+ const $ = load(response);
60
+ return {
61
+ description: $('.text').html(),
62
+ language: 'zh-cn',
63
+ ...item,
64
+ };
65
+ });
66
+ }
@@ -0,0 +1,6 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: '上海业余无线电协会',
5
+ url: 'www.sara.org.cn',
6
+ };
@@ -68,17 +68,19 @@ async function handler(ctx) {
68
68
  .text()
69
69
  .match(/window\.DATA = ({.+});/)[1]
70
70
  );
71
- const $data = load(data.originContent.text, null, false);
72
-
73
- $data('*')
74
- .contents()
75
- .filter((_, elem) => elem.type === 'comment')
76
- .replaceWith((_, elem) =>
77
- art(path.join(__dirname, '../templates/news/image.art'), {
78
- attribute: elem.data.trim(),
79
- originAttribute: data.originAttribute,
80
- })
81
- );
71
+ const $data = load(data.originContent?.text || '', null, false);
72
+ if ($data) {
73
+ // Not video page
74
+ $data('*')
75
+ .contents()
76
+ .filter((_, elem) => elem.type === 'comment')
77
+ .replaceWith((_, elem) =>
78
+ art(path.join(__dirname, '../templates/news/image.art'), {
79
+ attribute: elem.data.trim(),
80
+ originAttribute: data.originAttribute,
81
+ })
82
+ );
83
+ }
82
84
 
83
85
  return {
84
86
  title,
@@ -3,13 +3,14 @@ import { config } from '@/config';
3
3
  import got from '@/utils/got';
4
4
  import wait from '@/utils/wait';
5
5
  import cache from '@/utils/cache';
6
+ import { fetchArticle } from '@/utils/wechat-mp';
6
7
  import ConfigNotFoundError from '@/errors/types/config-not-found';
7
8
  import InvalidParameterError from '@/errors/types/invalid-parameter';
8
9
 
9
10
  let cacheIndex = 0;
10
11
 
11
12
  export const route: Route = {
12
- path: '/:id',
13
+ path: '/:id/:params?',
13
14
  name: 'Unknown',
14
15
  maintainers: ['DIYgod', 'NeverBehave'],
15
16
  handler,
@@ -384,6 +385,15 @@ async function handler(ctx) {
384
385
  ];
385
386
  }
386
387
 
388
+ if (ctx.req.param('id') === 'wechat-mp') {
389
+ const params = ctx.req.param('params');
390
+ if (!params) {
391
+ throw new InvalidParameterError('Invalid parameter');
392
+ }
393
+ const mpUrl = 'https:/mp.weixin.qq.com/s' + (params.includes('&') ? '?' : '/') + params;
394
+ item = [await fetchArticle(mpUrl)];
395
+ }
396
+
387
397
  return {
388
398
  title: `Test ${ctx.req.param('id')}`,
389
399
  itunes_author: ctx.req.param('id') === 'enclosure' ? 'DIYgod' : null,
@@ -6,12 +6,14 @@ import ofetch from '@/utils/ofetch';
6
6
  import crypto from 'crypto';
7
7
  import { config } from '@/config';
8
8
  import { v5 as uuidv5 } from 'uuid';
9
+ import { authenticator } from 'otplib';
9
10
  import logger from '@/utils/logger';
10
11
  import cache from '@/utils/cache';
11
12
 
12
13
  const NAMESPACE = 'd41d092b-b007-48f7-9129-e9538d2d8fe9';
13
14
  const username = config.twitter.username;
14
15
  const password = config.twitter.password;
16
+ const authenticationSecret = config.twitter.authenticationSecret;
15
17
 
16
18
  let authentication = null;
17
19
 
@@ -133,38 +135,37 @@ async function login() {
133
135
  });
134
136
  logger.debug('Twitter login 5 finished: AccountDuplicationCheck.');
135
137
 
136
- for (const subtask of task4.data?.subtasks || []) {
138
+ for await (const subtask of task4.data?.subtasks || []) {
137
139
  if (subtask.open_account) {
138
140
  authentication = subtask.open_account;
139
141
  break;
140
142
  } else if (subtask.subtask_id === 'LoginTwoFactorAuthChallenge') {
141
- // const token = authenticator.generate(authenticationSecret);
142
-
143
- // // eslint-disable-next-line no-await-in-loop
144
- // const task5 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', {
145
- // headers,
146
- // json: {
147
- // flow_token: task4.data.flow_token,
148
- // subtask_inputs: [
149
- // {
150
- // enter_text: {
151
- // suggestion_id: null,
152
- // text: token,
153
- // link: 'next_link',
154
- // },
155
- // subtask_id: 'LoginTwoFactorAuthChallenge',
156
- // },
157
- // ],
158
- // },
159
- // });
160
- // logger.debug('Twitter login 6 finished: LoginTwoFactorAuthChallenge.');
161
-
162
- // for (const subtask of task5.data?.subtasks || []) {
163
- // if (subtask.open_account) {
164
- // authentication = subtask.open_account;
165
- // break;
166
- // }
167
- // }
143
+ const token = authenticator.generate(authenticationSecret);
144
+
145
+ const task5 = await got.post('https://api.twitter.com/1.1/onboarding/task.json', {
146
+ headers,
147
+ json: {
148
+ flow_token: task4.data.flow_token,
149
+ subtask_inputs: [
150
+ {
151
+ enter_text: {
152
+ suggestion_id: null,
153
+ text: token,
154
+ link: 'next_link',
155
+ },
156
+ subtask_id: 'LoginTwoFactorAuthChallenge',
157
+ },
158
+ ],
159
+ },
160
+ });
161
+ logger.debug('Twitter login 6 finished: LoginTwoFactorAuthChallenge.');
162
+
163
+ for (const subtask of task5.data?.subtasks || []) {
164
+ if (subtask.open_account) {
165
+ authentication = subtask.open_account;
166
+ break;
167
+ }
168
+ }
168
169
  break;
169
170
  }
170
171
  }
@@ -40,8 +40,8 @@ generates
40
40
 
41
41
  Currently supports two authentication methods:
42
42
 
43
- - Using TWITTER_COOKIE (recommended): Configure the cookies of logged-in Twitter Web, at least including the fields auth_token and ct0. RSSHub will use this information to directly access Twitter's web API to obtain data.
43
+ - Using \`TWITTER_COOKIE\` (recommended): Configure the cookies of logged-in Twitter Web, at least including the fields auth_token and ct0. RSSHub will use this information to directly access Twitter's web API to obtain data.
44
44
 
45
- - Using TWITTER_USERNAME TWITTER_PASSWORD: Configure the Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism.
45
+ - Using \`TWITTER_USERNAME\` \`TWITTER_PASSWORD\` and \`TWITTER_AUTHENTICATION_SECRET\`: Configure the Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism.
46
46
  `,
47
47
  };
@@ -21,6 +21,11 @@ export const route: Route = {
21
21
  name: 'TWITTER_PASSWORD',
22
22
  description: 'Please see above for details.',
23
23
  },
24
+ {
25
+ name: 'TWITTER_AUTHENTICATION_SECRET',
26
+ description: 'TOTP 2FA secret, please see above for details.',
27
+ optional: true,
28
+ },
24
29
  {
25
30
  name: 'TWITTER_COOKIE',
26
31
  description: 'Please see above for details.',
@@ -6,7 +6,7 @@ import { load } from 'cheerio';
6
6
  export const route: Route = {
7
7
  path: ['/search/:keyword/:preview?', '/:type?/:preview?'],
8
8
  categories: ['multimedia'],
9
- example: '/u9a9/search/新片速递',
9
+ example: '/u3c3/search/新片速递',
10
10
  parameters: { keyword: 'Search keyword', preview: 'Show image preview, off by default, non empty value means on' },
11
11
  features: {
12
12
  requireConfig: false,
@@ -1,6 +1,6 @@
1
1
  import type { Namespace } from '@/types';
2
2
 
3
3
  export const namespace: Namespace = {
4
- name: 'U9A9',
4
+ name: 'U3C3',
5
5
  url: 'u3c3.com',
6
6
  };