UNPKG

rsshub

Version:
1,145 lines (1,141 loc) 29 kB
import { t as rofetch } from "./ofetch-C3ts-Hud.mjs"; import "./types-DNj6Etbg.mjs"; import { t as parseDate } from "./parse-date-Cr0wQjV_.mjs"; import { t as cache_default } from "./cache-BkqOokyU.mjs"; import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime"; import crypto from "node:crypto"; import { renderToString } from "hono/jsx/dom/server"; import { raw } from "hono/html"; //#region lib/routes/behance/queries.ts const getProfileProjectsAndSelectionsQuery = ` query GetProfileProjectsAndSections($username: String, $after: String) { user(username: $username) { hasPortfolio profileSections { ...profileSectionFields } profileProjects(first: 12, after: $after) { pageInfo { endCursor hasNextPage } nodes { __typename adminFlags { mature_lock privacy_lock dmca_lock flagged_lock privacy_violation_lock trademark_lock spam_lock eu_ip_lock } canBeBoosted colors { r g b } covers { size_202 { url } size_404 { url } size_808 { url } } features { url name featuredOn ribbon { image image2x image3x } } fields { id label slug url } hasMatureContent id isBoosted isFeatured isHiddenFromWorkTab isMatureReviewSubmitted isMonaReported isOwner isFounder isPinnedToSubscriptionOverview isPrivate sourceFiles { ...sourceFileWithCoverFields } matureAccess modifiedOn name owners { ...OwnerFields images { size_50 { url } } } premium publishedOn privacyLevel profileSectionId stats { appreciations { all } views { all } comments { all } } slug url } } } viewer { flags { hasClickedOnAddProfileSectionButton hasSeenProfilePortfolioUpsellModal hasSeenCreatorProIntroModal lastSeenMarketingPopupTimestamp onboardedAsHirer } } } fragment sourceFileWithCoverFields on SourceFile { __typename sourceFileId projectId userId title assetId renditionUrl mimeType size category licenseType unitAmount currency tier hidden extension hasUserPurchased description cover { coverUrl coverX coverY coverScale } } fragment OwnerFields on User { displayName hasPremiumAccess id isFollowing isProfileOwner location locationUrl url username isMessageButtonVisible availabilityInfo { availabilityTimeline isAvailableFullTime isAvailableFreelance hiringTimeline { key label } } creatorPro { isActive initialSubscriptionDate } } fragment profileSectionFields on ProfileSection { id isDefault name order projectCount userId } `; const getAppreciatedQuery = ` query GetAppreciatedProjects($username: String, $after: String) { user(username: $username) { appreciatedProjects(first: 24, after: $after) { nodes { __typename colors { r g b } covers { size_202 { url } size_404 { url } size_808 { url } } slug id name url owners { ...OwnerFields images { size_50 { url } } } stats { appreciations { all } views { all } } } pageInfo { endCursor hasNextPage } } } } fragment OwnerFields on User { displayName hasPremiumAccess id isFollowing isProfileOwner location locationUrl url username isMessageButtonVisible availabilityInfo { availabilityTimeline isAvailableFullTime isAvailableFreelance hiringTimeline { key label } } creatorPro { isActive initialSubscriptionDate } } `; const getProjectPageQuery = ` query ProjectPage($projectId: ProjectId!, $projectPassword: String) { viewer { ...Project_Viewer } project(id: $projectId) { id slug premium isPrivate isOwner canvasWidth embedTag url stylesInline ...Project_Project ...EmbedShareModal_Project creator { hasPremiumAccess } owners { id username displayName } allModules(projectPassword: $projectPassword) { __typename } } } fragment Avatar_UserImageSizes on UserImageSizes { ...AvatarImage_UserImageSizes } fragment Avatar_User on User { id url images { ...Avatar_UserImageSizes } creatorPro { isActive } ...CreatorProBadge_User } fragment AvatarImage_UserImageSizes on UserImageSizes { allAvailable { url width type } } fragment CreatorProBadge_User on User { creatorPro { initialSubscriptionDate } } fragment EmbedShareModal_Project on Project { ...EmbedShareProjectCover_Project } fragment Feature_ProjectFeature on ProjectFeature { url name featuredOn ribbon { image image2x } } fragment HireOverlay_User on User { id firstName isResponsiveToHiring isMessageButtonVisible ...Avatar_User ...WideMessageButton_User availabilityInfo { hiringTimeline { key } } } fragment ProjectInfoBox_User on User { id displayName url isFollowing ...MultipleOwners_User } fragment ProjectInfoBox_Project on Project { id name url isOwner covers { allAvailable { url width type } } owners { ...ProjectInfoBox_User } } fragment SourceAssetsPane_SourceFile on SourceFile { ...SourceFileRowsContainer_SourceFile } fragment SourceFileRowsContainer_SourceFile on SourceFile { tier hasUserPurchased ...SourceFileRow_SourceFile } fragment UserInfo_User on User { displayName id location locationUrl url country isProfileOwner city state creatorPro { isActive } ...Avatar_User } fragment UsersTooltip_User on User { displayName id isFollowing isProfileOwner availabilityInfo { isAvailableFullTime isAvailableFreelance } ...UserInfo_User } fragment DominantColor_Colors on Colors { r g b } fragment Tools_Tool on Tool { id title url backgroundImage { size_original { url } } backgroundColor synonym { tagId name title downloadUrl iconUrl } } fragment HireMeForm_UserAvailabilityInfo on UserAvailabilityInfo { isAvailableFreelance isAvailableFullTime budgetMin currency compensationMin hiringTimeline { key } } fragment MessageDialogManager_User on User { id displayName username images { ...AvatarImage_UserImageSizes } creatorPro { isActive } ...SendRegularMessagePane_User ...ServicesPane_User } fragment SendRegularMessagePane_User on User { displayName availabilityInfo { ...HireMeForm_UserAvailabilityInfo } } fragment ServicesPane_User on User { id displayName ...InquireServiceModal_User ...ViewServiceInfoModal_User } fragment MessageManager_User on User { id isMessageButtonVisible displayName username ...MessageDialogManager_User availabilityInfo { availabilityTimeline isAvailableFullTime isAvailableFreelance hiringTimeline { key } } } fragment WideMessageButton_User on User { firstName ...MessageManager_User } fragment AppreciationNotification_Viewer on Viewer { url } fragment Avatar_Project_User on User { id ...Avatar_User } fragment ImageElement_ImageModule on ImageModule { id altText height width fullBleed imageSizes { allAvailable(type: [JPG, WEBP]) { url width height type } size_max_158 { url } } } fragment Module_ProjectModule on ProjectModule { ...Embed_ProjectModule ... on AudioModule { id caption captionAlignment } ... on VideoModule { id caption captionAlignment } ... on EmbedModule { id caption captionAlignment } ... on ImageModule { id ...ThreeD_ImageModule ...SingleImage_ImageModule ...Image_ImageModule } ... on MediaCollectionModule { id caption: captionPlain captionAlignment components { ...Actions_MediaCollectionComponent filename flexHeight flexWidth height width id imageSizes { allAvailable(type: [JPG, WEBP]) { url width height type } } } fullBleed } ... on TextModule { id text alignment } } fragment MultipleOwners_User on User { ...UsersTooltip_User } fragment MultipleOwners_Project on Project { id name } fragment Project_Owners_User on User { id displayName username firstName url isFollowing isMessageButtonVisible isCreatorPro creatorPro { isActive } ...UsersTooltip_User ...MultipleOwners_User ...Avatar_Project_User ...Avatar_User ...HireOverlay_User } fragment Project_Creator_User on User { id isProfileOwner isFollowing hasPremiumAccess hasAllowEmbeds url ...PaneHeader_User } fragment Project_ProjectCoverImageSizes on ProjectCoverImageSizes { allAvailable { url width type } } fragment Project_Tool on Tool { ...Tools_Tool } fragment Project_SourceFile on SourceFile { ...SourceFileRowsContainer_SourceFile ...SourceFilesProjectOverlay_SourceFile ...ProjectExtras_SourceFile } fragment Project_ProjectFeature on ProjectFeature { ...Feature_ProjectFeature } fragment Project_Project on Project { id slug name url description tags { id title } privacyLevel matureAccess canvasWidth isOwner hasMatureContent isPrivate publishedOn canBeAddedToMoodboard isMonaReported isAppreciated projectCTA { ctaType link { description url title } isDefaultCTA } ...MultipleOwners_Project ...ProjectInfoBox_Project ...ProjectLightbox_Project ...ProjectExtras_Project covers { ...Project_ProjectCoverImageSizes } sourceFiles { ...Project_SourceFile } creator { ...Project_Creator_User } owners { ...Project_Owners_User } tools { ...Project_Tool } allModules(projectPassword: $projectPassword) { ... on AudioModule { id fullBleed caption } ... on EmbedModule { id caption } ... on ImageModule { id fullBleed caption imageSizes { size_disp { url } size_disp_still { url } } ...Actions_ImageModule } ... on MediaCollectionModule { id fullBleed caption: captionPlain components { id imageSizes { size_disp { url } size_disp_still { url } } ...Actions_MediaCollectionComponent } } ... on TextModule { id fullBleed } ... on VideoModule { id fullBleed caption } ...Module_ProjectModule } aeroData { externalUrl } adminNotices { title body isReviewable } license { license } features { ...Project_ProjectFeature } stats { appreciations { all } views { all } } styles { spacing { projectTopMargin } } colors { r g b } } fragment Project_Viewer on Viewer { stats { appreciations } pulsePoints { displayFollow displayAppreciate } flags { hasSeenCreatorProIntroModal onboardedAsHirer } creatorPro { isActive } createdOn ...AppreciationNotification_Viewer ...ProjectExtras_Viewer } fragment ProjectComments_Viewer on Viewer { ...ProjectCommentInput_Viewer id } fragment ProjectCommentInput_Viewer on Viewer { url images { size_50 { url } } } fragment ProjectExtras_SourceFile on SourceFile { ...ProjectInfo_SourceFile } fragment ProjectExtras_Project on Project { isCommentingAllowed } fragment ProjectExtras_Viewer on Viewer { ...ProjectInfo_Viewer } fragment ProjectInfo_SourceFile on SourceFile { ...SourceAssetsPane_SourceFile } fragment ProjectInfo_Viewer on Viewer { id ...ProjectComments_Viewer } fragment ProjectLightbox_Project on Project { id slug isOwner ...ProjectInfoBox_Project } fragment SourceFilesProjectOverlay_SourceFile on SourceFile { hasUserPurchased ...SourceFileRow_SourceFile } fragment Video_VideoDisplay on VideoModule { captionPlain embed height id width } fragment Actions_ImageModule on ImageModule { id hasCaiData projectId src width projectId exifData { lens { ...Actions_exifDataValue } software { ...Actions_exifDataValue } makeAndModel { ...Actions_exifDataValue } focalLength { ...Actions_exifDataValue } iso { ...Actions_exifDataValue } location { ...Actions_exifDataValue } flash { ...Actions_exifDataValue } exposureMode { ...Actions_exifDataValue } shutterSpeed { ...Actions_exifDataValue } aperture { ...Actions_exifDataValue } } } fragment Actions_exifDataValue on exifDataValue { id searchValue label value } fragment Actions_MediaCollectionComponent on MediaCollectionComponent { id } fragment Audio_AudioModule on AudioModule { captionPlain embed fullBleed id } fragment Embed_ProjectModule on ProjectModule { ... on EmbedModule { ...ExternalEmbed_EmbedModule } ... on AudioModule { isDoneProcessing ...Audio_AudioModule } ... on VideoModule { isDoneProcessing ...Video_VideoModule } } fragment ExternalEmbed_EmbedModule on EmbedModule { captionPlain fluidEmbed embedModuleFullBleed: fullBleed height id originalEmbed originalHeight originalWidth width widthUnit } fragment Image_ImageModule on ImageModule { id caption captionAlignment fullBleed height width altText ...Actions_ImageModule ...ImageElement_ImageModule } fragment SingleImage_ImageModule on ImageModule { id caption captionAlignment fullBleed height width ...Actions_ImageModule ...ImageElement_ImageModule } fragment ThreeD_ImageModule on ImageModule { id altText threeDData { iframeUrl } } fragment Video_VideoModule on VideoModule { fullBleed id ...Video_VideoDisplay } fragment Avatar_EmbedFragment on User { id images { allAvailable { url width } } displayName } fragment EmbedShareProjectCover_SourceFile on SourceFile { ...PaidAndFreeAssetsCountBadge_Embed_SourceFile } fragment EmbedShareProjectCover_User on User { ...Avatar_EmbedFragment } fragment EmbedShareProjectCover_Project on Project { id isPrivate isPublished hasMatureContent creator { hasAllowEmbeds } colors { ...DominantColor_Colors } sourceFiles { ...EmbedShareProjectCover_SourceFile } name url covers { allAvailable { url width type } size_original_webp { url width type } } owners { url ...EmbedShareProjectCover_User } } fragment PaidAndFreeAssetsCountBadge_Embed_SourceFile on SourceFile { unitAmount tier hidden } fragment ViewServiceInfoModal_User on User { id displayName url images { size_50 { url } } creatorPro { isActive } ...CreatorProBadge_User } fragment InquireServiceModal_User on User { id displayName images { size_50 { url } } availabilityInfo { hiringTimeline { key } } } fragment SourceFileRow_SourceFile on SourceFile { assetId cover { coverUrl } title extension currency unitAmount renditionUrl hasUserPurchased tier sourceFileId projectId mimeType category licenseType size } fragment PaneHeader_User on User { id url displayName images { allAvailable { width url type } } } `; //#endregion //#region lib/routes/behance/user.tsx const route = { path: "/:user/:type?", categories: ["design"], view: 2, 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_default.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/") }); }); const renderDescription = (description, modules) => renderToString(/* @__PURE__ */ jsxs(Fragment, { children: [description?.length ? /* @__PURE__ */ jsxs(Fragment, { children: [description, /* @__PURE__ */ jsx("br", {})] }) : null, modules?.map((module) => { if (module.__typename === "ImageModule") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", { src: module.imageSizes?.allAvailable?.[0]?.url, alt: module.altText ?? void 0 }), module.caption?.length ? /* @__PURE__ */ jsx("figcaption", { children: module.caption }) : null] }), /* @__PURE__ */ jsx("br", {})] }); if (module.__typename === "TextModule") return /* @__PURE__ */ jsxs(Fragment, { children: [module.text ? raw(module.text) : null, /* @__PURE__ */ jsx("br", {})] }); if (module.__typename === "MediaCollectionModule") return /* @__PURE__ */ jsxs(Fragment, { children: [module.components?.map((comp) => /* @__PURE__ */ jsx("img", { src: `https://mir-s3-cdn-cf.behance.net/project_modules/source/${comp.filename}`, alt: "" })), /* @__PURE__ */ jsx("br", {})] }); if (module.__typename === "EmbedModule") { const embed = module.fluidEmbed || module.originalEmbed; return /* @__PURE__ */ jsxs(Fragment, { children: [embed ? raw(embed) : null, /* @__PURE__ */ jsx("br", {})] }); } return /* @__PURE__ */ jsxs(Fragment, { children: [ "UNHANDLED MODULE: ", module.__typename, /* @__PURE__ */ jsx("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 rofetch("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") : void 0, 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_default.tryGet(item.link, async () => { const project = (await rofetch("https://www.behance.net/v3/graphql", { method: "POST", headers, body: { query: getProjectPageQuery, variables: { projectId: item.projectId } } })).data.project; item.description = renderDescription(project.description, project.allModules); item.category = [.../* @__PURE__ */ new Set([...item.category || [], ...project.tags?.map((tag) => tag.title.toLowerCase()) || []])]; item.pubDate ||= project.publishedOn ? parseDate(project.publishedOn, "X") : void 0; return item; }))); return { title: `${profile.displayName}'s ${type}`, link: `https://www.behance.net/${user}/${type}`, image: profile.image, item: items }; } //#endregion export { route };