rsshub 1.0.0-master.f9b85f5 → 1.0.0-master.f9c381a

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 (166) hide show
  1. package/lib/config.ts +10 -0
  2. package/lib/middleware/cache.ts +4 -0
  3. package/lib/middleware/parameter.ts +1 -1
  4. package/lib/registry.ts +6 -2
  5. package/lib/routes/51cto/utils.ts +1 -1
  6. package/lib/routes/abc/index.ts +1 -1
  7. package/lib/routes/acgvinyl/namespace.ts +6 -0
  8. package/lib/routes/acgvinyl/news.ts +86 -0
  9. package/lib/routes/ally/rail.ts +1 -1
  10. package/lib/routes/anthropic/news.ts +13 -11
  11. package/lib/routes/apnews/mobile-api.ts +1 -1
  12. package/lib/routes/apnews/sitemap.ts +1 -1
  13. package/lib/routes/apple/apps.ts +2 -2
  14. package/lib/routes/apple/podcast.ts +58 -25
  15. package/lib/routes/bangumi.tv/group/reply.ts +1 -1
  16. package/lib/routes/bilibili/cache.ts +18 -2
  17. package/lib/routes/bilibili/dynamic.ts +31 -7
  18. package/lib/routes/bilibili/page.ts +1 -1
  19. package/lib/routes/bilibili/ranking.ts +23 -17
  20. package/lib/routes/bilibili/wasm-exec.ts +1 -1
  21. package/lib/routes/bilibili/weekly-recommend.ts +22 -6
  22. package/lib/routes/bjp/apod.ts +1 -1
  23. package/lib/routes/buaa/jiaowu.ts +1 -1
  24. package/lib/routes/bullionvault/gold-news.ts +3 -3
  25. package/lib/routes/cast/index.ts +1 -1
  26. package/lib/routes/coolapk/utils.ts +5 -4
  27. package/lib/routes/coolbuy/index.ts +106 -0
  28. package/lib/routes/{xinhuanet → coolbuy}/namespace.ts +3 -3
  29. package/lib/routes/coolbuy/templates/description.art +48 -0
  30. package/lib/routes/coolidge/film-guide.ts +60 -0
  31. package/lib/routes/coolidge/namespace.ts +7 -0
  32. package/lib/routes/coolidge/news.ts +65 -0
  33. package/lib/routes/coolidge/templates/description.art +4 -0
  34. package/lib/routes/copymanga/comic.ts +1 -1
  35. package/lib/routes/cpta/handler.ts +1 -1
  36. package/lib/routes/creative-comic/book.ts +1 -1
  37. package/lib/routes/daum/potplayer.ts +1 -1
  38. package/lib/routes/dockerhub/utils.ts +1 -1
  39. package/lib/routes/dora-world/article.ts +2 -2
  40. package/lib/routes/ehentai/ehapi.ts +3 -3
  41. package/lib/routes/eventbrite/events.ts +152 -0
  42. package/lib/routes/eventbrite/namespace.ts +7 -0
  43. package/lib/routes/github/activity.ts +1 -1
  44. package/lib/routes/google/jules.ts +63 -0
  45. package/lib/routes/gov/mem/namespace.ts +7 -0
  46. package/lib/routes/gov/mem/zfxxgkpt.ts +96 -0
  47. package/lib/routes/gov/mot/index.ts +158 -53
  48. package/lib/routes/hameln/chapter.ts +1 -1
  49. package/lib/routes/hpoi/banner-item.ts +28 -11
  50. package/lib/routes/hpoi/info.ts +1 -1
  51. package/lib/routes/huggingface/daily-papers.ts +1 -1
  52. package/lib/routes/hupu/index.ts +158 -74
  53. package/lib/routes/hupu/types.ts +163 -0
  54. package/lib/routes/hust/gs.ts +1 -1
  55. package/lib/routes/hust/mse.ts +1 -1
  56. package/lib/routes/huxiu/util.ts +2 -2
  57. package/lib/routes/infoq/presentations.ts +1 -1
  58. package/lib/routes/instagram/common-utils.ts +3 -3
  59. package/lib/routes/itch/devlog.ts +7 -3
  60. package/lib/routes/javbus/index.ts +1 -1
  61. package/lib/routes/javlibrary/utils.ts +1 -1
  62. package/lib/routes/jbma/namespace.ts +17 -0
  63. package/lib/routes/jbma/report.ts +473 -0
  64. package/lib/routes/jetbrains/comments.ts +1 -1
  65. package/lib/routes/jingzhengu/utils.ts +1 -1
  66. package/lib/routes/juejin/aicoding.ts +102 -0
  67. package/lib/routes/juejin/utils.ts +36 -51
  68. package/lib/routes/kakuyomu/works.ts +1 -1
  69. package/lib/routes/kemono/index.ts +2 -2
  70. package/lib/routes/komiic/comic.ts +1 -1
  71. package/lib/routes/koyso/index.ts +338 -0
  72. package/lib/routes/koyso/namespace.ts +9 -0
  73. package/lib/routes/koyso/templates/description.art +13 -0
  74. package/lib/routes/letterboxd/index.ts +65 -0
  75. package/lib/routes/letterboxd/namespace.ts +8 -0
  76. package/lib/routes/maccms/index.ts +1 -1
  77. package/lib/routes/mercari/util.ts +1 -1
  78. package/lib/routes/mingpao/index.ts +1 -1
  79. package/lib/routes/nankai/ai-notice.ts +142 -0
  80. package/lib/routes/nankai/graduate-notice.ts +162 -0
  81. package/lib/routes/natgeo/natgeo.ts +1 -0
  82. package/lib/routes/nhk/news-web-easy.ts +1 -1
  83. package/lib/routes/nicovideo/mylist.ts +39 -0
  84. package/lib/routes/nicovideo/types.ts +27 -0
  85. package/lib/routes/nicovideo/utils.ts +27 -1
  86. package/lib/routes/nikkei/cn/index.ts +1 -4
  87. package/lib/routes/now/news.ts +1 -1
  88. package/lib/routes/nytimes/index.ts +1 -1
  89. package/lib/routes/pixiv/novel-api/user-novels/sfw.ts +1 -1
  90. package/lib/routes/pixivision/utils.ts +1 -1
  91. package/lib/routes/pku/hr.ts +1 -1
  92. package/lib/routes/pku/scc/recruit.ts +1 -1
  93. package/lib/routes/producthunt/templates/description.art +2 -2
  94. package/lib/routes/producthunt/today.ts +17 -8
  95. package/lib/routes/ps/trophy.ts +1 -1
  96. package/lib/routes/pubscholar/utils.ts +14 -1
  97. package/lib/routes/qweather/3days.ts +14 -14
  98. package/lib/routes/qweather/now.ts +12 -10
  99. package/lib/routes/qweather/util.tsx +89 -0
  100. package/lib/routes/qwenlm/blog.ts +75 -0
  101. package/lib/routes/qwenlm/namespace.ts +6 -0
  102. package/lib/routes/radio-canada/latest.ts +30 -17
  103. package/lib/routes/ruc/ai.ts +1 -1
  104. package/lib/routes/ruc/hr.ts +1 -1
  105. package/lib/routes/samrdprc/index.ts +241 -0
  106. package/lib/routes/samrdprc/namespace.ts +1 -1
  107. package/lib/routes/sdo/ff14risingstones/api.ts +78 -0
  108. package/lib/routes/sdo/ff14risingstones/constant.ts +338 -0
  109. package/lib/routes/sdo/ff14risingstones/posts.ts +80 -0
  110. package/lib/routes/sdo/ff14risingstones/strats.ts +75 -0
  111. package/lib/routes/sdo/ff14risingstones/templates/duties-party.art +41 -0
  112. package/lib/routes/sdo/ff14risingstones/templates/fc-party.art +26 -0
  113. package/lib/routes/sdo/ff14risingstones/templates/novice-network-party.art +9 -0
  114. package/lib/routes/sdo/ff14risingstones/templates/rp-party.art +15 -0
  115. package/lib/routes/sdo/ff14risingstones/timeline.ts +31 -0
  116. package/lib/routes/sdo/ff14risingstones/types/dynamic.ts +50 -0
  117. package/lib/routes/sdo/ff14risingstones/types/index.ts +3 -0
  118. package/lib/routes/sdo/ff14risingstones/types/other.ts +57 -0
  119. package/lib/routes/sdo/ff14risingstones/types/party.ts +111 -0
  120. package/lib/routes/sdo/ff14risingstones/user-dynamics.ts +32 -0
  121. package/lib/routes/sdo/ff14risingstones/user-posts.ts +32 -0
  122. package/lib/routes/sdo/ff14risingstones/user-resently.ts +38 -0
  123. package/lib/routes/sdo/ff14risingstones/user-strats.ts +32 -0
  124. package/lib/routes/sdo/ff14risingstones/utils.ts +215 -0
  125. package/lib/routes/sdo/namespace.ts +7 -0
  126. package/lib/routes/showstart/utils.ts +1 -1
  127. package/lib/routes/sohu/mp.ts +1 -1
  128. package/lib/routes/sotwe/user.ts +1 -1
  129. package/lib/routes/surfshark/blog.ts +273 -77
  130. package/lib/routes/surfshark/templates/description.art +16 -4
  131. package/lib/routes/sustainabilitymag/articles.ts +1 -1
  132. package/lib/routes/syosetu/dev.ts +1 -1
  133. package/lib/routes/syosetu/ranking-isekai.ts +1 -1
  134. package/lib/routes/szse/disclosure/listed-notice.ts +44 -6
  135. package/lib/routes/telegram/channel-media.ts +249 -0
  136. package/lib/routes/telegram/channel.ts +5 -4
  137. package/lib/routes/telegram/stories.ts +130 -0
  138. package/lib/routes/telegram/tglib/channel.ts +136 -118
  139. package/lib/routes/telegram/tglib/client.ts +37 -139
  140. package/lib/routes/tesla/cx.ts +1 -1
  141. package/lib/routes/theverge/index.ts +20 -6
  142. package/lib/routes/threads/utils.ts +7 -3
  143. package/lib/routes/tidb/blog.ts +1 -1
  144. package/lib/routes/toutiao/user.ts +2 -2
  145. package/lib/routes/twitter/api/mobile-api/api.ts +1 -1
  146. package/lib/routes/txrjy/fornumtopic.ts +2 -2
  147. package/lib/routes/typst/universe.ts +1 -1
  148. package/lib/routes/uber/blog.ts +87 -46
  149. package/lib/routes/weibo/utils.ts +17 -9
  150. package/lib/routes/xiaohongshu/user.ts +1 -1
  151. package/lib/routes/xjtu/ee-jzxx.ts +1 -1
  152. package/lib/routes/yahoo/news/utils.ts +1 -1
  153. package/lib/routes/ymgal/article.ts +1 -1
  154. package/lib/routes/yoasobi-music/media.ts +1 -1
  155. package/lib/routes/youtube/api/youtubei.ts +1 -1
  156. package/lib/routes/youtube/community.ts +4 -4
  157. package/lib/routes/zaker/utils.ts +1 -1
  158. package/lib/routes/zaobao/util.tsx +1 -1
  159. package/lib/server.ts +1 -1
  160. package/lib/types.ts +1 -1
  161. package/lib/utils/puppeteer-utils.test.ts +2 -2
  162. package/lib/views/index.tsx +4 -4
  163. package/package.json +40 -40
  164. package/lib/routes/qweather/templates/3days.art +0 -22
  165. package/lib/routes/qweather/templates/now.art +0 -16
  166. package/lib/routes/xinhuanet/app.ts +0 -109
