rsshub 1.0.0-master.f6cb490 → 1.0.0-master.f6f0273

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 (88) hide show
  1. package/lib/api/index.ts +1 -6
  2. package/lib/routes/2048/index.ts +24 -23
  3. package/lib/routes/anthropic/news.ts +27 -13
  4. package/lib/routes/asianfanfics/namespace.ts +7 -0
  5. package/lib/routes/asianfanfics/tag.ts +89 -0
  6. package/lib/routes/asianfanfics/text-search.ts +68 -0
  7. package/lib/routes/blockworks/index.ts +128 -0
  8. package/lib/routes/blockworks/namespace.ts +7 -0
  9. package/lib/routes/cmu/andypavlo/blog.ts +55 -0
  10. package/lib/routes/cmu/namespace.ts +7 -0
  11. package/lib/routes/coindesk/{index.ts → consensus-magazine.ts} +17 -21
  12. package/lib/routes/coindesk/namespace.ts +2 -1
  13. package/lib/routes/coindesk/news.ts +47 -0
  14. package/lib/routes/coindesk/utils.ts +26 -0
  15. package/lib/routes/cointelegraph/index.ts +106 -0
  16. package/lib/routes/cointelegraph/namespace.ts +7 -0
  17. package/lib/routes/collabo-cafe/category.ts +37 -0
  18. package/lib/routes/collabo-cafe/index.ts +35 -0
  19. package/lib/routes/collabo-cafe/namespace.ts +9 -0
  20. package/lib/routes/collabo-cafe/parser.ts +29 -0
  21. package/lib/routes/collabo-cafe/tag.ts +37 -0
  22. package/lib/routes/cryptoslate/index.ts +98 -0
  23. package/lib/routes/cryptoslate/namespace.ts +7 -0
  24. package/lib/routes/decrypt/index.ts +115 -0
  25. package/lib/routes/decrypt/namespace.ts +7 -0
  26. package/lib/routes/discuz/discuz.ts +7 -9
  27. package/lib/routes/fangchan/list.ts +224 -0
  28. package/lib/routes/fangchan/namespace.ts +9 -0
  29. package/lib/routes/fangchan/templates/description.art +7 -0
  30. package/lib/routes/foreignaffairs/namespace.ts +7 -0
  31. package/lib/routes/foreignaffairs/rss.ts +55 -0
  32. package/lib/routes/forklog/index.ts +72 -0
  33. package/lib/routes/forklog/namespace.ts +7 -0
  34. package/lib/routes/gcores/categories.ts +129 -0
  35. package/lib/routes/gcores/collections.ts +129 -0
  36. package/lib/routes/gcores/topics.ts +63 -0
  37. package/lib/routes/gov/moa/gjs.ts +210 -0
  38. package/lib/routes/gov/tianjin/tjftz.ts +53 -0
  39. package/lib/routes/gov/tianjin/tjrcgzw.ts +51 -0
  40. package/lib/routes/grainoil/category.ts +207 -0
  41. package/lib/routes/grainoil/namespace.ts +9 -0
  42. package/lib/routes/huxiu/util.ts +11 -9
  43. package/lib/routes/ifanr/category.ts +7 -2
  44. package/lib/routes/ifanr/digest.ts +1 -1
  45. package/lib/routes/ifanr/index.ts +1 -1
  46. package/lib/routes/instructables/projects.ts +20 -15
  47. package/lib/routes/juejin/collections.ts +1 -1
  48. package/lib/routes/komiic/comic.ts +88 -0
  49. package/lib/routes/komiic/namespace.ts +7 -0
  50. package/lib/routes/leagueoflegends/namespace.ts +8 -0
  51. package/lib/routes/leagueoflegends/patch-notes.ts +76 -0
  52. package/lib/routes/likeshop/index.ts +43 -0
  53. package/lib/routes/likeshop/namespace.ts +7 -0
  54. package/lib/routes/ltaaa/article.ts +180 -0
  55. package/lib/routes/ltaaa/namespace.ts +9 -0
  56. package/lib/routes/ltaaa/templates/description.art +7 -0
  57. package/lib/routes/mashiro/index.ts +1 -0
  58. package/lib/routes/nhentai/util.ts +4 -1
  59. package/lib/routes/pinterest/user.ts +9 -0
  60. package/lib/routes/sohu/mp.ts +3 -2
  61. package/lib/routes/spotify/show.ts +1 -1
  62. package/lib/routes/stcn/index.ts +241 -136
  63. package/lib/routes/stcn/kx.ts +144 -0
  64. package/lib/routes/swjtu/namespace.ts +1 -1
  65. package/lib/routes/swjtu/{scai/bks.ts → scai.ts} +34 -20
  66. package/lib/routes/swjtu/sports.ts +77 -0
  67. package/lib/routes/theblock/index.ts +142 -0
  68. package/lib/routes/theblock/namespace.ts +7 -0
  69. package/lib/routes/theverge/index.ts +73 -62
  70. package/lib/routes/theverge/templates/header.art +19 -0
  71. package/lib/routes/threads/index.ts +73 -54
  72. package/lib/routes/threads/utils.ts +60 -78
  73. package/lib/routes/tmtpost/column.ts +298 -0
  74. package/lib/routes/tmtpost/new.ts +4 -199
  75. package/lib/routes/tmtpost/util.ts +207 -0
  76. package/lib/routes/toranoana/namespace.ts +7 -0
  77. package/lib/routes/toranoana/news.ts +110 -0
  78. package/lib/routes/wainao/templates/description.art +9 -0
  79. package/lib/routes/wainao/topics.ts +214 -0
  80. package/lib/routes/xiaoyuzhou/podcast.ts +27 -27
  81. package/lib/routes/xjtu/yz.ts +74 -0
  82. package/lib/routes/youmemark/index.ts +6 -6
  83. package/lib/routes/zaobao/util.ts +11 -3
  84. package/lib/routes/zhihu/answers.ts +26 -54
  85. package/package.json +36 -35
  86. package/lib/routes/gcores/category.ts +0 -171
  87. package/lib/routes/gcores/collection.ts +0 -161
  88. package/lib/routes-deprecated/ltaaa/index.js +0 -69
