feat: v3 migration - 8/n

This commit is contained in:
DIYgod
2024-02-28 23:49:11 +08:00
parent 5e2986fcd9
commit b621d38e51
21 changed files with 5 additions and 757 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ import type { Hono, Handler } from 'hono';
import { serveStatic } from '@hono/node-server/serve-static';
import * as path from 'node:path';
import index from '@/v3/index';
import robotstxt from '@/v3/robots.txt';
import index from '@/v2/index';
import robotstxt from '@/v2/robots.txt';
type Root = {
get: (routePath: string, filePath: string) => void;
View File
+3
View File
@@ -0,0 +1,3 @@
module.exports = function (router) {
router.get('/:id', './index');
};
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
'/topic/text/:id': ['HenryQW'],
'/topic/:id/:showUid?': ['DIYgod', 'prnake'],
'/user/:id': ['DIYgod', 'prnake'],
};
-54
View File
@@ -1,54 +0,0 @@
module.exports = {
'okjike.com': {
_name: '即刻',
m: [
{
title: '用户动态',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/users/:uid',
target: '/jike/user/:uid',
},
{
title: '用户动态',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/reposts/:repostId',
target: (params, url, document) => {
const uid = document.querySelector('.avatar').getAttribute('href').replace('/users/', '');
return uid ? `/jike/user/${uid}` : '';
},
},
{
title: '圈子',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/topics/:id',
target: '/jike/topic/:id',
},
{
title: '圈子 - 纯文字',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/topics/:id',
target: '/jike/topic/text/:id',
},
],
web: [
{
title: '用户动态',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/u/:uid',
target: '/jike/user/:uid',
},
{
title: '圈子',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/topic/:id',
target: '/jike/topic/:id',
},
{
title: '圈子 - 纯文字',
docs: 'https://docs.rsshub.app/routes/social-media#ji-ke',
source: '/topic/:id',
target: '/jike/topic/text/:id',
},
],
},
};
-5
View File
@@ -1,5 +0,0 @@
module.exports = (router) => {
router.get('/topic/text/:id', require('./topic-text'));
router.get('/topic/:id/:showUid?', require('./topic'));
router.get('/user/:id', require('./user'));
};
-21
View File
@@ -1,21 +0,0 @@
const dayjs = require('dayjs');
const { constructTopicEntry } = require('./utils');
module.exports = async (ctx) => {
const id = ctx.req.param('id');
const topicUrl = `https://m.okjike.com/topics/${id}`;
const data = await constructTopicEntry(ctx, topicUrl);
if (data) {
ctx.state.data.item = data.posts.map((item) => {
const date = dayjs(item.createdAt);
return {
title: `${data.topic.content} ${date.format('MM月DD日')}`,
description: item.content.replaceAll('\n', '<br>'),
pubDate: date.toDate(),
link: `https://m.okjike.com/originalPosts/${item.id}`,
};
});
}
};
-48
View File
@@ -1,48 +0,0 @@
const got = require('@/utils/got');
const { topicDataHanding } = require('./utils');
const cheerio = require('cheerio');
const dayjs = require('dayjs');
const { constructTopicEntry } = require('@/v3/jike/utils');
module.exports = async (ctx) => {
const id = ctx.req.param('id');
const topicUrl = `https://m.okjike.com/topics/${id}`;
const data = await constructTopicEntry(ctx, topicUrl);
if (data) {
let item = topicDataHanding(data, ctx);
if (id === '553870e8e4b0cafb0a1bef68' || id === '55963702e4b0d84d2c30ce6f') {
item = await Promise.all(
item.map(async (one) => {
const item = { ...one };
const regResult = /https:\/\/www\.okjike\.com\/medium\/[\dA-Za-z]*/.exec(item.description);
if (regResult) {
const newsUrl = regResult[0];
item.description = await ctx.cache.tryGet(newsUrl, async () => {
const { data } = await got(newsUrl);
const $ = cheerio.load(data);
const upper = $('ul.main > li.item');
const links = upper.find('a').map((_, ele) => $(ele).attr('href'));
const texts = upper.find('span.text').map((_, ele) => $(ele).text());
let description = '';
for (const [i, link] of links.entries()) {
description += `${i + 1}、<a href="${link}">${texts[i]}</a><br>`;
}
description = description.replace(/<br>$/, '');
return description;
});
}
item.title = `${data.topic.content} ${dayjs(one.pubDate).format('MM月DD日')}`;
return item;
})
);
}
ctx.set('data', {
...ctx.get('data'),
item,
});
}
};
-158
View File
@@ -1,158 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const id = ctx.req.param('id');
const response = await got({
method: 'get',
url: `https://m.okjike.com/users/${id}`,
headers: {
Referer: `https://m.okjike.com/users/${id}`,
},
});
const html = response.data;
const $ = cheerio.load(html);
const raw = $('[type = "application/json"]').html();
const data = JSON.parse(raw).props.pageProps;
const getLink = (id, type) => {
switch (type) {
case 'REPOST':
return `https://m.okjike.com/reposts/${id}`;
case 'MEDIUM':
return `https://www.okjike.com/medium/${id}`;
default:
return `https://m.okjike.com/originalPosts/${id}`;
}
};
const items = await Promise.all(
data.posts.map(async (item) => {
const typeMap = {
ORIGINAL_POST: '发布',
REPOST: '转发',
ANSWER: '回答',
QUESTION: '提问',
PERSONAL_UPDATE: '创建新主题',
};
let linkTemplate = '';
if (item.linkInfo && item.linkInfo.linkUrl) {
linkTemplate = `<a href="${item.linkInfo.linkUrl}">${item.linkInfo.title}</a><br>`;
}
let imgTemplate = '';
if (item.pictures) {
for (const picture of item.pictures) {
imgTemplate += `<br><img src="${picture.picUrl}">`;
}
}
let content = item.content || (item.linkInfo && item.linkInfo.title) || (item.question && item.question.title) || item.title || '';
content = content.replaceAll(/\r\n|\n|\r/g, '<br>');
let shortenTitle = '一条动态';
if (content) {
shortenTitle = content.replaceAll(/(<br>)+/g, ' ');
content = `${content}<br><br>`;
}
let repostContent;
if (item.type === 'REPOST') {
const screenNameTemplate = item.target.user ? `<a href="https://m.okjike.com/users/${item.target.user.username}" target="_blank">@${item.target.user.screenName}</a>` : '';
let repostImgTemplate = '';
if (item.target.pictures) {
for (const picture of item.target.pictures) {
repostImgTemplate += `<br><img src="${picture.thumbnailUrl}">`;
}
}
repostContent = `<div class="rsshub-quote">转发 ${screenNameTemplate}: ${item.target.content}${repostImgTemplate}</div>`.replaceAll(/\r\n|\n|\r/g, '<br>');
content = `${content}${repostContent}`;
}
// 部分功能未知
/* else if (item.type === 'ANSWER') {
let answerTextTemplate = '';
let answerImgTemplate = '';
let answerImgKeys = [];
item.richtextContent.blocks &&
item.richtextContent.blocks.forEach((item) => {
if (item.entityRanges.length && item.text === '[图片]') {
answerImgKeys = [...answerImgKeys, ...Object.keys(item.entityRanges)];
} else {
answerTextTemplate += item.text;
}
});
if (answerImgKeys.length) {
answerImgKeys.forEach((key) => {
const entity = item.richtextContent.entityMap[key];
if (entity.type.toUpperCase() === 'IMAGE') {
answerImgTemplate += `<br><img src="${entity.data.pictureUrl.middlePicUrl}">`;
}
});
}
const answerContent = `回答: ${answerTextTemplate}${answerImgTemplate}`;
content = `${content}${answerContent}`.replace(/\n|\r/g, '<br>');
} else if (item.type === 'QUESTION') {
content = `在主题 <a href="https://web.okjike.com/topic/${item.topic.id}/official" target="_blank">${item.topic.content}</a> 提出了一个问题:<br><br>${content}`;
} else if (item.type === 'PERSONAL_UPDATE') {
shortenTitle = item.topic.content;
content = `<img src="${item.topic.squarePicture.picUrl}"> 主题简介:<br>${item.topic.briefIntro.replace(/(?:\r\n|\r|\n)/g, '<br>')}`;
}*/
const single = {
title: `${typeMap[item.type]}了: ${shortenTitle}`,
description: `${content}${linkTemplate}${imgTemplate}`.replace(/(<br>|\s)+$/, ''),
pubDate: parseDate(item.createdAt),
link: getLink(item.id, item.type),
_extra: repostContent && {
links: [
{
url: getLink(item.target.id, item.target.type),
type: 'quote',
},
],
},
};
if (id === 'wenhao1996' && item.topic.id === '553870e8e4b0cafb0a1bef68') {
single.link = item.urlsInText[0].url;
const { data } = await got({
method: 'get',
url: single.link,
headers: {
Referer: `https://m.okjike.com/users/${id}`,
},
});
const $$ = cheerio.load(data);
$$('span.num,span.arrow').remove();
single.title = `一觉醒来世界发生了什么 ${$$('title').text()}`;
single.description = '';
$$('div.container')
.find('li.item')
// eslint-disable-next-line array-callback-return
.map((i, j) => {
single.description += `<a href="${$$(j).find('a').attr('href')}">${$$(j).find('a').text()}</a><br>`;
});
}
return single;
})
);
ctx.set('data', {
title: `${data.user.screenName}的即刻动态`,
description: data.user.bio,
link: `https://m.okjike.com/users/${id}`,
image: data.user.avatarImage.picUrl,
item: items,
});
};
-198
View File
@@ -1,198 +0,0 @@
const cheerio = require('cheerio');
import parseDate from '@/utils/parse-date';
import got from '@/utils/got';
import { config } from '@/config';
import cache from '@/utils/cache';
const videoAPI = 'https://api.ruguoapp.com/1.0/mediaMeta/play?type=ORIGINAL_POST';
module.exports = {
topicDataHanding: (data, ctx) =>
data.posts.map((item) => {
let audioName, videoName, linkName;
// 获取纯文字内容 和 即刻原文链接
let content, link;
switch (item.type) {
case 'ORIGINAL_POST':
content = item.content;
link = `https://m.okjike.com/originalPosts/${item.id}`;
break;
/* case 'QUESTION':
content = item.title;
link = `https://m.okjike.com/questions/${item.id}`;
break;
case 'OFFICIAL_MESSAGE':
content = item.content;
link = `https://web.okjike.com/message-detail/${item.id}/officialMessage`;
break;*/
default:
content = '未知类型,请前往GitHub提交issue';
link = 'https://github.com/DIYgod/RSSHub/issues';
}
// rss内容
let description = '';
// 作者昵称
let author = '';
// 添加内容作者信息
if (item.user) {
author = item.user.screenName;
if (ctx.req.param('showUid')) {
description += `<span>用户昵称:${author} <br> Username:${item.user.username}</span><br>`;
}
}
if (item.linkInfo) {
const linkUrl = item.linkInfo.originalLinkUrl || item.linkInfo.linkUrl;
// 对于即刻抓取的微信公众号文章 特殊处理
// 此时 Rss原文链接 变为 微信公众号链接
if (new URL(linkUrl).host === 'mp.weixin.qq.com') {
link = linkUrl;
}
// 1. 音频
const audioObject = item.linkInfo.audio || item.audio;
if (audioObject) {
const audioImage = audioObject.image.picUrl || audioObject.image.thumbnailUrl;
const audioLink = linkUrl;
const audioTitle = audioObject.title;
const audioAuthor = audioObject.author;
audioName = `${audioTitle} - ${audioAuthor}`;
description += `
<img src="${audioImage}">
<a href="${audioLink}">${audioName}</a>
`;
}
// 2. 视频
const videoObject = item.linkInfo.video || item.video;
if (videoObject) {
const videoImage = videoObject.image.picUrl || videoObject.image.thumbnailUrl;
const videoLink = linkUrl;
const videoDuration = Math.floor(videoObject.duration / 60000);
videoName = item.linkInfo.title;
description += `
<img src="${videoImage}">
<a href="${videoLink}">${videoName || '观看视频'} - 约${videoDuration}分钟</a>
`;
}
// 3. 链接
if (!audioObject && !videoObject && linkUrl) {
// 部分链接有标题
linkName = item.linkInfo.title;
const linkTitle = linkName || '访问原文';
// 部分链接有缩略图
const linkImage = item.linkInfo.pictureUrl;
const imageTag = `<img src="${linkImage}">`;
description += `
${linkImage ? imageTag : ''}
<a href="${linkUrl}">${linkTitle}</a>
`;
}
}
// 4. 文字内容
description += description ? `<br>${content}` : content;
// 5. 图片
if (item.pictures) {
for (const pic of item.pictures) {
if (pic.format === 'gif') {
description += `<img src="${pic.picUrl.split('?imageMogr2/')[0]}">`;
} else {
// jpeg, bmp, png, gif, webp
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
// let type = 'jpeg';
// switch (pic.format) {
// case 'bmp':
// type = 'bmp';
// break;
// case 'png':
// type = 'png';
// break;
// default:
// break;
// }
const imgUrl = /\.[\da-z]+?\?imageMogr2/.test(pic.picUrl) ? pic.picUrl.split('?imageMogr2/')[0] : pic.picUrl.replace(/thumbnail\/.+/, '');
description += `<br><img src="${imgUrl}">`;
// description += `<br><picture><source srcset="${
// pic.picUrl.split('/thumbnail/')[0]
// }/strip/format/webp" type="image/webp"><source srcset="${imgUrl}" type="image/${type}"><img src="${imgUrl}"></picture>`;
}
}
}
// 6. 视频
if (item.video) {
description += `<br><video src="${item.video.url}" controls></video>`;
}
// rss标题
// 优先将音频和视频名作为标题
// 其次将正文内容作为标题
// 若都没有 则是推送型消息,将连接标题作为主题
// “无题” fallback
const title = audioName || videoName || content || linkName || '无题';
return {
title,
description: description.trim().replaceAll('\n', '<br>'),
pubDate: parseDate.parse(item.createdAt),
author,
link,
};
}),
constructTopicEntry: async (ctx, url) => {
const data = await cache.tryGet(
url,
async () => {
const resp = await got(url);
const html = resp.data;
const $ = cheerio.load(html);
const raw = $('[type = "application/json"]').html();
const data = JSON.parse(raw).props.pageProps;
data.posts = await Promise.all(
data.posts.map(async (item) => {
if (!item.video) {
return item;
}
const videoUrl = `${videoAPI}&id=${item.id}`;
const videoRes = await got(videoUrl);
item.video = videoRes.data;
return item;
})
);
return data;
},
false,
config.cache.routeExpire
);
if (data.length === 0) {
ctx.set('data', {
title: '主题 ID 不存在,或该主题暂无内容',
});
return null;
}
const topic = data.topic;
ctx.set('data', {
title: `${topic.content} - 即刻圈子`,
link: url,
description: topic.briefIntro,
image: topic.squarePicture.picUrl || topic.squarePicture.middlePicUrl || topic.squarePicture.thumbnailUrl,
// item: topicDataHanding(data),
});
return data;
},
};
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
'/routes/:lang?': ['DIYgod'],
'/transform/html/:url/:routeParams': ['ttttmr'],
'/transform/json/:url/:routeParams': ['ttttmr'],
'/transform/sitemap/:url/:routeParams?': ['flrngel'],
};
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
'rsshub.app': {
_name: 'RSSHub',
docs: [
{
title: '有新路由啦',
docs: 'https://docs.rsshub.app/routes/program-update#rsshub',
source: ['/*'],
target: '/rsshub/routes',
},
],
},
};
-6
View File
@@ -1,6 +0,0 @@
module.exports = (router) => {
router.get('/routes/:lang?', require('./routes'));
router.get('/transform/html/:url/:routeParams', require('./transform/html'));
router.get('/transform/json/:url/:routeParams', require('./transform/json'));
router.get('/transform/sitemap/:url/:routeParams?', require('./transform/sitemap'));
};
-65
View File
@@ -1,65 +0,0 @@
import got from '@/utils/got';
const cheerio = require('cheerio');
module.exports = async (ctx) => {
const isEnglish = ctx.req.param('lang') !== 'zh';
const lang = isEnglish ? '' : 'zh/';
const types = [
'social-media',
'new-media',
'traditional-media',
'bbs',
'blog',
'programming',
'design',
'live',
'multimedia',
'picture',
'anime',
'program-update',
'university',
'forecast',
'travel',
'shopping',
'game',
'reading',
'government',
'study',
'journal',
'finance',
'other',
];
const all = await Promise.all(
types.map(async (type) => {
const response = await got(`https://docs.rsshub.app/${lang}routes/${type}`);
const data = response.data;
const $ = cheerio.load(data);
const page = $('.page').toArray();
const item = $('.routeBlock').toArray();
return { page, item, type };
})
);
const list = all.flatMap(({ page, item, type }) => item.map((item) => ({ page, item, type })));
ctx.set('data', {
title: isEnglish ? 'RSSHub has new routes' : 'RSSHub 有新路由啦',
link: 'https://docs.rsshub.app',
description: isEnglish ? 'Everything is RSSible' : '万物皆可 RSS',
language: isEnglish ? 'en-us' : 'zh-cn',
item: list.map(({ page, item, type }) => {
const $ = cheerio.load(page);
item = $(item);
const h2Title = item.prevAll('h2').eq(0);
const h3Title = item.prevAll('h3').eq(0);
return {
title: `${h2Title.text().trim()} - ${h3Title.text().trim()}`,
description: item.html(),
link: `https://docs.rsshub.app/${lang}routes/${type}#${encodeURIComponent(h2Title.find('.hash-link').attr('href') && h3Title.find('.hash-link').attr('href')?.substring(1))}`,
guid: item.attr('id'),
};
}),
});
};
-65
View File
@@ -1,65 +0,0 @@
import got from '@/utils/got';
const cheerio = require('cheerio');
import { config } from '@/config';
module.exports = async (ctx) => {
if (!config.feature.allow_user_supply_unsafe_domain) {
throw new Error(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
}
const url = ctx.req.param('url');
const response = await got({
method: 'get',
url,
});
const routeParams = new URLSearchParams(ctx.req.param('routeParams'));
const $ = cheerio.load(response.data);
const rssTitle = routeParams.get('title') || $('title').text();
const item = routeParams.get('item') || 'html';
const items = $(item)
.toArray()
.map((item) => {
try {
item = $(item);
const titleEle = routeParams.get('itemTitle') ? item.find(routeParams.get('itemTitle')) : item;
const title = routeParams.get('itemTitleAttr') ? titleEle.attr(routeParams.get('itemTitleAttr')) : titleEle.text();
let link;
const linkEle = routeParams.get('itemLink') ? item.find(routeParams.get('itemLink')) : item;
if (routeParams.get('itemLinkAttr')) {
link = linkEle.attr(routeParams.get('itemLinkAttr'));
} else {
link = linkEle.is('a') ? linkEle.attr('href') : linkEle.find('a').attr('href');
}
// 补全绝对链接
link = link.trim();
if (link && !link.startsWith('http')) {
link = `${new URL(url).origin}${link}`;
}
const descEle = routeParams.get('itemDesc') ? item.find(routeParams.get('itemDesc')) : item;
const desc = routeParams.get('itemDescAttr') ? descEle.attr(routeParams.get('itemDescAttr')) : descEle.html();
const pubDateEle = routeParams.get('itemPubDate') ? item.find(routeParams.get('itemPubDate')) : item;
const pubDate = routeParams.get('itemPubDateAttr') ? pubDateEle.attr(routeParams.get('itemPubDateAttr')) : pubDateEle.html();
return {
title,
link,
description: desc,
pubDate,
};
} catch {
return null;
}
})
.filter(Boolean);
ctx.set('data', {
title: rssTitle,
link: url,
description: `Proxy ${url}`,
item: items,
});
};
-58
View File
@@ -1,58 +0,0 @@
import got from '@/utils/got';
const cheerio = require('cheerio');
import { config } from '@/config';
function jsonGet(obj, attr) {
if (typeof attr !== 'string') {
return obj;
}
// a.b.c
// a.b[0].c => a.b.0.c
for (const key of attr.split('.')) {
obj = obj[key];
}
return obj;
}
module.exports = async (ctx) => {
if (!config.feature.allow_user_supply_unsafe_domain) {
throw new Error(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
}
const url = ctx.req.param('url');
const response = await got({
method: 'get',
url,
});
const routeParams = new URLSearchParams(ctx.req.param('routeParams'));
let rssTitle = routeParams.get('title');
if (!rssTitle) {
const resp = await got({
method: 'get',
url: new URL(url).origin,
});
const $ = cheerio.load(resp.data);
rssTitle = $('title').text();
}
const items = jsonGet(response.data, routeParams.get('item')).map((item) => {
let link = jsonGet(item, routeParams.get('itemLink')).trim();
// 补全绝对链接
if (link && !link.startsWith('http')) {
link = `${new URL(url).origin}${link}`;
}
return {
title: jsonGet(item, routeParams.get('itemTitle')),
link,
description: routeParams.get('itemDesc') ? jsonGet(item, routeParams.get('itemDesc')) : '',
pubDate: routeParams.get('itemPubDate') ? jsonGet(item, routeParams.get('itemPubDate')) : '',
};
});
ctx.set('data', {
title: rssTitle,
link: url,
description: `Proxy ${url}`,
item: items,
});
};
-50
View File
@@ -1,50 +0,0 @@
import got from '@/utils/got';
const cheerio = require('cheerio');
import { config } from '@/config';
module.exports = async (ctx) => {
if (!config.feature.allow_user_supply_unsafe_domain) {
throw new Error(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`);
}
const url = ctx.req.param('url');
const response = await got({
method: 'get',
url,
});
const routeParams = new URLSearchParams(ctx.req.param('routeParams'));
const $ = cheerio.load(response.data, { xmlMode: true });
const rssTitle = routeParams.get('title') || ($('urlset url').length && $('urlset url').first().find('loc').text() ? $('urlset url').first().find('loc').text() : 'Sitemap');
const urls = $('urlset url').toArray();
const items =
urls && urls.length
? urls
.map((item) => {
try {
const title = $(item).find('loc').text() || '';
const link = $(item).find('loc').text() || '';
const description = $(item).find('loc').text() || '';
const pubDate = $(item).find('lastmod').text() || undefined;
return {
title,
link,
description,
pubDate,
};
} catch {
return null;
}
})
.filter(Boolean)
: [];
ctx.set('data', {
title: rssTitle,
link: url,
description: `Proxy ${url}`,
item: items,
});
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function (router) {
router.get('/:id', require('./index'));
};