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:
karasu 2024-06-19 22:05:49 +08:00 committed by GitHub
parent b664f558f2
commit 4a9d1cc32f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 276 additions and 0 deletions

View File

@ -0,0 +1,5 @@
export const HOST = 'https://cara.app';
export const API_HOST = `${HOST}/api`;
export const CDN_HOST = 'https://cdn.cara.app';

56
lib/routes/cara/likes.ts Normal file
View File

@ -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,
};
}

View File

@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Cara',
url: 'cara.app',
};

View File

@ -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,
};
}

View File

@ -0,0 +1,6 @@
{{ if content }}
<p>{{ content }}</p>
{{ /if }}
{{ each images image }}
<img src="{{ image.src }}" />
{{ /each }}

View File

@ -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,
};
}

45
lib/routes/cara/types.ts Normal file
View File

@ -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;
}[];
}

62
lib/routes/cara/utils.ts Normal file
View File

@ -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;
}