@@ -0,0 +1,77 @@
1
+ import { Route } from '@/types';
2
+ import cache from '@/utils/cache';
3
+ import { load } from 'cheerio';
4
+ import { parseDate } from '@/utils/parse-date';
5
+ import { ofetch } from 'ofetch';
6
+
7
+ const rootURL = 'https://sports.swjtu.edu.cn';
8
+ const pageURL = `${rootURL}/xwzx.htm`;
9
+
10
+ export const route: Route = {
11
+ path: '/sports',
12
+ categories: ['university'],
13
+ example: '/swjtu/sports',
14
+ parameters: {},
15
+ features: {
16
+ requireConfig: false,
17
+ requirePuppeteer: false,
18
+ antiCrawler: true,
19
+ supportBT: false,
20
+ supportPodcast: false,
21
+ supportScihub: false,
22
+ },
23
+ radar: [
24
+ {
25
+ source: ['sports.swjtu.edu.cn/'],
26
+ },
27
+ ],
28
+ name: '体育学院',
29
+ description: '新闻资讯',
30
+ maintainers: ['AzureG03'],
31
+ handler,
32
+ };
33
+
34
+ const getItem = (item, cache) => {
35
+ const title = item.find('p.toe').text();
36
+ const link = `${rootURL}/${item.find('a').attr('href')}`;
37
+
38
+ return cache.tryGet(link, async () => {
39
+ const res = await ofetch(link);
40
+ const $ = load(res);
41
+
42
+ const pubDate = parseDate(
43
+ $('div.info span:nth-of-type(3)')
44
+ .text()
45
+ .slice(3)
46
+ .match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)?.[0]
47
+ );
48
+ const description = $('div.detail-wrap').html();
49
+ return {
50
+ title,
51
+ pubDate,
52
+ link,
53
+ description,
54
+ };
55
+ });
56
+ };
57
+
58
+ async function handler() {
59
+ const res = await ofetch(pageURL);
60
+
61
+ const $ = load(res);
62
+ const $list = $('div.news-list > ul > li');
63
+
64
+ const items = await Promise.all(
65
+ $list.toArray().map((i) => {
66
+ const $item = $(i);
67
+ return getItem($item, cache);
68
+ })
69
+ );
70
+
71
+ return {
72
+ title: '西南交大体院-新闻资讯',
73
+ link: pageURL,
74
+ item: items,
75
+ allowEmpty: true,
76
+ };
77
+ }
@@ -0,0 +1,142 @@
1
+ import { Route, Data } from '@/types';
2
+ import cache from '@/utils/cache';
3
+ import ofetch from '@/utils/ofetch';
4
+ import { parseDate } from '@/utils/parse-date';
5
+ import { load } from 'cheerio';
6
+ import logger from '@/utils/logger';
7
+
8
+ export const route: Route = {
9
+ path: '/category/:category',
10
+ categories: ['finance'],
11
+ example: '/theblock/category/crypto-ecosystems',
12
+ parameters: { category: 'News category' },
13
+ features: {
14
+ requireConfig: false,
15
+ requirePuppeteer: false,
16
+ antiCrawler: false,
17
+ supportBT: false,
18
+ supportPodcast: false,
19
+ supportScihub: false,
20
+ },
21
+ name: 'Category',
22
+ maintainers: ['pseudoyu'],
23
+ handler,
24
+ radar: [
25
+ {
26
+ source: ['theblock.co/category/:category'],
27
+ target: '/category/:category',
28
+ },
29
+ ],
30
+ description: 'Get latest news from TheBlock by category. Note that due to website limitations, only article summaries may be available.',
31
+ };
32
+
33
+ async function handler(ctx): Promise<Data> {
34
+ const category = ctx.req.param('category');
35
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10;
36
+
37
+ const apiUrl = `https://www.theblock.co/api/category/${category}`;
38
+
39
+ try {
40
+ const response = await ofetch(apiUrl);
41
+
42
+ // Extract articles from the nested data structure
43
+ const articles = response.data?.articles || [];
44
+
45
+ if (!articles.length) {
46
+ throw new Error(`No articles found for category: ${category}`);
47
+ }
48
+
49
+ const items = await Promise.all(
50
+ articles.slice(0, limit).map((article) =>
51
+ cache.tryGet(`theblock:article:${article.url}`, async () => {
52
+ try {
53
+ // Try to get the full article
54
+ const articleResponse = await ofetch(`https://www.theblock.co/api/post/${article.id}/`);
55
+
56
+ const post = articleResponse.post;
57
+ const $ = load(post.body, null, false);
58
+
59
+ // If we successfully got the article content
60
+ if (post.body.length) {
61
+ // Remove unwanted elements
62
+ $('.copyright').remove();
63
+
64
+ let fullText = '';
65
+
66
+ if (article.thumbnail) {
67
+ fullText += `<p><img src="${post.thumbnail}" alt="${article.title}"></p>`;
68
+ }
69
+ fullText += post.intro + $.html();
70
+
71
+ if (fullText) {
72
+ return {
73
+ title: article.title,
74
+ link: article.url,
75
+ pubDate: parseDate(post.published),
76
+ description: fullText,
77
+ author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock',
78
+ category: [...new Set([post.categories.name, ...post.categories.map((cat) => cat.name), ...post.tags.map((tag) => tag.name)])],
79
+ guid: article.url,
80
+ image: article.thumbnail,
81
+ };
82
+ }
83
+ }
84
+
85
+ // If we couldn't extract specific content, fall back to a summary-based approach
86
+ logger.info(`Using summary-based approach for article: ${article.url}`);
87
+ return createSummaryItem(article);
88
+ } catch (error: any) {
89
+ // If we got a 403 error or any other error, use summary approach
90
+ logger.warn(`Couldn't fetch full content for ${article.url}: ${error.message}`);
91
+ return createSummaryItem(article);
92
+ }
93
+ })
94
+ )
95
+ );
96
+
97
+ return {
98
+ title: `TheBlock - ${category.charAt(0).toUpperCase() + category.slice(1).replaceAll('-', ' ')}`,
99
+ link: `https://www.theblock.co/category/${category}`,
100
+ item: items,
101
+ description: `Latest articles from TheBlock in the ${category} category`,
102
+ language: 'en',
103
+ } as Data;
104
+ } catch (error: any) {
105
+ logger.error(`Error in TheBlock handler: ${error.message}`);
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ // Helper function to create a summary-based item when full content isn't available
111
+ function createSummaryItem(article: any) {
112
+ let description = '';
113
+
114
+ // Add thumbnail if available
115
+ if (article.thumbnail) {
116
+ description += `<p><img src="${article.thumbnail}" alt="${article.title}"></p>`;
117
+ }
118
+
119
+ // Add subheading if available
120
+ if (article.subheading) {
121
+ description += `<p><strong>${article.subheading}</strong></p>`;
122
+ }
123
+
124
+ // Add preview if available
125
+ if (article.preview) {
126
+ description += `<p>${article.preview}</p>`;
127
+ }
128
+
129
+ // Add link to original article
130
+ description += `<p><a href="${article.url}">Read the full article at TheBlock</a></p>`;
131
+
132
+ return {
133
+ title: article.title,
134
+ link: article.url,
135
+ pubDate: parseDate(article.publishedFormatted, 'MMMM D, YYYY, h:mmA [EST]'),
136
+ description,
137
+ author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock',
138
+ category: article.primaryCategory?.name || [],
139
+ guid: article.url,
140
+ image: article.thumbnail,
141
+ };
142
+ }
@@ -0,0 +1,7 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: 'TheBlock',
5
+ url: 'theblock.co',
6
+ lang: 'en',
7
+ };
@@ -1,8 +1,13 @@
1
1
  import { Route } from '@/types';
2
2
  import cache from '@/utils/cache';
3
- import got from '@/utils/got';
3
+ import ofetch from '@/utils/ofetch';
4
4
  import parser from '@/utils/rss-parser';
5
5
  import { load } from 'cheerio';
6
+ import path from 'node:path';
7
+ import { art } from '@/utils/render';
8
+ import { getCurrentPath } from '@/utils/helpers';
9
+
10
+ const __dirname = getCurrentPath(import.meta.url);
6
11
 
7
12
  export const route: Route = {
8
13
  path: '/:hub?',
@@ -22,7 +27,7 @@ export const route: Route = {
22
27
  source: ['theverge.com/:hub', 'theverge.com/'],
23
28
  },
24
29
  ],
25
- name: 'The Verge',
30
+ name: 'Category',
26
31
  maintainers: ['HenryQW', 'vbali'],
27
32
  handler,
28
33
  description: `| Hub | Hub name |
@@ -43,6 +48,37 @@ export const route: Route = {
43
48
  Provides a better reading experience (full text articles) over the official one.`,
44
49
  };
