Files
chatroom/index.js

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 });
}
}