- 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.
113 lines
3.7 KiB
TypeScript
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
|
|
}
|