- Add `UserBind` model to Prisma schema for linking Logto and Kaiqiu user IDs. - Implement `/api/account/bind` endpoint (GET/PUT) to handle Kaiqiu account binding and retrieval. - Refactor `KaiqiuService` to include a `login` method that performs browser automation-like login on kaiqiuwang.cc using `cheerio` and `fetch`. - Update `UserCenter` page to include a new `BindKaiqiuAccount` component. - Restructure `WebSocketService`: - Change from a simple global connection map to a per-user client tracking system (`#userClients`). - Update topic naming conventions (e.g., `ONLINE_MEMBER_CHANGE` -> `MEMBER_CHANGE`). - Add client-side broadcast capabilities for user-specific events like `MY_CLIENT_ONLINE`. - Add support for dynamic subscription handling (SUB/UNSUB) via WebSocket messages. - Update `verifyLogtoToken` to accept either `Headers` or a raw token string for flexibility in WebSocket auth. - Minor fixes: typo corrections in `WSTopic` enum and commented out debug logs. BREAKING CHANGE: WebSocket payload structure has changed. The `ws.data` property now contains a `WsPaylaod` object with a `user` field (previously it was a JSON string of the JWT payload). The `WSTopic` names have been updated (e.g., `ONLINE_MEMBER_CHANGE` is now `MEMBER_CHANGE`), requiring updates to any client code subscribing to these topics.
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { fromCustomMessage, toCustomMessage, WSTopic } from "../utils/common";
|
|
import { EventSubscribeService } from "./EventSubscribeService";
|
|
import type { WsPaylaod } from "../types";
|
|
|
|
const publicTopics = [
|
|
WSTopic.MEMBER_CHANGE,
|
|
];
|
|
|
|
type BunServerWebSocket = Bun.ServerWebSocket<WsPaylaod>;
|
|
|
|
export class WebSocketService {
|
|
static #connections = new Set<BunServerWebSocket>();
|
|
static #userSubTopics = new Map<string, Set<string>>();
|
|
static #userClients = new Map<string, Set<BunServerWebSocket>>();
|
|
|
|
static async addConnection(ws: BunServerWebSocket) {
|
|
this.#connections.add(ws);
|
|
const user = ws.data.user;
|
|
let isNewMember = false;
|
|
if (!this.#userClients.has(user.sub)) {
|
|
this.#userClients.set(user.sub, new Set());
|
|
isNewMember = true;
|
|
}
|
|
this.#userClients.get(user.sub)?.add(ws);
|
|
await this.#initSubscribe(ws);
|
|
const message = toCustomMessage(WSTopic.MEMBER_CHANGE, this.#userClients.size);
|
|
if (isNewMember) {
|
|
this.broadcast(WSTopic.MEMBER_CHANGE, message);
|
|
} else {
|
|
ws.send(message);
|
|
}
|
|
this.userClientsBroadcast(
|
|
ws.data.user.sub,
|
|
WSTopic.MY_CLIENT_ONLINE,
|
|
toCustomMessage(WSTopic.MY_CLIENT_ONLINE, this.#userClients.get(user.sub)?.size),
|
|
);
|
|
}
|
|
|
|
static async #initSubscribe(ws: BunServerWebSocket) {
|
|
const user = ws.data.user
|
|
const subEvets = await EventSubscribeService.getEvents(user.sub).then(e => e.map(v => `event:${v}`));
|
|
this.userSub(ws, user.sub, publicTopics);
|
|
this.userSub(ws, user.sub, subEvets);
|
|
}
|
|
|
|
static publish(ws: BunServerWebSocket, topic: string, message: string, withSelf?: boolean) {
|
|
ws.publish(topic, message);
|
|
if (withSelf) {
|
|
ws.send(message);
|
|
}
|
|
}
|
|
|
|
static async userClientsBroadcast(user: string, topic: string, message: string) {
|
|
this.#userClients.get(user)?.forEach(ws => ws.send(message));
|
|
}
|
|
|
|
static removeConnection(ws: BunServerWebSocket) {
|
|
this.#connections.delete(ws);
|
|
console.debug('Someone disconnected. User: ', ws.data.user.sub);
|
|
this.#userClients.get(ws.data.user.sub)?.delete(ws);
|
|
if (this.#userClients.get(ws.data.user.sub)?.size === 0) {
|
|
this.#userClients.delete(ws.data.user.sub);
|
|
this.publish(ws, WSTopic.MEMBER_CHANGE, toCustomMessage(WSTopic.MEMBER_CHANGE, this.#userClients.size));
|
|
}
|
|
this.userClientsBroadcast(
|
|
ws.data.user.sub,
|
|
WSTopic.MY_CLIENT_ONLINE,
|
|
toCustomMessage(WSTopic.MY_CLIENT_ONLINE, this.#userClients.get(ws.data.user.sub)?.size),
|
|
);
|
|
}
|
|
|
|
static userSub(ws: BunServerWebSocket, user: string, topics: string[]) {
|
|
if (!this.#userSubTopics.has(user)) {
|
|
this.#userSubTopics.set(user, new Set<string>());
|
|
}
|
|
if (!topics.length) return;
|
|
console.debug('User %s subscribe keys: %s', user, topics.join(','));
|
|
topics.forEach(topic => {
|
|
this.#userSubTopics.get(user)?.add(topic);
|
|
ws.subscribe(topic);
|
|
});
|
|
}
|
|
|
|
static broadcast(topic: string, message: string) {
|
|
this.#connections.values().forEach(con => {
|
|
if (con.isSubscribed(topic)) {
|
|
con.send(message);
|
|
}
|
|
});
|
|
}
|
|
|
|
static processMessage(ws: BunServerWebSocket, message: string | Buffer<ArrayBuffer>) {
|
|
const { clientTopic: action, data } = fromCustomMessage(message.toString());
|
|
if (!action) return;
|
|
console.debug('Recieve action: %s', action, message);
|
|
switch (action) {
|
|
case WSTopic.SUB:
|
|
ws.subscribe(data.topic);
|
|
break;
|
|
case WSTopic.UNSUB:
|
|
ws.unsubscribe(data.topic);
|
|
break;
|
|
case WSTopic.SEND:
|
|
ws.publish(data.topic, data.message);
|
|
break;
|
|
default:
|
|
this.broadcast("Test", 'This is a broadcast message. Everyone should recieved.');
|
|
break;
|
|
}
|
|
}
|
|
} |