feat(route): cara.app (#15921)
* feat(route): cara.app * feat: add `limit` parameter * fix: remove `title` from template * fix: remove fetch limit and cache response * refactor: fetch userData from api * fix: template
This commit is contained in:
parent
b664f558f2
commit
4a9d1cc32f
|
|
@ -0,0 +1,5 @@
|
|||
export const HOST = 'https://cara.app';
|
||||
|
||||
export const API_HOST = `${HOST}/api`;
|
||||
|
||||
export const CDN_HOST = 'https://cdn.cara.app';
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { Data, DataItem, Route } from '@/types';
|
||||
import type { PostsResponse } from './types';
|
||||
import { customFetch, parseUserData } from './utils';
|
||||
import { API_HOST, CDN_HOST, HOST } from './constant';
|
||||
import { getCurrentPath } from '@/utils/helpers';
|
||||
import { art } from '@/utils/render';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import path from 'node:path';
|
||||
|
||||
const __dirname = getCurrentPath(import.meta.url);
|
||||
|
||||
export const route: Route = {
|
||||
path: ['/likes/:user'],
|
||||
categories: ['social-media'],
|
||||
example: '/cara/likes/fengz',
|
||||
parameters: { user: 'username' },
|
||||
name: 'Likes',
|
||||
maintainers: ['KarasuShin'],
|
||||
handler,
|
||||
radar: [
|
||||
{
|
||||
source: ['cara.app/:user', 'cara.app/:user/*'],
|
||||
target: '/likes/:user',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function handler(ctx): Promise<Data> {
|
||||
const user = ctx.req.param('user');
|
||||
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
|
||||
const userInfo = await parseUserData(user);
|
||||
|
||||
const api = `${API_HOST}/posts/getAllLikesByUser?slug=${userInfo.slug}&take=${limit}`;
|
||||
|
||||
const timelineResponse = await customFetch<PostsResponse>(api);
|
||||
|
||||
const items = timelineResponse.data.map((item) => {
|
||||
const description = art(path.join(__dirname, 'templates/post.art'), {
|
||||
content: item.content,
|
||||
images: item.images.filter((i) => !i.isCoverImg).map((i) => ({ ...i, src: `${CDN_HOST}/${i.src}` })),
|
||||
});
|
||||
return {
|
||||
title: item.title || item.content,
|
||||
pubDate: parseDate(item.createdAt),
|
||||
link: `${HOST}/post/${item.id}`,
|
||||
description,
|
||||
} as DataItem;
|
||||
});
|
||||
|
||||
return {
|
||||
title: `Likes - ${userInfo.name}`,
|
||||
link: `${HOST}/${user}/likes`,
|
||||
image: `${CDN_HOST}/${userInfo.photo}`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'Cara',
|
||||
url: 'cara.app',
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { Data, DataItem, Route } from '@/types';
|
||||
import type { PortfolioResponse } from './types';
|
||||
import { customFetch, fetchPortfolioItem, parseUserData } from './utils';
|
||||
import { API_HOST, CDN_HOST, HOST } from './constant';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: ['/portfolio/:user'],
|
||||
categories: ['social-media'],
|
||||
example: '/cara/portfolio/fengz',
|
||||
parameters: { user: 'username' },
|
||||
name: 'Portfolio',
|
||||
maintainers: ['KarasuShin'],
|
||||
handler,
|
||||
radar: [
|
||||
{
|
||||
source: ['cara.app/:user', 'cara.app/:user/*'],
|
||||
target: '/portfolio/:user',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function handler(ctx): Promise<Data> {
|
||||
const user = ctx.req.param('user');
|
||||
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
|
||||
const userInfo = await parseUserData(user);
|
||||
|
||||
const api = `${API_HOST}/profiles/portfolio?id=${userInfo.id}&take=${limit}`;
|
||||
|
||||
const portfolioResponse = await customFetch<PortfolioResponse>(api);
|
||||
|
||||
const items = await Promise.all(portfolioResponse.data.map((item) => cache.tryGet(`${HOST}/post/${item.postId}`, async () => await fetchPortfolioItem(item)) as unknown as DataItem));
|
||||
|
||||
return {
|
||||
title: `Portfolio - ${userInfo.name}`,
|
||||
link: `${HOST}/${user}/portfolio`,
|
||||
image: `${CDN_HOST}/${userInfo.photo}`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{{ if content }}
|
||||
<p>{{ content }}</p>
|
||||
{{ /if }}
|
||||
{{ each images image }}
|
||||
<img src="{{ image.src }}" />
|
||||
{{ /each }}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { Data, DataItem, Route } from '@/types';
|
||||
import type { PostsResponse } from './types';
|
||||
import { customFetch, parseUserData } from './utils';
|
||||
import { API_HOST, CDN_HOST, HOST } from './constant';
|
||||
import { getCurrentPath } from '@/utils/helpers';
|
||||
import { art } from '@/utils/render';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import path from 'node:path';
|
||||
|
||||
const __dirname = getCurrentPath(import.meta.url);
|
||||
|
||||
export const route: Route = {
|
||||
path: ['/timeline/:user'],
|
||||
categories: ['social-media'],
|
||||
example: '/cara/timeline/fengz',
|
||||
parameters: { user: 'username' },
|
||||
name: 'Timeline',
|
||||
maintainers: ['KarasuShin'],
|
||||
handler,
|
||||
radar: [
|
||||
{
|
||||
source: ['cara.app/:user', 'cara.app/:user/*'],
|
||||
target: '/timeline/:user',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function handler(ctx): Promise<Data> {
|
||||
const user = ctx.req.param('user');
|
||||
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
|
||||
const userInfo = await parseUserData(user);
|
||||
|
||||
const api = `${API_HOST}/posts/getAllByUser?slug=${userInfo.slug}&take=${limit}`;
|
||||
|
||||
const timelineResponse = await customFetch<PostsResponse>(api);
|
||||
|
||||
const items = timelineResponse.data.map((item) => {
|
||||
const description = art(path.join(__dirname, 'templates/post.art'), {
|
||||
content: item.content,
|
||||
images: item.images.filter((i) => !i.isCoverImg).map((i) => ({ ...i, src: `${CDN_HOST}/${i.src}` })),
|
||||
});
|
||||
return {
|
||||
title: item.title || item.content,
|
||||
pubDate: parseDate(item.createdAt),
|
||||
link: `${HOST}/post/${item.id}`,
|
||||
description,
|
||||
} as DataItem;
|
||||
});
|
||||
|
||||
return {
|
||||
title: `Timeline - ${userInfo.name}`,
|
||||
link: `${HOST}/${user}/all`,
|
||||
image: `${CDN_HOST}/${userInfo.photo}`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
export interface UserNextData {
|
||||
pageProps: {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
photo: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface PortfolioResponse {
|
||||
data: {
|
||||
url: string;
|
||||
postId: string;
|
||||
imageNum: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PortfolioDetailResponse {
|
||||
data: {
|
||||
createdAt: string;
|
||||
images: {
|
||||
src: string;
|
||||
isCoverImg: boolean;
|
||||
}[];
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PostsResponse {
|
||||
data: {
|
||||
name: string;
|
||||
photo: string;
|
||||
createdAt: string;
|
||||
images: {
|
||||
src: string;
|
||||
isCoverImg: boolean;
|
||||
}[];
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}[];
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { config } from '@/config';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import type { FetchOptions, FetchRequest, ResponseType } from 'ofetch';
|
||||
import asyncPool from 'tiny-async-pool';
|
||||
import type { PortfolioDetailResponse, PortfolioResponse, UserNextData } from './types';
|
||||
import type { DataItem } from '@/types';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import { API_HOST, CDN_HOST, HOST } from './constant';
|
||||
import { load } from 'cheerio';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
export function customFetch<T = any, R extends ResponseType = 'json'>(request: FetchRequest, options?: FetchOptions<R>) {
|
||||
return ofetch<T, R>(request, {
|
||||
...options,
|
||||
headers: {
|
||||
'user-agent': config.trueUA,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseUserData(user: string) {
|
||||
const buildId = await cache.tryGet(
|
||||
`${HOST}:buildId`,
|
||||
async () => {
|
||||
const res = await customFetch(`${HOST}/explore`);
|
||||
const $ = load(res);
|
||||
return JSON.parse($('#__NEXT_DATA__')?.text() ?? '{}').buildId;
|
||||
},
|
||||
config.cache.routeExpire,
|
||||
false
|
||||
);
|
||||
return (await cache.tryGet(`${HOST}:${user}`, async () => {
|
||||
const data = await customFetch<UserNextData>(`${HOST}/_next/data/${buildId}/${user}.json`);
|
||||
return data.pageProps.user;
|
||||
})) as Promise<UserNextData['pageProps']['user']>;
|
||||
}
|
||||
|
||||
export async function asyncPoolAll<IN, OUT>(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise<OUT>) {
|
||||
const results: Awaited<OUT[]> = [];
|
||||
for await (const result of asyncPool(poolLimit, array, iteratorFn)) {
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function fetchPortfolioItem(item: PortfolioResponse['data'][number]) {
|
||||
const res = await customFetch<PortfolioDetailResponse>(`${API_HOST}/posts/${item.postId}`);
|
||||
|
||||
const description = res.data.images
|
||||
.filter((i) => !i.isCoverImg)
|
||||
.map((image) => `<img src="${CDN_HOST}/${image.src}" />`)
|
||||
.join('<br />');
|
||||
|
||||
const dataItem: DataItem = {
|
||||
title: res.data.title || res.data.content,
|
||||
pubDate: parseDate(res.data.createdAt),
|
||||
link: `${HOST}/post/${item.postId}`,
|
||||
description,
|
||||
};
|
||||
|
||||
return dataItem;
|
||||
}
|
||||
Loading…
Reference in New Issue