import { createServer } from "http"; import { Server, Socket } from "socket.io"; import { MongoClient, ObjectId } from "mongodb"; import dotenv from "dotenv"; import { decode } from "next-auth/jwt"; import { NET_EVENTS, signMessage, verifyMessage } from "../lib/vendor-metrics"; // Load environment variables dotenv.config({ path: '.env.local' }); if (!process.env.MONGODB_URI) { dotenv.config({ path: '.env' }); } const PORT = 6767; const MONGODB_URI = process.env.MONGODB_URI; const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET; const RAIN_DISCORD_WEBHOOK_URL = ''; const RAIN_ROLE_ID = '1465848784528474202'; // Set your rain role ID here if (!MONGODB_URI) { console.error("MONGODB_URI is not defined in .env"); process.exit(1); } const httpServer = createServer(); const io = new Server(httpServer, { cors: { origin: ["http://localhost:6969", "http://127.0.0.1:6969", "https://pummmp.fun", "https://www.pummmp.fun"], methods: ["GET", "POST"], credentials: true } }); interface AuthenticatedSocket extends Socket { userId?: string; sessionConfig?: { market: string; nonce: string; nonceTimestamp: number; }; } async function sendDiscordRainAlert(rain: any, usersCollection: any) { try { const host = await usersCollection.findOne({ _id: new ObjectId(rain.hostId) }); const hostName = host ? host.name : 'Unknown'; const embed = { title: '🌧️ Rain Started!', description: `A new rain has been created! Join now to participate.`, color: 0x00ff00, fields: [ { name: 'Amount', value: `${rain.amount} SOL`, inline: true }, { name: 'Host', value: hostName, inline: true }, { name: 'Ends', value: ``, inline: true } ], timestamp: new Date().toISOString(), footer: { text: 'pummmp.fun', icon_url: 'https://pummmp.fun/logo.png' } }; const components = [ { type: 1, // Action row components: [ { type: 2, // Button style: 5, // Link style label: 'Participate in Rain', url: 'https://pummmp.fun' } ] } ]; await fetch(RAIN_DISCORD_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: `<@&${RAIN_ROLE_ID}> 🌧️ A new rain has started!`, embeds: [embed], components: components // <-- this must be a top-level property, not inside embeds }) }); } catch (error) { console.error('Failed to send Discord rain webhook:', error); } } async function startServer() { try { const client = new MongoClient(MONGODB_URI!); await client.connect(); console.log("βœ… Custom server connected to MongoDB"); const db = client.db("pummmpfun"); const usersCollection = db.collection("users"); const coinsCollection = db.collection("coins"); const commentsCollection = db.collection("comments"); const tradesCollection = db.collection("trades"); // Rate limiting configuration - very generous for active chat const RATE_LIMIT_WINDOW = 60000; // 1 minute const CHAT_RATE_LIMIT = 50; // 50 messages per minute const ACTION_RATE_LIMIT = 30; // 30 actions per minute const rateLimits = new Map(); // User mute system for rate limit violations const userMutes = new Map(); function checkRateLimit(userId: string, type: 'chat' | 'action'): boolean { const now = Date.now(); // Check if user is currently muted const muteInfo = userMutes.get(userId); if (muteInfo && now < muteInfo.mutedUntil) { console.log(`[RateLimit] User ${userId} is muted until ${new Date(muteInfo.mutedUntil).toISOString()}: ${muteInfo.reason}`); return true; } let limit = rateLimits.get(userId); if (!limit || now - limit.lastReset > RATE_LIMIT_WINDOW) { limit = { chatCount: 0, actionCount: 0, lastReset: now }; } if (type === 'chat') { if (limit.chatCount >= CHAT_RATE_LIMIT) { // Mute user for 10 seconds const muteUntil = now + 10000; userMutes.set(userId, { mutedUntil: muteUntil, reason: 'Rate limit exceeded - sending messages too fast' }); console.log(`[RateLimit] User ${userId} muted for 10 seconds due to chat rate limit`); return true; } limit.chatCount++; } else { if (limit.actionCount >= ACTION_RATE_LIMIT) { // Mute user for 10 seconds const muteUntil = now + 10000; userMutes.set(userId, { mutedUntil: muteUntil, reason: 'Rate limit exceeded - too many actions' }); console.log(`[RateLimit] User ${userId} muted for 10 seconds due to action rate limit`); return true; } limit.actionCount++; } rateLimits.set(userId, limit); return false; } // Middleware to authenticate socket connections io.use(async (socket: AuthenticatedSocket, next) => { try { const cookieHeader = socket.handshake.headers.cookie; // console.log(`[Auth] Cookie header length: ${cookieHeader ? cookieHeader.length : 0}`); if (!cookieHeader) { // Allow guests? Or strict auth? // For now, let's allow connection but they won't have userId return next(); } // Simple cookie parsing const getCookie = (name: string) => { const value = `; ${cookieHeader}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift(); return null; }; const sessionToken = getCookie("next-auth.session-token") || getCookie("__Secure-next-auth.session-token"); // console.log(`[Auth] Session token found: ${!!sessionToken}`); if (sessionToken && NEXTAUTH_SECRET) { const decoded = await decode({ token: sessionToken, secret: NEXTAUTH_SECRET }); if (decoded?.sub) { socket.userId = decoded.sub; console.log(`Authenticated user: ${socket.userId}`); } else { console.log(`[Auth] Failed to decode token`); } } } catch (err) { console.error("Auth middleware error:", err); } next(); }); console.log("βœ… Watching for changes in 'coins', 'comments', and 'trades'..."); // 1. Watch for new coins and updates const coinStream = coinsCollection.watch([], { fullDocument: 'updateLookup' }); coinStream.on("change", (change) => { if (change.operationType === 'insert') { io.emit('new_coin', change.fullDocument); } else if (change.operationType === 'update') { io.emit('update_coin', { _id: change.documentKey._id, ...change.updateDescription?.updatedFields }); } }); // 2. Watch for new comments (Global Chat & Coin Chat) const commentStream = commentsCollection.watch([], { fullDocument: 'updateLookup' }); commentStream.on("change", async (change) => { if (change.operationType === 'insert') { const comment = change.fullDocument; // Emit to specific coin room/channel io.emit(`comment:${comment.coinId}`, comment); // Also emit to global if we decide to aggregate or if coinId is specific if (comment.coinId === 'global') { // V6 Broadcast const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(comment), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } } } }); // 3. Watch for new trades const tradeStream = tradesCollection.watch([], { fullDocument: 'updateLookup' }); tradeStream.on("change", (change) => { if (change.operationType === 'insert') { const trade = change.fullDocument; io.emit(`trade:${trade.coinId}`, trade); // Also update basic price info for listings globally io.emit('trade_update', trade); } }); // 4. Watch for Rain events const rainsCollection = db.collection("rains"); const rainStream = rainsCollection.watch([], { fullDocument: 'updateLookup' }); rainStream.on("change", async (change) => { if (change.operationType === 'insert' || change.operationType === 'update') { const rain = change.fullDocument; if (rain && rain.participants && rain.participants.length > 0) { const participantIds = rain.participants.map((id: string) => new ObjectId(id)); const users = await usersCollection.find({ _id: { $in: participantIds } }, { projection: { name: 1, image: 1 } }).toArray(); rain.participants = users.map(user => ({ id: user._id.toString(), name: user.name, image: user.image })); } io.emit('rain_update', rain); } if (change.operationType === 'insert') { await sendDiscordRainAlert(change.fullDocument, usersCollection); } }); // Periodic cleanup of expired mutes setInterval(() => { const now = Date.now(); for (const [userId, muteInfo] of userMutes.entries()) { if (now > muteInfo.mutedUntil) { userMutes.delete(userId); } } }, 30000); // Clean up every 30 seconds setInterval(async () => { try { const now = new Date(); // console.log("Checking for expired rains at", now.toISOString()); const activeRains = await rainsCollection.find({ active: true, endsAt: { $lte: now } }).toArray(); if (activeRains.length > 0) { console.log(`Found ${activeRains.length} expired rains to process.`); } for (const rain of activeRains) { console.log(`🌧️ Ending rain ${rain._id}`); const participantCount = rain.participants ? rain.participants.length : 0; if (participantCount === 0) { // Refund host await usersCollection.updateOne( { _id: new ObjectId(rain.hostId) }, { $inc: { balance: rain.amount } } ); io.emit('rain_ended', { _id: rain._id, amount: rain.amount, payoutPerUser: 0, totalParticipants: 0, refunded: true }); } else { const payout = rain.amount / participantCount; // Bulk update participants // We need to convert string IDs to ObjectIds const pIds = rain.participants.map((uid: string) => new ObjectId(uid)); await usersCollection.updateMany( { _id: { $in: pIds } }, { $inc: { balance: payout } } ); io.emit('rain_ended', { _id: rain._id, amount: rain.amount, payoutPerUser: payout, totalParticipants: participantCount }); // Announce in chat const botMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'pummmp.rain', userName: 'pummmp.rain', userImage: 'https://pummmp.fun/rain.png', text: `🌧️ RAIN ENDED! ${rain.amount} SOL distributed to ${participantCount} users (${payout.toFixed(4)} SOL each)!`, createdAt: new Date().toISOString(), userVerified: true }; // Broadcast via VM const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(botMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } } // Deactivate rain await rainsCollection.updateOne( { _id: rain._id }, { $set: { active: false } } ); } } catch (e) { console.error("Rain loop error:", e); } }, 5000); io.on("connection", (socket: AuthenticatedSocket) => { const clientIP = socket.handshake.address; console.log(`Client ${socket.id} connected from ${socket.handshake.headers.origin} (${clientIP})`); // Hyperion Handshake const marketId = Math.random().toString(36).substring(7); const nonce = Math.random().toString(36).substring(7) + Date.now().toString(36); socket.sessionConfig = { market: marketId, nonce: nonce, nonceTimestamp: Date.now() }; socket.emit(NET_EVENTS.CONNECT, { _h_mid: marketId, // Handshake Market ID _h_nonce: nonce, // Session Nonce _h_shard: "us-east-1", }); // socket.onAny((eventName, ...args) => { // console.log(`[Socket] Received event: ${eventName}`, args); // }); socket.on("join_room", (room) => { console.log(`[Socket] Joining room: ${room}`); if (typeof room !== 'string' || room.length > 50) return; socket.join(room); }); socket.on("leave_room", (room) => { if (typeof room !== 'string') return; socket.leave(room); }); // Hyperion Uplink Listener - Validates nonce before processing socket.on(NET_EVENTS.OUTGOING, async (messageData: any) => { // console.log(`[Hyperion] Received message of size ${JSON.stringify(messageData)?.length}`); if (!socket.sessionConfig || !messageData) { console.log("[Hyperion] Invalid message structure or no session config"); return; } // Additional security checks if (typeof messageData === 'string' && messageData.length > 10000) { console.log("[Security] Message too long, rejecting"); socket.disconnect(); return; } if (typeof messageData === 'object' && JSON.stringify(messageData).length > 10000) { console.log("[Security] Payload too large, rejecting"); socket.disconnect(); return; } // Validate nonce let payload: any = { text: messageData }; try { if (typeof messageData === 'string' && messageData.startsWith('{')) { payload = JSON.parse(messageData); } else if (typeof messageData === 'object') { payload = messageData; } } catch(e) { console.log("JSON Parse error on payload", e); return; } // Check nonce validity if (!payload._nonce || payload._nonce !== socket.sessionConfig.nonce) { console.log("[Hyperion] Invalid or missing nonce - possible replay attack"); socket.disconnect(); return; } // Check nonce age (prevent replay of old nonces) const nonceAge = Date.now() - socket.sessionConfig.nonceTimestamp; if (nonceAge > 600000) { // 600 second expiry (10 minutes) console.log("[Hyperion] Nonce expired"); // socket.disconnect(); return; } // Rotate nonce after successful validation (only for actions, not chat) if (payload.type === 'tip' || payload.type === 'gamble') { const newNonce = Math.random().toString(36).substring(7) + Date.now().toString(36); socket.sessionConfig.nonce = newNonce; socket.sessionConfig.nonceTimestamp = Date.now(); // Send new nonce to client socket.emit('nonce_update', { nonce: newNonce }); } // Remove nonce from payload before processing delete payload._nonce; console.log(`[Hyperion] Processing: "${typeof payload.text === 'string' ? payload.text.substring(0, 50) : JSON.stringify(payload).substring(0, 50)}..." from user ${socket.userId || 'guest'}`); // Handle Tip / Gamble / Chat based on payload internal structure or text commands if (payload.type === 'gamble') { await handleGambleNotification(socket, usersCollection, payload); } else if (payload.type === 'tip') { await handleTipNotification(socket, usersCollection, payload); } else { await handleGlobalMessage(socket, usersCollection, payload); } }); // Standard events for trades/price updates remain for public data socket.on("new_trade", (trade) => { io.emit(`trade:${trade.coinId}`, trade); io.emit('trade_update', trade); // For listings }); socket.on("disconnect", () => {}); }); // Defined Handlers async function handleGlobalMessage(socket: Socket, usersCollection: any, payload: any) { try { if (!payload || typeof payload !== 'object' || typeof payload.text !== 'string') { console.warn('[Chat Debug] Dropped: Invalid payload', payload); return; } const userId = (socket as AuthenticatedSocket).userId; if (!userId) { console.warn('[Chat Debug] Dropped: Guests cannot send messages (only authenticated users)'); return; } const ipAddress = socket.handshake.address; if (checkRateLimit(userId, 'chat')) { const muteInfo = userMutes.get(userId); const message = muteInfo ? `You're temporarily muted for sending messages too fast. Try again in ${Math.ceil((muteInfo.mutedUntil - Date.now()) / 1000)} seconds.` : 'You are sending messages too fast.'; console.warn('[Chat Debug] Dropped: Rate limit exceeded for user', userId); socket.emit('error_message', message); return; } if (!payload.text.trim()) { console.warn('[Chat Debug] Dropped: Empty message from user', userId); return; } if (payload.text.length > 500) { payload.text = payload.text.substring(0, 500); } let user = await usersCollection.findOne({ discordId: userId }); if (!user && ObjectId.isValid(userId)) { user = await usersCollection.findOne({ _id: new ObjectId(userId) }); } if (!user) { console.warn('[Chat Debug] Dropped: No user found for userId', userId); return; } // Check if user is chat banned if (user.chatBannedUntil) { const banExpiry = new Date(user.chatBannedUntil); if (banExpiry > new Date()) { const isPermanent = banExpiry.getFullYear() === 9999; const remainingTime = isPermanent ? 'permanently' : `${Math.ceil((banExpiry.getTime() - Date.now()) / (1000 * 60))} minutes`; socket.emit('error_message', `You are banned from chat ${isPermanent ? 'permanently' : `for ${remainingTime} more minutes`}.`); return; } else { // Ban expired, remove it await usersCollection.updateOne( { _id: user._id }, { $unset: { chatBannedUntil: 1 } } ); } } // Handle admin commands if (user.isAdmin && payload.text.startsWith('.')) { // .ban if (payload.text.startsWith('.ban ')) { const parts = payload.text.split(' '); if (parts.length >= 3) { const targetUsername = parts[1]; const durationStr = parts[2]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found.`); return; } if (targetUser.isAdmin) { socket.emit('error_message', 'Cannot ban admin users.'); return; } let banUntil: Date | null = null; if (durationStr === 'perm' || durationStr === 'permanent') { banUntil = new Date('9999-12-31'); } else { const durationMatch = durationStr.match(/^(\d+)([mhd])$/); if (durationMatch) { const amount = parseInt(durationMatch[1]); const unit = durationMatch[2]; let milliseconds = 0; switch (unit) { case 'm': milliseconds = amount * 60 * 1000; break; case 'h': milliseconds = amount * 60 * 60 * 1000; break; case 'd': milliseconds = amount * 24 * 60 * 60 * 1000; break; } banUntil = new Date(Date.now() + milliseconds); } else { socket.emit('error_message', 'Invalid duration format. Use: 30m, 2h, 1d, or "perm"'); return; } } await usersCollection.updateOne( { _id: targetUser._id }, { $set: { chatBannedUntil: banUntil } } ); const durationText = banUntil.getFullYear() === 9999 ? 'permanently' : `for ${Math.ceil((banUntil.getTime() - Date.now()) / (1000 * 60))} minutes`; const banMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `🚫 ${targetUser.name} got pwn'd ${durationText} by @${user.name} πŸ˜† np`, createdAt: new Date().toISOString(), userVerified: true }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(banMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Banned ${targetUser.name} ${durationText}`); return; } } // .unban else if (payload.text.startsWith('.unban ')) { const parts = payload.text.split(' '); if (parts.length >= 2) { const targetUsername = parts[1]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found.`); return; } await usersCollection.updateOne( { _id: targetUser._id }, { $unset: { chatBannedUntil: 1 } } ); const unbanMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `βœ… ${targetUser.name} has been unbanned from chat by ${user.name}`, createdAt: new Date().toISOString(), userVerified: true }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(unbanMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Unbanned ${targetUser.name}`); return; } } // .tipban else if (payload.text.startsWith('.tipban ')) { const parts = payload.text.split(' '); if (parts.length >= 3) { const targetUsername = parts[1]; const durationStr = parts[2]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found.`); return; } if (targetUser.isAdmin) { socket.emit('error_message', 'Cannot tip ban admin users.'); return; } let banUntil: Date | null = null; if (durationStr === 'perm' || durationStr === 'permanent') { banUntil = new Date('9999-12-31'); } else { const durationMatch = durationStr.match(/^(\d+)([mhd])$/); if (durationMatch) { const amount = parseInt(durationMatch[1]); const unit = durationMatch[2]; let milliseconds = 0; switch (unit) { case 'm': milliseconds = amount * 60 * 1000; break; case 'h': milliseconds = amount * 60 * 60 * 1000; break; case 'd': milliseconds = amount * 24 * 60 * 60 * 1000; break; } banUntil = new Date(Date.now() + milliseconds); } else { socket.emit('error_message', 'Invalid duration format. Use: 30m, 2h, 1d, or "perm"'); return; } } await usersCollection.updateOne( { _id: targetUser._id }, { $set: { tipBannedUntil: banUntil } } ); const durationText = banUntil.getFullYear() === 9999 ? 'permanently' : `for ${Math.ceil((banUntil.getTime() - Date.now()) / (1000 * 60))} minutes`; const banMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `πŸ’ΈπŸš« ${targetUser.name} got tip banned ${durationText} by @${user.name}`, createdAt: new Date().toISOString(), userVerified: true }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(banMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Tip banned ${targetUser.name} ${durationText}`); return; } } // .tipunban else if (payload.text.startsWith('.tipunban ')) { const parts = payload.text.split(' '); if (parts.length >= 2) { const targetUsername = parts[1]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found.`); return; } await usersCollection.updateOne( { _id: targetUser._id }, { $unset: { tipBannedUntil: 1 } } ); const unbanMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `πŸ’Έβœ… ${targetUser.name} has been unbanned from tipping by ${user.name}`, createdAt: new Date().toISOString(), userVerified: true }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(unbanMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Tip unbanned ${targetUser.name}`); return; } } // .tradeban else if (payload.text.startsWith('.tradeban ')) { const parts = payload.text.split(' '); if (parts.length >= 3) { const targetUsername = parts[1]; const durationStr = parts[2]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found`); return; } if (targetUser.isAdmin) { socket.emit('error_message', 'Cannot trade ban admin users.'); return; } let banUntil: Date | null = null; if (durationStr === 'perm' || durationStr === 'permanent') { banUntil = new Date('9999-12-31'); } else { const durationMatch = durationStr.match(/^(\d+)([mhd])$/); if (durationMatch) { const amount = parseInt(durationMatch[1]); const unit = durationMatch[2]; let milliseconds = 0; switch (unit) { case 'm': milliseconds = amount * 60 * 1000; break; case 'h': milliseconds = amount * 60 * 60 * 1000; break; case 'd': milliseconds = amount * 24 * 60 * 60 * 1000; break; } banUntil = new Date(Date.now() + milliseconds); } else { socket.emit('error_message', 'Invalid duration format. Use: 30m, 2h, 1d, or "perm"'); return; } } await usersCollection.updateOne( { _id: targetUser._id }, { $set: { tradeBannedUntil: banUntil } } ); const durationText = banUntil!.getFullYear() === 9999 ? 'permanently' : `for ${Math.ceil((banUntil!.getTime() - Date.now()) / (1000 * 60))} minutes`; const banMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `⛔️ ${targetUser.name} has been trade-banned ${durationText} by ${user.name}`, createdAt: new Date().toISOString(), userVerified: true }; const sockets2 = io.sockets.sockets; for (const [id, s] of sockets2) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(banMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Trade banned ${targetUser.name} ${durationText}`); return; } } // .tradeunban else if (payload.text.startsWith('.tradeunban ')) { const parts = payload.text.split(' '); if (parts.length >= 2) { const targetUsername = parts[1]; const targetUser = await usersCollection.findOne({ name: { $regex: new RegExp(`^${targetUsername}$`, 'i') } }); if (!targetUser) { socket.emit('error_message', `User "${targetUsername}" not found`); return; } await usersCollection.updateOne( { _id: targetUser._id }, { $unset: { tradeBannedUntil: 1 } } ); const unbanMsg2 = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'system', userName: 'System', userImage: '/logo.ico', text: `βœ… ${targetUser.name} has been unbanned from trading by ${user.name}`, createdAt: new Date().toISOString(), userVerified: true }; const sockets3 = io.sockets.sockets; for (const [id, s] of sockets3) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(unbanMsg2), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } socket.emit('error_message', `Trade unbanned ${targetUser.name}`); return; } } } const chatMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: user._id.toString(), userName: user.name, userImage: user.image, text: payload.text, createdAt: new Date().toISOString(), userVerified: !!user.verified, userIsAdmin: !!user.isAdmin, userIsBetaTester: !!user.isBetaTester, userIsBugHunter: !!user.isBugHunter, }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(chatMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } } catch (e) { console.error("Error processing chat message:", e); } } async function handleTipNotification(socket: Socket, usersCollection: any, payload: any) { try { console.log(`[Tip] Processing tip from user ${(socket as AuthenticatedSocket).userId}:`, payload); if (!payload || typeof payload !== 'object') return; // Validations... const userId = (socket as AuthenticatedSocket).userId; if (!userId) return; if (checkRateLimit(userId, 'action')) { const muteInfo = userMutes.get(userId); const message = muteInfo ? `You're temporarily muted for too many actions. Try again in ${Math.ceil((muteInfo.mutedUntil - Date.now()) / 1000)} seconds.` : 'You are performing actions too fast.'; socket.emit('error_message', message); return; } let sender = await usersCollection.findOne({ discordId: userId }); if (!sender && ObjectId.isValid(userId)) { sender = await usersCollection.findOne({ _id: new ObjectId(userId) }); } if (!sender) { socket.emit('error_message', 'Sender not found.'); return; } // Check if sender is tip banned if (sender.tipBannedUntil) { const banExpiry = new Date(sender.tipBannedUntil); if (banExpiry > new Date()) { const isPermanent = banExpiry.getFullYear() === 9999; const remainingTime = isPermanent ? 'permanently' : `${Math.ceil((banExpiry.getTime() - Date.now()) / (1000 * 60))} minutes`; socket.emit('error_message', `You are tip banned ${isPermanent ? 'permanently' : `for ${remainingTime} more minutes`}.`); return; } else { // Ban expired, remove it await usersCollection.updateOne( { _id: sender._id }, { $unset: { tipBannedUntil: 1 } } ); } } // Find recipient let recipient = null; if (payload.recipientId) { recipient = await usersCollection.findOne({ _id: new ObjectId(payload.recipientId) }); } else if (payload.recipientName) { recipient = await usersCollection.findOne({ name: { $regex: new RegExp(`^${payload.recipientName}$`, 'i') } }); } if (!recipient) { socket.emit('error_message', 'Recipient not found.'); return; } // Check if recipient is tip banned if (recipient.tipBannedUntil) { const banExpiry = new Date(recipient.tipBannedUntil); if (banExpiry > new Date()) { socket.emit('error_message', 'Cannot tip this user - they are tip banned.'); return; } else { // Ban expired, remove it await usersCollection.updateOne( { _id: recipient._id }, { $unset: { tipBannedUntil: 1 } } ); } } if (sender) { const botMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'pummmp.bot', userName: 'pummmp.bot', userImage: 'https://pummmp.fun/logo.ico', text: `πŸ’Έ @${sender.name} tipped @${payload.recipientName || recipient.name} ${payload.amount} SOL!`, createdAt: new Date().toISOString(), userVerified: true }; console.log(`[Tip] Broadcasting tip notification: ${botMsg.text}`); const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(botMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } } } catch (e) { console.error("Error handling tip notification:", e); } } async function handleGambleNotification(socket: Socket, usersCollection: any, payload: any) { try { if (!payload || typeof payload !== 'object') return; const userId = (socket as AuthenticatedSocket).userId; if (!userId) return; if (checkRateLimit(userId, 'action')) { const muteInfo = userMutes.get(userId); const message = muteInfo ? `You're temporarily muted for too many actions. Try again in ${Math.ceil((muteInfo.mutedUntil - Date.now()) / 1000)} seconds.` : 'You are performing actions too fast.'; socket.emit('error_message', message); return; } let sender = await usersCollection.findOne({ discordId: userId }); if (!sender && ObjectId.isValid(userId)) { sender = await usersCollection.findOne({ _id: new ObjectId(userId) }); } if (sender) { const isBigWin = payload.won && payload.payout > payload.amount * 5; const emoji = payload.won ? (isBigWin ? '🎰 JACKPOT! 🎰' : '🎲 WINNER!') : 'πŸ’€ RIPPED'; let text = ''; if (payload.won) { text = `${emoji} @${sender.name} bet ${payload.amount} SOL with ${payload.chance}% chance and WON ${payload.payout.toFixed(4)} SOL!`; } else { text = `${emoji} @${sender.name} lost ${payload.amount} SOL rolling the dice (${payload.chance}% chance)...`; } const botMsg = { _id: Math.random().toString(36).substring(7), coinId: 'global', userId: 'pummmp.casino', userName: 'pummmp.casino', userImage: 'https://pummmp.fun/logo.ico', text: text, createdAt: new Date().toISOString(), userVerified: true }; const sockets = io.sockets.sockets; for (const [id, s] of sockets) { const authSocket = s as AuthenticatedSocket; if (authSocket.sessionConfig) { const bytecode = await signMessage(JSON.stringify(botMsg), authSocket.sessionConfig.market); authSocket.emit(NET_EVENTS.INCOMING, bytecode); } } } } catch (e) { console.error("Error handling gamble notification:", e); } } httpServer.listen(PORT, () => { console.log(`πŸš€ WebSocket server running on port ${PORT}`); }); } catch (error) { console.error("Failed to start socket server:", error); process.exit(1); } } startServer();