diff --git a/lib/routes/daily/discussed.ts b/lib/routes/daily/discussed.ts index 66ce51037d7858..54a5957276b6d4 100644 --- a/lib/routes/daily/discussed.ts +++ b/lib/routes/daily/discussed.ts @@ -59,21 +59,10 @@ const graphqlQuery = { export const route: Route = { path: '/discussed', - categories: ['social-media'], example: '/daily/discussed', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['daily.dev/popular'], - target: '', }, ], name: 'Most Discussed', diff --git a/lib/routes/daily/index.ts b/lib/routes/daily/index.ts index b10418e9160a43..031f47c0a773eb 100644 --- a/lib/routes/daily/index.ts +++ b/lib/routes/daily/index.ts @@ -69,13 +69,13 @@ const graphqlQuery = { export const route: Route = { path: '/', + example: '/daily', radar: [ { source: ['daily.dev/popular'], - target: '', }, ], - name: 'Unknown', + name: 'Popular', maintainers: ['Rjnishant530'], handler, url: 'daily.dev/popular', diff --git a/lib/routes/daily/namespace.ts b/lib/routes/daily/namespace.ts index acddafe867b238..2dbd9bf58eba9c 100644 --- a/lib/routes/daily/namespace.ts +++ b/lib/routes/daily/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Daily.dev', url: 'daily.dev', + categories: ['social-media'], }; diff --git a/lib/routes/daily/templates/posts.art b/lib/routes/daily/templates/posts.art new file mode 100644 index 00000000000000..b8d94338e03448 --- /dev/null +++ b/lib/routes/daily/templates/posts.art @@ -0,0 +1,7 @@ +{{ if image }} +
+{{ /if }} + +{{ if content }} +{{@ content }} +{{ /if }} diff --git a/lib/routes/daily/upvoted.ts b/lib/routes/daily/upvoted.ts index 1b2c2ab9783691..68a2a19d6cc784 100644 --- a/lib/routes/daily/upvoted.ts +++ b/lib/routes/daily/upvoted.ts @@ -16,7 +16,7 @@ const query = ` ...FeedPostConnection } } - + fragment FeedPostConnection on PostConnection { edges { node { @@ -24,11 +24,11 @@ const query = ` } } } - + fragment FeedPost on Post { ...SharedPostInfo } - + fragment SharedPostInfo on Post { id title @@ -44,7 +44,7 @@ const query = ` } tags } - + fragment UserShortInfo on User { id name @@ -53,7 +53,7 @@ const query = ` username bio } - + `; const graphqlQuery = { @@ -63,21 +63,10 @@ const graphqlQuery = { export const route: Route = { path: '/upvoted', - categories: ['social-media'], example: '/daily/upvoted', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, radar: [ { source: ['daily.dev/popular'], - target: '', }, ], name: 'Most upvoted', diff --git a/lib/routes/daily/user.ts b/lib/routes/daily/user.ts new file mode 100644 index 00000000000000..7abaf15a029cb8 --- /dev/null +++ b/lib/routes/daily/user.ts @@ -0,0 +1,227 @@ +import { Route } from '@/types'; +import { baseUrl, getBuildId, getData } from './utils'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'path'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +const userPostQuery = ` + query AuthorFeed( + $loggedIn: Boolean! = false + $userId: ID! + $after: String + $first: Int + $supportedTypes: [String!] = [ + "article" + "share" + "freeform" + "video:youtube" + "collection" + ] + ) { + page: authorFeed( + author: $userId + after: $after + first: $first + ranking: TIME + supportedTypes: $supportedTypes + ) { + ...FeedPostConnection + } + } + fragment FeedPostConnection on PostConnection { + pageInfo { + hasNextPage + endCursor + } + edges { + node { + ...FeedPost + contentHtml + ...UserPost @include(if: $loggedIn) + } + } + } + fragment FeedPost on Post { + ...SharedPostInfo + sharedPost { + ...SharedPostInfo + } + trending + feedMeta + collectionSources { + handle + image + } + numCollectionSources + updatedAt + slug + } + fragment SharedPostInfo on Post { + id + title + titleHtml + image + readTime + permalink + commentsPermalink + summary + createdAt + private + upvoted + commented + bookmarked + views + numUpvotes + numComments + videoId + scout { + ...UserShortInfo + } + author { + ...UserShortInfo + } + type + tags + source { + ...SourceBaseInfo + } + downvoted + flags { + promoteToPublic + } + userState { + vote + flags { + feedbackDismiss + } + } + slug + } + fragment SourceBaseInfo on Source { + id + active + handle + name + permalink + public + type + description + image + membersCount + privilegedMembers { + user { + id + } + role + } + currentMember { + ...CurrentMember + } + memberPostingRole + memberInviteRole + } + fragment CurrentMember on SourceMember { + user { + id + } + permissions + role + referralToken + flags { + hideFeedPosts + collapsePinnedPosts + } + } + fragment UserShortInfo on User { + id + name + image + permalink + username + bio + createdAt + reputation + } + fragment UserPost on Post { + read + upvoted + commented + bookmarked + downvoted + }`; + +const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data); + +export const route: Route = { + path: '/user/:userId', + example: '/daily/user/kramer', + radar: [ + { + source: ['daily.dev/:userId/posts', 'daily.dev/:userId'], + }, + ], + name: 'User Posts', + maintainers: ['TonyRL'], + handler, + url: 'daily.dev', +}; + +async function handler(ctx) { + const userId = ctx.req.param('userId'); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7; + + const buildId = await getBuildId(); + + const userData = await cache.tryGet(`daily:user:${userId}`, async () => { + const resposne = await ofetch(`${baseUrl}/_next/data/${buildId}/en/${userId}.json`, { + query: { + userId, + }, + }); + return resposne.pageProps; + }); + const user = (userData as any).user; + + const items = await cache.tryGet( + `daily:user:${userId}:posts`, + async () => { + const edges = await getData({ + query: userPostQuery, + variables: { + userId: user.id, + first: limit, + loggedIn: false, + }, + }); + return edges.map(({ node }) => ({ + title: node.title, + description: render({ + image: node.image, + content: node.contentHtml?.replaceAll('\n', '
') ?? node.summary, + }), + link: node.permalink, + author: node.author?.name, + category: node.tags, + pubDate: parseDate(node.createdAt), + })); + }, + config.cache.routeExpire, + false + ); + + return { + title: `${user.name} | daily.dev`, + description: user.bio, + link: `${baseUrl}/${userId}/posts`, + item: items, + image: user.image, + logo: user.image, + icon: user.image, + language: 'en-us', + }; +} diff --git a/lib/routes/daily/utils.ts b/lib/routes/daily/utils.ts index d9c67025b85d2b..8f203c744d15a3 100644 --- a/lib/routes/daily/utils.ts +++ b/lib/routes/daily/utils.ts @@ -1,19 +1,34 @@ -import dayjs from 'dayjs'; -import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { config } from '@/config'; -const getData = async (graphqlQuery) => - ( - await got - .post('https://app.daily.dev/api/graphql', { - json: graphqlQuery, - }) - .json() - ).data.page.edges; +const baseUrl = 'https://app.daily.dev'; + +const getBuildId = () => + cache.tryGet( + 'daily:buildId', + async () => { + const response = await ofetch(`${baseUrl}/onboarding`); + const buildId = response.match(/"buildId":"(.*?)"/)[1]; + return buildId; + }, + config.cache.routeExpire, + false + ); + +const getData = async (graphqlQuery) => { + const response = await ofetch(`${baseUrl}/api/graphql`, { + method: 'POST', + body: graphqlQuery, + }); + return response.data.page.edges; +}; const getList = (data) => data.map((value) => { const { id, title, image, permalink, summary, createdAt, numUpvotes, author, tags, numComments } = value.node; - const pubDate = dayjs(createdAt); + const pubDate = parseDate(createdAt); return { id, title, @@ -30,10 +45,12 @@ const getList = (data) => const getRedirectedLink = (data) => Promise.all( - data.map(async (v) => { - const resp = await got(v.link); - return { ...v, link: resp.headers.link.split(/[<>]/g)[1] }; - }) + data.map((v) => + cache.tryGet(v.link, async () => { + const resp = await ofetch.raw(v.link); + return { ...v, link: resp.headers.get('location') }; + }) + ) ); -export { getData, getList, getRedirectedLink }; +export { baseUrl, getBuildId, getData, getList, getRedirectedLink };