my-kaiqiuwang/src/utils/server.ts
kyuuseiryuu 2774037012 feat(app): add push notification support and event management logic
- Introduce PermissionControlPanel in UserCenter for managing notification permissions.
- Update Service Worker (sw.ts) to handle background push notifications via Firebase Cloud Messaging.
- Implement EventSubscribeService logic to fetch active events, filter expired ones, and clean up subscriptions.
- Refactor KaiqiuService and parsing utilities to include eventId in event details.
- Remove deprecated ScheduleService and inline sendNotification logic.
- Update utility functions to support new types and notification checks.
- Add VAPI public key configuration and update Firebase exports.
2026-03-24 17:21:50 +09:00

113 lines
3.7 KiB
TypeScript

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-(?<uid>\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
}