Dhurgh's picture
Configure server to read cloud database environment variables
09675a5
Raw
History Blame Contribute Delete
57.9 kB
const express = require('express');
const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const https = require('https');
const { Sequelize, DataTypes, Op } = require('sequelize');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const multer = require('multer');
const bodyParser = require('body-parser');
const { v4: uuidv4 } = require('uuid');
require('dotenv').config();
const app = express();
const cors = require('cors');
const allowedOrigins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()).filter(Boolean) : ['*'];
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.includes('*')) return true;
return allowedOrigins.some((allowed) => {
if (allowed === origin) return true;
if (allowed.endsWith(':*')) {
const prefix = allowed.slice(0, -2);
return origin.startsWith(prefix + ':');
}
return false;
});
}
app.use(cors({
origin: (origin, callback) => {
if (isAllowedOrigin(origin)) return callback(null, true);
return callback(new Error('Not allowed by CORS'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use((req, res, next) => {
if (req.path.endsWith('.html') || req.path.endsWith('.js') || req.path.endsWith('.css')) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
}
next();
});
app.use(express.static(__dirname));
app.get('/api/version', (req, res) => {
try {
const packageJson = require('./package.json');
res.json({ version: packageJson.version || 'unknown' });
} catch (error) {
res.status(500).json({ version: 'unknown' });
}
});
app.get('/api/rtc-config', (req, res) => {
const defaultIceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun3.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:19302' },
{ urls: 'stun:stun.cloudflare.com:3478' }
];
const turnUrlsRaw = process.env.TURN_URLS || '';
const turnUsername = process.env.TURN_USERNAME || '';
const turnCredential = process.env.TURN_CREDENTIAL || '';
const relayFlag = String(process.env.RTC_FORCE_RELAY || 'false').toLowerCase();
const forceRelay = relayFlag === 'true' || relayFlag === '1' || relayFlag === 'yes';
let iceServers = [...defaultIceServers];
if (turnUrlsRaw && turnUsername && turnCredential) {
const turnUrls = turnUrlsRaw
.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
if (turnUrls.length) {
iceServers = [
...defaultIceServers,
{
urls: turnUrls,
username: turnUsername,
credential: turnCredential
}
];
}
}
res.json({
iceServers,
iceTransportPolicy: forceRelay ? 'relay' : 'all'
});
});
const isProduction = process.env.NODE_ENV === 'production';
const connectionString = process.env.DATABASE_URL;
if (isProduction && !connectionString) {
console.error('Missing DATABASE_URL in production. Add it in Render Environment settings or via render.yaml fromDatabase binding.');
process.exit(1);
}
const resolvedConnectionString = connectionString || 'postgres://postgres:postgres@localhost:5432/discord-app';
const isLocalConnection = resolvedConnectionString.includes('localhost') || resolvedConnectionString.includes('127.0.0.1');
const sequelize = new Sequelize(resolvedConnectionString, {
dialect: 'postgres',
logging: false,
protocol: 'postgres',
dialectOptions: isLocalConnection ? {} : { ssl: { require: true, rejectUnauthorized: false } }
});
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function syncDatabaseWithRetry(maxAttempts = 8) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await sequelize.authenticate();
await sequelize.sync({ alter: true });
return;
} catch (err) {
const message = err && err.message ? err.message : String(err);
const retryable = /ECONNRESET|ECONNREFUSED|ETIMEDOUT|SequelizeConnection/i.test(message);
const shouldRetry = retryable && attempt < maxAttempts;
console.error(`Database connect/sync attempt ${attempt}/${maxAttempts} failed:`, err);
if (!shouldRetry) {
throw err;
}
const delayMs = Math.min(30000, 2000 * attempt);
console.log(`Retrying database connection in ${delayMs}ms...`);
await sleep(delayMs);
}
}
}
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
avatar: {
type: DataTypes.TEXT,
defaultValue: ''
},
bio: {
type: DataTypes.STRING,
defaultValue: 'No bio yet'
},
phoneNumber: {
type: DataTypes.STRING,
defaultValue: ''
},
status: {
type: DataTypes.ENUM('online', 'offline', 'away'),
defaultValue: 'offline'
},
coins: {
type: DataTypes.INTEGER,
defaultValue: 0
}
});
const Channel = sequelize.define('Channel', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
channelId: DataTypes.STRING,
name: DataTypes.STRING,
type: {
type: DataTypes.ENUM('text', 'voice'),
defaultValue: 'text'
},
description: DataTypes.STRING,
isPrivate: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
});
const Message = sequelize.define('Message', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
messageId: DataTypes.STRING,
channelId: DataTypes.STRING,
senderUsername: DataTypes.STRING,
content: DataTypes.TEXT,
mediaType: {
type: DataTypes.ENUM('text', 'image', 'video', 'emoji', 'sticker', 'gif', 'voice'),
defaultValue: 'text'
},
mediaUrl: DataTypes.TEXT
});
const ShopItem = sequelize.define('ShopItem', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
itemId: DataTypes.STRING,
name: DataTypes.STRING,
description: DataTypes.STRING,
price: DataTypes.INTEGER,
category: DataTypes.STRING,
image: DataTypes.TEXT
});
const Inventory = sequelize.define('Inventory', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
itemId: DataTypes.STRING,
quantity: {
type: DataTypes.INTEGER,
defaultValue: 1
}
});
const FriendRequest = sequelize.define('FriendRequest', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
status: {
type: DataTypes.ENUM('pending', 'accepted', 'rejected'),
defaultValue: 'pending'
}
});
const DirectMessage = sequelize.define('DirectMessage', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
messageId: DataTypes.STRING,
fromUsername: DataTypes.STRING,
toUsername: DataTypes.STRING,
content: DataTypes.TEXT,
mediaType: {
type: DataTypes.ENUM('text', 'image', 'video', 'emoji', 'sticker', 'gif', 'voice'),
defaultValue: 'text'
},
mediaUrl: DataTypes.TEXT
});
User.belongsToMany(User, {
as: 'friends',
through: 'UserFriends',
foreignKey: 'UserId',
otherKey: 'FriendId'
});
User.hasMany(Channel, { foreignKey: 'createdById', as: 'channels' });
Channel.belongsTo(User, { foreignKey: 'createdById', as: 'creator' });
Channel.belongsToMany(User, { through: 'ChannelMembers', foreignKey: 'ChannelId', otherKey: 'UserId' });
User.belongsToMany(Channel, { through: 'ChannelMembers', foreignKey: 'UserId', otherKey: 'ChannelId' });
User.hasMany(Message, { foreignKey: 'senderId' });
Message.belongsTo(User, { foreignKey: 'senderId' });
User.hasMany(Inventory, { foreignKey: 'userId' });
Inventory.belongsTo(User, { foreignKey: 'userId' });
ShopItem.hasMany(Inventory, { foreignKey: 'shopItemId' });
Inventory.belongsTo(ShopItem, { foreignKey: 'shopItemId' });
FriendRequest.belongsTo(User, { as: 'from', foreignKey: 'fromId' });
FriendRequest.belongsTo(User, { as: 'to', foreignKey: 'toId' });
DirectMessage.belongsTo(User, { as: 'sender', foreignKey: 'fromId' });
DirectMessage.belongsTo(User, { as: 'recipient', foreignKey: 'toId' });
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Access token required' });
jwt.verify(token, process.env.JWT_SECRET || 'secret_key', (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
};
app.post('/api/register', async (req, res) => {
try {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields required' });
}
const existingUser = await User.findOne({ where: { [Op.or]: [{ username }, { email }] } });
if (existingUser) {
return res.status(400).json({ error: existingUser.username === username ? 'Username already taken' : 'Email already registered' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({ username, email, password: hashedPassword });
const token = jwt.sign(
{ userId: user.id, username: user.username },
process.env.JWT_SECRET || 'secret_key'
);
res.status(201).json({
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
bio: user.bio,
phoneNumber: user.phoneNumber,
coins: user.coins
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, username: user.username },
process.env.JWT_SECRET || 'secret_key'
);
res.json({
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
bio: user.bio,
phoneNumber: user.phoneNumber,
coins: user.coins
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/reset-password', async (req, res) => {
try {
const { email, newPassword } = req.body;
if (!email || !newPassword) {
return res.status(400).json({ error: 'Email and new password are required' });
}
if (String(newPassword).length < 6) {
return res.status(400).json({ error: 'New password must be at least 6 characters' });
}
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(404).json({ error: 'No account found with this email' });
}
user.password = await bcrypt.hash(newPassword, 10);
await user.save();
res.json({ success: true, message: 'Password reset successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
try {
const user = await User.findByPk(req.params.userId, {
attributes: ['id', 'username', 'avatar', 'bio', 'phoneNumber', 'coins', 'status']
});
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/profile/coins', authenticateToken, async (req, res) => {
try {
const { coins } = req.body;
if (typeof coins !== 'number' || !Number.isFinite(coins) || coins < 0) {
return res.status(400).json({ error: 'Invalid coins value' });
}
const user = await User.findByPk(req.user.userId);
if (!user) return res.status(404).json({ error: 'User not found' });
user.coins = Math.floor(coins);
await user.save();
res.json({ message: 'Coins synced', coins: user.coins });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.put('/api/profile', authenticateToken, async (req, res) => {
try {
const { username, bio, avatar, phoneNumber } = req.body;
const user = await User.findByPk(req.user.userId);
if (!user) return res.status(404).json({ error: 'User not found' });
if (username && username !== user.username) {
const existing = await User.findOne({ where: { username } });
if (existing) {
return res.status(400).json({ error: 'Username already taken' });
}
user.username = username;
}
if (bio) user.bio = bio;
if (avatar) user.avatar = avatar;
if (typeof phoneNumber === 'string') user.phoneNumber = phoneNumber;
await user.save();
for (const [socketId, onlineUser] of onlineUsers.entries()) {
if (onlineUser.userId === user.id) {
onlineUsers.set(socketId, {
...onlineUser,
username: user.username,
avatar: user.avatar
});
}
}
io.emit('users update', Array.from(onlineUsers.values()));
res.json({
message: 'Profile updated',
user: {
id: user.id,
username: user.username,
avatar: user.avatar,
bio: user.bio,
phoneNumber: user.phoneNumber
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/profile/upload', authenticateToken, async (req, res) => {
try {
const { avatar } = req.body;
const user = await User.findByPk(req.user.userId);
if (!user) return res.status(404).json({ error: 'User not found' });
if (avatar) {
user.avatar = avatar;
await user.save();
for (const [socketId, onlineUser] of onlineUsers.entries()) {
if (onlineUser.userId === user.id) {
onlineUsers.set(socketId, {
...onlineUser,
avatar: user.avatar
});
}
}
io.emit('users update', Array.from(onlineUsers.values()));
}
res.json({
message: 'Profile image updated',
avatar: user.avatar
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/friends/request', authenticateToken, async (req, res) => {
try {
const { toUserId } = req.body;
if (!toUserId) return res.status(400).json({ error: 'toUserId is required' });
if (String(toUserId) === String(req.user.userId)) return res.status(400).json({ error: 'Cannot send request to yourself' });
const targetUser = await User.findByPk(toUserId);
if (!targetUser) return res.status(404).json({ error: 'User not found' });
const fromUser = await User.findByPk(req.user.userId);
const existingFriend = await fromUser.getFriends({ where: { id: toUserId } });
if (existingFriend.length > 0) return res.status(400).json({ error: 'Already friends' });
const existingPending = await FriendRequest.findOne({
where: {
status: 'pending',
[Op.or]: [
{ fromId: req.user.userId, toId: toUserId },
{ fromId: toUserId, toId: req.user.userId }
]
}
});
if (existingPending) return res.status(400).json({ error: 'Friend request already pending' });
await FriendRequest.create({ fromId: req.user.userId, toId: toUserId });
io.emit('friend request update', { toUserId });
res.status(201).json({ message: 'Friend request sent' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/friends/requests', authenticateToken, async (req, res) => {
try {
const requests = await FriendRequest.findAll({
where: { toId: req.user.userId, status: 'pending' },
include: [{ model: User, as: 'from', attributes: ['id', 'username', 'avatar'] }]
});
res.json(requests);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/friends/accept', authenticateToken, async (req, res) => {
try {
const { requestId } = req.body;
const request = await FriendRequest.findByPk(requestId);
if (!request) return res.status(404).json({ error: 'Request not found' });
if (String(request.toId) !== String(req.user.userId)) {
return res.status(403).json({ error: 'Not allowed to accept this request' });
}
if (request.status !== 'pending') {
return res.status(400).json({ error: `Request already ${request.status}` });
}
request.status = 'accepted';
await request.save();
const fromUser = await User.findByPk(request.fromId);
const toUser = await User.findByPk(request.toId);
await fromUser.addFriend(toUser);
await toUser.addFriend(fromUser);
io.emit('friend request accepted', {
requestId: request.id,
fromId: request.fromId,
toId: request.toId
});
res.json({ message: 'Friend request accepted' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/friends/reject', authenticateToken, async (req, res) => {
try {
const { requestId } = req.body;
const request = await FriendRequest.findByPk(requestId);
if (!request) return res.status(404).json({ error: 'Request not found' });
if (String(request.toId) !== String(req.user.userId)) {
return res.status(403).json({ error: 'Not allowed to reject this request' });
}
if (request.status !== 'pending') {
return res.status(400).json({ error: `Request already ${request.status}` });
}
request.status = 'rejected';
await request.save();
io.emit('friend request rejected', {
requestId: request.id,
fromId: request.fromId,
toId: request.toId
});
res.json({ message: 'Friend request rejected' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/friends', authenticateToken, async (req, res) => {
try {
const user = await User.findByPk(req.user.userId, {
include: {
association: 'friends',
attributes: ['id', 'username', 'avatar', 'status', 'bio'],
through: { attributes: [] }
}
});
res.json(user.friends || []);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/dms/:friendId', authenticateToken, async (req, res) => {
try {
const { friendId } = req.params;
const messages = await DirectMessage.findAll({
where: {
[Op.or]: [
{ fromId: req.user.userId, toId: friendId },
{ fromId: friendId, toId: req.user.userId }
]
},
order: [['createdAt', 'ASC']]
});
const userIds = [...new Set(messages.map(msg => msg.fromId))];
const users = await User.findAll({
where: { id: userIds },
attributes: ['id', 'avatar', 'username']
});
const userMap = new Map(users.map(user => [String(user.id), user]));
const enriched = messages.map(msg => {
const sender = userMap.get(String(msg.fromId));
return {
...msg.toJSON(),
fromAvatar: sender?.avatar || null,
fromUsername: sender?.username || msg.fromUsername,
timestamp: msg.createdAt
};
});
res.json(enriched);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/dms', authenticateToken, async (req, res) => {
try {
const { toUserId, content, mediaType, mediaUrl } = req.body;
const fromUser = await User.findByPk(req.user.userId);
const toUser = await User.findByPk(toUserId);
if (!toUser) return res.status(404).json({ error: 'User not found' });
const dm = await DirectMessage.create({
messageId: uuidv4(),
fromId: req.user.userId,
toId: toUserId,
fromUsername: fromUser.username,
toUsername: toUser.username,
content,
mediaType: mediaType || 'text',
mediaUrl: mediaUrl || null
});
res.status(201).json(dm);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.delete('/api/dms/:friendId', authenticateToken, async (req, res) => {
try {
const { friendId } = req.params;
await DirectMessage.destroy({
where: {
[Op.or]: [
{ fromId: req.user.userId, toId: friendId },
{ fromId: friendId, toId: req.user.userId }
]
}
});
emitToUser(friendId, 'dm history cleared', {
fromUserId: req.user.userId,
clearedAt: new Date().toISOString()
});
res.json({ message: 'DM history cleared for both users' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/shop', authenticateToken, async (req, res) => {
try {
const items = await ensureShopItemsAvailable();
res.json(items);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/shop/buy', authenticateToken, async (req, res) => {
try {
const requestedItemId = String(req.body?.itemId || '').trim();
if (!requestedItemId) {
return res.status(400).json({ error: 'itemId is required' });
}
let item = await ShopItem.findByPk(requestedItemId);
if (!item) {
item = await ShopItem.findOne({ where: { itemId: requestedItemId } });
}
const user = await User.findByPk(req.user.userId);
if (!item) return res.status(404).json({ error: 'Item not found' });
if (user.coins < item.price) return res.status(400).json({ error: 'Not enough coins' });
user.coins -= item.price;
await user.save();
let inventoryItem = await Inventory.findOne({
where: {
userId: user.id,
shopItemId: item.id
}
});
if (inventoryItem) {
inventoryItem.quantity += 1;
await inventoryItem.save();
} else {
inventoryItem = await Inventory.create({ userId: user.id, shopItemId: item.id, quantity: 1 });
}
res.json({
message: 'Item purchased',
coins: user.coins,
purchasedItem: {
id: item.id,
itemId: item.itemId,
name: item.name,
category: item.category,
description: item.description
},
quantity: inventoryItem.quantity
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/inventory', authenticateToken, async (req, res) => {
try {
const inventory = await Inventory.findAll({
where: { userId: req.user.userId },
include: [{ model: ShopItem }]
});
res.json(inventory);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
function createServer(appInstance) {
const pfxPath = process.env.SSL_PFX_PATH;
const pfxPassphrase = process.env.SSL_PFX_PASSPHRASE;
const keyPath = process.env.SSL_KEY_PATH;
const certPath = process.env.SSL_CERT_PATH;
if (pfxPath) {
const httpsOptions = {
pfx: fs.readFileSync(path.resolve(pfxPath)),
passphrase: pfxPassphrase
};
return { server: https.createServer(httpsOptions, appInstance), protocol: 'https' };
}
if (keyPath && certPath) {
const httpsOptions = {
key: fs.readFileSync(path.resolve(keyPath)),
cert: fs.readFileSync(path.resolve(certPath))
};
return { server: https.createServer(httpsOptions, appInstance), protocol: 'https' };
}
return { server: http.createServer(appInstance), protocol: 'http' };
}
const { server, protocol } = createServer(app);
const io = require('socket.io')(server, {
maxHttpBufferSize: 40 * 1024 * 1024,
cors: {
origin: (origin, callback) => {
if (isAllowedOrigin(origin)) return callback(null, true);
return callback(new Error('Not allowed by Socket.IO CORS'));
},
methods: ['GET', 'POST']
}
});
const onlineUsers = new Map();
const roomMessages = new Map();
const roomMembers = new Map();
const roomRoles = new Map();
const roomProfiles = new Map();
const roomMeta = new Map();
function normalizeRoomKey(roomType, roomName) {
return String(roomType || 'community') + ':' + String(roomName || '').trim().toLowerCase();
}
function getRoomMessages(roomType, roomName) {
const key = normalizeRoomKey(roomType, roomName);
return roomMessages.get(key) || [];
}
async function loadRoomMessagesFromDb(roomType, roomName, limit = 150) {
const key = normalizeRoomKey(roomType, roomName);
const rows = await Message.findAll({
where: { channelId: key },
include: [{ model: User, attributes: ['id', 'username', 'avatar'], required: false }],
order: [['createdAt', 'ASC']],
limit
});
return rows.map((row) => ({
messageId: row.messageId || row.id,
from: row.senderId || row.User?.id || null,
fromUsername: row.senderUsername || row.User?.username || 'User',
fromAvatar: row.User?.avatar || null,
content: row.content || '',
mediaType: row.mediaType || 'text',
mediaUrl: row.mediaUrl || null,
roomType,
roomName,
timestamp: row.createdAt
}));
}
function appendRoomMessage(roomType, roomName, payload) {
const key = normalizeRoomKey(roomType, roomName);
const messages = roomMessages.get(key) || [];
messages.push(payload);
if (messages.length > 300) {
messages.splice(0, messages.length - 300);
}
roomMessages.set(key, messages);
}
function emitToUser(userId, eventName, payload) {
let delivered = 0;
for (const [socketId, userData] of onlineUsers.entries()) {
if (String(userData.userId) === String(userId)) {
io.to(socketId).emit(eventName, payload);
delivered += 1;
}
}
return delivered;
}
function getRoomMembersPayload(roomType, roomName) {
const roomKey = normalizeRoomKey(roomType, roomName);
const membersBySocket = roomMembers.get(roomKey) || new Map();
const dedupByUserId = new Map();
for (const member of membersBySocket.values()) {
const userId = String(member.userId);
if (!dedupByUserId.has(userId)) {
dedupByUserId.set(userId, {
id: member.userId,
username: member.username,
avatar: member.avatar || null,
status: 'online',
role: member.role || 'Member'
});
}
}
return Array.from(dedupByUserId.values());
}
function emitRoomMembersUpdate(roomType, roomName) {
const roomKey = normalizeRoomKey(roomType, roomName);
io.to(roomKey).emit('room members update', {
roomType,
roomName,
members: getRoomMembersPayload(roomType, roomName)
});
}
function upsertRoomMember(roomType, roomName, socket) {
const roomKey = normalizeRoomKey(roomType, roomName);
if (!roomMembers.has(roomKey)) {
roomMembers.set(roomKey, new Map());
}
if (!roomRoles.has(roomKey)) {
roomRoles.set(roomKey, new Map());
}
const membersBySocket = roomMembers.get(roomKey);
const rolesByUser = roomRoles.get(roomKey);
const userIdKey = String(socket.userId);
const meta = getOrCreateRoomMeta(roomType, roomName, socket.userId);
if (!meta.creatorId) {
meta.creatorId = userIdKey;
}
if (!rolesByUser.has(userIdKey)) {
if (String(meta.creatorId) === userIdKey) {
rolesByUser.set(userIdKey, 'Owner');
} else if (meta.contributorIds.has(userIdKey)) {
rolesByUser.set(userIdKey, 'Admin');
} else {
rolesByUser.set(userIdKey, 'Member');
}
}
membersBySocket.set(socket.id, {
userId: socket.userId,
username: socket.username,
avatar: socket.userAvatar || null,
role: rolesByUser.get(userIdKey)
});
emitRoomMembersUpdate(roomType, roomName);
}
function removeRoomMember(roomType, roomName, socketId) {
const roomKey = normalizeRoomKey(roomType, roomName);
const membersBySocket = roomMembers.get(roomKey);
if (!membersBySocket) return;
membersBySocket.delete(socketId);
if (membersBySocket.size === 0) {
roomMembers.delete(roomKey);
roomRoles.delete(roomKey);
roomMeta.delete(roomKey);
return;
}
emitRoomMembersUpdate(roomType, roomName);
}
function emitUsersUpdate() {
io.emit('users update', Array.from(onlineUsers.values()));
}
function getOrCreateRoomMeta(roomType, roomName, creatorId = null) {
const roomKey = normalizeRoomKey(roomType, roomName);
if (!roomMeta.has(roomKey)) {
roomMeta.set(roomKey, {
creatorId: creatorId ? String(creatorId) : null,
contributorIds: new Set(),
updatedAt: new Date().toISOString()
});
}
return roomMeta.get(roomKey);
}
function getOrCreateRoomProfile(roomType, roomName) {
const roomKey = normalizeRoomKey(roomType, roomName);
if (!roomProfiles.has(roomKey)) {
roomProfiles.set(roomKey, {
name: String(roomName || ''),
icon: roomType === 'community' ? '🌐' : 'πŸ‘₯',
image: ''
});
}
return roomProfiles.get(roomKey);
}
function emitRoomProfileUpdate(roomType, roomName, profile, previousRoomName = null) {
const roomKey = normalizeRoomKey(roomType, roomName);
// Broadcast to room members first
io.to(roomKey).emit('room profile updated', {
roomType,
roomName,
previousRoomName,
profile
});
// Also broadcast to all connected users so they see updates in lists/modals even if not in the room
io.emit('room profile updated broadcast', {
roomType,
roomName,
previousRoomName,
profile
});
}
function renameRoomState(roomType, previousRoomName, nextRoomName) {
const oldKey = normalizeRoomKey(roomType, previousRoomName);
const newKey = normalizeRoomKey(roomType, nextRoomName);
if (oldKey === newKey) return newKey;
if (roomMessages.has(oldKey)) {
roomMessages.set(newKey, roomMessages.get(oldKey));
roomMessages.delete(oldKey);
}
if (roomRoles.has(oldKey)) {
roomRoles.set(newKey, roomRoles.get(oldKey));
roomRoles.delete(oldKey);
}
if (roomProfiles.has(oldKey)) {
roomProfiles.set(newKey, roomProfiles.get(oldKey));
roomProfiles.delete(oldKey);
}
if (roomMeta.has(oldKey)) {
roomMeta.set(newKey, roomMeta.get(oldKey));
roomMeta.delete(oldKey);
}
if (roomMembers.has(oldKey)) {
const membersBySocket = roomMembers.get(oldKey);
roomMembers.set(newKey, membersBySocket);
roomMembers.delete(oldKey);
for (const socketId of membersBySocket.keys()) {
const memberSocket = io.sockets.sockets.get(socketId);
if (!memberSocket) continue;
memberSocket.leave(oldKey);
memberSocket.join(newKey);
memberSocket.currentRoomKey = newKey;
memberSocket.currentRoomMeta = {
roomType,
roomName: nextRoomName
};
}
}
return newKey;
}
function getBotResponse(input = '') {
const text = String(input || '').trim().toLowerCase();
const cleaned = text.replace(/^\/firetech\s*/i, '').replace(/^@firetech\s*/i, '').trim();
if (!cleaned || cleaned.includes('help')) {
return 'FireTech commands: help, security, encryption, network, password, twofa, phishing, malware, vpn, hello';
}
if (cleaned.includes('security')) {
return 'Security tip: Keep software updated, use strong passwords, enable 2FA, and never share credentials.';
}
if (cleaned.includes('encryption')) {
return 'Encryption fact: End-to-end encryption ensures only sender and receiver can read messages. FireTech uses modern protocols.';
}
if (cleaned.includes('network')) {
return 'Network insight: Your messages are routed through secure servers. Network traffic is protected from eavesdropping.';
}
if (cleaned.includes('password')) {
return 'Password best practice: Use 12+ characters, mix uppercase/lowercase/numbers/symbols, and never reuse passwords.';
}
if (cleaned.includes('2fa') || cleaned.includes('twofa')) {
return '2FA (Two-Factor Authentication) significantly reduces account takeover risk. Enable it on all important accounts.';
}
if (cleaned.includes('phishing')) {
return 'Phishing warning: Check sender email, verify links before clicking, and never provide credentials to unsolicited requests.';
}
if (cleaned.includes('malware')) {
return 'Malware protection: Use antivirus software, avoid downloading from untrusted sources, and keep OS updated.';
}
if (cleaned.includes('vpn')) {
return 'VPN (Virtual Private Network) encrypts your connection and masks your IP. Useful on public WiFi networks.';
}
if (cleaned.includes('hello') || cleaned.includes('hi')) {
return '⚑ FireTech Bot online. Your security is my priority. Ask for security tips or network info!';
}
return `I heard: "${cleaned}". Type "help" for my security commands.`;
}
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join', async (data) => {
try {
const decoded = jwt.verify(data.token, process.env.JWT_SECRET || 'secret_key');
const user = await User.findByPk(decoded.userId);
if (user) {
socket.userId = decoded.userId;
socket.username = user.username;
socket.userAvatar = user.avatar;
onlineUsers.set(socket.id, {
userId: decoded.userId,
username: user.username,
avatar: user.avatar,
status: 'online'
});
user.status = 'online';
await user.save();
emitUsersUpdate();
}
} catch (error) {
socket.emit('error', 'Authentication failed');
}
});
socket.on('update profile cache', (data) => {
const nextUsername = String(data?.username || socket.username || '').trim();
const nextAvatar = data?.avatar ?? socket.userAvatar ?? null;
if (nextUsername) {
socket.username = nextUsername;
}
socket.userAvatar = nextAvatar;
const existing = onlineUsers.get(socket.id);
if (existing) {
existing.username = socket.username;
existing.avatar = socket.userAvatar;
onlineUsers.set(socket.id, existing);
}
if (socket.currentRoomMeta) {
const { roomType, roomName } = socket.currentRoomMeta;
const roomKey = normalizeRoomKey(roomType, roomName);
const membersBySocket = roomMembers.get(roomKey);
if (membersBySocket && membersBySocket.has(socket.id)) {
const member = membersBySocket.get(socket.id);
member.username = socket.username;
member.avatar = socket.userAvatar;
membersBySocket.set(socket.id, member);
emitRoomMembersUpdate(roomType, roomName);
}
}
emitUsersUpdate();
});
socket.on('join dm', (friendId) => {
const roomId = [socket.userId, friendId].sort().join('_');
socket.join(roomId);
socket.currentDmRoom = roomId;
});
socket.on('leave dm', (friendId) => {
const roomId = [socket.userId, friendId].sort().join('_');
socket.leave(roomId);
socket.currentDmRoom = null;
});
socket.on('send dm', async (data) => {
try {
const { toUserId, content, mediaType, mediaUrl } = data;
const fromUser = await User.findByPk(socket.userId);
if (String(toUserId) === 'bot_firetech_0') {
const dmPayload = {
messageId: uuidv4(),
from: socket.userId,
fromUsername: fromUser.username,
fromAvatar: fromUser.avatar,
to: 'bot_firetech_0',
toUsername: '⚑ FireTech Bot',
content,
mediaType: mediaType || 'text',
mediaUrl: mediaUrl || null,
timestamp: new Date().toISOString()
};
emitToUser(socket.userId, 'dm message', dmPayload);
const botPayload = {
messageId: uuidv4(),
from: 'bot_firetech_0',
fromUsername: '⚑ FireTech Bot',
fromAvatar: null,
to: socket.userId,
toUsername: fromUser.username,
content: getBotResponse(content),
mediaType: 'text',
mediaUrl: null,
isBot: true,
timestamp: new Date().toISOString()
};
setTimeout(() => {
emitToUser(socket.userId, 'dm message', botPayload);
}, 350 + Math.floor(Math.random() * 500));
return;
}
const toUser = await User.findByPk(toUserId);
if (!toUser) {
socket.emit('error', 'User not found');
return;
}
const dm = await DirectMessage.create({
messageId: uuidv4(),
fromId: socket.userId,
toId: toUserId,
fromUsername: fromUser.username,
toUsername: toUser.username,
content,
mediaType: mediaType || 'text',
mediaUrl: mediaUrl || null
});
const dmPayload = {
messageId: dm.messageId,
from: socket.userId,
fromUsername: fromUser.username,
fromAvatar: fromUser.avatar,
to: toUserId,
toUsername: toUser.username,
content: dm.content,
mediaType: dm.mediaType,
mediaUrl: dm.mediaUrl,
timestamp: dm.createdAt
};
emitToUser(socket.userId, 'dm message', dmPayload);
const deliveredToRecipient = emitToUser(toUserId, 'dm message', dmPayload);
if (deliveredToRecipient === 0) {
socket.emit('dm delivery status', {
messageId: dm.messageId,
toUserId,
delivered: false
});
}
const rawContent = String(content || '').trim();
const askBot = rawContent.toLowerCase().startsWith('/firetech') || rawContent.toLowerCase().startsWith('@firetech');
if (askBot) {
const botPayload = {
messageId: uuidv4(),
from: 'bot_firetech_0',
fromUsername: '⚑ FireTech Bot',
fromAvatar: null,
to: toUserId,
toUsername: toUser.username,
content: getBotResponse(rawContent),
mediaType: 'text',
mediaUrl: null,
isBot: true,
timestamp: new Date().toISOString()
};
setTimeout(() => {
emitToUser(socket.userId, 'dm message', botPayload);
emitToUser(toUserId, 'dm message', botPayload);
}, 400 + Math.floor(Math.random() * 600));
}
} catch (error) {
socket.emit('error', error.message);
}
});
socket.on('dm typing', (friendId) => {
const roomId = [socket.userId, friendId].sort().join('_');
socket.broadcast.to(roomId).emit('dm user typing', {
userId: socket.userId,
username: socket.username
});
});
socket.on('dm stop typing', (friendId) => {
const roomId = [socket.userId, friendId].sort().join('_');
socket.broadcast.to(roomId).emit('dm user stop typing', {
userId: socket.userId,
username: socket.username
});
});
socket.on('community typing', (data) => {
const roomName = data?.community;
if (!roomName) return;
const roomKey = normalizeRoomKey('community', roomName);
socket.broadcast.to(roomKey).emit('community user typing', {
community: roomName,
userId: socket.userId,
username: socket.username
});
});
socket.on('community stop typing', (data) => {
const roomName = data?.community;
if (!roomName) return;
const roomKey = normalizeRoomKey('community', roomName);
socket.broadcast.to(roomKey).emit('community user stop typing', {
community: roomName,
userId: socket.userId,
username: socket.username
});
});
socket.on('room typing', (data) => {
const roomName = data?.roomName || data?.roomId;
if (!roomName) return;
const roomType = data?.roomType || 'group';
const roomKey = normalizeRoomKey(roomType, roomName);
socket.broadcast.to(roomKey).emit('room user typing', {
roomType,
roomId: roomName,
userId: socket.userId,
username: socket.username
});
});
socket.on('room stop typing', (data) => {
const roomName = data?.roomName || data?.roomId;
if (!roomName) return;
const roomType = data?.roomType || 'group';
const roomKey = normalizeRoomKey(roomType, roomName);
socket.broadcast.to(roomKey).emit('room user stop typing', {
roomType,
roomId: roomName,
userId: socket.userId,
username: socket.username
});
});
socket.on('join room chat', async (data) => {
const roomType = data?.roomType;
const roomName = data?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKey(roomType, roomName);
if (socket.currentRoomKey && socket.currentRoomKey !== roomKey) {
if (socket.currentRoomMeta) {
removeRoomMember(socket.currentRoomMeta.roomType, socket.currentRoomMeta.roomName, socket.id);
}
socket.leave(socket.currentRoomKey);
}
socket.join(roomKey);
socket.currentRoomKey = roomKey;
socket.currentRoomMeta = { roomType, roomName };
upsertRoomMember(roomType, roomName, socket);
const roomProfile = getOrCreateRoomProfile(roomType, roomName);
socket.emit('room profile updated', {
roomType,
roomName,
previousRoomName: roomName,
profile: roomProfile
});
let history = getRoomMessages(roomType, roomName);
try {
history = await loadRoomMessagesFromDb(roomType, roomName, 180);
roomMessages.set(roomKey, history);
} catch (error) {
console.warn('Failed loading DB room history:', error.message);
}
socket.emit('room chat history', {
roomType,
roomName,
messages: history
});
});
socket.on('leave room chat', (data) => {
const roomType = data?.roomType;
const roomName = data?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKey(roomType, roomName);
socket.leave(roomKey);
removeRoomMember(roomType, roomName, socket.id);
if (socket.currentRoomKey === roomKey) {
socket.currentRoomKey = null;
socket.currentRoomMeta = null;
}
});
socket.on('set room role', (data) => {
const roomType = data?.roomType;
const roomName = data?.roomName;
const targetUserId = data?.targetUserId;
const nextRoleRaw = String(data?.role || '').trim();
const nextRole = ['Owner', 'Admin', 'Member'].includes(nextRoleRaw) ? nextRoleRaw : null;
if (!roomType || !roomName || !targetUserId || !nextRole) return;
const roomKey = normalizeRoomKey(roomType, roomName);
const rolesByUser = roomRoles.get(roomKey);
if (!rolesByUser) return;
const actorRole = rolesByUser.get(String(socket.userId));
if (actorRole !== 'Owner' && actorRole !== 'Admin') {
socket.emit('error', 'Only Owner/Admin can change roles');
return;
}
if (actorRole === 'Admin' && nextRole === 'Owner') {
socket.emit('error', 'Admin cannot assign Owner role');
return;
}
rolesByUser.set(String(targetUserId), nextRole);
const membersBySocket = roomMembers.get(roomKey);
if (membersBySocket) {
for (const [memberSocketId, member] of membersBySocket.entries()) {
if (String(member.userId) === String(targetUserId)) {
member.role = nextRole;
membersBySocket.set(memberSocketId, member);
}
}
}
emitRoomMembersUpdate(roomType, roomName);
});
socket.on('update room profile', (data) => {
const roomType = data?.roomType;
const roomName = String(data?.roomName || '').trim();
const patch = data?.profilePatch || {};
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKey(roomType, roomName);
const meta = getOrCreateRoomMeta(roomType, roomName, socket.userId);
if (!meta.creatorId) {
meta.creatorId = String(socket.userId);
}
let rolesByUser = roomRoles.get(roomKey);
if (!rolesByUser) {
rolesByUser = new Map();
rolesByUser.set(String(socket.userId), 'Owner');
roomRoles.set(roomKey, rolesByUser);
}
const actorRole = rolesByUser.get(String(socket.userId));
let effectiveRole = actorRole;
if (roomType === 'group' && !effectiveRole) {
effectiveRole = 'Owner';
rolesByUser.set(String(socket.userId), effectiveRole);
}
if (effectiveRole !== 'Owner' && effectiveRole !== 'Admin') {
socket.emit('error', roomType === 'group'
? 'Only Owner or Admin can edit group appearance'
: 'Only Leader/Vice Leader can edit room appearance');
return;
}
if (effectiveRole === 'Owner' || effectiveRole === 'Admin') {
meta.contributorIds.add(String(socket.userId));
meta.updatedAt = new Date().toISOString();
}
const safeName = String(patch?.name || '').trim();
const safeIcon = typeof patch?.icon === 'string' ? patch.icon.trim().slice(0, 4) : null;
const safeImage = typeof patch?.image === 'string' ? patch.image : null;
let nextRoomName = roomName;
if (safeName) {
nextRoomName = safeName;
renameRoomState(roomType, roomName, nextRoomName);
}
const profile = getOrCreateRoomProfile(roomType, nextRoomName);
if (safeName) profile.name = safeName;
if (safeIcon !== null) profile.icon = safeIcon;
if (safeImage !== null) profile.image = safeImage;
const nextRoomKey = normalizeRoomKey(roomType, nextRoomName);
roomProfiles.set(nextRoomKey, profile);
emitRoomProfileUpdate(roomType, nextRoomName, profile, roomName);
emitRoomMembersUpdate(roomType, nextRoomName);
});
socket.on('send room message', async (data) => {
const roomType = data?.roomType;
const roomName = data?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKey(roomType, roomName);
const messagePayload = {
messageId: uuidv4(),
from: socket.userId,
fromUsername: socket.username,
fromAvatar: socket.userAvatar || null,
content: data?.content || '',
mediaType: data?.mediaType || 'text',
mediaUrl: data?.mediaUrl || null,
roomType,
roomName,
timestamp: new Date().toISOString()
};
appendRoomMessage(roomType, roomName, messagePayload);
try {
await Message.create({
messageId: messagePayload.messageId,
channelId: roomKey,
senderId: socket.userId,
senderUsername: socket.username,
content: messagePayload.content,
mediaType: messagePayload.mediaType,
mediaUrl: messagePayload.mediaUrl || null
});
} catch (error) {
console.warn('Failed persisting room message:', error.message);
}
io.to(roomKey).emit('room message', messagePayload);
const rawContent = String(data?.content || '').trim();
const askBot = rawContent.toLowerCase().startsWith('/firetech') || rawContent.toLowerCase().startsWith('@firetech');
if (askBot) {
const botPayload = {
messageId: uuidv4(),
from: 'bot_firetech_0',
fromUsername: '⚑ FireTech Bot',
fromAvatar: null,
content: getBotResponse(rawContent),
mediaType: 'text',
mediaUrl: null,
roomType,
roomName,
isBot: true,
timestamp: new Date().toISOString()
};
setTimeout(() => {
appendRoomMessage(roomType, roomName, botPayload);
io.to(roomKey).emit('room message', botPayload);
}, 500 + Math.floor(Math.random() * 800));
}
});
socket.on('clear room chat', (data) => {
const roomType = data?.roomType;
const roomName = data?.roomName;
if (!roomType || !roomName) return;
const roomKey = normalizeRoomKey(roomType, roomName);
roomMessages.set(roomKey, []);
Message.destroy({ where: { channelId: roomKey } }).catch(() => {});
io.to(roomKey).emit('room chat cleared', { roomType, roomName, clearedBy: socket.userId });
});
socket.on('send friend request', async (data) => {
try {
const toUserId = data?.toUserId;
if (!toUserId) {
socket.emit('error', 'Target user is required');
return;
}
if (String(toUserId) === String(socket.userId)) {
socket.emit('error', 'Cannot send request to yourself');
return;
}
const targetUser = await User.findByPk(toUserId);
if (!targetUser) {
socket.emit('error', 'User not found');
return;
}
const fromUser = await User.findByPk(socket.userId);
const existingFriend = await fromUser.getFriends({ where: { id: toUserId } });
if (existingFriend.length > 0) {
socket.emit('error', 'Already friends');
return;
}
const existingPending = await FriendRequest.findOne({
where: {
status: 'pending',
[Op.or]: [
{ fromId: socket.userId, toId: toUserId },
{ fromId: toUserId, toId: socket.userId }
]
}
});
if (existingPending) {
socket.emit('error', 'Friend request already pending');
return;
}
const newRequest = await FriendRequest.create({ fromId: socket.userId, toId: toUserId });
socket.emit('friend request sent', { requestId: newRequest.id, toUserId });
emitToUser(toUserId, 'friend request received', {
requestId: newRequest.id,
fromId: socket.userId,
fromUsername: socket.username,
fromAvatar: socket.userAvatar || null
});
} catch (error) {
socket.emit('error', error.message);
}
});
socket.on('start video call', async (data) => {
try {
const targetId = data?.targetId;
if (!targetId) {
socket.emit('error', 'Call target is required');
return;
}
const targetUser = await User.findByPk(targetId);
if (!targetUser) {
socket.emit('error', 'User not found');
return;
}
const callId = uuidv4();
const payload = {
callId,
fromId: socket.userId,
fromUsername: socket.username,
fromAvatar: socket.userAvatar || null,
toId: targetId,
type: data?.type || 'dm',
startedAt: new Date().toISOString()
};
const deliveredToTarget = emitToUser(targetId, 'incoming video call', payload);
socket.emit('video call ringing', {
...payload,
deliveredToTarget: deliveredToTarget > 0
});
if (deliveredToTarget === 0) {
socket.emit('video call unavailable', {
callId,
targetId,
message: 'User is currently offline or not connected.'
});
}
} catch (error) {
socket.emit('error', error.message);
}
});
socket.on('accept video call', (data) => {
const callId = data?.callId;
const targetCallerId = data?.toId;
if (!callId || !targetCallerId) return;
emitToUser(targetCallerId, 'video call accepted', {
callId,
byUserId: socket.userId,
byUsername: socket.username,
acceptedAt: new Date().toISOString()
});
});
socket.on('reject video call', (data) => {
const callId = data?.callId;
const targetCallerId = data?.toId;
if (!callId || !targetCallerId) return;
emitToUser(targetCallerId, 'video call rejected', {
callId,
byUserId: socket.userId,
byUsername: socket.username,
rejectedAt: new Date().toISOString()
});
});
socket.on('end video call', (data) => {
const callId = data?.callId;
const peerUserId = data?.toId;
if (!callId || !peerUserId) return;
emitToUser(peerUserId, 'video call ended', {
callId,
byUserId: socket.userId,
byUsername: socket.username,
endedAt: new Date().toISOString()
});
});
socket.on('video signal', (data) => {
const toId = data?.toId;
const signal = data?.signal;
if (!toId || !signal) return;
emitToUser(toId, 'video signal', {
fromId: socket.userId,
fromUsername: socket.username,
callId: data?.callId || null,
signal
});
});
socket.on('disconnect', async () => {
try {
const userData = onlineUsers.get(socket.id);
if (userData) {
const user = await User.findByPk(socket.userId);
if (user) {
user.status = 'offline';
await user.save();
}
onlineUsers.delete(socket.id);
if (socket.currentRoomMeta) {
removeRoomMember(socket.currentRoomMeta.roomType, socket.currentRoomMeta.roomName, socket.id);
}
emitUsersUpdate();
}
} catch (error) {
console.error(error);
}
});
});
function getLocalNetworkAddress() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const net of interfaces[name]) {
if (net.family === 'IPv4' && !net.internal) {
return net.address;
}
}
}
return '127.0.0.1';
}
async function migrateMediaTypeEnum() {
try {
const [results] = await sequelize.query(`
SELECT EXISTS (
SELECT 1 FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname = 'enum_DirectMessages_mediaType'
AND e.enumlabel = 'voice'
) as has_voice;
`);
if (!results[0].has_voice) {
console.log('πŸ“¦ Migrating mediaType ENUM to include sticker, gif, voice...');
await sequelize.query(`
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'voice';
`);
await sequelize.query(`
ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'voice';
`);
console.log('βœ… ENUM migration complete!');
} else {
console.log('βœ… mediaType ENUM already up to date');
}
} catch (error) {
console.log('ℹ️ ENUM migration skipped (likely first run or different DB)');
}
}
async function seedShopItems() {
const existingCount = await ShopItem.count();
if (existingCount > 0) return;
await ShopItem.bulkCreate(DEFAULT_SHOP_ITEMS);
}
const DEFAULT_SHOP_ITEMS = [
{ itemId: 'banner_neon', name: 'Neon Banner Effect', description: 'Animated neon gradient banner for your profile.', price: 450, category: 'Banners', image: '' },
{ itemId: 'badge_founder', name: 'Founder Badge', description: 'Exclusive badge shown next to your username.', price: 320, category: 'Badges', image: '' },
{ itemId: 'color_pack_burst', name: 'Color Burst Pack', description: 'Unlock vibrant accent color themes.', price: 380, category: 'Themes', image: '' },
{ itemId: 'chat_fx_glow', name: 'Message Glow FX', description: 'Subtle glow animation for sent messages.', price: 260, category: 'Effects', image: '' },
{ itemId: 'avatar_ring_aura', name: 'Aura Avatar Ring', description: 'Premium animated ring around your avatar.', price: 520, category: 'Avatar', image: '' },
{ itemId: 'nameplate_crystal', name: 'Crystal Nameplate', description: 'Polished nameplate style in member list.', price: 410, category: 'Nameplates', image: '' },
{ itemId: 'theme_midnight', name: 'Midnight Theme Pack', description: 'Elegant dark gradients and UI accents.', price: 290, category: 'Themes', image: '' },
{ itemId: 'emoji_pack_pro', name: 'Pro Emoji Pack', description: 'Premium emoji reactions collection.', price: 210, category: 'Emotes', image: '' }
];
async function ensureShopItemsAvailable() {
let items = await ShopItem.findAll();
if (items.length > 0) return items;
await ShopItem.bulkCreate(DEFAULT_SHOP_ITEMS);
items = await ShopItem.findAll();
return items;
}
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
syncDatabaseWithRetry().then(async () => {
await migrateMediaTypeEnum();
await seedShopItems();
server.listen(PORT, HOST, () => {
const networkAddress = getLocalNetworkAddress();
console.log(`
╔═══════════════════════════════════════╗
β•‘ πŸš€ Discord-Like Server Running β•‘
β•‘ β•‘
β•‘ πŸ“‘ Local: ${protocol}://localhost:${PORT} β•‘
β•‘ 🌐 LAN: ${protocol}://${networkAddress}:${PORT} β•‘
β•‘ ✨ Status: Online & Ready β•‘
β•‘ β•‘
β•‘ Features: β•‘
β•‘ βœ“ User Authentication β•‘
β•‘ βœ“ Profiles & Friends β•‘
β•‘ βœ“ Text & Voice Channels β•‘
β•‘ βœ“ Media Sharing β•‘
β•‘ βœ“ Shop System β•‘
β•‘ βœ“ Real-time Messaging β•‘
β•‘ βœ“ PostgreSQL Database β•‘
β•‘ β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
`);
});
}).catch(err => {
console.error('Database startup error:', err);
process.exit(1);
});