45
50
 
51
+ const renderBlock = (b) => {
52
+ switch (b.__typename) {
53
+ case 'CoreEmbedBlockType':
54
+ return b.embedHtml;
55
+ case 'CoreGalleryBlockType':
56
+ return b.images.map((i) => `<figure><img src="${i.image.thumbnails.horizontal.url.split('?')[0]}" alt="${i.alt}" /><figcaption>${i.caption.html}</figcaption></figure>`).join('');
57
+ case 'CoreHeadingBlockType':
58
+ return `<h${b.level}>${b.contents.html}</h${b.level}>`;
59
+ case 'CoreHTMLBlockType':
60
+ return b.markup;
61
+ case 'CoreImageBlockType':
62
+ return `<figure><img src="${b.thumbnail.url.split('?')[0]}" alt="${b.alt}" /><figcaption>${b.caption.html}</figcaption></figure>`;
63
+ case 'CoreListBlockType':
64
+ return `${b.ordered ? '<ol>' : '<ul>'}${b.items.map((i) => `<li>${i.contents.html}</li>`).join('')}${b.ordered ? '</ol>' : '</ul>'}`;
65
+ case 'CoreParagraphBlockType':
66
+ return b.contents.html;
67
+ case 'CorePullquoteBlockType':
68
+ return `<blockquote>${b.contents.html}</blockquote>`;
69
+ case 'CoreQuoteBlockType':
70
+ return `<blockquote>${b.children.map((child) => renderBlock(child)).join('')}</blockquote>`;
71
+ case 'CoreSeparatorBlockType':
72
+ return '<hr>';
73
+ case 'HighlightBlockType':
74
+ return b.children.map((c) => renderBlock(c)).join('');
75
+ case 'MethodologyAccordionBlockType':
76
+ return `<h2>${b.heading.html}</h2>${b.sections.map((s) => `<h3>${s.heading.html}</h3>${s.content.html}`).join('')}`;
77
+ default:
78
+ throw new Error(`Unsupported block type: ${b.__typename}`);
79
+ }
80
+ };
81
+
46
82
  async function handler(ctx) {
47
83
  const link = ctx.req.param('hub') ? `https://www.theverge.com/${ctx.req.param('hub')}/rss/index.xml` : 'https://www.theverge.com/rss/index.xml';
48
84
 
@@ -51,73 +87,48 @@ async function handler(ctx) {
51
87
  const items = await Promise.all(
52
88
  feed.items.map((item) =>
53
89
  cache.tryGet(item.link, async () => {
54
- const response = await got(item.link);
55
-
56
- const $ = load(response.data);
57
-
58
- const content = $('#content');
59
- const body = $('.duet--article--article-body-component-container');
60
-
61
- // 处理封面图片
62
-
63
- const cover = $('meta[property="og:image"]');
64
-
65
- if (cover.length > 0) {
66
- $(`<img src=${cover[0].attribs.content}>`).insertBefore(body[0].childNodes[0]);
67
- }
90
+ const response = await ofetch(item.link);
68
91
 
69
- // 处理封面视频
70
- $('div.l-col__main > div.c-video-embed, div.c-entry-hero > div.c-video-embed').each((i, e) => {
71
- const src = `https://volume.vox-cdn.com/embed/${e.attribs['data-volume-uuid']}?autoplay=false`;
92
+ const $ = load(response);
72
93
 
73
- $(`<iframe src="${src}" style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;" allowfullscreen scrolling="no"></iframe>`).insertBefore(body[0].childNodes[0]);
74
- });
94
+ const nextData = JSON.parse($('script#__NEXT_DATA__').text());
95
+ const node = nextData.props.pageProps.hydration.responses.find((x) => x.operationName === 'PostLayoutQuery' || x.operationName === 'StreamLayoutQuery').data.node;
75
96
 
76
- // 处理封面视频
77
- $('div.l-col__main > div.c-video-embed--media iframe').each((i, e) => {
78
- $(e).insertBefore(body[0].childNodes[0]);
97
+ let description = art(path.join(__dirname, 'templates/header.art'), {
98
+ featuredImage: node.featuredImage,
99
+ ledeMediaData: node.ledeMediaData,
79
100
  });
80
101
 
81
- // 处理文章图片
82
- content.find('figure.e-image').each((i, e) => {
83
- let src, caption;
84
-
85
- // 处理 jpeg, png
86
- if ($(e).find('picture > source').length > 0) {
87
- src = $(e)
88
- .find('picture > img')[0]
89
- .attribs.srcset.match(/(?<=320w,).*?(?=520w)/g)[0]
90
- .trim();
91
- } else if ($(e).find('img.c-dynamic-image').length > 0) {
92
- // 处理 gif
93
- src = $(e).find('span.e-image__image')[0].attribs['data-original'];
94
- }
95
-
96
- // 处理 caption
97
- if ($(e).find('span.e-image__meta').length > 0) {
98
- caption = $(e).find('span.e-image__meta').text();
99
- }
100
-
101
- const figure = `<figure><img src=${src}>${caption ? `<br><figcaption>${caption}</figcaption>` : ''}</figure>`;
102
-
103
- $(figure).insertBefore(e);
104
-
105
- $(e).remove();
106
- });
107
-
108
- const lede = $('.duet--article--lede h2:first');
109
- if (lede[0]) {
110
- lede.insertBefore(body[0].childNodes[0]);
102
+ description += node.blocks
103
+ .filter((b) => b.__typename !== 'NewsletterBlockType' && b.__typename !== 'RelatedPostsBlockType' && b.__typename !== 'ProductBlockType' && b.__typename !== 'TableOfContentsBlockType')
104
+ .map((b) => renderBlock(b))
105
+ .join('<br><br>');
106
+
107
+ if (node.__typename === 'StreamResourceType') {
108
+ description += node.posts.edges
109
+ .map(({ node: n }) => {
110
+ let d =
111
+ `<h2><a href="${n.permalink}">${n.promo.headline || n.title}</a></h2>` +
112
+ art(path.join(__dirname, 'templates/header.art'), {
113
+ ledeMediaData: n.ledeMediaData,
114
+ });
115
+ switch (n.__typename) {
116
+ case 'PostResourceType':
117
+ d += n.excerpt.map((e) => e.contents.html).join('<br>');
118
+ break;
119
+ case 'QuickPostResourceType':
120
+ d += n.blocks.map((b) => renderBlock(b)).join('<br>');
121
+ break;
122
+ default:
123
+ break;
124
+ }
125
+ return d;
126
+ })
127
+ .join('<br>');
111
128
  }
112
129
 
113
- // 移除无用 DOM
114
- content.find('.duet--article--comments-join-the-conversation').remove();
115
- content.find('.duet--recirculation--related-list').remove();
116
- delete item.content;
117
- delete item.contentSnippet;
118
- delete item.isoDate;
119
-
120
- item.description = body.html();
130
+ item.description = description;
131
+ item.category = node.categories;
121
132
 
122
133
  return item;
123
134
  })
@@ -0,0 +1,19 @@
1
+ {{ if featuredImage }}
2
+ <figure>
3
+ <img src="{{ featuredImage.image.originalUrl.split('?')[0] }}" alt="{{ featuredImage.image.originalUrl.alt }}">
4
+ <figcaption>{{ featuredImage.image.title }}</figcaption>
5
+ </figure>
6
+ {{ /if }}
7
+
8
+ {{ if ledeMediaData }}
9
+ {{ if ledeMediaData.__typename === 'LedeMediaEmbedType'}}
10
+ {{@ ledeMediaData.embedHtml }}
11
+ {{ else if ledeMediaData.__typename === 'LedeMediaImageType' && !featuredImage }}
12
+ <figure>
13
+ <img src="{{ ledeMediaData.image.thumbnails.horizontal.url.split('?')[0] }}" alt="{{ ledeMediaData.image.title }}">
14
+ <figcaption>{{ ledeMediaData.image.credit.plaintext || ledeMediaData.image.title }}</figcaption>
15
+ </figure>
16
+ {{ else if ledeMediaData.__typename === 'LedeMediaVideoType'}}
17
+ <iframe src="https://volume.vox-cdn.com/embed/{{ ledeMediaData.video.volumeUuid }}" allowfullscreen></iframe>
18
+ {{ /if }}
19
+ {{ /if }}
@@ -1,10 +1,9 @@
1
1
  import { Route, ViewType } from '@/types';
2
- import ofetch from '@/utils/ofetch';
3
2
  import { parseDate } from '@/utils/parse-date';
4
- import { REPLIES_QUERY, THREADS_QUERY, apiUrl, threadUrl, profileUrl, extractTokens, makeHeader, getUserId, buildContent } from './utils';
5
- import { destr } from 'destr';
6
- import cache from '@/utils/cache';
7
- import { config } from '@/config';
3
+ import { threadUrl, profileUrl, extractTokens, getUserId, buildContent } from './utils';
4
+ import { JSDOM } from 'jsdom';
5
+ import { JSONPath } from 'jsonpath-plus';
6
+ import ofetch from '@/utils/ofetch';
8
7
 
9
8
  export const route: Route = {
10
9
  path: '/:user/:routeParams?',
@@ -25,13 +24,7 @@ Specify options (in the format of query string) in parameter \`routeParams\` to
25
24
  | \`showAuthorAvatarInDesc\` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`falseP\` |
26
25
  | \`showEmojiForQuotesAndReply\` | Use "🔁" instead of "QT", "↩️" instead of "Re" | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |
27
26
  | \`showQuotedInTitle\` | Show quoted tweet in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |
28
- | \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |
29
-
30
- Specify different option values than default values to improve readability. The URL
31
-
32
- \`\`\`
33
- https://rsshub.app/threads/zuck/showAuthorInTitle=1&showAuthorInDesc=1&showQuotedAuthorAvatarInDesc=1&showAuthorAvatarInDesc=1&showEmojiForQuotesAndReply=1&showQuotedInTitle=1
34
- \`\`\``,
27
+ | \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |`,
35
28
  },
36
29
  },
37
30
  name: 'User timeline',
@@ -42,10 +35,10 @@ https://rsshub.app/threads/zuck/showAuthorInTitle=1&showAuthorInDesc=1&showQuote
42
35
  async function handler(ctx) {
43
36
  const { user, routeParams } = ctx.req.param();
44
37
  const { lsd } = await extractTokens(user);
45
- const userId = await getUserId(user, lsd);
38
+ const userId = await getUserId(user);
46
39
 
47
40
  const params = new URLSearchParams(routeParams);
48
- const debugJson = {
41
+ const debugJson: any = {
49
42
  params: routeParams,
50
43
  lsd,
51
44
  };
@@ -60,48 +53,59 @@ async function handler(ctx) {
60
53
  replies: params.get('replies') ?? false,
61
54
  };
62
55
 
63
- const threadsResponse = await cache.tryGet(
64
- `threads:${userId}:${options.replies}`,
65
- () =>
66
- ofetch(apiUrl, {
67
- method: 'POST',
68
- headers: {
69
- ...makeHeader(user, lsd),
70
- 'content-type': 'application/x-www-form-urlencoded',
71
- },
72
- body: new URLSearchParams({
73
- lsd,
74
- variables: JSON.stringify({ userID: userId }),
75
- doc_id: String(options.replies ? REPLIES_QUERY : THREADS_QUERY),
76
- }).toString(),
77
- parseResponse: (txt) => destr(txt),
78
- }),
79
- config.cache.routeExpire,
80
- false
81
- );
56
+ const response = await ofetch(profileUrl(user), {
57
+ headers: {
58
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
59
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
60
+ 'Accept-Encoding': 'gzip, br',
61
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
62
+ 'Cache-Control': 'no-cache',
63
+ Pragma: 'no-cache',
64
+ 'Sec-Fetch-Dest': 'document',
65
+ 'Sec-Fetch-Mode': 'navigate',
66
+ 'Sec-Fetch-Site': 'none',
67
+ 'Sec-Fetch-User': '?1',
68
+ 'Upgrade-Insecure-Requests': '1',
69
+ },
70
+ });
71
+
72
+ const dom = new JSDOM(response);
73
+
74
+ let threadsData: ThreadItem[] | null = null;
75
+ for (const el of dom.window.document.querySelectorAll('script[data-sjs]')) {
76
+ try {
77
+ const data = JSONPath({
78
+ path: '$..thread_items[0]',
79
+ json: JSON.parse(el.textContent || ''),
80
+ });
81
+
82
+ if (data?.length > 0) {
83
+ threadsData = data as ThreadItem[];
84
+ break;
85
+ }
86
+ } catch {
87
+ // Skip invalid JSON
88
+ }
89
+ }
90
+
91
+ if (!threadsData) {
92
+ throw new Error('Failed to fetch thread data');
93
+ }
82
94
 
83
95
  debugJson.profileId = userId;
84
- debugJson.response = {
85
- response: threadsResponse,
86
- };
96
+ debugJson.response = { response: threadsData };
87
97
 
88
- const threads = threadsResponse?.data?.mediaData?.threads || [];
89
- const userData = threadsResponse?.data?.mediaData?.threads?.[0]?.thread_items?.[0]?.post?.user || {};
90
-
91
- const items = threads.flatMap((thread) =>
92
- thread.thread_items
93
- .filter((item) => user === item.post.user?.username)
94
- .map((item) => {
95
- const { title, description } = buildContent(item, options);
96
- return {
97
- author: user,
98
- title,
99
- description,
100
- pubDate: parseDate(item.post.taken_at, 'X'),
101
- link: threadUrl(item.post.code),
102
- };
103
- })
104
- );
98
+ const userData: ThreadUser = threadsData[0]?.post?.user || { username: user, profile_pic_url: '' };
99
+
100
+ const items = threadsData
101
+ .filter((item) => user === item.post.user?.username)
102
+ .map((item) => ({
103
+ author: user,
104
+ title: buildContent(item, options).title,
105
+ description: buildContent(item, options).description,
106
+ pubDate: parseDate(item.post.taken_at, 'X'),
107
+ link: threadUrl(item.post.code),
108
+ }));
105
109
 
106
110
  debugJson.items = items;
107
111
  ctx.set('json', debugJson);
@@ -110,7 +114,22 @@ async function handler(ctx) {
110
114
  title: `${user} (@${user}) on Threads`,
111
115
  link: profileUrl(user),
112
116
  image: userData?.profile_pic_url,
113
- // description: userData.biography,
114
117
  item: items,
115
118
  };
116
119
  }
120
+
121
+ interface ThreadUser {
122
+ username: string;
123
+ profile_pic_url: string;
124
+ }
125
+
126
+ interface ThreadItem {
127
+ post: {
128
+ user?: ThreadUser;
129
+ taken_at: number;
130
+ code: string;
131
+ caption?: {
132
+ text: string;
133
+ };
134
+ };
135
+ }