import type { EventDetail, Player } from "../types"; import * as cheerio from "cheerio"; import { XCXAPI } from "../services/xcxApi"; import { KAIQIU_BASE_URL, LOGTO_DOMAIN } from "./common"; import { RedisClient } from "bun"; import { createRemoteJWKSet, jwtVerify } from 'jose'; import { LOGTO_RESOURCE } from "./constants"; const REQUIRED_ENVS = [ process.env.KAIQIUCC_TOKEN, process.env.REDIS, ]; 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 matchId 比赛 ID * @returns HTML */ export async function fetchEventContentHTML(matchId: string) { const url = `${KAIQIU_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, eventId: string): EventDetail { 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, eventId, title, players, } } export const extractBearerTokenFromHeaders = (authorization: string | null) => { if (!authorization) { return null; } if (!authorization.startsWith('Bearer')) { return authorization; } return authorization.slice(7); // The length of 'Bearer ' is 7 }; const jwks = createRemoteJWKSet(new URL(`${LOGTO_DOMAIN}/oidc/jwks`)); export const verifyLogtoToken = async (headers: Headers | string) => { const auth = typeof headers === 'string' ? headers : headers.get('Authorization'); const token = extractBearerTokenFromHeaders(auth); // console.debug('Authorization', { auth, token }); if (!token) return {}; const { payload } = await jwtVerify( // The raw Bearer Token extracted from the request header token, jwks, { // Expected issuer of the token, issued by the Logto server issuer: `${LOGTO_DOMAIN}/oidc`, // Expected audience token, the resource indicator of the current API audience: LOGTO_RESOURCE, } ); // console.debug('Payload', payload); // Sub is the user ID, used for user identification return payload }