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.
- package/lib/config.ts +10 -0
- package/lib/middleware/cache.ts +4 -0
- package/lib/middleware/parameter.ts +1 -1
- package/lib/registry.ts +6 -2
- package/lib/routes/51cto/utils.ts +1 -1
- package/lib/routes/abc/index.ts +1 -1
- package/lib/routes/acgvinyl/namespace.ts +6 -0
- package/lib/routes/acgvinyl/news.ts +86 -0
- package/lib/routes/ally/rail.ts +1 -1
- package/lib/routes/anthropic/news.ts +13 -11
- package/lib/routes/apnews/mobile-api.ts +1 -1
- package/lib/routes/apnews/sitemap.ts +1 -1
- package/lib/routes/apple/apps.ts +2 -2
- package/lib/routes/apple/podcast.ts +58 -25
- package/lib/routes/bangumi.tv/group/reply.ts +1 -1
- package/lib/routes/bilibili/cache.ts +18 -2
- package/lib/routes/bilibili/dynamic.ts +31 -7
- package/lib/routes/bilibili/page.ts +1 -1
- package/lib/routes/bilibili/ranking.ts +23 -17
- package/lib/routes/bilibili/wasm-exec.ts +1 -1
- package/lib/routes/bilibili/weekly-recommend.ts +22 -6
- package/lib/routes/bjp/apod.ts +1 -1
- package/lib/routes/buaa/jiaowu.ts +1 -1
- package/lib/routes/bullionvault/gold-news.ts +3 -3
- package/lib/routes/cast/index.ts +1 -1
- package/lib/routes/coolapk/utils.ts +5 -4
- package/lib/routes/coolbuy/index.ts +106 -0
- package/lib/routes/{xinhuanet → coolbuy}/namespace.ts +3 -3
- package/lib/routes/coolbuy/templates/description.art +48 -0
- package/lib/routes/coolidge/film-guide.ts +60 -0
- package/lib/routes/coolidge/namespace.ts +7 -0
- package/lib/routes/coolidge/news.ts +65 -0
- package/lib/routes/coolidge/templates/description.art +4 -0
- package/lib/routes/copymanga/comic.ts +1 -1
- package/lib/routes/cpta/handler.ts +1 -1
- package/lib/routes/creative-comic/book.ts +1 -1
- package/lib/routes/daum/potplayer.ts +1 -1
- package/lib/routes/dockerhub/utils.ts +1 -1
- package/lib/routes/dora-world/article.ts +2 -2
- package/lib/routes/ehentai/ehapi.ts +3 -3
- package/lib/routes/eventbrite/events.ts +152 -0
- package/lib/routes/eventbrite/namespace.ts +7 -0
- package/lib/routes/github/activity.ts +1 -1
- package/lib/routes/google/jules.ts +63 -0
- package/lib/routes/gov/mem/namespace.ts +7 -0
- package/lib/routes/gov/mem/zfxxgkpt.ts +96 -0
- package/lib/routes/gov/mot/index.ts +158 -53
- package/lib/routes/hameln/chapter.ts +1 -1
- package/lib/routes/hpoi/banner-item.ts +28 -11
- package/lib/routes/hpoi/info.ts +1 -1
- package/lib/routes/huggingface/daily-papers.ts +1 -1
- package/lib/routes/hupu/index.ts +158 -74
- package/lib/routes/hupu/types.ts +163 -0
- package/lib/routes/hust/gs.ts +1 -1
- package/lib/routes/hust/mse.ts +1 -1
- package/lib/routes/huxiu/util.ts +2 -2
- package/lib/routes/infoq/presentations.ts +1 -1
- package/lib/routes/instagram/common-utils.ts +3 -3
- package/lib/routes/itch/devlog.ts +7 -3
- package/lib/routes/javbus/index.ts +1 -1
- package/lib/routes/javlibrary/utils.ts +1 -1
- package/lib/routes/jbma/namespace.ts +17 -0
- package/lib/routes/jbma/report.ts +473 -0
- package/lib/routes/jetbrains/comments.ts +1 -1
- package/lib/routes/jingzhengu/utils.ts +1 -1
- package/lib/routes/juejin/aicoding.ts +102 -0
- package/lib/routes/juejin/utils.ts +36 -51
- package/lib/routes/kakuyomu/works.ts +1 -1
- package/lib/routes/kemono/index.ts +2 -2
- package/lib/routes/komiic/comic.ts +1 -1
- package/lib/routes/koyso/index.ts +338 -0
- package/lib/routes/koyso/namespace.ts +9 -0
- package/lib/routes/koyso/templates/description.art +13 -0
- package/lib/routes/letterboxd/index.ts +65 -0
- package/lib/routes/letterboxd/namespace.ts +8 -0
- package/lib/routes/maccms/index.ts +1 -1
- package/lib/routes/mercari/util.ts +1 -1
- package/lib/routes/mingpao/index.ts +1 -1
- package/lib/routes/nankai/ai-notice.ts +142 -0
- package/lib/routes/nankai/graduate-notice.ts +162 -0
- package/lib/routes/natgeo/natgeo.ts +1 -0
- package/lib/routes/nhk/news-web-easy.ts +1 -1
- package/lib/routes/nicovideo/mylist.ts +39 -0
- package/lib/routes/nicovideo/types.ts +27 -0
- package/lib/routes/nicovideo/utils.ts +27 -1
- package/lib/routes/nikkei/cn/index.ts +1 -4
- package/lib/routes/now/news.ts +1 -1
- package/lib/routes/nytimes/index.ts +1 -1
- package/lib/routes/pixiv/novel-api/user-novels/sfw.ts +1 -1
- package/lib/routes/pixivision/utils.ts +1 -1
- package/lib/routes/pku/hr.ts +1 -1
- package/lib/routes/pku/scc/recruit.ts +1 -1
- package/lib/routes/producthunt/templates/description.art +2 -2
- package/lib/routes/producthunt/today.ts +17 -8
- package/lib/routes/ps/trophy.ts +1 -1
- package/lib/routes/pubscholar/utils.ts +14 -1
- package/lib/routes/qweather/3days.ts +14 -14
- package/lib/routes/qweather/now.ts +12 -10
- package/lib/routes/qweather/util.tsx +89 -0
- package/lib/routes/qwenlm/blog.ts +75 -0
- package/lib/routes/qwenlm/namespace.ts +6 -0
- package/lib/routes/radio-canada/latest.ts +30 -17
- package/lib/routes/ruc/ai.ts +1 -1
- package/lib/routes/ruc/hr.ts +1 -1
- package/lib/routes/samrdprc/index.ts +241 -0
- package/lib/routes/samrdprc/namespace.ts +1 -1
- package/lib/routes/sdo/ff14risingstones/api.ts +78 -0
- package/lib/routes/sdo/ff14risingstones/constant.ts +338 -0
- package/lib/routes/sdo/ff14risingstones/posts.ts +80 -0
- package/lib/routes/sdo/ff14risingstones/strats.ts +75 -0
- package/lib/routes/sdo/ff14risingstones/templates/duties-party.art +41 -0
- package/lib/routes/sdo/ff14risingstones/templates/fc-party.art +26 -0
- package/lib/routes/sdo/ff14risingstones/templates/novice-network-party.art +9 -0
- package/lib/routes/sdo/ff14risingstones/templates/rp-party.art +15 -0
- package/lib/routes/sdo/ff14risingstones/timeline.ts +31 -0
- package/lib/routes/sdo/ff14risingstones/types/dynamic.ts +50 -0
- package/lib/routes/sdo/ff14risingstones/types/index.ts +3 -0
- package/lib/routes/sdo/ff14risingstones/types/other.ts +57 -0
- package/lib/routes/sdo/ff14risingstones/types/party.ts +111 -0
- package/lib/routes/sdo/ff14risingstones/user-dynamics.ts +32 -0
- package/lib/routes/sdo/ff14risingstones/user-posts.ts +32 -0
- package/lib/routes/sdo/ff14risingstones/user-resently.ts +38 -0
- package/lib/routes/sdo/ff14risingstones/user-strats.ts +32 -0
- package/lib/routes/sdo/ff14risingstones/utils.ts +215 -0
- package/lib/routes/sdo/namespace.ts +7 -0
- package/lib/routes/showstart/utils.ts +1 -1
- package/lib/routes/sohu/mp.ts +1 -1
- package/lib/routes/sotwe/user.ts +1 -1
- package/lib/routes/surfshark/blog.ts +273 -77
- package/lib/routes/surfshark/templates/description.art +16 -4
- package/lib/routes/sustainabilitymag/articles.ts +1 -1
- package/lib/routes/syosetu/dev.ts +1 -1
- package/lib/routes/syosetu/ranking-isekai.ts +1 -1
- package/lib/routes/szse/disclosure/listed-notice.ts +44 -6
- package/lib/routes/telegram/channel-media.ts +249 -0
- package/lib/routes/telegram/channel.ts +5 -4
- package/lib/routes/telegram/stories.ts +130 -0
- package/lib/routes/telegram/tglib/channel.ts +136 -118
- package/lib/routes/telegram/tglib/client.ts +37 -139
- package/lib/routes/tesla/cx.ts +1 -1
- package/lib/routes/theverge/index.ts +20 -6
- package/lib/routes/threads/utils.ts +7 -3
- package/lib/routes/tidb/blog.ts +1 -1
- package/lib/routes/toutiao/user.ts +2 -2
- package/lib/routes/twitter/api/mobile-api/api.ts +1 -1
- package/lib/routes/txrjy/fornumtopic.ts +2 -2
- package/lib/routes/typst/universe.ts +1 -1
- package/lib/routes/uber/blog.ts +87 -46
- package/lib/routes/weibo/utils.ts +17 -9
- package/lib/routes/xiaohongshu/user.ts +1 -1
- package/lib/routes/xjtu/ee-jzxx.ts +1 -1
- package/lib/routes/yahoo/news/utils.ts +1 -1
- package/lib/routes/ymgal/article.ts +1 -1
- package/lib/routes/yoasobi-music/media.ts +1 -1
- package/lib/routes/youtube/api/youtubei.ts +1 -1
- package/lib/routes/youtube/community.ts +4 -4
- package/lib/routes/zaker/utils.ts +1 -1
- package/lib/routes/zaobao/util.tsx +1 -1
- package/lib/server.ts +1 -1
- package/lib/types.ts +1 -1
- package/lib/utils/puppeteer-utils.test.ts +2 -2
- package/lib/views/index.tsx +4 -4
- package/package.json +40 -40
- package/lib/routes/qweather/templates/3days.art +0 -22
- package/lib/routes/qweather/templates/now.art +0 -16
- package/lib/routes/xinhuanet/app.ts +0 -109
|
@@ -1,160 +1,180 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
return [];
|
|
38
|
+
if (m instanceof Api.MessageMediaPhoto || mime.startsWith('image/')) {
|
|
39
|
+
return `<img src="${src}" alt=""/>`;
|
|
11
40
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 (
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
92
|
+
return getFilename(x);
|
|
113
93
|
}
|
|
114
94
|
|
|
115
|
-
export
|
|
116
|
-
const
|
|
117
|
-
const
|
|
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
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
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
|
-
|
|
124
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
137
|
-
|
|
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 (
|
|
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:
|
|
171
|
+
author: getDisplayName(message.sender ?? entity),
|
|
152
172
|
});
|
|
153
173
|
}
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
return {
|
|
157
|
-
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.
|
|
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
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
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 (
|
|
122
|
-
return
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
90
|
+
return media;
|
|
191
91
|
}
|
|
192
|
-
|
|
193
|
-
export { client, getMediaLink, decodeMedia, getFilename, streamDocument, streamThumbnail };
|
package/lib/routes/tesla/cx.ts
CHANGED
|
@@ -147,7 +147,7 @@ async function handler(ctx) {
|
|
|
147
147
|
alt: item.venueName ?? item.title,
|
|
148
148
|
}
|
|
149
149
|
: undefined,
|
|
150
|
-
description: item.description?.
|
|
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.
|
|
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')}/
|
|
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 (
|
|
82
|
-
|
|
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
|
-
|
|
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;
|
package/lib/routes/tidb/blog.ts
CHANGED
|
@@ -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?.
|
|
10
|
+
const escapeHtml = (text: string): string => text?.replaceAll('&', '&')?.replaceAll('<', '<')?.replaceAll('>', '>')?.replaceAll("'", '"')?.replaceAll("'", ''') ?? 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.
|
|
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.
|
|
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
|
-
.
|
|
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;
|