UNPKG

rsshub

Version:
170 lines (146 loc) • 6.29 kB
import { DataItem, Route, ViewType } 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'], view: ViewType.Articles, 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, };