feat(route/threads): add post (#23409)

This commit is contained in:
Tony
2026-09-27 23:26:16 +08:00
committed by GitHub
parent 2f236f8756
commit ead57e593d
2 changed files with 84 additions and 1 deletions
+2 -1
View File
@@ -35,7 +35,8 @@ Specify options (in the format of query string) in parameter \`routeParams\` to
};
async function handler(ctx) {
const { user, routeParams } = ctx.req.param();
const { user: rawUser, routeParams } = ctx.req.param();
const user = rawUser.startsWith('@') ? rawUser.slice(1) : rawUser;
const options = parseRouteOptions(new URLSearchParams(routeParams));
const response = await ofetch(profileUrl(user));
+82
View File
@@ -0,0 +1,82 @@
import type { CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
import type { Context } from 'hono';
import type { Route } from '@/types';
import { ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import { buildContent, parseRouteOptions, profileUrl, threadUrl } from './utils';
export const route: Route = {
path: '/:user/post/:id/:routeParams?',
categories: ['social-media'],
view: ViewType.SocialMedia,
example: '/threads/@zuck/post/Ddt7cL5EfUG',
parameters: {
user: 'Username',
id: 'Post ID, the last segment of the post URL',
routeParams: 'Extra parameters, in the format of query string. Accepts the same options as User timeline',
},
radar: [
{
source: ['www.threads.com/:user/post/:id'],
target: '/:user/post/:id',
},
],
name: 'Post & Replies',
maintainers: ['TonyRL'],
handler,
};
const findNodes = (node, predicate: (node) => boolean, acc: any[] = []): any[] => {
if (node instanceof Object) {
if (predicate(node)) {
acc.push(node);
}
for (const value of Object.values(node)) {
findNodes(value, predicate, acc);
}
}
return acc;
};
const extractRelayNodes = ($: CheerioAPI, predicate: (node) => boolean): any[] =>
$('script[data-sjs]:contains("RelayPrefetchedStreamCache")')
.toArray()
.flatMap((script) => findNodes(JSON.parse($(script).text()), predicate));
async function handler(ctx: Context) {
const { user: rawUser, id, routeParams } = ctx.req.param();
const user = rawUser.startsWith('@') ? rawUser.slice(1) : rawUser;
const options = parseRouteOptions(new URLSearchParams(routeParams));
const link = `${profileUrl(user)}/post/${id}`;
const response = await ofetch(link);
const $ = load(response);
const [post] = extractRelayNodes($, (node) => node.code === id && node.taken_at);
if (!post) {
throw new Error('Failed to fetch post data');
}
const replies = extractRelayNodes($, (node) => node.direct_replies).flatMap((node) => node.direct_replies.edges.flatMap((thread) => thread.node.posts.edges.map((edge) => edge.node)));
const items = [post, ...replies].map((post) => {
const { title, description } = buildContent({ post }, options);
return {
author: post.user?.username,
title,
description,
pubDate: parseDate(post.taken_at, 'X'),
link: threadUrl(post.code),
};
});
return {
title: `@${post.user?.username}: ${post.caption?.text.split('\n', 1)[0] ?? ''}`,
link,
image: post.user?.profile_pic_url,
item: items,
};
}