From 73f33f9f3c0037fa2ca15bb755ec413668c4f504 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 22 Jul 2025 00:42:19 +0800 Subject: [PATCH] feat(route/sotwe): add sotwe (#19649) --- lib/routes/sotwe/namespace.ts | 1 + lib/routes/sotwe/user.ts | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 lib/routes/sotwe/namespace.ts create mode 100644 lib/routes/sotwe/user.ts diff --git a/lib/routes/sotwe/namespace.ts b/lib/routes/sotwe/namespace.ts new file mode 100644 index 0000000000..ecea84aa69 --- /dev/null +++ b/lib/routes/sotwe/namespace.ts @@ -0,0 +1 @@ +export { namespace } from '@/routes/twitter/namespace'; diff --git a/lib/routes/sotwe/user.ts b/lib/routes/sotwe/user.ts new file mode 100644 index 0000000000..18bbe5de88 --- /dev/null +++ b/lib/routes/sotwe/user.ts @@ -0,0 +1,98 @@ +import { Route, ViewType } from '@/types'; + +import { parseDate } from '@/utils/parse-date'; +import sanitizeHtml from 'sanitize-html'; +import puppeteer from '@/utils/puppeteer'; +import logger from '@/utils/logger'; +import cache from '@/utils/cache'; +import { config } from '@/config'; + +export const route: Route = { + path: '/user/:id', + categories: ['social-media'], + example: '/sotwe/user/_RSSHub', + parameters: { + id: 'Twitter username', + }, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.sotwe.com/:id'], + target: '/user/:id', + }, + ], + name: 'User timeline - Sotwe', + maintainers: ['TonyRL'], + handler, + view: ViewType.Pictures, +}; + +const renderMedia = (mediaEntities) => + mediaEntities + .map((e) => { + switch (e.type) { + case 'photo': + return ``; + case 'video': { + const video = e.videoInfo.variants.filter((v) => v.type === 'video/mp4').sort((a, b) => b.bitrate - a.bitrate)[0]; + return ``; + } + default: + return ''; + } + }) + .join('
'); + +const renderDescription = (item) => + `${renderMedia(item.mediaEntities)}
${item.text.replaceAll('\n', '
')}${item.quotedStatus ? `
${renderDescription(item.quotedStatus)}` : ''}${item.retweetedStatus ? `
${renderDescription(item.retweetedStatus)}` : ''}`; + +async function handler(ctx) { + const baseUrl = 'https://www.sotwe.com'; + const { id } = ctx.req.param(); + + const data = await cache.tryGet( + `sotwe:user:${id}`, + async () => { + const browser = await puppeteer(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + const apiUrl = `${baseUrl}/api/v3/user/${id}/`; + logger.http(`Requesting ${apiUrl}`); + await page.goto(apiUrl, { + waitUntil: 'domcontentloaded', + }); + const response = await page.evaluate(() => document.documentElement.textContent); + await page.close(); + await browser.close(); + + return JSON.parse(response || '{}'); + }, + config.cache.routeExpire, + false + ); + + const items = data.data.map((item) => ({ + title: sanitizeHtml(item.text.split('\n')[0], { allowedTags: [], allowedAttributes: {} }), + description: renderDescription(item), + link: `https://x.com/${id}/status/${item.id}`, + pubDate: parseDate(item.createdAt, 'x'), + })); + + return { + title: `${data.info.name} @${data.info.screenName} - Twitter Profile | Sotwe`, + description: data.info.description, + link: `${baseUrl}/${id}`, + image: data.info.profileImageThumbnail, + item: items, + }; +}