import type { IEventInfo, Player } from "../types"; import * as cheerio from "cheerio"; import { XCXAPI } from "../services/xcxApi"; import { BASE_URL } from "./common"; import { RedisClient } from "bun"; import dayjs from "dayjs"; const REQUIRED_ENVS = [ process.env.KAIQIUCC_TOKEN, process.env.REDIS, ]; console.debug('ENVS: \n%s', REQUIRED_ENVS.join('\n')); if (!REQUIRED_ENVS.every(v => !!v)) { console.error('Missing required environment variables. Please check your .env'); process.exit(1); } export const REDIS_CACHE_HOUR = Number(process.env.REDIS_CACHE_HOUR) || 8; console.debug('Cache hour: %s', REDIS_CACHE_HOUR); export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? ''); export const redis = new RedisClient(process.env.REDIS ?? ''); export const htmlRequestHeaders = { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "accept-language": "zh-CN,zh;q=0.8", "cache-control": "max-age=0", "priority": "u=0, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1", "sec-gpc": "1", "upgrade-insecure-requests": "1", "cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1" } /** * @param tagid 俱乐部 ID */ export async function listEvent(tagid: string): Promise { const key = `my-kaiqiuwang:club-events:${tagid}`; let html = await redis.get(key).catch(() => ''); if (!html) { html = await fetchEventListHTML(tagid); redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html); } return await parseEventList(html); } /** * @param tagid 俱乐部 ID * @return HTML */ export async function fetchEventListHTML(tagid: string) { const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`; const resp = await fetch(url, { headers: htmlRequestHeaders }); return resp.text() ?? ''; } export async function parseEventList(html: string) { const $ = cheerio.load(html); const blockList = $('div.event_list > ol > li'); const list: IEventInfo[] = []; for (const block of blockList) { const titleEl = $(block).find('.event_title'); const title = titleEl.text(); const eventPath = $(titleEl).find('a').attr('href') ?? ''; const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim(); const eventURL = `${BASE_URL}/home/${eventPath}`; const matchId = /\S+-(\d+).html$/.exec(eventPath)?.[1] ?? ''; let eventPage = await redis.get(`my-kaiqiuwang:match:${matchId}`) ?? ''; if (!eventPage) { eventPage = await fetch(eventURL, { headers: htmlRequestHeaders }).then(res => res.text() ?? ''); await redis.setex(`my-kaiqiuwang:match:${matchId}`, 60 * 60 * 10, eventPage) } const $eventPage = cheerio.load(eventPage); const eventContent = $eventPage('.event_content').text().replace(/(\r|\n)/g, ',').split(',').filter(Boolean).join(' '); const { y, M, D, H, m} = /比赛开始:.*?(?\d{4})年(?\d{2})月(?\d{2})日 \w+ (?\d{2}):(?\d{2})/ .exec(eventContent)?.groups ?? {}; const startDate = y ? `${y}-${M}-${D} ${H}:${m}` : ''; const event: IEventInfo = { title, info: [`比赛时间:${startDate}`, place], url: eventURL, startDate, matchId, isFinished: dayjs(startDate).isBefore(), } list.push(event); } return list; } /** * * @param matchId 比赛 ID * @returns HTML */ export async function fetchEventContentHTML(matchId: string) { const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`; const resp = await fetch(url, { headers: htmlRequestHeaders }); return resp.text() ?? '' } export function parseEventInfo(html: string) { const $ = cheerio.load(html); const title = $('h2.title a').text(); const itemHref = $('.sub_menu a.active').attr('href') ?? ''; const itemId = /\S+item_id=(\d+)$/.exec(itemHref)?.[1] ?? ''; const players: Player[] = []; const playersEl = $('.thumb'); for (const player of playersEl) { const img = $(player).find('.image img').attr('src') ?? ''; const name = $(player).find('h6').text().trim(); const uid = /space-(?\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? ''; const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, ''); const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? ''; players.push({ name, avatar: img, score, info, uid }); } return { itemId, title, players, } } export async function getMatchInfo(matchId: string) { const key = `my-kaiqiuwang:match-member:${matchId}`; let html = await redis.get(key).catch(() => ''); if (!html) { html = await fetchEventContentHTML(matchId); redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html); } return parseEventInfo(html); }