From 304c84a0d12ace7368770294480db539da5c4878 Mon Sep 17 00:00:00 2001 From: Felix Hsu Date: Mon, 30 Jan 2023 17:41:58 +0800 Subject: [PATCH] feat(route): add linkedin.cn route (#11718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(route): xiaomiyoupin crowdfunding error and migrate to v2 * initial commit * add searchHit * add jobPosting * linkedincn * add linkedincn ok * feat(route): 中国新闻周刊 (#11665) * feat(route): 中国新闻周刊 * fix: decode via iconv instead of puppeteer * fix: cache full text request * Update lib/v2/inewsweek/index.js * fix: exact publish time * fix: timezone * feat(route): 修复 HelloGitHub 的 月刊 路由 (#11675) * feat(route): 添加 百度股市通 * fix: 修改 百度股市通 路径 * docs: 添加 百度股市通 文档 * feat: 添加 radar 支持 * fix: 按字母顺序插入新路由 * feat(route): 新增 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 * fix: 优化 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 的标题 * fix: 修复 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 部分情况下的非空判断 * fix: 修复 地区名称标题的问题 * fix: 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 的 guid 增加 pubDate * fix: 修复 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 guid 中添加 pubDate * fix: 修改 腾讯新闻 - 新型冠状病毒肺炎疫情实时追踪 的 title * feat(route): 修复 HelloGitHub 的 月刊 路由 * fix: remove guid * fix(route): x6d (#11676) * chore(deps): bump rand-user-agent from 1.0.101 to 1.0.102 (#11678) Bumps [rand-user-agent](https://github.com/WebScrapingAPI/rand-user-agent) from 1.0.101 to 1.0.102. - [Release notes](https://github.com/WebScrapingAPI/rand-user-agent/releases) - [Commits](https://github.com/WebScrapingAPI/rand-user-agent/commits) --- updated-dependencies: - dependency-name: rand-user-agent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump @sentry/node from 7.31.1 to 7.32.1 (#11680) Bumps [@sentry/node](https://github.com/getsentry/sentry-javascript) from 7.31.1 to 7.32.1. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/7.31.1...7.32.1) --- updated-dependencies: - dependency-name: "@sentry/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump jsdom from 21.0.0 to 21.1.0 (#11681) Bumps [jsdom](https://github.com/jsdom/jsdom) from 21.0.0 to 21.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/21.0.0...21.1.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump puppeteer from 19.5.2 to 19.6.0 (#11679) Bumps [puppeteer](https://github.com/puppeteer/puppeteer) from 19.5.2 to 19.6.0. - [Release notes](https://github.com/puppeteer/puppeteer/releases) - [Changelog](https://github.com/puppeteer/puppeteer/blob/main/release-please-config.json) - [Commits](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.2...puppeteer-v19.6.0) --- updated-dependencies: - dependency-name: puppeteer dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(route): famitsu (#11682) * fix(route): xiaomiyoupin crowdfunding error and migrate to v2 (#11677) * fix(route): xiaomiyoupin crowdfunding error and migrate to v2 * Update lib/v2/xiaomiyoupin/maintainer.js * Update docs/shopping.md * use single api endpoint for both module * add pubDate() * fix(route): ikea (#11685) * chore(deps): bump @sentry/node from 7.32.1 to 7.33.0 (#11687) Bumps [@sentry/node](https://github.com/getsentry/sentry-javascript) from 7.32.1 to 7.33.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/7.32.1...7.33.0) --- updated-dependencies: - dependency-name: "@sentry/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump jest from 29.3.1 to 29.4.0 (#11686) Bumps [jest](https://github.com/facebook/jest/tree/HEAD/packages/jest) from 29.3.1 to 29.4.0. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/commits/v29.4.0/packages/jest) --- updated-dependencies: - dependency-name: jest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(route): bloomberg videopage parse error (#11688) * fix(route): clickme (#11689) * fix(route): lofter (#11696) * fix(route): caixin blog (#11695) * fix(route): caixin blog * fix: url * chore(deps): bump ioredis from 5.2.5 to 5.3.0 (#11697) Bumps [ioredis](https://github.com/luin/ioredis) from 5.2.5 to 5.3.0. - [Release notes](https://github.com/luin/ioredis/releases) - [Changelog](https://github.com/luin/ioredis/blob/main/CHANGELOG.md) - [Commits](https://github.com/luin/ioredis/compare/v5.2.5...v5.3.0) --- updated-dependencies: - dependency-name: ioredis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(route): bioone (#11699) * feat(route): github wiki history (#11700) * feat(route): wuhan (#11701) Co-authored-by: nczitzk * chore(deps): bump puppeteer from 19.6.0 to 19.6.1 (#11702) Bumps [puppeteer](https://github.com/puppeteer/puppeteer) from 19.6.0 to 19.6.1. - [Release notes](https://github.com/puppeteer/puppeteer/releases) - [Changelog](https://github.com/puppeteer/puppeteer/blob/main/release-please-config.json) - [Commits](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.0...puppeteer-v19.6.1) --- updated-dependencies: - dependency-name: puppeteer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump jest from 29.4.0 to 29.4.1 (#11703) Bumps [jest](https://github.com/facebook/jest/tree/HEAD/packages/jest) from 29.4.0 to 29.4.1. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/commits/v29.4.1/packages/jest) --- updated-dependencies: - dependency-name: jest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(route): fix date err and refactor to v2 (#11705) * fix(route): fix date err and refactor to v2 * docs: add en docs * fix: typo * feat(route): add mixcloud (#11672) * feat(route): add mixcloud * docs * feat(route): add mixcloud * convert linebreaks to
* docs: add supportPodcast * codestyle: use template literals * refactor: split graph ql queries * fix(route): 软餐 (#11708) * feat(route): gelonghui (#11710) * chore(deps): bump @sentry/node from 7.33.0 to 7.34.0 (#11711) Bumps [@sentry/node](https://github.com/getsentry/sentry-javascript) from 7.33.0 to 7.34.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/7.33.0...7.34.0) --- updated-dependencies: - dependency-name: "@sentry/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(route): fix not unique guid and refactor to v2 (#11712) * add null check * add more null check for compensation * remove unused key * refactor: change route * fix typo --------- --- docs/other.md | 23 +++++ lib/v2/linkedin/cn/index.js | 13 +++ lib/v2/linkedin/cn/utils.js | 119 +++++++++++++++++++++++ lib/v2/linkedin/maintainer.js | 1 + lib/v2/linkedin/radar.js | 16 ++- lib/v2/linkedin/router.js | 1 + lib/v2/linkedin/templates/cn/posting.art | 25 +++++ 7 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 lib/v2/linkedin/cn/index.js create mode 100644 lib/v2/linkedin/cn/utils.js create mode 100644 lib/v2/linkedin/templates/cn/posting.art diff --git a/docs/other.md b/docs/other.md index b6842d9ff2..174456d1a5 100644 --- a/docs/other.md +++ b/docs/other.md @@ -163,6 +163,29 @@ pageClass: routes 该网站原始 RSS 数据源无人维护,故重新抓取数据并生成数据源。 +## LinkedIn 领英中国 + +### Jobs + + + +另外,可以通过添加额外的以下 query 参数来输出满足特定要求的工作职位: + +| 参数 | 描述 | 举例 | 默认值 | +| ---------- | -------------------------------- | ----------------------------------------- | ------- | +| `geo` | geo 编码 | 102890883(中国)、102772228(上海)、103873152(北京) | 空 | +| `remote` | 是否只显示远程工作 | `true/false` | `false` | +| `location` | 工作地点 | `china/shanghai/beijing` | 空 | +| `relevant` | 排序方式 (true: 按相关性排序,false: 按日期排序) | `true/false` | `false` | +| `period` | 发布时间 | `1/7/30` | 空 | + +例如: +[`/linkedin/cn/jobs/Software?location=shanghai&period=1`](https://rsshub.app/linkedin/cn/jobs/Software?location=shanghai\&period=1): 查找所有在上海的今日发布的所有 Software 工作 + +**为了方便起见,建议您在 [LinkedIn.cn](https://www.linkedin.cn/incareer/jobs/search) 上进行搜索,并使用 [RSSHub Radar](https://github.com/DIYgod/RSSHub-Radar) 加载特定的 feed。** + + + ## MiniFlux 因需设置 API Key,故请自行架设 RSSHub。API 密钥则请于 MiniFlux 实例中的 `设置` -> `API密钥` -> `创建一个新的API密钥` 处获取。 diff --git a/lib/v2/linkedin/cn/index.js b/lib/v2/linkedin/cn/index.js new file mode 100644 index 0000000000..341c5f5d3b --- /dev/null +++ b/lib/v2/linkedin/cn/index.js @@ -0,0 +1,13 @@ +const { parseSearchHit, parseJobPosting } = require('./utils'); + +const siteUrl = 'https://www.linkedin.cn/incareer/jobs/search'; + +module.exports = async (ctx) => { + const { title, jobs } = await parseSearchHit(ctx); + const items = await Promise.all(jobs.map((job) => parseJobPosting(job))); + ctx.state.data = { + title: `领英 - ${title}`, + link: siteUrl, + item: items, + }; +}; diff --git a/lib/v2/linkedin/cn/utils.js b/lib/v2/linkedin/cn/utils.js new file mode 100644 index 0000000000..ec7f148cf5 --- /dev/null +++ b/lib/v2/linkedin/cn/utils.js @@ -0,0 +1,119 @@ +const crypto = require('crypto'); +const path = require('path'); +const { art } = require('@/utils/render'); +const got = require('@/utils/got'); + +const apiUrl = 'https://www.linkedin.cn/karpos/api/graphql'; +const searchHitQueryId = 'searchSearchHitsByJob.be362cd720abd0ebf89b4bbc3253047f'; +const jobPostingQueryId = 'jobsJobPostingsById.3b9573e88687a86607ddb74ff013ef50'; + +const makeHeader = () => { + const sessionId = crypto.randomBytes(8).toString('hex').slice(0, 8); + const headers = { + Accept: '*/*', + Cookie: `JSESSIONID="ajax:${sessionId}"`, + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: 'https://www.linkedin.cn/incareer/jobs/search', + 'csrf-token': `ajax:${sessionId}`, + 'x-http-method-override': 'GET', + 'x-restli-protocol-version': '2.0.0', + }; + return headers; +}; + +const period = { + 1: 'r86400', + 7: 'r604800', + 30: 'r2592000', +}; + +const location = { + china: '102890883', + shanghai: '102772228', + beijing: '103873152', +}; + +const makeVariables = (variables) => + '(' + + Object.entries(variables) + .filter(([, v]) => v) + .map(([k, v]) => `${k}:${encodeURIComponent(v)}`) + .join(',') + + ')'; + +const makeBody = (body) => { + let output = ''; + Object.entries(body).forEach(([key, value], index) => { + output += `${key}=${value}`; + if (index < Object.keys(body).length - 1) { + output += '&'; + } + }); + return output; +}; + +const ctxToTitle = (ctx) => { + const { keywords } = ctx.params; + const { geo, remote, location, period, relevant } = ctx.query; + const g = location || geo || 'China'; + const r = remote ? '远程' : ''; + const p = period ? `近${period}天` : ''; + const o = relevant ? '相关' : '最新'; + return `${o}${p}在${g}的${keywords || ''}${r}工作机会`; +}; + +const parseSearchHit = async (ctx) => { + const variables = { + origin: 'jserp', + isForRemoteJobsPage: !!ctx.query.remote, + isChinaMultiNationalCorporation: false, + count: ctx.query.limit || 20, + start: '0', + geoUrn: `urn:li:ks_geo:${location[ctx.query.location] || ctx.query.geo || location.china}`, + keywords: decodeURIComponent(ctx.params.keywords || ''), + f_TPR: period[ctx.query.period] || '', + sortType: ctx.query.relevant ? '' : 'DATE_DESCENDING', + }; + + const resp = await got.post(apiUrl, { + headers: makeHeader(), + body: makeBody({ + operationName: 'searchSearchHitsByJob', + variables: makeVariables(variables), + queryId: searchHitQueryId, + }), + }); + + return { + jobs: resp.data.data.searchSearchHitsByJob.elements.map((e) => e.target.jobPosting), + title: ctxToTitle(ctx), + }; +}; + +const parseJobPosting = async (jobPosting) => { + const entityUrn = jobPosting.entityUrn; + const variables = { + jobPostingUrn: entityUrn, + }; + const resp = await got.post(apiUrl, { + headers: makeHeader(), + body: makeBody({ + operationName: 'jobViewPage', + variables: makeVariables(variables), + queryId: jobPostingQueryId, + }), + }); + const job = resp.data.data.jobsJobPostingsById; + return { + title: `${jobPosting.companyName} 正在找 ${jobPosting.title}`, + link: `https://www.linkedin.cn/incareer/jobs/view/${entityUrn.split(':').pop()}`, + guid: `linkedincn:${entityUrn}`, + description: art(path.join(__dirname, '../templates/cn/posting.art'), job), + pubDate: jobPosting.listedAt, + }; +}; + +module.exports = { + parseSearchHit, + parseJobPosting, +}; diff --git a/lib/v2/linkedin/maintainer.js b/lib/v2/linkedin/maintainer.js index 6a246d058d..424a4a60e9 100644 --- a/lib/v2/linkedin/maintainer.js +++ b/lib/v2/linkedin/maintainer.js @@ -1,3 +1,4 @@ module.exports = { + '/cn/jobs/:keywords?': ['bigfei'], '/jobs': ['BrandNewLifeJackie26'], }; diff --git a/lib/v2/linkedin/radar.js b/lib/v2/linkedin/radar.js index 9183332ff4..6422b73ab4 100644 --- a/lib/v2/linkedin/radar.js +++ b/lib/v2/linkedin/radar.js @@ -1,6 +1,6 @@ module.exports = { 'linkedin.com': { - _name: 'linkedin', + _name: 'LinkedIn', '.': [ { title: 'Job Listing', @@ -19,4 +19,18 @@ module.exports = { }, ], }, + 'linkedin.cn': { + _name: 'LinkedIn 领英中国', + '.': [ + { + title: 'Jobs', + docs: 'https://docs.rsshub.app/other.html#linkedin-ling-ying-zhong-guo', + source: '/incareer/jobs/search', + target: (params, url) => { + const searchParams = new URL(url).searchParams; + return `/linkedin/cn/jobs/${searchParams.get('keywords') || ''}`; + }, + }, + ], + }, }; diff --git a/lib/v2/linkedin/router.js b/lib/v2/linkedin/router.js index 78eecc8d28..a40b719708 100644 --- a/lib/v2/linkedin/router.js +++ b/lib/v2/linkedin/router.js @@ -1,3 +1,4 @@ module.exports = function (router) { + router.get('/cn/jobs/:keywords?', require('./cn/index.js')); router.get('/jobs/:job_types/:exp_levels/:keywords?', require('./jobs.js')); }; diff --git a/lib/v2/linkedin/templates/cn/posting.art b/lib/v2/linkedin/templates/cn/posting.art new file mode 100644 index 0000000000..e406bfa998 --- /dev/null +++ b/lib/v2/linkedin/templates/cn/posting.art @@ -0,0 +1,25 @@ +

{{ title }}

+

+ {{ if (applyMethod.instantOffsiteApply)}} + 点击申请 + {{ /if }} +

+

+ {{ if (applyMethod.basicOffsiteApply)}} + 点击申请 + {{ /if }} +

+

已有{{numApplies}}人申请此职位, {{numViews}}人查看此职位

+{{ if(compensationDescription) }} +

薪资:{{ compensationDescription || 'N/A' }}

+{{ /if }} +

工作地点: {{ geo.defaultLocalizedName }}

+ +{{ if (company) }} +

公司介绍

+

{{company.name}}

+

员工人数:{{company.employeeCount}}

+

{{company.localizedDescription}}

+{{ /if }} +

职位介绍

+

{{description.text}}