mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-01 14:57:14 +08:00
feat(route): binance announcement (#16400)
* feat: binance announcement * fix: remove baseUrl in targetItem * fix: add type guard remove all any * fix: type guard function logic * fix: review issues
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { DataItem, Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { AnnouncementCatalog, AnnouncementsConfig } from './types';
|
||||
|
||||
interface AnnouncementFragment {
|
||||
reactRoot: [{ id: 'Fragment'; children: { id: string; props: object }[]; props: object }];
|
||||
}
|
||||
|
||||
const ROUTE_PARAMETERS_CATALOGID_MAPPING = {
|
||||
'new-cryptocurrency-listing': 48,
|
||||
'latest-binance-news': 49,
|
||||
'latest-activities': 93,
|
||||
'new-fiat-listings': 50,
|
||||
'api-updates': 51,
|
||||
'crypto-airdrop': 128,
|
||||
'wallet-maintenance-updates': 157,
|
||||
delisting: 161,
|
||||
};
|
||||
|
||||
function assertAnnouncementsConfig(playlist: unknown): playlist is AnnouncementFragment {
|
||||
if (!playlist || typeof playlist !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (!('reactRoot' in (playlist as { reactRoot: unknown[] }))) {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray((playlist as { reactRoot: unknown[] }).reactRoot)) {
|
||||
return false;
|
||||
}
|
||||
if ((playlist as { reactRoot: { id: string }[] }).reactRoot?.[0]?.id !== 'Fragment') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function assertAnnouncementsConfigList(props: unknown): props is { config: { list: AnnouncementsConfig[] } } {
|
||||
if (!props || typeof props !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (!('config' in props)) {
|
||||
return false;
|
||||
}
|
||||
if (!('list' in (props.config as { list: AnnouncementsConfig[] }))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const handler: Route['handler'] = async (ctx) => {
|
||||
const baseUrl = 'https://www.binance.com';
|
||||
const announcementCategoryUrl = `${baseUrl}/support/announcement`;
|
||||
const { type } = ctx.req.param<'/binance/announcement/:type'>();
|
||||
const language = ctx.req.header('Accept-Language');
|
||||
const headers = {
|
||||
Referer: baseUrl,
|
||||
'Accept-Language': language ?? 'en-US,en;q=0.9',
|
||||
};
|
||||
const announcementsConfig = (await cache.tryGet(`binance:announcements:${language}`, async () => {
|
||||
const announcementRes = await ofetch<string>(announcementCategoryUrl, { headers });
|
||||
const $ = cheerio.load(announcementRes);
|
||||
|
||||
const appData = JSON.parse($('#__APP_DATA').text());
|
||||
|
||||
const announcements = Object.values(appData.appState.loader.dataByRouteId as Record<string, object>).find((value) => 'playlist' in value) as { playlist: unknown };
|
||||
|
||||
if (!assertAnnouncementsConfig(announcements.playlist)) {
|
||||
throw new Error('Get announcement config failed');
|
||||
}
|
||||
|
||||
const listConfigProps = announcements.playlist.reactRoot[0].children.find((i) => i.id === 'TopicCardList')?.props;
|
||||
|
||||
if (!assertAnnouncementsConfigList(listConfigProps)) {
|
||||
throw new Error("Can't get announcement config list");
|
||||
}
|
||||
|
||||
return listConfigProps.config.list;
|
||||
})) as AnnouncementsConfig[];
|
||||
|
||||
const announcementCatalogId = ROUTE_PARAMETERS_CATALOGID_MAPPING[type];
|
||||
|
||||
if (!announcementCatalogId) {
|
||||
throw new Error(`${type} is not supported`);
|
||||
}
|
||||
|
||||
const targetItem = announcementsConfig.find((i) => i.url.includes(`c-${announcementCatalogId}`));
|
||||
|
||||
if (!targetItem) {
|
||||
throw new Error('Unexpected announcements config');
|
||||
}
|
||||
|
||||
const link = new URL(targetItem.url, baseUrl).toString();
|
||||
|
||||
const response = await ofetch<string>(link, { headers });
|
||||
|
||||
const $ = cheerio.load(response);
|
||||
const appData = JSON.parse($('#__APP_DATA').text());
|
||||
|
||||
const values = Object.values(appData.appState.loader.dataByRouteId as Record<string, object>);
|
||||
const catalogs = values.find((value) => 'catalogs' in value) as { catalogs: AnnouncementCatalog[] };
|
||||
const catalog = catalogs.catalogs.find((catalog) => catalog.catalogId === announcementCatalogId);
|
||||
|
||||
const item = await Promise.all(
|
||||
catalog!.articles.map((i) => {
|
||||
const link = `${announcementCategoryUrl}/${i.code}`;
|
||||
const item = {
|
||||
title: i.title,
|
||||
link,
|
||||
description: i.title,
|
||||
pubDate: parseDate(i.releaseDate),
|
||||
} as DataItem;
|
||||
return cache.tryGet(`binance:announcement:${i.code}:${language}`, async () => {
|
||||
const res = await ofetch(link, { headers });
|
||||
const $ = cheerio.load(res);
|
||||
const descriptionEl = $('#support_article > div').first();
|
||||
descriptionEl.find('style').remove();
|
||||
item.description = descriptionEl.html() ?? '';
|
||||
return item;
|
||||
}) as Promise<DataItem>;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
title: targetItem.title,
|
||||
link,
|
||||
description: targetItem.description,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
||||
export const route: Route = {
|
||||
path: '/announcement/:type',
|
||||
categories: ['finance'],
|
||||
example: '/binance/announcement/new-cryptocurrency-listing',
|
||||
parameters: {
|
||||
type: {
|
||||
description: 'Binance Announcement type',
|
||||
default: 'new-cryptocurrency-listing',
|
||||
options: [
|
||||
{ value: 'new-cryptocurrency-listing', label: 'New Cryptocurrency Listing' },
|
||||
{ value: 'latest-binance-news', label: 'Latest Binance News' },
|
||||
{ value: 'latest-activities', label: 'Latest Activities' },
|
||||
{ value: 'new-fiat-listings', label: 'New Fiat Listings' },
|
||||
{ value: 'api-updates', label: 'API Updates' },
|
||||
{ value: 'crypto-airdrop', label: 'Crypto Airdrop' },
|
||||
{ value: 'wallet-maintenance-updates', label: 'Wallet Maintenance Updates' },
|
||||
{ value: 'delisting', label: 'Delisting' },
|
||||
],
|
||||
},
|
||||
},
|
||||
name: 'Announcement',
|
||||
description: `
|
||||
Type category
|
||||
|
||||
- new-cryptocurrency-listing => New Cryptocurrency Listing
|
||||
- latest-binance-news => Latest Binance News
|
||||
- latest-activities => Latest Activities
|
||||
- new-fiat-listings => New Fiat Listings
|
||||
- api-updates => API Updates
|
||||
- crypto-airdrop => Crypto Airdrop
|
||||
- wallet-maintenance-updates => Wallet Maintenance Updates
|
||||
- delisting => Delisting
|
||||
`,
|
||||
maintainers: ['enpitsulin'],
|
||||
handler,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface AnnouncementsConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
imgUrl: string;
|
||||
}
|
||||
|
||||
export interface AnnouncementCatalog {
|
||||
articles: AnnouncementArticle[];
|
||||
catalogId: number;
|
||||
catalogName: string;
|
||||
catalogType: 1;
|
||||
catalogs: [];
|
||||
description: null;
|
||||
icon: string;
|
||||
parentCatalogId: null;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AnnouncementArticle {
|
||||
id: number;
|
||||
code: string;
|
||||
title: string;
|
||||
type: number;
|
||||
releaseDate: number;
|
||||
}
|
||||
Reference in New Issue
Block a user