mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-01 14:57:14 +08:00
@@ -286,6 +286,10 @@ export type Config = {
|
||||
xueqiu: {
|
||||
cookies?: string;
|
||||
};
|
||||
yamibo: {
|
||||
salt?: string;
|
||||
auth?: string;
|
||||
};
|
||||
youtube: {
|
||||
key?: string;
|
||||
clientId?: string;
|
||||
@@ -642,6 +646,10 @@ const calculateValue = () => {
|
||||
xueqiu: {
|
||||
cookies: envs.XUEQIU_COOKIES,
|
||||
},
|
||||
yamibo: {
|
||||
salt: envs.YAMIBO_SALT,
|
||||
auth: envs.YAMIBO_AUTH,
|
||||
},
|
||||
youtube: {
|
||||
key: envs.YOUTUBE_KEY,
|
||||
clientId: envs.YOUTUBE_CLIENT_ID,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { Data, DataItem, Route } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { config } from '@/config';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { load } from 'cheerio';
|
||||
import { asyncPoolAll, fetchThread, generateDescription, getDate, bbsOrigin } from '../utils';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
export const route: Route = {
|
||||
name: 'BBS - 板块',
|
||||
categories: ['bbs'],
|
||||
path: '/bbs/forum/:fid/:type?',
|
||||
example: '/yamibo/bbs/forum/5/404',
|
||||
parameters: {
|
||||
fid: '板块 id,可从URL中提取。https://bbs.yamibo.com/forum-aa-b.html中的aa部分即为fid值',
|
||||
type: '板块子分类,网页中选中板块分类后URL中的typeid值',
|
||||
},
|
||||
maintainers: ['KarasuShin'],
|
||||
handler,
|
||||
features: {
|
||||
antiCrawler: true,
|
||||
requireConfig: [
|
||||
{
|
||||
optional: true,
|
||||
name: 'YAMIBO_SALT',
|
||||
description:
|
||||
'百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_saltkey 值',
|
||||
},
|
||||
{
|
||||
optional: true,
|
||||
name: 'YAMIBO_AUTH',
|
||||
description:
|
||||
'百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_auth 值',
|
||||
},
|
||||
],
|
||||
},
|
||||
description: `:::warning
|
||||
百合会BBS访问部分板块需要用户登录认证,请参考配置说明
|
||||
:::`,
|
||||
};
|
||||
|
||||
async function handler(ctx: Context): Promise<Data> {
|
||||
const fid = ctx.req.param('fid');
|
||||
const type = ctx.req.param('type');
|
||||
const { auth, salt } = config.yamibo;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('mod', 'forumdisplay');
|
||||
params.set('fid', fid);
|
||||
params.set('orderby', 'dateline');
|
||||
if (type) {
|
||||
params.set('filter', 'typeid');
|
||||
params.set('typeid', type);
|
||||
}
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (auth && salt) {
|
||||
headers.cookie = `EeqY_2132_saltkey=${salt}; EeqY_2132_auth=${auth}`;
|
||||
}
|
||||
|
||||
const link = `${bbsOrigin}/forum.php?${params.toString()}`;
|
||||
|
||||
const $ = load(await ofetch<string>(link, { headers }));
|
||||
|
||||
const title = $('title').text().replace(' - 百合会 - Powered by Discuz!', '');
|
||||
|
||||
let items: DataItem[] = $('tbody[id^="normalthread_"]')
|
||||
.toArray()
|
||||
.map((item) => {
|
||||
const $item = $(item);
|
||||
const id = $item.attr('id')!.match(/\d+/)![0];
|
||||
const title = $item.find('th em').text() + $item.find('th .s.xst').text();
|
||||
const link = `${bbsOrigin}/thread-${id}-1-1.html`;
|
||||
const pubDate = getDate($item.find('td.by').first().find('em').text());
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
link,
|
||||
pubDate,
|
||||
};
|
||||
});
|
||||
|
||||
items = await asyncPoolAll(
|
||||
5,
|
||||
items,
|
||||
async (item) =>
|
||||
(await cache.tryGet(item.link!, async () => {
|
||||
let description: string | undefined;
|
||||
const { data } = await fetchThread(item.id!);
|
||||
if (data && !data.startsWith('<script type="text/javascript">')) {
|
||||
const $ = load(data);
|
||||
if ($('#postlist>div[id^="post_"]').length) {
|
||||
const op = $('#postlist>div[id^="post_"]').first();
|
||||
const postId = op.attr('id')?.match(/\d+/)?.[0];
|
||||
if (postId) {
|
||||
description = generateDescription(op, postId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
link: item.link,
|
||||
description,
|
||||
pubDate: item.pubDate,
|
||||
};
|
||||
})) as DataItem
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
link,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Data, DataItem, Route } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { load } from 'cheerio';
|
||||
import { fetchThread, generateDescription, getDate, bbsOrigin } from '../utils';
|
||||
|
||||
export const route: Route = {
|
||||
name: 'BBS - 讨论串',
|
||||
categories: ['bbs'],
|
||||
path: '/bbs/thread/:tid',
|
||||
example: '/yamibo/bbs/thread/541914',
|
||||
parameters: {
|
||||
tid: '讨论串 id,可从URL中提取。https://bbs.yamibo.com/forum.php?mod=viewthread&tid=xxxx中的xxx或https://bbs.yamibo.com/thread-aaa-b-c.html中的aaa部分即为tid值',
|
||||
},
|
||||
maintainers: ['KarasuShin'],
|
||||
handler,
|
||||
features: {
|
||||
antiCrawler: true,
|
||||
requireConfig: [
|
||||
{
|
||||
optional: true,
|
||||
name: 'YAMIBO_SALT',
|
||||
description:
|
||||
'百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_saltkey 值',
|
||||
},
|
||||
{
|
||||
optional: true,
|
||||
name: 'YAMIBO_AUTH',
|
||||
description:
|
||||
'百合会BBS登录后的认证信息,获取方式:1. 登录百合会BBS网页版 2. 打开浏览器开发者工具,切换到 Application 面板\n3. 点击侧边栏中的Storage -> Cookies -> https://bbs.yamibo.com 4. 复制 Cookie 中的 EeqY_2132_auth 值',
|
||||
},
|
||||
],
|
||||
},
|
||||
description: `:::warning
|
||||
百合会BBS访问部分讨论串需要用户登录认证,请参考配置说明
|
||||
:::`,
|
||||
};
|
||||
|
||||
async function handler(ctx: Context): Promise<Data> {
|
||||
const tid = ctx.req.param('tid');
|
||||
|
||||
const { data, link } = await fetchThread(tid, { ordertype: '1' });
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: '讨论串不存在',
|
||||
link,
|
||||
item: [],
|
||||
};
|
||||
}
|
||||
|
||||
const $ = load(data);
|
||||
const title = $('title').text().replace(' - 百合会 - Powered by Discuz!', '');
|
||||
const items: DataItem[] = $('#postlist>div[id^="post_"]')
|
||||
.toArray()
|
||||
.map((item) => {
|
||||
const $item = $(item);
|
||||
const isOP = !!$item.has('#fj').length;
|
||||
const postId = $item.attr('id')!.match(/\d+/)![0];
|
||||
const $tr = $item.find('table').find('tr').first();
|
||||
const profileBlock = $tr.find(`#favatar${postId}`);
|
||||
const nickName = profileBlock.find('.authi').text();
|
||||
const floor = isOP ? '主楼' : $tr.find(`#postnum${postId} em`).text();
|
||||
const link = isOP ? `${bbsOrigin}/forum.php?mod=viewthread&tid=${tid}` : `${bbsOrigin}/forum.php?mod=redirect&goto=findpost&ptid=${tid}&pid=${postId}`;
|
||||
const description = generateDescription($item, postId);
|
||||
|
||||
const createTime = $tr
|
||||
.find(`#authorposton${postId}`)
|
||||
.text()
|
||||
.match(/\d{4}(?:-\d{1,2}){2} \d{2}:\d{2}/)![0];
|
||||
|
||||
return {
|
||||
title: `${floor} - ${nickName}`,
|
||||
link,
|
||||
description,
|
||||
pubDate: getDate(createTime),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title,
|
||||
link,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '百合会',
|
||||
url: 'yamibo.com',
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import timezone from '@/utils/timezone';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { config } from '@/config';
|
||||
import asyncPool from 'tiny-async-pool';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import type { Cheerio, Element } from 'cheerio';
|
||||
|
||||
export const bbsOrigin = 'https://bbs.yamibo.com';
|
||||
|
||||
export function getDate(date: string): Date {
|
||||
return timezone(parseDate(date), 8);
|
||||
}
|
||||
|
||||
export async function fetchThread(
|
||||
tid: string,
|
||||
options?: {
|
||||
ordertype?: string;
|
||||
_dsign?: string;
|
||||
},
|
||||
retry = 0
|
||||
): Promise<{
|
||||
link: string;
|
||||
data?: string;
|
||||
}> {
|
||||
const { auth, salt } = config.yamibo;
|
||||
const params = new URLSearchParams();
|
||||
params.set('mod', 'viewthread');
|
||||
params.set('tid', tid);
|
||||
if (options?.ordertype) {
|
||||
params.set('ordertype', options.ordertype);
|
||||
}
|
||||
if (options?._dsign) {
|
||||
params.set('_dsign', options._dsign);
|
||||
}
|
||||
const link = `https://bbs.yamibo.com/forum.php?${params.toString()}`;
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (auth && salt) {
|
||||
headers.cookie = `EeqY_2132_saltkey=${salt}; EeqY_2132_auth=${auth}`;
|
||||
}
|
||||
|
||||
const data = await ofetch<string>(link, { headers });
|
||||
|
||||
// sometimes may trigger anti-crawling measures
|
||||
if (data.startsWith('<script type="text/javascript">') && retry <= 3) {
|
||||
let script = data.match(/<script type="text\/javascript">([\S\s]*?)<\/script>/)![1];
|
||||
script = script.replace(/= location;|=location;/, '=fakeLocation;');
|
||||
script = script.replace('location.replace', 'foo');
|
||||
script = script.replace('location.assign', 'foo');
|
||||
script = script.replace(/location\[[^\]]*]\(/, 'foo(');
|
||||
script = script.replace(/location\[[^\]]*]=/, 'window.locationValue=');
|
||||
script = script.replace('location.href=', 'window.locationValue=');
|
||||
script = script.replace('location=', 'window.locationValue=');
|
||||
const dom = new JSDOM(
|
||||
`<script>
|
||||
function foo(value) { window.locationValue = value; };
|
||||
fakeLocation = { href: '', replace: foo, assign: foo };
|
||||
Object.defineProperty(fakeLocation, 'href', {
|
||||
set: function (value) {
|
||||
window.locationValue = value;
|
||||
}
|
||||
});
|
||||
${script}
|
||||
</script>`,
|
||||
{
|
||||
runScripts: 'dangerously',
|
||||
}
|
||||
);
|
||||
const locationValue = dom.window.locationValue;
|
||||
if (locationValue) {
|
||||
const searchParams = new URLSearchParams(locationValue);
|
||||
const _dsign = searchParams.get('_dsign');
|
||||
if (_dsign) {
|
||||
options = {
|
||||
...options,
|
||||
_dsign,
|
||||
};
|
||||
}
|
||||
}
|
||||
return await fetchThread(tid, options, ++retry);
|
||||
}
|
||||
|
||||
return {
|
||||
link,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDescription($item: Cheerio<Element>, postId: string) {
|
||||
const content = $item.find(`#postmessage_${postId}`).parent();
|
||||
content.find('img').each((_, img) => {
|
||||
const src = img.attribs.zoomfile ?? img.attribs.src;
|
||||
img.attribs.src = `${bbsOrigin}/${src}`;
|
||||
});
|
||||
let description = content.html() ?? '';
|
||||
|
||||
const images = $item.find('.pattl img').toArray();
|
||||
for (const img of images) {
|
||||
const src = img.attribs.zoomfile ?? img.attribs.src;
|
||||
description += `<img src="${bbsOrigin}/${src}" />`;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
export async function asyncPoolAll<IN, OUT>(poolLimit: number, array: readonly IN[], iteratorFn: (generator: IN) => Promise<OUT>) {
|
||||
const results: Awaited<OUT[]> = [];
|
||||
for await (const result of asyncPool(poolLimit, array, iteratorFn)) {
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user