|
|
import type { ServerWebSocket } from "bun"; |
|
|
|
|
|
interface Session { |
|
|
sessionId: string; |
|
|
sender: string; |
|
|
createdAt: number; |
|
|
ws?: ServerWebSocket; |
|
|
} |
|
|
|
|
|
interface ComicSession { |
|
|
sessionId: string; |
|
|
sender: string; |
|
|
password: string; |
|
|
data: any; |
|
|
createdAt: number; |
|
|
expiresAt: number; |
|
|
clientWs?: ServerWebSocket; |
|
|
} |
|
|
|
|
|
const sessions = new Map<string, Session>(); |
|
|
const comicSessions = new Map<string, ComicSession>(); |
|
|
const botConnections = new Set<ServerWebSocket>(); |
|
|
const clientConnections = new Map<string, ServerWebSocket>(); |
|
|
|
|
|
export function handleWebSocketConnection(ws: ServerWebSocket) { |
|
|
|
|
|
ws.data = { |
|
|
handleMessage: (message: any) => handleMessage(ws, message), |
|
|
cleanup: () => cleanup(ws), |
|
|
}; |
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
type: 'connected', |
|
|
message: 'Connected to server' |
|
|
})); |
|
|
} |
|
|
|
|
|
function handleMessage(ws: ServerWebSocket, message: any) { |
|
|
const { type, sessionId } = message; |
|
|
|
|
|
switch (type) { |
|
|
case 'bot_connect': |
|
|
botConnections.add(ws); |
|
|
console.log('[WebSocket] Bot connected, total bots:', botConnections.size); |
|
|
break; |
|
|
|
|
|
case 'create_session': |
|
|
sessions.set(sessionId, { |
|
|
sessionId, |
|
|
sender: message.sender, |
|
|
createdAt: Date.now(), |
|
|
}); |
|
|
console.log('[WebSocket] Session created:', sessionId); |
|
|
break; |
|
|
|
|
|
case 'force_cleanup_session': |
|
|
handleForceCleanupSession(message); |
|
|
break; |
|
|
|
|
|
case 'user_connect': |
|
|
const session = sessions.get(sessionId); |
|
|
if (session) { |
|
|
session.ws = ws; |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'session_valid', |
|
|
sessionId |
|
|
})); |
|
|
} else { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'session_invalid', |
|
|
message: 'Session not found or expired' |
|
|
})); |
|
|
} |
|
|
break; |
|
|
|
|
|
case 'registration_submit': |
|
|
handleRegistrationSubmit(sessionId, message.data); |
|
|
break; |
|
|
|
|
|
case 'comic_stream': |
|
|
case 'chapter_stream': |
|
|
handleComicStream(message); |
|
|
break; |
|
|
|
|
|
case 'chapter_fetched': |
|
|
handleChapterFetched(message); |
|
|
break; |
|
|
|
|
|
case 'request_comic_data': |
|
|
handleComicDataRequest(ws, message); |
|
|
break; |
|
|
|
|
|
case 'validate_comic_password': |
|
|
handlePasswordValidation(ws, message); |
|
|
break; |
|
|
|
|
|
case 'request_chapter_data': |
|
|
handleChapterDataRequest(ws, message); |
|
|
break; |
|
|
|
|
|
default: |
|
|
console.log('[WebSocket] Unknown message type:', type); |
|
|
} |
|
|
} |
|
|
|
|
|
function handleForceCleanupSession(message: any) { |
|
|
const { sessionId, sender, reason } = message; |
|
|
const comicSession = comicSessions.get(sessionId); |
|
|
|
|
|
if (comicSession) { |
|
|
comicSessions.delete(sessionId); |
|
|
|
|
|
const clientWs = clientConnections.get(sessionId); |
|
|
if (clientWs) { |
|
|
try { |
|
|
clientWs.send(JSON.stringify({ |
|
|
type: 'session_force_closed', |
|
|
reason: reason || 'Session removed by admin', |
|
|
message: 'Your session has been terminated' |
|
|
})); |
|
|
} catch (err) { |
|
|
console.error('[WebSocket] Error notifying client:', err); |
|
|
} |
|
|
clientConnections.delete(sessionId); |
|
|
} |
|
|
|
|
|
console.log(`[WebSocket] Force cleanup session: ${sessionId}, reason: ${reason}`); |
|
|
|
|
|
botConnections.forEach(botWs => { |
|
|
try { |
|
|
botWs.send(JSON.stringify({ |
|
|
type: 'cleanup_confirmed', |
|
|
sessionId, |
|
|
sender |
|
|
})); |
|
|
} catch (error) { |
|
|
console.error('[WebSocket] Error sending cleanup confirmation:', error); |
|
|
} |
|
|
}); |
|
|
} else { |
|
|
console.log(`[WebSocket] Session not found for cleanup: ${sessionId}`); |
|
|
} |
|
|
} |
|
|
|
|
|
function handleComicStream(message: any) { |
|
|
const { sessionId, sender, password, data, type } = message; |
|
|
const expiresAt = Date.now() + (24 * 60 * 60 * 1000); |
|
|
|
|
|
comicSessions.set(sessionId, { |
|
|
sessionId, |
|
|
sender, |
|
|
password, |
|
|
data, |
|
|
createdAt: Date.now(), |
|
|
expiresAt |
|
|
}); |
|
|
|
|
|
console.log(`[WebSocket] Comic stream created: ${sessionId}, type: ${type}`); |
|
|
} |
|
|
|
|
|
function handleComicDataRequest(ws: ServerWebSocket, message: any) { |
|
|
const { sessionId, password } = message; |
|
|
|
|
|
const comicSession = comicSessions.get(sessionId); |
|
|
|
|
|
if (!comicSession) { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'comic_data_response', |
|
|
success: false, |
|
|
error: 'Session not found or expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (comicSession.password !== password) { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'comic_data_response', |
|
|
success: false, |
|
|
error: 'Invalid password' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (Date.now() > comicSession.expiresAt) { |
|
|
comicSessions.delete(sessionId); |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'comic_data_response', |
|
|
success: false, |
|
|
error: 'Session expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
comicSession.clientWs = ws; |
|
|
clientConnections.set(sessionId, ws); |
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
type: 'comic_data_response', |
|
|
success: true, |
|
|
data: comicSession.data, |
|
|
expiresAt: comicSession.expiresAt |
|
|
})); |
|
|
} |
|
|
|
|
|
function handlePasswordValidation(ws: ServerWebSocket, message: any) { |
|
|
const { sessionId } = message; |
|
|
|
|
|
const comicSession = comicSessions.get(sessionId); |
|
|
|
|
|
if (!comicSession) { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'password_validation_response', |
|
|
success: false, |
|
|
error: 'Session not found or expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (Date.now() > comicSession.expiresAt) { |
|
|
comicSessions.delete(sessionId); |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'password_validation_response', |
|
|
success: false, |
|
|
error: 'Session expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
type: 'password_validation_response', |
|
|
success: true, |
|
|
requiresPassword: true |
|
|
})); |
|
|
} |
|
|
|
|
|
function handleChapterDataRequest(ws: ServerWebSocket, message: any) { |
|
|
const { sessionId, password, chapterSlug, allChapters } = message; |
|
|
|
|
|
const comicSession = comicSessions.get(sessionId); |
|
|
|
|
|
if (!comicSession) { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'chapter_data_response', |
|
|
success: false, |
|
|
error: 'Session not found or expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (comicSession.password !== password) { |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'chapter_data_response', |
|
|
success: false, |
|
|
error: 'Invalid password' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (Date.now() > comicSession.expiresAt) { |
|
|
comicSessions.delete(sessionId); |
|
|
ws.send(JSON.stringify({ |
|
|
type: 'chapter_data_response', |
|
|
success: false, |
|
|
error: 'Session expired' |
|
|
})); |
|
|
return; |
|
|
} |
|
|
|
|
|
comicSession.clientWs = ws; |
|
|
clientConnections.set(sessionId, ws); |
|
|
|
|
|
botConnections.forEach(botWs => { |
|
|
try { |
|
|
botWs.send(JSON.stringify({ |
|
|
type: 'fetch_chapter_request', |
|
|
sessionId, |
|
|
chapterSlug, |
|
|
sender: comicSession.sender, |
|
|
allChapters: allChapters || comicSession.data?.chapters || [] |
|
|
})); |
|
|
} catch (error) { |
|
|
console.error('[WebSocket] Error sending to bot:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log(`[WebSocket] Chapter fetch requested: ${chapterSlug} for session: ${sessionId}`); |
|
|
} |
|
|
|
|
|
function handleChapterFetched(message: any) { |
|
|
const { sessionId, chapterData, success, error } = message; |
|
|
const clientWs = clientConnections.get(sessionId); |
|
|
|
|
|
if (!clientWs) { |
|
|
console.log(`[WebSocket] No client connection found for session: ${sessionId}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
clientWs.send(JSON.stringify({ |
|
|
type: 'chapter_data_response', |
|
|
success, |
|
|
data: chapterData, |
|
|
error: error || null |
|
|
})); |
|
|
console.log(`[WebSocket] Chapter data sent to client for session: ${sessionId}`); |
|
|
} catch (err) { |
|
|
console.error('[WebSocket] Error sending chapter data to client:', err); |
|
|
} |
|
|
} |
|
|
|
|
|
function handleRegistrationSubmit(sessionId: string, data: any) { |
|
|
const session = sessions.get(sessionId); |
|
|
if (!session) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const { username, age, password, confirmPassword } = data; |
|
|
let success = true; |
|
|
let errorMessage = ''; |
|
|
|
|
|
if (!username || username.trim().length < 3) { |
|
|
success = false; |
|
|
errorMessage = 'Username must be at least 3 characters long'; |
|
|
} else if (!age || age < 13 || age > 100) { |
|
|
success = false; |
|
|
errorMessage = 'Age must be between 13 and 100'; |
|
|
} else if (!password || password.length < 6 || password.length > 20) { |
|
|
success = false; |
|
|
errorMessage = 'Password must be 6-20 characters long'; |
|
|
} else if (password !== confirmPassword) { |
|
|
success = false; |
|
|
errorMessage = 'Passwords do not match'; |
|
|
} |
|
|
|
|
|
if (session.ws) { |
|
|
session.ws.send(JSON.stringify({ |
|
|
type: 'registration_result', |
|
|
success, |
|
|
message: success ? 'Registration successful! Please check your WhatsApp.' : errorMessage |
|
|
})); |
|
|
} |
|
|
|
|
|
const registrationData = { |
|
|
type: 'registration_complete', |
|
|
sessionId, |
|
|
data: { |
|
|
success, |
|
|
message: errorMessage, |
|
|
userData: success ? { |
|
|
username: username.trim(), |
|
|
age: parseInt(age), |
|
|
password |
|
|
} : undefined |
|
|
} |
|
|
}; |
|
|
|
|
|
botConnections.forEach(botWs => { |
|
|
try { |
|
|
botWs.send(JSON.stringify(registrationData)); |
|
|
} catch (error) { |
|
|
console.error('[WebSocket] Error sending to bot:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
setTimeout(() => { |
|
|
sessions.delete(sessionId); |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
function cleanup(ws: ServerWebSocket) { |
|
|
botConnections.delete(ws); |
|
|
|
|
|
for (const [_, session] of sessions.entries()) { |
|
|
if (session.ws === ws) { |
|
|
session.ws = undefined; |
|
|
} |
|
|
} |
|
|
|
|
|
for (const [sessionId, clientWs] of clientConnections.entries()) { |
|
|
if (clientWs === ws) { |
|
|
clientConnections.delete(sessionId); |
|
|
} |
|
|
} |
|
|
|
|
|
for (const [_, comicSession] of comicSessions.entries()) { |
|
|
if (comicSession.clientWs === ws) { |
|
|
comicSession.clientWs = undefined; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
setInterval(() => { |
|
|
const now = Date.now(); |
|
|
|
|
|
for (const [sessionId, session] of sessions.entries()) { |
|
|
if (now - session.createdAt > 600000) { |
|
|
sessions.delete(sessionId); |
|
|
console.log('[WebSocket] Cleaned up expired session:', sessionId); |
|
|
} |
|
|
} |
|
|
|
|
|
for (const [sessionId, comicSession] of comicSessions.entries()) { |
|
|
if (now > comicSession.expiresAt) { |
|
|
botConnections.forEach(botWs => { |
|
|
try { |
|
|
botWs.send(JSON.stringify({ |
|
|
type: 'session_cleanup', |
|
|
sessionId, |
|
|
sender: comicSession.sender |
|
|
})); |
|
|
} catch (error) { |
|
|
console.error('[WebSocket] Error notifying bot:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
comicSessions.delete(sessionId); |
|
|
clientConnections.delete(sessionId); |
|
|
console.log('[WebSocket] Cleaned up expired comic session:', sessionId); |
|
|
} |
|
|
} |
|
|
}, 60 * 60 * 1000); |