181 lines
6.2 KiB
TypeScript
181 lines
6.2 KiB
TypeScript
import crypto from 'node:crypto';
|
|
|
|
import { raw } from 'hono/html';
|
|
import { renderToString } from 'hono/jsx/dom/server';
|
|
|
|
import type { Route } from '@/types';
|
|
import { ViewType } from '@/types';
|
|
import cache from '@/utils/cache';
|
|
import ofetch from '@/utils/ofetch';
|
|
import { parseDate } from '@/utils/parse-date';
|
|
|
|
import { getAppreciatedQuery, getProfileProjectsAndSelectionsQuery, getProjectPageQuery } from './queries';
|
|
|
|
export const route: Route = {
|
|
path: '/:user/:type?',
|
|
categories: ['design'],
|
|
view: ViewType.Pictures,
|
|
example: '/behance/mishapetrick',
|
|
parameters: {
|
|
user: 'username',
|
|
type: {
|
|
description: 'type',
|
|
options: [
|
|
{ value: 'projects', label: 'projects' },
|
|
{ value: 'appreciated', label: 'appreciated' },
|
|
],
|
|
default: 'projects',
|
|
},
|
|
},
|
|
features: {
|
|
requireConfig: false,
|
|
requirePuppeteer: false,
|
|
antiCrawler: false,
|
|
supportBT: false,
|
|
supportPodcast: false,
|
|
supportScihub: false,
|
|
},
|
|
name: 'User Works',
|
|
maintainers: ['MisteryMonster'],
|
|
handler,
|
|
description: `Behance user's profile URL, like <https://www.behance.net/mishapetrick> the username will be \`mishapetrick\`。`,
|
|
};
|
|
|
|
const getUserProfile = async (nodes, user) =>
|
|
(await cache.tryGet(`behance:profile:${user}`, () => {
|
|
const profile = nodes.flatMap((item) => item.owners).find((owner) => owner.username === user);
|
|
|
|
return Promise.resolve({
|
|
displayName: profile.displayName,
|
|
id: profile.id,
|
|
link: profile.url,
|
|
image: profile.images.size_50.url.replace('/user/50/', '/user/source/'),
|
|
});
|
|
})) as { displayName: string; id: string; link: string; image: string };
|
|
|
|
const renderDescription = (description, modules) =>
|
|
renderToString(
|
|
<>
|
|
{description?.length ? (
|
|
<>
|
|
{description}
|
|
<br />
|
|
</>
|
|
) : null}
|
|
{modules?.map((module) => {
|
|
if (module.__typename === 'ImageModule') {
|
|
return (
|
|
<>
|
|
<figure>
|
|
<img src={module.imageSizes?.allAvailable?.[0]?.url} alt={module.altText ?? undefined} />
|
|
{module.caption?.length ? <figcaption>{module.caption}</figcaption> : null}
|
|
</figure>
|
|
<br />
|
|
</>
|
|
);
|
|
}
|
|
if (module.__typename === 'TextModule') {
|
|
return (
|
|
<>
|
|
{module.text ? raw(module.text) : null}
|
|
<br />
|
|
</>
|
|
);
|
|
}
|
|
if (module.__typename === 'MediaCollectionModule') {
|
|
return (
|
|
<>
|
|
{module.components?.map((comp) => (
|
|
<img src={`https://mir-s3-cdn-cf.behance.net/project_modules/source/${comp.filename}`} alt="" />
|
|
))}
|
|
<br />
|
|
</>
|
|
);
|
|
}
|
|
if (module.__typename === 'EmbedModule') {
|
|
const embed = module.fluidEmbed || module.originalEmbed;
|
|
return (
|
|
<>
|
|
{embed ? raw(embed) : null}
|
|
<br />
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
UNHANDLED MODULE: {module.__typename}
|
|
<br />
|
|
</>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
|
|
async function handler(ctx) {
|
|
const { user, type = 'projects' } = ctx.req.param();
|
|
|
|
const uuid = crypto.randomUUID();
|
|
const headers = {
|
|
Cookie: `gk_suid=${Math.random().toString().slice(2, 10)}, gki=; originalReferrer=; bcp=${uuid}`,
|
|
'X-BCP': uuid,
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
};
|
|
|
|
const response = await ofetch('https://www.behance.net/v3/graphql', {
|
|
method: 'POST',
|
|
headers,
|
|
body: {
|
|
query: type === 'projects' ? getProfileProjectsAndSelectionsQuery : getAppreciatedQuery,
|
|
variables: {
|
|
username: user,
|
|
after: '',
|
|
},
|
|
},
|
|
});
|
|
|
|
const nodes = type === 'projects' ? response.data.user.profileProjects.nodes : response.data.user.appreciatedProjects.nodes;
|
|
const list = nodes.map((item) => ({
|
|
title: item.name,
|
|
link: item.url,
|
|
author: item.owners.map((owner) => owner.displayName).join(', '),
|
|
image: item.covers.size_202.url.replace('/202/', '/source/'),
|
|
pubDate: item.publishedOn ? parseDate(item.publishedOn, 'X') : undefined,
|
|
category: item.fields?.map((field) => field.label.toLowerCase()),
|
|
projectId: item.id,
|
|
}));
|
|
|
|
const profile = await getUserProfile(nodes, user);
|
|
|
|
const items = await Promise.all(
|
|
list.map((item) =>
|
|
cache.tryGet(item.link, async () => {
|
|
const response = await ofetch('https://www.behance.net/v3/graphql', {
|
|
method: 'POST',
|
|
headers,
|
|
body: {
|
|
query: getProjectPageQuery,
|
|
variables: {
|
|
projectId: item.projectId,
|
|
},
|
|
},
|
|
});
|
|
const project = response.data.project;
|
|
|
|
item.description = renderDescription(project.description, project.allModules);
|
|
item.category = [...new Set([...(item.category || []), ...(project.tags?.map((tag) => tag.title.toLowerCase()) || [])])];
|
|
item.pubDate = item.pubDate || (project.publishedOn ? parseDate(project.publishedOn, 'X') : undefined);
|
|
|
|
return item;
|
|
})
|
|
)
|
|
);
|
|
|
|
return {
|
|
title: `${profile.displayName}'s ${type}`,
|
|
link: `https://www.behance.net/${user}/${type}`,
|
|
image: profile.image,
|
|
item: items,
|
|
};
|
|
}
|