mirror of
https://github.com/sune-org/chatroom.git
synced 2026-01-13 16:17:59 +00:00
82 lines
3.6 KiB
JavaScript
82 lines
3.6 KiB
JavaScript
const CORS_HEADERS = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
'Access-Control-Max-Age': '86400'
|
|
};
|
|
|
|
const withCORS = (resp) => {
|
|
const headers = new Headers(resp.headers);
|
|
Object.entries(CORS_HEADERS).forEach(([k, v]) => headers.set(k, v));
|
|
return new Response(resp.body, { ...resp, headers });
|
|
};
|
|
|
|
export default {
|
|
async fetch(req, env) {
|
|
const url = new URL(req.url);
|
|
if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
if (url.pathname !== '/ws') return withCORS(new Response('not found', { status: 404 }));
|
|
if (req.method !== 'GET' || req.headers.get('Upgrade') !== 'websocket') return withCORS(new Response('method not allowed', { status: 405 }));
|
|
return env.CHATSUNE_DURABLE_OBJECT.get(env.CHATSUNE_DURABLE_OBJECT.idFromName("global")).fetch(req);
|
|
}
|
|
};
|
|
|
|
export class ChatsuneDurableObject {
|
|
constructor(state, env) {
|
|
this.state = state;
|
|
this.env = env;
|
|
this.sockets = new Set();
|
|
this.rateLimiter = new Map();
|
|
this.state.blockConcurrencyWhile(async () => this.messages = await this.state.storage.get('messages') || []);
|
|
}
|
|
|
|
broadcast(message) { this.sockets.forEach(s => s.readyState === WebSocket.OPEN && s.send(JSON.stringify(message))); }
|
|
|
|
broadcastConnectionCount() { this.broadcast({ type: 'CONNECTION_COUNT', payload: { count: this.sockets.size } }); }
|
|
|
|
async fetch(req) {
|
|
if (req.headers.get('Upgrade') !== 'websocket') return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json', ...CORS_HEADERS } });
|
|
|
|
const [client, server] = Object.values(new WebSocketPair());
|
|
server.accept();
|
|
this.sockets.add(server);
|
|
server.ip = req.headers.get('cf-connecting-ip');
|
|
|
|
server.addEventListener('message', async (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
switch (data.type) {
|
|
case 'USER_JOINED': {
|
|
server.username = data.payload.name || `anon-${crypto.randomUUID().slice(0, 8)}`;
|
|
server.send(JSON.stringify({ type: 'HISTORY', payload: this.messages }));
|
|
this.broadcast({ type: 'NEW_MESSAGE', payload: { author: { name: 'system' }, text: `${server.username} has joined.`, timestamp: new Date().toISOString() } });
|
|
this.broadcastConnectionCount();
|
|
break;
|
|
}
|
|
case 'NEW_MESSAGE': {
|
|
if (!server.username) return;
|
|
const now = Date.now(), timestamps = (this.rateLimiter.get(server.ip) || []).filter(t => now - t < 20000);
|
|
if (timestamps.length >= 3) return server.send(JSON.stringify({ type: 'ERROR', payload: 'Rate limit exceeded.' }));
|
|
this.rateLimiter.set(server.ip, [...timestamps, now]);
|
|
|
|
const messagePayload = { author: { name: server.username }, text: data.payload.text, timestamp: new Date().toISOString() };
|
|
this.messages.push(messagePayload);
|
|
this.messages = this.messages.slice(-4);
|
|
this.broadcast({ type: 'NEW_MESSAGE', payload: messagePayload });
|
|
await this.state.storage.put('messages', this.messages);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) { /* Ignore invalid JSON */ }
|
|
});
|
|
|
|
server.addEventListener('close', () => {
|
|
this.sockets.delete(server);
|
|
if (server.username) this.broadcast({ type: 'NEW_MESSAGE', payload: { author: { name: 'system' }, text: `${server.username} has left.`, timestamp: new Date().toISOString() } });
|
|
this.broadcastConnectionCount();
|
|
});
|
|
|
|
return new Response(null, { status: 101, webSocket: client });
|
|
}
|
|
}
|