fix(route): Fix xiaoheihe (#14764)

This commit is contained in:
tssujt
2024-03-19 13:06:27 +08:00
committed by GitHub
parent ccad5e8f87
commit 2dd1b75f7b
9 changed files with 312 additions and 179 deletions
-6
View File
@@ -520,12 +520,6 @@ router.get('/gov/guangdong/eea/:caty', lazyloadRouteHandler('./routes/gov/guangd
// 日本国外務省記者会見
router.get('/go.jp/mofa', lazyloadRouteHandler('./routes/go.jp/mofa/main'));
// 小黑盒
router.get('/xiaoheihe/user/:id', lazyloadRouteHandler('./routes/xiaoheihe/user'));
router.get('/xiaoheihe/news', lazyloadRouteHandler('./routes/xiaoheihe/news'));
router.get('/xiaoheihe/discount/:platform?', lazyloadRouteHandler('./routes/xiaoheihe/discount'));
// ebb
router.get('/ebb', lazyloadRouteHandler('./routes/ebb'));
@@ -1,71 +0,0 @@
const got = require('@/utils/got');
module.exports = async (ctx) => {
const platform = ctx.params.platform || 'pc';
const response = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/game/get_game_list_v3?sort_type=discount&filter_head=${platform}&offset=0&limit=30&os_type=web`,
});
const data = response.data.result.games;
ctx.state.data = {
title: `小黑盒${platform.toUpperCase()}游戏折扣`,
link: 'https://xiaoheihe.cn/games/index',
item: data.map((item) => {
const title = `${item.name}${item.name_en ? '/' + item.name_en : ''}`;
let description = `
<img src="${item.image}"/> <br/>
`;
if (item.platform_infos) {
for (const platform of item.platform_infos) {
if (platform.price) {
if (platform.key) {
description += `平台: ${platform.key}<br/>>`;
}
if (platform.price.current) {
description += `当前价格: ${platform.price.current}${platform.price.discount === platform.price.lowest_discount ? '[史低]' : ''}<br/>`;
}
if (platform.price.initial) {
description += `原价: ${platform.price.initial}<br/>`;
}
if (platform.price.discount_desc) {
description += `折扣力度: ${platform.price.discount_desc}<br/>`;
}
if (platform.price.deadline_date) {
description += `截止时间: ${platform.price.deadline_date}<br/>`;
}
description += '<br/>';
}
}
} else {
if (item.price) {
description += `平台: ${platform.toUpperCase()}<br/>`;
if (item.price.discount) {
description += `折扣力度: ${(100 - item.price.discount) / 10}折<br/>`;
}
if (item.price.initial && item.price.discount) {
const current = Math.round((item.price.initial * (100 - item.price.discount)) / 100);
description += `当前价格: ${current}${item.price.discount === item.price.lowest_discount ? '[史低]' : ''}&nbsp;&nbsp;`;
}
if (item.price.initial) {
description += `原价: ${item.price.initial}<br/>`;
}
if (item.score) {
description += `评分: ${item.score}<br/>`;
}
description += '<br/>';
}
}
let link = `https://xiaoheihe.cn/games/detail/${item.steam_appid}`;
if (platform === 'pc') {
link = `https://store.steampowered.com/app/${item.steam_appid}`;
}
return {
title,
description,
link,
};
}),
};
};
-51
View File
@@ -1,51 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
module.exports = async (ctx) => {
const response = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/maxnews/app/list?tag=-1&offset=0&limit=30&rec_mark=timeline&heybox_id=12777814&imei=867252032615972&os_type=Android&os_version=9&version=1.1.55&_time=1551801017&hkey=b28cd7a1cba463b4d9176ba2f8f42d35`,
});
const data = response.data.result.filter((item) => item.content_type !== 7);
const result = await Promise.all(
data.map(async (item) => {
const newsId = item.newsid;
const cacheKey = `xiaoheihe_${newsId}`;
// 最终需要返回的对象
const news = {
title: item.title,
pubDate: item.data,
link: `https://api.xiaoheihe.cn/maxnews/app/share/detail/${newsId}`,
guid: cacheKey,
};
// 判断缓存中是否存在
const cacheValue = await ctx.cache.get(cacheKey);
if (cacheValue) {
news.description = cacheValue;
} else {
const newsUrl = `https://api.xiaoheihe.cn/maxnews/app/detail/${newsId}?from_tag=-1&newsid=${newsId}&rec_mark=timeline&pos=2&index=1&page_tab=1&from_recommend_list=3&h_src=LTE%3D&os_type=Android&os_version=9&hkey=c25f13a25787311aca69d031280f4682&imei=867252032615972&version=1.2.57&_time=1552533062&heybox_id=12777814`;
const article = await got({
method: 'get',
url: newsUrl,
});
// 解析html内容
const $ = cheerio.load(article.data);
const content = $('.article-content').html().replaceAll('data-original', 'src');
// 存放到缓存区
ctx.cache.set(cacheKey, content);
news.description = content;
}
return news;
})
);
ctx.state.data = {
title: `小黑盒游戏新闻`,
link: `https://xiaoheihe.cn/community/index`,
item: result,
};
};
-51
View File
@@ -1,51 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const { id } = ctx.params;
const limit = Number.parseInt(ctx.query.limit) || 10;
const rootUrl = 'https://api.xiaoheihe.cn';
const currentUrl = `${rootUrl}/bbs/web/profile/post/links?userid=${id}&limit=${limit}&offset=0&version=999.0.0`;
const data = await got(currentUrl).then((response) => response.data);
const username = data.user.username;
const articleList = data.post_links.map((post) => ({
title: post.title || post.description,
pubDate: parseDate(post.create_at * 1000),
link: post.share_url,
linkId: post.linkid,
}));
const items = await Promise.all(
articleList.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const url = `${rootUrl}/bbs/app/api/share/data/?offset=0&limit=3&link_id=${item.linkId}&os_type=web`;
const link = await got(url).then((response) => response.data.link);
const content = link.content;
const video = link.video;
let description;
for (const item of content) {
if (item.type === 'html') {
const $ = cheerio.load(item.text, { decodeEntities: false });
$('[data-gameid]').each((i, e) => $(e).remove());
$('[data-original]').each((i, e) => $(e).replaceWith(`<img src = "${$(e).attr('data-original')}">`));
description = $.html();
}
}
if (video) {
description = `<video src="${video.video_url}" poster="${video.thumb}" controls="controls"></video>`;
}
item.description = description;
return item;
})
)
);
ctx.state.data = {
title: `${username} 的动态`,
link: `https://xiaoheihe.cn/community/user/${id}/post_list`,
item: items,
};
};
+102
View File
@@ -0,0 +1,102 @@
import { Route } from '@/types';
import got from '@/utils/got';
export const route: Route = {
path: '/discount/:platform',
categories: ['game'],
example: '/xiaoheihe/discount/pc',
parameters: { platform: '平台分类,见下表' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '游戏折扣',
maintainers: ['tssujt'],
handler,
description: `| PC | Switch | PSN | Xbox |
| ----- | ------ | ----- | ----- |
| pc | switch | psn | xbox |`,
};
const PLATFORM_MAP = {
pc: 'PC',
switch: 'Switch',
psn: 'PSN',
xbox: 'Xbox',
};
async function handler(ctx) {
const platform = ctx.req.param('platform');
const response = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/game/get_game_list_v3?sort_type=discount&filter_head=${platform}&offset=0&limit=30&os_type=web`,
});
const data = response.data.result.games;
const items = data.map((item) => {
const title = `${item.name}${item.name_en ? '/' + item.name_en : ''}`;
let description = `
<img src="${item.image}"/> <br/>
`;
if (item.platform_infos) {
for (const platform of item.platform_infos) {
if (platform.price) {
if (platform.key) {
description += `平台: ${platform.key}<br/>>`;
}
if (platform.price.current) {
description += `当前价格: ${platform.price.current}${platform.price.discount === platform.price.lowest_discount ? '[史低]' : ''}<br/>`;
}
if (platform.price.initial) {
description += `原价: ${platform.price.initial}<br/>`;
}
if (platform.price.discount_desc) {
description += `折扣力度: ${platform.price.discount_desc}<br/>`;
}
if (platform.price.deadline_date) {
description += `截止时间: ${platform.price.deadline_date}<br/>`;
}
description += '<br/>';
}
}
} else {
if (item.price) {
description += `平台: ${platform.toUpperCase()}<br/>`;
if (item.price.discount) {
description += `折扣力度: ${(100 - item.price.discount) / 10}折<br/>`;
}
if (item.price.initial && item.price.discount) {
const current = Math.round((item.price.initial * (100 - item.price.discount)) / 100);
description += `当前价格: ${current}${item.price.discount === item.price.lowest_discount ? '[史低]' : ''}&nbsp;&nbsp;`;
}
if (item.price.initial) {
description += `原价: ${item.price.initial}<br/>`;
}
if (item.score) {
description += `评分: ${item.score}<br/>`;
}
description += '<br/>';
}
}
let link = `https://api.xiaoheihe.cn/game/share_game_detail?appid=${item.steam_appid}`;
if (platform === 'pc') {
link = `https://store.steampowered.com/app/${item.steam_appid}`;
}
return {
title,
description,
link,
};
});
return {
title: `小黑盒 ${PLATFORM_MAP[platform]} 游戏折扣`,
link: `https://xiaoheihe.cn`,
item: items,
};
}
+6
View File
@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '小黑盒',
url: 'xiaoheihe.cn',
};
+60
View File
@@ -0,0 +1,60 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import { calculate } from './util';
export const route: Route = {
path: '/news',
categories: ['game'],
example: '/xiaoheihe/news',
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '游戏新闻',
maintainers: ['tssujt'],
handler,
};
async function handler() {
const response = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/bbs/app/feeds/news?lang=zh-cn&limit=20&offset=0&tag=-1&version=1.3.303`,
});
const data = response.data.result.links.filter((item) => item.linkid !== undefined);
let items = data.map((item) => ({
linkId: item.linkid,
link: `https://api.xiaoheihe.cn/v3/bbs/app/api/web/share?link_id=${item.linkid}`,
title: item.title,
pubDate: parseDate(item.modify_at * 1000),
description: item.description,
}));
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
const dataUrl = calculate(
`https://api.xiaoheihe.cn/bbs/app/api/share/data/?os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&offset=0&limit=3&link_id=${item.linkId}&use_concept_type=`
);
const dataResponse = await got({
method: 'get',
url: dataUrl,
});
item.description = dataResponse.data.link.content[0].text;
delete item.linkId;
return item;
})
)
);
return {
title: `小黑盒游戏新闻`,
link: `https://xiaoheihe.cn`,
item: items,
};
}
+69
View File
@@ -0,0 +1,69 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import { calculate } from './util';
export const route: Route = {
path: '/user/:id',
categories: ['game'],
example: '/xiaoheihe/user/30664023',
parameters: { id: '用户 ID' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '用户动态',
maintainers: ['tssujt'],
handler,
};
async function handler(ctx) {
const userId = ctx.req.param('id');
const userResponse = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/bbs/app/profile/user/profile?lang=zh-cn&version=1.3.303&userid=${userId}`,
});
const username = userResponse.data.result.account_detail.username;
const response = await got({
method: 'get',
url: `https://api.xiaoheihe.cn/bbs/app/profile/events?lang=zh-cn&version=1.3.303&userid=${userId}&list_type=moment`,
});
const data = response.data.result.moments.filter((item) => item.linkid !== undefined);
let items = data.map((item) => ({
linkId: item.linkid,
link: `https://api.xiaoheihe.cn/v3/bbs/app/api/web/share?link_id=${item.linkid}`,
title: item.title,
pubDate: parseDate(item.modify_at * 1000),
description: item.description,
}));
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
const dataUrl = calculate(
`https://api.xiaoheihe.cn/bbs/app/api/share/data/?os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&offset=0&limit=3&link_id=${item.linkId}&use_concept_type=`
);
const dataResponse = await got({
method: 'get',
url: dataUrl,
});
item.description = dataResponse.data.link.content[0].text;
delete item.linkId;
return item;
})
)
);
return {
title: `${username} 的动态`,
link: `https://xiaoheihe.cn`,
item: items,
};
}
+75
View File
@@ -0,0 +1,75 @@
// Copied from https://github.com/huandu/heybox-url
import { createHash } from 'node:crypto';
const dict = 'JKMNPQRTX1234OABCDFG56789H';
const md5 = (str) => {
const h = createHash('md5');
h.update(str);
return h.digest();
};
function c0(v) {
return c1(v) ^ c2(v) ^ c3(v);
}
function c1(v) {
return c2(c3(convertByte(v)));
}
function c2(v) {
return c3(convertByte(v));
}
function c3(v) {
return convertByte(v) ^ v;
}
function convertByte(v) {
return v & 0x80 ? 0xff & ((v << 1) ^ 0x1b) : v << 1;
}
/**
* Calculate checksum for a 4-byte data.
* @param {number[]} data
* @returns {number}
*/
const checksum = (data) =>
[c0(data[0]) ^ c1(data[1]) ^ c2(data[2]) ^ c3(data[3]), c3(data[0]) ^ c0(data[1]) ^ c1(data[2]) ^ c2(data[3]), c2(data[0]) ^ c3(data[1]) ^ c0(data[2]) ^ c1(data[3]), c1(data[0]) ^ c2(data[1]) ^ c3(data[2]) ^ c0(data[3])].reduce(
(prev, value) => prev + value
) % 100;
export const calculate = (url, timestamp = 0, nonce = '') => {
timestamp ||= Math.trunc(Date.now() / 1000);
nonce ||= md5(Math.random().toString()).toString('hex').toUpperCase();
const { pathname } = new URL(url);
const ts = timestamp + 1;
const u = '/' + pathname.split('/').filter(Boolean).join('/') + '/';
let key = '';
const nonceHash = md5((nonce + dict).replaceAll(/\D/g, ''))
.toString('hex')
.toLowerCase();
const rnd = md5(ts + u + nonceHash)
.toString('hex')
.replaceAll(/\D/g, '')
.slice(0, 9)
.padEnd(9, '0');
for (let c = +rnd, i = 0; i < 5; i++) {
const u = c % dict.length;
c = Math.trunc(c / dict.length);
key += dict[u];
}
const suffix = checksum([...key].slice(-4).map((c) => c.codePointAt(0)))
.toString()
.padStart(2, '0');
const query = `hkey=${key}${suffix}&_time=${timestamp}&nonce=${nonce}`;
const urlObj = new URL(url);
urlObj.search += urlObj.search ? '&' + query : query;
return urlObj.toString();
};