From f93bb8cb9a67f97b7c08a4d8f945a2dc1886696e Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 24 May 2024 16:12:42 +0800 Subject: [PATCH] feat(route): add back matters (#15687) --- lib/router.js | 6 -- lib/routes-deprecated/matters/author.js | 49 -------------- lib/routes-deprecated/matters/latest.js | 69 -------------------- lib/routes-deprecated/matters/tags.js | 56 ---------------- lib/routes/matters/author.ts | 63 ++++++++++++++++++ lib/routes/matters/latest.ts | 74 +++++++++++++++++++++ lib/routes/matters/namespace.ts | 7 ++ lib/routes/matters/tags.ts | 85 +++++++++++++++++++++++++ lib/routes/matters/utils.ts | 30 +++++++++ 9 files changed, 259 insertions(+), 180 deletions(-) delete mode 100644 lib/routes-deprecated/matters/author.js delete mode 100644 lib/routes-deprecated/matters/latest.js delete mode 100644 lib/routes-deprecated/matters/tags.js create mode 100644 lib/routes/matters/author.ts create mode 100644 lib/routes/matters/latest.ts create mode 100644 lib/routes/matters/namespace.ts create mode 100644 lib/routes/matters/tags.ts create mode 100644 lib/routes/matters/utils.ts diff --git a/lib/router.js b/lib/router.js index 36c0aa9fbe5954..08571fa43c4a3d 100644 --- a/lib/router.js +++ b/lib/router.js @@ -594,12 +594,6 @@ router.get('/zucc/cssearch/latest/:webVpn/:key', lazyloadRouteHandler('./routes/ // checkee router.get('/checkee/:dispdate', lazyloadRouteHandler('./routes/checkee/index')); -// Matters -router.get('/matters/latest/:type?', lazyloadRouteHandler('./routes/matters/latest')); -router.redirect('/matters/hot', '/matters/latest/heat'); // Deprecated -router.get('/matters/tags/:tid', lazyloadRouteHandler('./routes/matters/tags')); -router.get('/matters/author/:uid', lazyloadRouteHandler('./routes/matters/author')); - // 古诗文网 router.get('/gushiwen/recommend/:annotation?', lazyloadRouteHandler('./routes/gushiwen/recommend')); diff --git a/lib/routes-deprecated/matters/author.js b/lib/routes-deprecated/matters/author.js deleted file mode 100644 index 2e6e65730bb87b..00000000000000 --- a/lib/routes-deprecated/matters/author.js +++ /dev/null @@ -1,49 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const uid = ctx.params.uid; - const host = `https://matters.news`; - const url = `https://server.matters.news/graphql`; - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - user(input: { userName: "${uid}" }) { - displayName - info { - description - } - articles(input: { first: 20 }) { - edges { - node { - slug - mediaHash - title - content - createdAt - } - } - } - } - }`, - }, - }); - - const user = response.data.data.user; - - ctx.state.data = { - title: `Matters | ${user.displayName}`, - link: `${host}/@${uid}`, - description: user.info.description, - item: user.articles.edges.map(({ node: article }) => ({ - title: article.title, - author: user.displayName, - description: article.content, - link: `${host}/@${uid}/${article.slug}-${article.mediaHash}`, - pubDate: parseDate(article.createdAt), - })), - }; -}; diff --git a/lib/routes-deprecated/matters/latest.js b/lib/routes-deprecated/matters/latest.js deleted file mode 100644 index a68eb370111f3a..00000000000000 --- a/lib/routes-deprecated/matters/latest.js +++ /dev/null @@ -1,69 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const type = ctx.params.type || 'latest'; - const url = `https://server.matters.news/graphql`; - const options = { - latest: { - title: '最新', - apiType: 'newest', - }, - heat: { - title: '熱議', - apiType: 'hottest', - }, - essence: { - title: '精華', - apiType: 'icymi', - }, - }; - - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - viewer { - id - recommendation { - feed: ${options[type].apiType}(input: { first: 10 }) { - edges { - node { - author { - userName - displayName - } - slug - mediaHash - title - content - createdAt - } - } - } - } - } - }`, - }, - }); - - const item = response.data.data.viewer.recommendation.feed.edges.map(({ node }) => { - const link = `https://matters.news/@${node.author.userName}/${node.slug}-${node.mediaHash}`; - const article = node.content; - return { - title: node.title, - link, - description: article, - author: node.author.displayName, - pubDate: parseDate(node.createdAt), - }; - }); - - ctx.state.data = { - title: `Matters | ${options[type].title}`, - link: 'https://matters.news/', - item, - }; -}; diff --git a/lib/routes-deprecated/matters/tags.js b/lib/routes-deprecated/matters/tags.js deleted file mode 100644 index 14629f5a7a2e48..00000000000000 --- a/lib/routes-deprecated/matters/tags.js +++ /dev/null @@ -1,56 +0,0 @@ -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -module.exports = async (ctx) => { - const tid = ctx.params.tid; - const host = `https://matters.news`; - const url = `https://server.matters.news/graphql`; - const response = await got({ - method: 'post', - url, - json: { - query: ` - { - node(input: { id: "${tid}" }) { - ... on Tag { - content - articles(input:{first: 20}){ - edges { - node { - id - title - slug - cover - summary - mediaHash - content - createdAt - author { - id - userName - displayName - } - } - } - } - } - } - }`, - }, - }); - - const node = response.data.data.node; - - ctx.state.data = { - title: `Matters | ${node.content}`, - link: `${host}/tags/${tid}`, - description: node.content, - item: node.articles.edges.map(({ node: article }) => ({ - title: article.title, - author: article.author.displayName, - description: article.content, - link: `${host}/@${article.author.id}/${article.slug}-${article.mediaHash}`, - pubDate: parseDate(article.createdAt), - })), - }; -}; diff --git a/lib/routes/matters/author.ts b/lib/routes/matters/author.ts new file mode 100644 index 00000000000000..0526f437bbccf7 --- /dev/null +++ b/lib/routes/matters/author.ts @@ -0,0 +1,63 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +const handler = async (ctx) => { + const { uid } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const response = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + user(input: {userName: "${uid}"}) { + displayName + avatar + info { + description + } + articles(input: {first: ${limit}}) { + edges { + node { + shortHash + title + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + }`, + }, + }); + + const user = response.data.user; + + return { + title: `Matters | ${user.displayName}`, + link: `${baseUrl}/@${uid}`, + description: user.info.description, + image: user.avatar, + item: user.articles.edges.map(({ node }) => parseItem(node)), + }; +}; + +export const route: Route = { + path: '/author/:uid', + name: 'Author', + example: '/matters/author/robertu', + parameters: { uid: "Author id, can be found at author's homepage url" }, + maintainers: ['Cerebrater', 'xosdy'], + handler, + radar: [ + { + source: ['matters.town/:uid'], + target: (params) => `/matters/author/${params.uid.slice(1)}`, + }, + ], +}; diff --git a/lib/routes/matters/latest.ts b/lib/routes/matters/latest.ts new file mode 100644 index 00000000000000..dfb7c297377d4b --- /dev/null +++ b/lib/routes/matters/latest.ts @@ -0,0 +1,74 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +const handler = async (ctx) => { + const { type = 'latest' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const options = { + latest: { + title: '最新', + apiType: 'newest', + }, + heat: { + title: '熱議', + apiType: 'hottest', + }, + essence: { + title: '精華', + apiType: 'icymi', + }, + }; + + const response = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + viewer { + recommendation { + feed: ${options[type].apiType}(input: {first: ${limit}}) { + edges { + node { + shortHash + title + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + } + }`, + }, + }); + + const item = response.data.viewer.recommendation.feed.edges.map(({ node }) => parseItem(node)); + + return { + title: `Matters | ${options[type].title}`, + link: baseUrl, + item, + }; +}; +export const route: Route = { + path: '/latest/:type?', + name: 'Latest, heat, essence', + example: '/matters/latest/heat', + parameters: { uid: 'Defaults to latest, see table below' }, + maintainers: ['xyqfer', 'Cerebrater', 'xosdy'], + handler, + radar: [ + { + source: ['matters.town'], + }, + ], + description: `| 最新 | 热门 | 精华 | + | ------ | ---- | ------- | + | latest | heat | essence |`, +}; diff --git a/lib/routes/matters/namespace.ts b/lib/routes/matters/namespace.ts new file mode 100644 index 00000000000000..dcc9c9d8dc2e94 --- /dev/null +++ b/lib/routes/matters/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Matters', + url: 'matters.town', + categories: ['new-media'], +}; diff --git a/lib/routes/matters/tags.ts b/lib/routes/matters/tags.ts new file mode 100644 index 00000000000000..73d96f1146a92a --- /dev/null +++ b/lib/routes/matters/tags.ts @@ -0,0 +1,85 @@ +import { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import cache from '@/utils/cache'; +import { baseUrl, gqlEndpoint, parseItem } from './utils'; + +interface Tag { + type: string; + generated: boolean; + id: string; + typename: string; +} + +const getTagId = (tid: string) => + cache.tryGet(`matters:tags:${tid}`, async () => { + const response = await ofetch(`${baseUrl}/tags/${tid}`); + const $ = cheerio.load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + + const node = Object.entries(nextData.props.apolloState.data.ROOT_QUERY) + .find(([key]) => key.startsWith('node')) + ?.pop() as Tag; + + return node?.id.split(':')[1]; + }); + +const handler = async (ctx) => { + const { tid } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const tagId = await getTagId(tid); + + const gqlResponse = await ofetch(gqlEndpoint, { + method: 'POST', + body: { + query: `{ + node(input: {id: "${tagId}"}) { + ... on Tag { + content + description + articles(input: {first: ${limit}}) { + edges { + node { + title + shortHash + content + createdAt + author { + displayName + } + tags { + content + } + } + } + } + } + } + }`, + }, + }); + + const node = gqlResponse.data.node; + + return { + title: `Matters | ${node.content}`, + link: `${baseUrl}/tags/${tid}`, + description: node.description, + item: node.articles.edges.map(({ node }) => parseItem(node)), + }; +}; + +export const route: Route = { + path: '/tags/:tid', + name: 'Tags', + example: '/matters/tags/972-哲學', + parameters: { tid: 'Tag id, can be found in the url of the tag page' }, + maintainers: ['Cerebrater'], + handler, + radar: [ + { + source: ['matters.town/tags/:tid'], + }, + ], +}; diff --git a/lib/routes/matters/utils.ts b/lib/routes/matters/utils.ts new file mode 100644 index 00000000000000..010244dfbe1e69 --- /dev/null +++ b/lib/routes/matters/utils.ts @@ -0,0 +1,30 @@ +import { parseDate } from '@/utils/parse-date'; + +export const baseUrl = 'https://matters.town'; +export const gqlEndpoint = 'https://server.matters.town/graphql'; + +interface Tag { + content: string; +} + +interface Author { + displayName: string; +} + +interface Article { + shortHash: string; + title: string; + content: string; + createdAt: string; + author: Author; + tags: Tag[]; +} + +export const parseItem = (node: Article) => ({ + title: node.title, + description: node.content, + link: `${baseUrl}/a/${node.shortHash}`, + author: node.author.displayName, + pubDate: parseDate(node.createdAt), + category: node.tags.map((tag) => tag.content), +});