@@ -1,160 +1,180 @@
1
- import InvalidParameterError from '@/errors/types/invalid-parameter';
2
- import { client, decodeMedia, getClient, getFilename, getMediaLink, streamDocument, streamThumbnail } from './client';
3
- import { returnBigInt } from 'telegram/Helpers.js';
4
- import { HTMLParser } from 'telegram/extensions/html.js';
1
+ /* eslint-disable no-await-in-loop */
5
2
  import { DataItem } from '@/types';
6
- import type { Api } from 'telegram';
3
+ import { Context } from 'hono';
4
+ import { Api } from 'telegram';
5
+ import { HTMLParser } from 'telegram/extensions/html.js';
6
+ import { getClient, getDocument, getFilename, unwrapMedia } from './client';
7
+ import { getDisplayName } from 'telegram/Utils.js';
8
+
9
+ export function getGeoLink(geo: Api.GeoPoint) {
10
+ return `<a href="https://www.google.com/maps/search/?api=1&query=${geo.lat}%2C${geo.long}" target="_blank">Geo LatLon: ${geo.lat}, ${geo.long}</a>`;
11
+ }
12
+
13
+ export async function getPollResults(client, message, m: Api.MessageMediaPoll) {
14
+ const resultsUpdateResponse = await client.invoke(new Api.messages.GetPollResults({ peer: message.peerId, msgId: message.id }));
15
+ let results: Api.PollResults;
16
+ if (resultsUpdateResponse?.updates[0] instanceof Api.UpdateMessagePoll) {
17
+ results = resultsUpdateResponse.updates[0].results as Api.PollResults;
18
+ }
19
+ const txt = `<h4>${m.poll.quiz ? 'Quiz' : 'Poll'}: ${m.poll.question.text}</h4>
20
+ <div><ul>${m.poll.answers
21
+ .map((a) => {
22
+ let answerTxt = a.text.text;
23
+ const result = results.results?.find((r) => r.option.buffer === a.option.buffer);
24
+ if (result && results.totalVoters) {
25
+ answerTxt = `<strong>${Math.round((result.voters / results.totalVoters) * 100)}%</strong>: ${answerTxt}`;
26
+ }
27
+ return `<li>${answerTxt}</li>`;
28
+ })
29
+ .join('')}</ul></div>
30
+ `;
31
+ return txt;
32
+ }
33
+
34
+ export function getMediaLink(src: string, m: Api.TypeMessageMedia) {
35
+ const doc = getDocument(m);
36
+ const mime = doc ? doc.mimeType : '';
7
37
 
8
- function parseRange(range, length) {
9
- if (!range) {
10
- return [];
38
+ if (m instanceof Api.MessageMediaPhoto || mime.startsWith('image/')) {
39
+ return `<img src="${src}" alt=""/>`;
11
40
  }
12
- const [typ, segstr] = range.split('=');
13
- if (typ !== 'bytes') {
14
- throw new InvalidParameterError(`unsupported range: ${typ}`);
41
+ if (doc && mime.startsWith('video/')) {
42
+ const vid = (doc.attributes.find((t) => t instanceof Api.DocumentAttributeVideo) ?? { w: 1080, h: 720 }) as { w: number; h: number };
43
+ return `<video controls preload="metadata" poster="${src}?thumb" width="${vid.w / 2}" height="${vid.h / 2}"><source src="${src}" type="${mime}"></video>`;
15
44
  }
16
- const segs = segstr.split(',').map((s) => s.trim());
17
- const parsedSegs = [];
18
- for (const seg of segs) {
19
- const range = seg
20
- .split('-', 2)
21
- .filter((v) => !!v)
22
- .map((elem) => returnBigInt(elem));
23
- if (range.length < 2) {
24
- if (seg.startsWith('-')) {
25
- range.unshift(0);
26
- } else {
27
- range.push(length);
28
- }
29
- }
30
- parsedSegs.push(range);
45
+ if (doc && mime.startsWith('audio/')) {
46
+ return `<div>${getAudioTitle(m)}</div><div><audio src="${src}"></audio></div>`;
31
47
  }
32
- return parsedSegs;
33
- }
34
48
 
35
- async function getMedia(ctx) {
36
- const media = await decodeMedia(ctx.req.param('username'), ctx.req.param('media'));
37
- if (!media) {
38
- ctx.status = 500;
39
- return ctx.res.end();
49
+ if (doc && mime.startsWith('application/')) {
50
+ let linkText = `${getFilename(m)} (${humanFileSize(doc.size.valueOf())})`;
51
+ if (mime.endsWith('x-tgsticker')) {
52
+ linkText = ''; // remove filename, it's only an animated sticker
53
+ }
54
+ if ((doc.thumbs?.length ?? 0) > 0) {
55
+ linkText = `<div><img src="${src}?thumb" alt=""/></div><div>${linkText}</div>`;
56
+ }
57
+ return `<a href="${src}" target="_blank">${linkText}</a>`;
58
+ }
59
+ if ((m instanceof Api.MessageMediaGeo || m instanceof Api.MessageMediaGeoLive) && m.geo instanceof Api.GeoPoint) {
60
+ return getGeoLink(m.geo);
40
61
  }
41
- if (ctx.res.closed) {
42
- // console.log(`prematurely closed ${ctx.req.param('media')}`);
43
- return;
62
+ if (m instanceof Api.MessageMediaWebPage) {
63
+ return ''; // a link without a document attach, usually is in the message text, so we can skip here
64
+ }
65
+ if (m instanceof Api.MessageMediaContact) {
66
+ return `Contact: <a href="tel:${m.phoneNumber}" target="_blank">${m.firstName} ${m.lastName} ${m.phoneNumber}</a>`;
67
+ // TODO: download vCard as media ?
68
+ }
69
+ if (m instanceof Api.MessageMediaInvoice) {
70
+ let description = m.description;
71
+ if (m.photo?.url) {
72
+ description = `<img src="${m.photo?.url}" /><br />${description}`;
73
+ }
74
+ return `<h4>${m.test ? 'TEST ' : ''}Invoice: ${m.title}</h4><div>${description}</div>`;
44
75
  }
45
76
 
46
- if (media.document) {
47
- ctx.status = 200;
48
- let stream;
49
- if ('thumb' in ctx.req.query()) {
50
- try {
51
- stream = streamThumbnail(media);
52
- ctx.set('Content-Type', 'image/jpeg');
53
- } catch {
54
- ctx.status = 404;
55
- return ctx.res.end();
56
- }
57
- } else {
58
- ctx.set('Content-Type', media.document.mimeType);
59
-
60
- ctx.set('Accept-Ranges', 'bytes');
61
- const range = parseRange(ctx.get('Range'), media.document.size - 1);
62
- if (range.length > 1) {
63
- ctx.status = 416; // range not satisfiable
64
- return ctx.res.end();
65
- }
66
- if (range.length === 1) {
67
- // console.log(`${ctx.method} ${ctx.req.url} Range: ${ctx.get('Range')}`);
68
- ctx.status = 206; // partial content
69
- const [offset, limit] = range[0];
70
- ctx.set('Content-Length', limit - offset + 1);
71
- ctx.set('Content-Range', `bytes ${offset}-${limit}/${media.document.size}`);
72
-
73
- const stream = streamDocument(media.document, '', offset, limit);
74
- for await (const chunk of stream) {
75
- ctx.res.write(chunk);
76
- if (ctx.res.closed) {
77
- break;
78
- }
79
- }
80
- return ctx.res.end();
81
- }
77
+ return m.className;
78
+ }
82
79
 
83
- ctx.set('Content-Length', media.document.size);
84
- if (media.document.mimeType.startsWith('application/')) {
85
- ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(getFilename(media))}"`);
86
- }
87
- stream = streamDocument(media.document);
88
- }
89
- // const addr = JSON.stringify(ctx.res.socket.address());
90
- // console.log(`streaming ${ctx.req.param('media')} to ${addr}`);
80
+ function humanFileSize(size: number) {
81
+ const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
82
+ return (size / Math.pow(1024, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
83
+ }
91
84
 
92
- for await (const chunk of stream) {
93
- if (ctx.res.closed) {
94
- // console.log(`closed ${addr}`);
95
- break;
96
- }
97
- // console.log(`writing ${chunk.length / 1024} to ${addr}`);
98
- ctx.res.write(chunk);
99
- }
100
- if ('close' in stream) {
101
- stream.close();
85
+ export function getAudioTitle(x: Api.TypeMessageMedia) {
86
+ if (x instanceof Api.MessageMediaDocument && x.document instanceof Api.Document) {
87
+ const attr = x.document.attributes.find((x) => x instanceof Api.DocumentAttributeAudio);
88
+ if (attr) {
89
+ return `${attr.performer} - ${attr.title} (${humanDuration(attr.duration)})`;
102
90
  }
103
- } else if (media.photo) {
104
- ctx.status = 200;
105
- ctx.set('Content-Type', 'image/jpeg');
106
- const buf = await client.downloadMedia(media);
107
- ctx.res.write(buf);
108
- } else {
109
- ctx.status = 415;
110
- ctx.write(media.className);
111
91
  }
112
- return ctx.res.end();
92
+ return getFilename(x);
113
93
  }
114
94
 
115
- export default async function handler(ctx) {
116
- const { username } = ctx.req.param();
117
- const client = await getClient();
95
+ export function humanDuration(seconds: number) {
96
+ const hours = Math.floor(seconds / 3600);
97
+ const minutes = Math.floor((seconds % 3600) / 60);
98
+ const remainingSeconds = seconds % 60;
118
99
 
119
- const item: DataItem[] = [];
120
- const chat = (await client.getInputEntity(username)) as Api.InputPeerChannel;
121
- const channelInfo = await client.getEntity(chat);
100
+ // Format time components with leading zeros if necessary
101
+ const paddedMinutes = String(minutes).padStart(2, '0');
102
+ const paddedSeconds = String(remainingSeconds).padStart(2, '0');
122
103
 
123
- if (channelInfo.className !== 'Channel') {
124
- throw new Error(`${username} is not a channel`);
104
+ // Construct the time string conditionally
105
+ if (hours > 0) {
106
+ return `${hours}:${paddedMinutes}:${paddedSeconds}`; // Show hours, minutes, and seconds
107
+ } else if (minutes > 0) {
108
+ return `${minutes}:${paddedSeconds}`; // Show minutes and seconds
109
+ } else {
110
+ return `0:${paddedSeconds}`; // Show only seconds
125
111
  }
112
+ }
113
+
114
+ export default async function handler(ctx: Context) {
115
+ const client = await getClient();
116
+ const username = ctx.req.param('username');
117
+ const peer = await client.getInputEntity(username);
118
+ const entity = await client.getEntity(peer);
126
119
 
127
120
  let attachments: string[] = [];
128
- const messages = await client.getMessages(chat, { limit: 50 });
121
+ const messages = await client.getMessages(peer, { limit: 50 });
129
122
 
123
+ let i = 0;
124
+ const item: DataItem[] = [];
130
125
  for (const message of messages) {
131
- if (message.media) {
126
+ let text = message.text; // must not be HTML
127
+
128
+ if (message.fwdFrom?.fromId) {
129
+ const fwdFrom = await client.getEntity(message.fwdFrom.fromId);
130
+ text = `Forwarded From: ${getDisplayName(fwdFrom)}: ${text}`;
131
+ }
132
+ const media = await unwrapMedia(message.media, message.peerId);
133
+ if (message.media instanceof Api.MessageMediaStory && media) {
134
+ // if successfully loaded the story
135
+ const storyFrom = await client.getEntity(message.media.peer);
136
+ text = `Story From: ${getDisplayName(storyFrom)}: ${text}`;
137
+ }
138
+ if (media) {
139
+ if (media instanceof Api.MessageMediaPoll) {
140
+ attachments.push(await getPollResults(client, message, media));
141
+ continue;
142
+ }
132
143
  // messages that have no text are shown as if they're one post
133
144
  // because in TG only 1 attachment per message is possible
134
- attachments.push(getMediaLink(ctx, chat, username, message));
145
+ const src = `${new URL(ctx.req.url).origin}/telegram/media/${username}/${message.id}`;
146
+ attachments.push(getMediaLink(src, media));
135
147
  }
136
- if (message.text !== '') {
137
- let description = attachments.join('\n');
148
+ if (message.replyMarkup instanceof Api.ReplyInlineMarkup) {
149
+ for (const buttonRow of message.replyMarkup.rows) {
150
+ for (const button of buttonRow.buttons) {
151
+ if (button instanceof Api.KeyboardButtonUrl) {
152
+ attachments.push(`<div><a href="${button.url}" target="_blank">${button.text}</a></div>`);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ if (text !== '' || ++i === messages.length - 1) {
158
+ let description = attachments.join('<br/>\n');
138
159
  attachments = []; // emitting these, buffer other ones
139
160
 
140
- if (message.text) {
161
+ if (text) {
141
162
  description += `<p>${HTMLParser.unparse(message.message, message.entities).replaceAll('\n', '<br/>')}</p>`;
142
163
  }
143
164
 
144
165
  const title = message.text ? message.text.slice(0, 80) + (message.text.length > 80 ? '...' : '') : new Date(message.date * 1000).toUTCString();
145
-
146
166
  item.push({
147
167
  title,
148
168
  description,
149
169
  pubDate: new Date(message.date * 1000).toUTCString(),
150
170
  link: `https://t.me/s/${username}/${message.id}`,
151
- author: `${channelInfo.title} (@${username})`,
171
+ author: getDisplayName(message.sender ?? entity),
152
172
  });
153
173
  }
154
174
  }
155
175
 
156
176
  return {
157
- title: channelInfo.title,
177
+ title: getDisplayName(entity),
158
178
  language: null,
159
179
  link: `https://t.me/${username}`,
160
180
  item,
@@ -162,5 +182,3 @@ export default async function handler(ctx) {
162
182
  description: `@${username} on Telegram`,
163
183
  };
164
184
  }
165
-
166
- export { getMedia };
@@ -1,7 +1,6 @@
1
1
  import { Api, TelegramClient } from 'telegram';
2
2
  import { UserAuthParams } from 'telegram/client/auth';
3
3
  import { StringSession } from 'telegram/sessions/index.js';
4
- import { getAppropriatedPartSize } from 'telegram/Utils.js';
5
4
 
6
5
  import { config } from '@/config';
7
6
  import ConfigNotFoundError from '@/errors/types/config-not-found';
@@ -34,160 +33,59 @@ export async function getClient(authParams?: UserAuthParams, session?: string) {
34
33
  : undefined,
35
34
  });
36
35
 
37
- await client.connect();
36
+ await client.start(
37
+ Object.assign(authParams ?? {}, {
38
+ onError: (err: Error) => {
39
+ throw new Error('Cannot start TG: ' + err);
40
+ },
41
+ }) as any
42
+ );
38
43
  return client;
39
44
  }
40
45
 
41
- function humanFileSize(size) {
42
- const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
43
- return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
44
- }
45
-
46
- /**
47
- * https://core.telegram.org/api/files#stripped-thumbnails
48
- * @param bytes Buffer
49
- * @returns Buffer jpeg
50
- */
51
- function ExpandInlineBytes(bytes) {
52
- if (bytes.length < 3 || bytes[0] !== 0x1) {
53
- return [];
54
- }
55
- const header = Buffer.from([
56
- 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x28, 0x1C, 0x1E, 0x23, 0x1E, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2D, 0x2B,
57
- 0x28, 0x30, 0x3C, 0x64, 0x41, 0x3C, 0x37, 0x37, 0x3C, 0x7B, 0x58, 0x5D, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8F, 0x80, 0x8C, 0x8A, 0xA0, 0xB4, 0xE6, 0xC3, 0xA0, 0xAA, 0xDA, 0xAD, 0x8A, 0x8C, 0xC8, 0xFF, 0xCB, 0xDA, 0xEE,
58
- 0xF5, 0xFF, 0xFF, 0xFF, 0x9B, 0xC1, 0xFF, 0xFF, 0xFF, 0xFA, 0xFF, 0xE6, 0xFD, 0xFF, 0xF8, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x2B, 0x2D, 0x2D, 0x3C, 0x35, 0x3C, 0x76, 0x41, 0x41, 0x76, 0xF8, 0xA5, 0x8C, 0xA5, 0xF8, 0xF8, 0xF8,
59
- 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8,
60
- 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05,
61
- 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04,
62
- 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1,
63
- 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
64
- 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
65
- 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
66
- 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
67
- 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,
68
- 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62,
69
- 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57,
70
- 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A,
71
- 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2,
72
- 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00,
73
- ]);
74
- const footer = Buffer.from([0xFF, 0xD9]);
75
- const real = Buffer.alloc(header.length + bytes.length + footer.length);
76
- header.copy(real);
77
- bytes.copy(real, header.length, 3);
78
- bytes.copy(real, 164, 1, 2);
79
- bytes.copy(real, 166, 2, 3);
80
- footer.copy(real, header.length + bytes.length, 0);
81
- return real;
82
- }
83
-
84
- function getMediaLink(ctx, channel: Api.InputPeerChannel, channelName: string, message: Api.Message) {
85
- const base = `${ctx.protocol}://${ctx.host}/telegram/channel/${channelName}`;
86
- const src = base + `${channel.channelId}_${message.id}`;
87
-
88
- const x = message.media;
89
- if (x instanceof Api.MessageMediaPhoto || (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('image/'))) {
90
- return `<img src="${src}" alt=""/>`;
91
- }
92
- if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('video/')) {
93
- const vid = x.document.attributes.find((t) => t.className === 'DocumentAttributeVideo') ?? { w: 1080, h: 720 };
94
- return `<video controls preload="metadata" poster="${src}?thumb" width="${vid.w / 2}" height="${vid.h / 2}"><source src="${src}" type="${x.document.mimeType}"></video>`;
95
- }
96
- if (x instanceof Api.MessageMediaDocument && x.document.mimeType.startsWith('audio/')) {
97
- return `<audio src="${src}"></audio>`;
98
- }
99
-
100
- let linkText = getFilename(x);
101
- if (x instanceof Api.MessageMediaDocument) {
102
- linkText += ` (${humanFileSize(x.document.size)})`;
103
- return `<a href="${src}" target="_blank"><img src="${src}?thumb" alt=""/><br/>${linkText}</a>`;
104
- }
105
- return '';
106
- }
107
- function getFilename(x) {
46
+ export function getFilename(x: Api.TypeMessageMedia) {
108
47
  if (x instanceof Api.MessageMediaDocument) {
109
- const docFilename = x.document.attributes.find((a) => a.className === 'DocumentAttributeFilename');
110
- if (docFilename) {
111
- return docFilename.fileName;
48
+ for (const a of (x.document as Api.Document).attributes) {
49
+ if (a instanceof Api.DocumentAttributeFilename) {
50
+ return a.fileName;
51
+ }
112
52
  }
113
53
  }
114
54
  return x.className;
115
55
  }
116
56
 
117
- function sortThumb(thumb) {
118
- if (thumb instanceof Api.PhotoStrippedSize) {
119
- return thumb.bytes.length;
57
+ export function getDocument(m: Api.TypeMessageMedia) {
58
+ if (m instanceof Api.MessageMediaDocument && m.document && !(m.document instanceof Api.DocumentEmpty)) {
59
+ return m.document;
120
60
  }
121
- if (thumb instanceof Api.PhotoCachedSize) {
122
- return thumb.bytes.length;
61
+ if (m instanceof Api.MessageMediaWebPage && m.webpage instanceof Api.WebPage && m.webpage.document instanceof Api.Document) {
62
+ return m.webpage.document;
123
63
  }
124
- if (thumb instanceof Api.PhotoSize) {
125
- return thumb.size;
126
- }
127
- if (thumb instanceof Api.PhotoSizeProgressive) {
128
- return Math.max(...thumb.sizes);
129
- }
130
- if (thumb instanceof Api.VideoSize) {
131
- return thumb.size;
132
- }
133
- return 0;
134
64
  }
135
65
 
136
- function chooseLargestThumb(thumbs) {
137
- thumbs = [...thumbs].sort((a, b) => sortThumb(a) - sortThumb(b));
138
- return thumbs.pop();
66
+ export async function getStory(entity: Api.TypeEntityLike, id: number) {
67
+ const result = await (
68
+ await getClient()
69
+ ).invoke(
70
+ new Api.stories.GetStoriesByID({
71
+ id: [id],
72
+ peer: entity,
73
+ })
74
+ );
75
+ return result.stories[0] as Api.StoryItem;
139
76
  }
140
77
 
141
- function streamThumbnail(x) {
142
- if (x instanceof Api.MessageMediaDocument && x.document.thumbs.length > 0) {
143
- const size = chooseLargestThumb(x.document.thumbs);
144
- if (size instanceof Api.PhotoCachedSize || size instanceof Api.PhotoStrippedSize) {
145
- return (function* () {
146
- yield ExpandInlineBytes(size.bytes);
147
- })();
78
+ export async function unwrapMedia(media: Api.TypeMessageMedia | undefined, backupPeerId?: Api.TypePeer) {
79
+ if (media instanceof Api.MessageMediaStory) {
80
+ if (media.story instanceof Api.StoryItem && media.story.media) {
81
+ return media.story.media;
148
82
  }
149
- return streamDocument(x.document, size && 'type' in size ? size.type : '');
150
- }
151
- throw 'not supported';
152
- }
153
-
154
- async function decodeMedia(channelName, x, retry = false) {
155
- const [channel, msg] = x.split('_');
156
-
157
- try {
158
- const msgs = await client.getMessages(channel, {
159
- ids: [Number(msg)],
160
- });
161
- return msgs[0]?.media;
162
- } catch (error) {
163
- if (!retry) {
164
- // channel likely not seen before, we need to resolve ID and retry
165
- await client.getInputEntity(channelName);
166
- return decodeMedia(channelName, x, true);
83
+ let storyItem = await getStory(media.peer, media.id);
84
+ if (!storyItem?.media && backupPeerId) {
85
+ // it's possible the story got hidden by the original user, but we've saved it into Saved Messages - we can still get it
86
+ storyItem = await getStory(backupPeerId, media.id);
167
87
  }
168
- throw error;
169
- }
170
- }
171
-
172
- function streamDocument(obj, thumbSize = '', offset, limit) {
173
- const chunkSize = (obj.size ? getAppropriatedPartSize(obj.size) : 64) * 1024;
174
- const iterFileParams = {
175
- file: new Api.InputDocumentFileLocation({
176
- id: obj.id,
177
- accessHash: obj.accessHash,
178
- fileReference: obj.fileReference,
179
- thumbSize,
180
- }),
181
- chunkSize,
182
- dcId: obj.dcId,
183
- };
184
- if (offset) {
185
- iterFileParams.offset = offset;
186
- }
187
- if (limit) {
188
- iterFileParams.limit = limit;
88
+ return storyItem?.media;
189
89
  }
190
- return client.iterDownload(iterFileParams);
90
+ return media;
191
91
  }
192
-
193
- export { client, getMediaLink, decodeMedia, getFilename, streamDocument, streamThumbnail };
@@ -147,7 +147,7 @@ async function handler(ctx) {
147
147
  alt: item.venueName ?? item.title,
148
148
  }
149
149
  : undefined,
150
- description: item.description?.replace(/\["|"]/g, '') ?? undefined,
150
+ description: item.description?.replaceAll(/\["|"]/g, '') ?? undefined,
151
151
  data: item.parkingLocationId
152
152
  ? {
153
153
  title: item.venueName ?? item.title,
@@ -6,6 +6,10 @@ import { load } from 'cheerio';
6
6
  import path from 'node:path';
7
7
  import { art } from '@/utils/render';
8
8
 
9
+ const excludeTypes = new Set(['NewsletterBlockType', 'RelatedPostsBlockType', 'ProductsTableBlockType', 'TableOfContentsBlockType']);
10
+
11
+ const shouldKeep = (b: any) => !excludeTypes.has(b.__typename.trim());
12
+
9
13
  export const route: Route = {
10
14
  path: '/:hub?',
11
15
  categories: ['new-media'],
@@ -46,6 +50,9 @@ export const route: Route = {
46
50
  };
47
51
 
48
52
  const renderBlock = (b) => {
53
+ if (!shouldKeep(b)) {
54
+ return '';
55
+ }
49
56
  switch (b.__typename) {
50
57
  case 'CoreEmbedBlockType':
51
58
  return b.embedHtml;
@@ -60,7 +67,7 @@ const renderBlock = (b) => {
60
67
  case 'CoreListBlockType':
61
68
  return `${b.ordered ? '<ol>' : '<ul>'}${b.items.map((i) => `<li>${i.contents.html}</li>`).join('')}${b.ordered ? '</ol>' : '</ul>'}`;
62
69
  case 'CoreParagraphBlockType':
63
- return b.contents.html;
70
+ return b.tempContents.map((c) => c.html).join('');
64
71
  case 'CorePullquoteBlockType':
65
72
  return `<blockquote>${b.contents.html}</blockquote>`;
66
73
  case 'CoreQuoteBlockType':
@@ -69,15 +76,25 @@ const renderBlock = (b) => {
69
76
  return '<hr>';
70
77
  case 'HighlightBlockType':
71
78
  return b.children.map((c) => renderBlock(c)).join('');
79
+ case 'ImageCompareBlockType':
80
+ return `<figure><img src="${b.leftImage.thumbnails.horizontal.url.split('?')[0]}" alt="${b.leftImage.alt}" /><img src="${b.rightImage.thumbnails.horizontal.url.split('?')[0]}" alt="${b.rightImage.alt}" /><figcaption>${b.caption.html}</figcaption></figure>`;
81
+ case 'ImageSliderBlockType':
82
+ return b.images.map((i) => `<figure><img src="${i.image.originalUrl.split('?')[0]}" alt="${i.alt}" /><figcaption>${i.caption.html}</figcaption></figure>`).join('');
72
83
  case 'MethodologyAccordionBlockType':
73
84
  return `<h2>${b.heading.html}</h2>${b.sections.map((s) => `<h3>${s.heading.html}</h3>${s.content.html}`).join('')}`;
85
+ case 'ProductBlockType': {
86
+ const product = b.product;
87
+ return `<div><figure><img src="${product.image.thumbnails.horizontal.url.split('?')[0]}" alt="${product.image.alt}" /><figcaption>${product.image.alt}</figcaption></figure><br><a href="${product.bestRetailLink.url}">${product.title} $${product.bestRetailLink.price}</a><br>${product.description.html}${product.pros.html ? `<br>The Good${product.pros.html}The Bad${product.cons.html}` : ''}</div>`;
88
+ }
89
+ case 'TableBlockType':
90
+ return `<table><tr>${b.header.map((cell) => `<th>${cell}</th>`).join('')}</tr>${b.rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`).join('')}</table>`;
74
91
  default:
75
92
  throw new Error(`Unsupported block type: ${b.__typename}`);
76
93
  }
77
94
  };
78
95
 
79
96
  async function handler(ctx) {
80
- const link = ctx.req.param('hub') ? `https://www.theverge.com/${ctx.req.param('hub')}/rss/index.xml` : 'https://www.theverge.com/rss/index.xml';
97
+ const link = ctx.req.param('hub') ? `https://www.theverge.com/rss/${ctx.req.param('hub')}/index.xml` : 'https://www.theverge.com/rss/index.xml';
81
98
 
82
99
  const feed = await parser.parseURL(link);
83
100
 
@@ -96,10 +113,7 @@ async function handler(ctx) {
96
113
  ledeMediaData: node.ledeMediaData,
97
114
  });
98
115
 
99
- description += node.blocks
100
- .filter((b) => b.__typename !== 'NewsletterBlockType' && b.__typename !== 'RelatedPostsBlockType' && b.__typename !== 'ProductBlockType' && b.__typename !== 'TableOfContentsBlockType')
101
- .map((b) => renderBlock(b))
102
- .join('<br><br>');
116
+ description += node.blocks.map((b) => renderBlock(b)).join('<br><br>');
103
117
 
104
118
  if (node.__typename === 'StreamResourceType') {
105
119
  description += node.posts.edges
@@ -78,10 +78,14 @@ const getUserId = (user: string): Promise<string> =>
78
78
  throw new NotFoundError('User ID not found');
79
79
  })
80
80
  .then((result): string => {
81
- if (!result || typeof result !== 'string') {
82
- throw new TypeError('Invalid user ID type');
81
+ if (result) {
82
+ if (typeof result === 'string') {
83
+ return result;
84
+ } else if (typeof result === 'number') {
85
+ return result.toString();
86
+ }
83
87
  }
84
- return result;
88
+ throw new TypeError('Invalid user ID type');
85
89
  });
86
90
 
87
91
  const hasMedia = (post) => post.image_versions2 || post.carousel_media || post.video_versions;
@@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date';
7
7
  import { type CheerioAPI, load } from 'cheerio';
8
8
  import { type Context } from 'hono';
9
9
 
10
- const escapeHtml = (text: string): string => text?.replace(/&/g, '&amp;')?.replace(/</g, '&lt;')?.replace(/>/g, '&gt;')?.replace(/'/g, '&quot;')?.replace(/'/g, '&#039;') ?? text;
10
+ const escapeHtml = (text: string): string => text?.replaceAll('&', '&amp;')?.replaceAll('<', '&lt;')?.replaceAll('>', '&gt;')?.replaceAll("'", '&quot;')?.replaceAll("'", '&#039;') ?? text;
11
11
 
12
12
  const parseTextChildren = (children: any[]): string => children.map((child: any) => escapeHtml(child.text)).join('');
13
13
 
@@ -57,12 +57,12 @@ async function handler(ctx) {
57
57
  switch (item.cell_type) {
58
58
  case 0:
59
59
  case 49: {
60
- const video = item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0];
60
+ const video = item.video.play_addr_list.toSorted((a, b) => b.bitrate - a.bitrate)[0];
61
61
  return {
62
62
  title: item.title,
63
63
  description: art(path.join(__dirname, 'templates/video.art'), {
64
64
  poster: item.video.origin_cover.url_list[0],
65
- url: item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0].play_url_list[0],
65
+ url: item.video.play_addr_list.toSorted((a, b) => b.bitrate - a.bitrate)[0].play_url_list[0],
66
66
  }),
67
67
  link: `https://www.toutiao.com/video/${item.id}/`,
68
68
  pubDate: parseDate(item.publish_time, 'X'),
@@ -280,7 +280,7 @@ const getUserTweets = async (id, params = {}) => {
280
280
  return !idSet.has(id_str) && idSet.add(id_str) && tweet;
281
281
  }) // deduplicate
282
282
  .filter(Boolean) // remove null
283
- .sort((a, b) => (b.id_str || b.conversation_id_str) - (a.id_str || a.conversation_id_str)) // desc
283
+ .toSorted((a, b) => (b.id_str || b.conversation_id_str) - (a.id_str || a.conversation_id_str)) // desc
284
284
  .slice(0, 20);
285
285
  cache.set(cacheKey, JSON.stringify(tweets));
286
286
  return tweets;