Files
rltn/server.js
2025-08-31 15:08:38 +03:00

1486 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let dotenv, express, db, Economy, GameTime, pathLib, fs, virtualWorldPool, new_quest_Base;
try {
dotenv = require('dotenv').config();
console.log('dotenv успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте dotenv:', e);
console.log('Продолжаем без .env файла');
}
// Устанавливаем fallback значения для критических переменных окружения
if (!process.env.JWT_SECRET) {
process.env.JWT_SECRET = 'fallback-secret-key-for-development';
console.warn('JWT_SECRET не найден, используем fallback ключ (НЕ ДЛЯ ПРОДАКШЕНА!)');
}
if (!process.env.DATABASE_URL) {
process.env.DATABASE_URL = 'postgresql://postgres:password@localhost:5432/revproj';
console.warn('DATABASE_URL не найден, используем fallback (НЕ ДЛЯ ПРОДАКШЕНА!)');
}
try {
express = require('express');
console.log('express успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте express:', e);
throw e;
}
try {
virtualWorldPool = require('./db1');
console.log('db1 - virtualWorld - успешно импротирован');
}
catch (e) {
console.error('Ошибка при импорте db1 - virtual_World:', e);
throw e;
}
try {
new_quest_Base = require('./db2');
console.log('db2 - new_quest_Base - успешно импортирован');
}
catch (e) {
console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
throw e;
}
try {
db = require('./db');
console.log('db успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте db:', e);
throw e;
}
try {
Economy = require('./economy');
console.log('Economy успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте economy:', e);
throw e;
}
try {
GameTime = require('./gameTime');
console.log('GameTime успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте gameTime:', e);
throw e;
}
try {
pathLib = require('path');
console.log('path успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте path:', e);
throw e;
}
try {
fs = require('fs');
console.log('fs успешно импортирован');
} catch (e) {
console.error('Ошибка при импорте fs:', e);
throw e;
}
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const http = require('http').createServer(app);
const io = require('socket.io')(http, {
cors: {
origin: [
'http://localhost:4000',
'http://rltn.online',
'https://rltn.online',
'http://www.rltn.online',
'https://www.rltn.online'
],
methods: ['GET', 'POST']
}
});
let onlineUsers = {};
const organizationsRouter = require('./server/organizations')(io, onlineUsers);
app.use('/api/organizations', organizationsRouter);
// Инициализация игрового времени (ускорение 8x) и вещание клиентам
try {
if (GameTime && typeof GameTime === 'function') {
global.__gameTimeInstance = global.__gameTimeInstance || new GameTime(io, 8);
console.log('GameTime таймер запущен');
}
} catch (e) {
console.error('GameTime не запущен:', e);
}
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error('No token'));
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = payload.id;
onlineUsers[socket.userId] = socket.id; // Добавить пользователя в онлайн
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
function authenticate(req, res, next) {
const auth = req.headers.authorization?.split(' ');
try {
if (!auth || auth[0] !== 'Bearer') return res.status(401).send('No token');
const payload = jwt.verify(auth[1], process.env.JWT_SECRET);
req.user = payload;
next();
} catch {
res.status(401).send('Invalid token');
}
}
app.use(express.static(pathLib.join(__dirname, 'build')));
app.use(
'/models',
express.static(pathLib.join(__dirname, 'public', 'models'))
);
let players = {};
// --- Расширяем players для поддержки городов ---
let playersByCity = {};
io.on('connection', socket => {
console.log('Player connected:', socket.id);
// Получаем город игрока из БД
(async () => {
const { rows } = await db.query('SELECT last_city_id, last_pos_x, last_pos_z FROM users WHERE id = $1', [socket.userId]);
const cityId = rows[0]?.last_city_id || 1;
const x = rows[0]?.last_pos_x || 0;
const z = rows[0]?.last_pos_z || 0;
if (!playersByCity[cityId]) playersByCity[cityId] = {};
playersByCity[cityId][socket.id] = {
socketId: socket.id,
userId: socket.userId,
x,
y: 0,
z,
cityId,
avatarURL: null,
gender: null,
firstName: null,
lastName: null
};
players[socket.id] = playersByCity[cityId][socket.id];
socket.cityId = cityId;
socket.x = x;
socket.y = 0;
socket.z = z;
// Отправляем только игроков этого города
socket.emit('currentPlayers', playersByCity[cityId]);
})();
// --- Новый игрок ---
socket.on('newPlayer', data => {
const cityId = socket.cityId || 1;
if (!playersByCity[cityId]) playersByCity[cityId] = {};
const p = playersByCity[cityId][socket.id] || {};
Object.assign(p, {
x: data.x,
y: data.y ?? p.y ?? 0,
z: data.z,
cityId,
avatarURL: data.avatarURL || null,
gender: data.gender || null,
firstName: data.firstName || '',
lastName: data.lastName || ''
});
playersByCity[cityId][socket.id] = p;
players[socket.id] = p;
socket.cityId = cityId;
// Сообщаем всем игрокам этого города (без учёта интерьера)
for (const id in playersByCity[cityId]) {
if (id === socket.id) continue;
io.to(id).emit('newPlayer', {
playerId: socket.id,
x: p.x,
y: p.y ?? 0,
z: p.z,
avatarURL: p.avatarURL,
gender: p.gender,
firstName: p.firstName,
lastName: p.lastName
});
}
});
// --- Перемещение игрока ---
socket.on('playerMovement', movementData => {
const cityId = socket.cityId;
if (playersByCity[cityId] && playersByCity[cityId][socket.id]) {
playersByCity[cityId][socket.id].x = movementData.x;
if (typeof movementData.y === 'number') {
playersByCity[cityId][socket.id].y = movementData.y;
}
playersByCity[cityId][socket.id].z = movementData.z;
// Сообщаем только игрокам этого города (без учёта интерьера)
for (const id in playersByCity[cityId]) {
if (id !== socket.id) {
io.to(id).emit('playerMoved', {
playerId: socket.id,
x: movementData.x,
y: typeof movementData.y === 'number' ? movementData.y : (playersByCity[cityId][socket.id].y ?? 0),
z: movementData.z
});
}
}
// Voice chat nearby только в этом городе
const norm = v => (v == null ? null : String(v));
const sender = playersByCity[cityId][socket.id];
for (const [id, other] of Object.entries(playersByCity[cityId])) {
if (id === socket.id) continue;
// Голосовой чат больше не фильтруем по интерьеру
const dx = sender.x - other.x;
const dz = sender.z - other.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= 50) {
io.to(id).emit('voiceChatNearby', { playerId: socket.id });
io.to(socket.id).emit('voiceChatNearby', { playerId: id });
}
}
}
});
// --- Смена интерьера (вход/выход) ---
socket.on('interiorChange', ({ interiorId }) => {
const cityId = socket.cityId;
if (!playersByCity[cityId] || !playersByCity[cityId][socket.id]) return;
const norm = v => (v == null ? null : String(v));
const prevInterior = playersByCity[cityId][socket.id].interiorId ?? null;
const nextInterior = norm(interiorId);
playersByCity[cityId][socket.id].interiorId = nextInterior;
socket.interiorId = nextInterior;
// Сообщаем игрокам старого интерьера, что этот игрок ушёл
for (const id in playersByCity[cityId]) {
if (id === socket.id) continue;
const other = playersByCity[cityId][id];
const samePrev = norm(other?.interiorId) === norm(prevInterior);
if (samePrev && prevInterior !== nextInterior) {
io.to(id).emit('playerDisconnected', socket.id);
}
}
// Сообщаем всем игрокам города о появлении (без учёта интерьера)
for (const id in playersByCity[cityId]) {
if (id === socket.id) continue;
const p = playersByCity[cityId][socket.id];
io.to(id).emit('newPlayer', {
playerId: socket.id,
x: p.x,
z: p.z,
avatarURL: p.avatarURL,
gender: p.gender,
firstName: p.firstName,
lastName: p.lastName
});
}
// Отправляем текущий список видимых игроков только для этого сокета
// Отправляем полный список игроков города
socket.emit('currentPlayers', playersByCity[cityId]);
});
socket.on('sendMessage', async ({ receiverId, message }, callback) => {
try {
const senderId = socket.userId;
const recvId = parseInt(receiverId, 10);
// Проверка получателя
const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]);
if (receiverCheck.rows.length === 0) {
return callback({ error: 'Пользователь не найден' });
}
// Сохранение сообщения
const result = await virtualWorldPool.query(
`INSERT INTO messages (sender_id, receiver_id, message)
VALUES ($1, $2, $3)
RETURNING id, created_at, is_read`,
[senderId, recvId, message]
);
const newMessage = result.rows[0];
const receiverSocketId = onlineUsers[recvId];
// Отправка получателю
if (receiverSocketId) {
io.to(receiverSocketId).emit('newMessage', {
id: newMessage.id,
text: message,
senderId,
timestamp: newMessage.created_at,
isRead: newMessage.is_read
});
}
callback({ success: true, message: newMessage });
} catch (err) {
callback({ error: 'Ошибка отправки сообщения' });
}
});
// --- Чат ---
socket.on('chatMessage', ({ message, name }) => {
const cityId = socket.cityId;
const sender = playersByCity[cityId]?.[socket.id];
if (!sender) return;
for (const [id, other] of Object.entries(playersByCity[cityId])) {
const dx = sender.x - other.x;
const dz = sender.z - other.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= 50 || id === socket.id) {
io.to(id).emit('chatMessage', {
playerId: socket.id,
position: { x: sender.x, z: sender.z },
name: name || '???',
message: message
});
}
}
});
// --- WebRTC signaling ---
socket.on('voiceChatOffer', ({ to, offer }) => {
io.to(to).emit('voiceChatOffer', { from: socket.id, offer });
});
socket.on('voiceChatAnswer', ({ to, answer }) => {
io.to(to).emit('voiceChatAnswer', { from: socket.id, answer });
});
socket.on('voiceChatIceCandidate', ({ to, candidate }) => {
io.to(to).emit('voiceChatIceCandidate', { from: socket.id, candidate });
});
socket.on('voiceChatToggle', ({ enabled }) => {
if (players[socket.id]) {
players[socket.id].voiceEnabled = enabled;
}
socket.broadcast.emit('voiceChatStatus', { playerId: socket.id, enabled });
});
// --- Смена города ---
socket.on('cityChange', async ({ cityId }) => {
const oldCity = socket.cityId;
if (playersByCity[oldCity]) {
delete playersByCity[oldCity][socket.id];
// Сообщаем игрокам старого города о выходе
for (const id in playersByCity[oldCity]) {
io.to(id).emit('playerDisconnected', socket.id);
}
}
if (!playersByCity[cityId]) playersByCity[cityId] = {};
playersByCity[cityId][socket.id] = {
socketId: socket.id,
userId: socket.userId,
x: 0,
z: 0,
cityId,
avatarURL: null,
gender: null,
firstName: null,
lastName: null
};
players[socket.id] = playersByCity[cityId][socket.id];
socket.cityId = cityId;
// Отправляем новых игроков этого города
socket.emit('currentPlayers', playersByCity[cityId]);
});
// --- Отключение ---
socket.on('disconnect', async () => {
delete onlineUsers[socket.userId];
const cityId = socket.cityId;
const player = playersByCity[cityId]?.[socket.id];
if (player) {
// Сохраняем координаты и город выхода
await db.query(
'UPDATE users SET last_city_id = $1, last_pos_x = $2, last_pos_z = $3 WHERE id = $4',
[cityId, player.x, player.z, player.userId]
);
delete playersByCity[cityId][socket.id];
delete players[socket.id];
// Сообщаем игрокам города о выходе
for (const id in playersByCity[cityId]) {
io.to(id).emit('playerDisconnected', socket.id);
}
}
});
});
// Маршрут для получения списка пользователей
// Получить список пользователей (кроме текущего)
app.get('/api/users', authenticate, async (req, res) => {
try {
const { rows } = await db.query(`
SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
FROM users
WHERE id != $1
`, [req.user.id]);
res.json(rows);
} catch (e) {
console.error('Ошибка получения списка пользователей', e);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Новый маршрут для получения сообщений с конкретным контактом
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
const userId = req.user.id;
const contactId = parseInt(req.params.contactId, 10);
try {
// Ensure table exists
await virtualWorldPool.query(`
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_read BOOLEAN NOT NULL DEFAULT FALSE
)`);
} catch (e) {
console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message);
}
try {
const sql = `SELECT * FROM messages
WHERE (sender_id = $1 AND receiver_id = $2)
OR (sender_id = $2 AND receiver_id = $1)
ORDER BY created_at ASC`;
const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]);
res.json(messagesRes.rows);
} catch (err) {
console.error('[GET /api/messages/:contactId] error:', err);
res.status(500).json({ error: 'Ошибка получения сообщений' });
}
});
app.post('/api/messages/send', authenticate, async (req, res) => {
const senderId = req.user.id;
const { receiverId, message } = req.body || {};
const recvId = parseInt(receiverId, 10);
console.log('[POST /api/messages/send] sender:', senderId, 'receiver:', recvId);
try {
const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]);
if (receiverCheck.rows.length === 0) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
try {
await virtualWorldPool.query(`
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_read BOOLEAN NOT NULL DEFAULT FALSE
)`);
} catch (e) {
console.warn('[POST /api/messages/send] ensure table failed:', e.message);
}
const result = await virtualWorldPool.query(
`INSERT INTO messages (sender_id, receiver_id, message, created_at)
VALUES ($1, $2, $3, NOW())
RETURNING id, created_at, is_read`,
[senderId, recvId, message]
);
const newMessage = result.rows[0];
const receiverSocketId = onlineUsers[recvId];
if (receiverSocketId) {
io.to(receiverSocketId).emit('newMessage', {
id: newMessage.id,
text: message,
senderId,
timestamp: newMessage.created_at,
isRead: newMessage.is_read
});
}
res.status(201).json(newMessage);
} catch (err) {
console.error('[POST /api/messages/send] error:', err);
res.status(500).json({ error: 'Ошибка отправки сообщения' });
}
});
app.get('/api/messages', authenticate, async (req, res) => {
const userId = req.user.id;
try {
await virtualWorldPool.query(`
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_read BOOLEAN NOT NULL DEFAULT FALSE
)`);
} catch (e) {
console.warn('[GET /api/messages] ensure table failed:', e.message);
}
try {
const messagesRes = await virtualWorldPool.query(
`SELECT * FROM messages
WHERE sender_id = $1 OR receiver_id = $1
ORDER BY created_at DESC`,
[userId]
);
if (messagesRes.rows.length === 0) {
return res.json([]);
}
const userIds = new Set();
messagesRes.rows.forEach(msg => { userIds.add(msg.sender_id); userIds.add(msg.receiver_id); });
const usersRes = await virtualWorldPool.query(
`SELECT id, first_name, last_name, avatar_url FROM users WHERE id = ANY($1)`,
[Array.from(userIds)]
);
const userMap = {};
usersRes.rows.forEach(user => {
userMap[user.id] = { name: `${user.first_name} ${user.last_name}`, avatar: user.avatar_url };
});
const messages = messagesRes.rows.map(msg => ({
id: msg.id,
text: msg.message,
senderId: msg.sender_id,
receiverId: msg.receiver_id,
sender: userMap[msg.sender_id] || { name: 'Неизвестный', avatar: null },
receiver: userMap[msg.receiver_id] || { name: 'Неизвестный', avatar: null },
timestamp: msg.created_at,
isRead: msg.is_read
}));
res.json(messages);
} catch (err) {
console.error('[GET /api/messages] error:', err);
res.status(500).json({ error: 'Ошибка получения сообщений' });
}
});
app.patch('/api/messages/:id/read', authenticate, async (req, res) => {
const messageId = req.params.id;
const userId = req.user.id;
try {
await virtualWorldPool.query(`
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_read BOOLEAN NOT NULL DEFAULT FALSE
)`);
} catch (e) {
console.warn('[PATCH /api/messages/:id/read] ensure table failed:', e.message);
}
try {
const checkRes = await virtualWorldPool.query(`SELECT id FROM messages WHERE id = $1 AND receiver_id = $2`, [messageId, userId]);
if (checkRes.rows.length === 0) {
return res.status(404).json({ error: 'Сообщение не найдено или доступ запрещен' });
}
await virtualWorldPool.query(`UPDATE messages SET is_read = true WHERE id = $1`, [messageId]);
res.status(204).end();
} catch (err) {
console.error('[PATCH /api/messages/:id/read] error:', err);
res.status(500).json({ error: 'Ошибка обновления сообщения' });
}
});
app.get('/api/me', authenticate, async (req, res) => {
const userId = req.user.id;
const { rows } = await db.query(`
SELECT
email,
first_name AS "firstName",
last_name AS "lastName",
gender,
age,
city,
avatar_url AS "avatarURL",
balance,
hours_played AS "hoursPlayed",
reputation,
phone,
sportiness,
health_level AS "healthLevel",
stress_level AS "stressLevel",
satiety,
thirst,
diseases
FROM users
WHERE id = $1
`, [userId]);
if (!rows.length) return res.status(404).json({ error: 'User not found' });
const user = rows[0];
// Автоматически исправляем неправильный avatarURL
if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
console.log(`Исправляем неправильный avatarURL для пользователя ${userId}: ${user.avatarURL} -> /models/character.glb`);
try {
await db.query(
'UPDATE users SET avatar_url = $1 WHERE id = $2',
['/models/character.glb', userId]
);
user.avatarURL = '/models/character.glb';
} catch (e) {
console.error('Ошибка обновления avatarURL:', e);
}
}
res.json(user);
});
app.get('/api/players/:socketId', authenticate, async (req, res) => {
const socketId = req.params.socketId;
let p = players[socketId];
if (!p) {
for (const city of Object.values(playersByCity)) {
if (city[socketId]) {
p = city[socketId];
break;
}
}
}
if (!p) return res.status(404).json({ error: 'Player not found' });
const dbId = p.userId;
if (!dbId) return res.status(404).json({ error: 'User profile missing' });
const { rows } = await db.query(`
SELECT
first_name AS "firstName",
last_name AS "lastName",
gender,
age,
city,
avatar_url AS "avatarURL",
balance,
hours_played AS "hoursPlayed",
reputation,
phone,
sportiness,
health_level AS "healthLevel",
stress_level AS "stressLevel",
satiety,
thirst,
diseases
FROM users
WHERE id = $1
`, [dbId]);
if (!rows.length) return res.status(404).json({ error: 'User not found in database' });
const user = rows[0];
// Автоматически исправляем неправильный avatarURL
if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
console.log(`Исправляем неправильный avatarURL для игрока ${dbId}: ${user.avatarURL} -> /models/character.glb`);
try {
await db.query(
'UPDATE users SET avatar_url = $1 WHERE id = $2',
['/models/character.glb', dbId]
);
user.avatarURL = '/models/character.glb';
} catch (e) {
console.error('Ошибка обновления avatarURL:', e);
}
}
res.json(user);
});
app.post('/api/register', async (req, res) => {
try {
console.log('register request:', req.body?.email);
const { email, password, firstName, lastName, gender, age, city, avatarURL } = req.body || {};
if (!email || !password || !firstName || !lastName) {
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
}
const { rowCount } = await db.query(`SELECT 1 FROM users WHERE email = $1`, [email]);
if (rowCount) return res.status(400).json({ error: 'Почта уже занята' });
const hash = await bcrypt.hash(password, 10);
const insertSQL = `
INSERT INTO users(email, password_hash, first_name, last_name, gender, age, city, avatar_url)
VALUES($1,$2,$3,$4,$5,$6,$7,$8)
RETURNING id, email, created_at
`;
const result = await db.query(insertSQL, [
email, hash, firstName, lastName, gender ?? null, age ?? null, city ?? null, avatarURL ?? null
]);
const user = result.rows[0];
// Не даём регистрации упасть, если экономика не завелась
try {
await Economy.createAccount(user.id, 'USD');
} catch (e) {
console.error('Economy.createAccount failed:', e);
}
if (!process.env.JWT_SECRET) {
console.error('JWT_SECRET не задан в окружении (.env)');
return res.status(500).json({ error: 'Ошибка конфигурации сервера' });
}
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '12h' });
res.json({ success: true, token });
} catch (e) {
console.error('Ошибка регистрации:', e);
res.status(500).json({ error: 'Внутренняя ошибка регистрации' });
}
});
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const { rows } = await db.query(
`SELECT id, password_hash,
first_name AS "firstName",
last_name AS "lastName",
gender,
age,
city,
avatar_url AS "avatarURL"
FROM users
WHERE email = $1`,
[email]
);
if (!rows.length) {
return res.status(401).json({ error: 'Неверный логин или пароль' });
}
const user = rows[0];
const ok = await bcrypt.compare(password, user.password_hash);
if (!ok) {
return res.status(401).json({ error: 'Неверный логин или пароль' });
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: '12h'
});
res.json({
token,
profile: {
id: user.id,
email,
firstName: user.firstName,
lastName: user.lastName,
gender: user.gender,
age: user.age,
city: user.city,
avatarURL: user.avatarURL
}
});
});
// Получить объекты города по cityId
app.get('/api/cities/:cityId/objects', authenticate, async (req, res) => {
const cityId = req.params.cityId;
try {
const { rows } = await db.query(`
SELECT id,
name,
model_url,
pos_x, pos_y, pos_z,
rot_x, rot_y, rot_z,
COALESCE(scale_x, 1) AS scale_x,
COALESCE(scale_y, 1) AS scale_y,
COALESCE(scale_z, 1) AS scale_z,
organization_id,
COALESCE(collidable, true) AS collidable,
COALESCE(textures, '-') AS textures
FROM city_objects
WHERE city_id = $1
`, [cityId]);
res.json(rows);
} catch (e) {
console.error('Ошибка в /api/cities/:cityId/objects:', e);
res.status(500).json({ error: 'Ошибка получения объектов города' });
}
});
// Получить список доступных моделей из public/models/copied
app.get('/api/models', authenticate, async (req, res) => {
try {
const dir = pathLib.join(__dirname, 'public', 'models', 'copied');
const files = await fs.promises.readdir(dir);
const glbs = files.filter(f => f.toLowerCase().endsWith('.glb'));
res.json(glbs);
} catch (e) {
res.status(500).json({ error: 'Ошибка чтения списка моделей' });
}
});
// Обновить avatarURL пользователя
app.put('/api/profile/avatar', authenticate, async (req, res) => {
const { avatarURL } = req.body;
const userId = req.user.id;
try {
// Проверяем, что avatarURL не пустой и валидный
if (!avatarURL || avatarURL === 'try' || avatarURL === 'undefined' || avatarURL === 'null') {
return res.status(400).json({ error: 'Неправильный avatarURL' });
}
await db.query(
'UPDATE users SET avatar_url = $1 WHERE id = $2',
[avatarURL, userId]
);
res.json({ success: true, avatarURL });
} catch (e) {
console.error('Ошибка обновления avatarURL:', e);
res.status(500).json({ error: 'Ошибка обновления avatarURL' });
}
});
// Регистрируем маршрут на старте приложения:
app.get(
'/api/city_objects/:objectId/interior',
authenticate,
async (req, res) => {
const objectId = parseInt(req.params.objectId, 10);
try {
const { rows } = await db.query(
'SELECT interior_id FROM city_objects WHERE id = $1',
[objectId]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'Объект с таким id не найден' });
}
res.json({ interiorId: rows[0].interior_id });
} catch (e) {
console.error('Ошибка в /api/city_objects/:objectId/interior', e);
res.status(500).json({ error: 'Не удалось получить interior_id' });
}
}
);
// Новый эндпоинт для входа в интерьер:
app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => {
const interiorId = parseInt(req.params.interiorId, 10);
try {
const base = await db.query(
'SELECT spawn_x, spawn_y, spawn_z, spawn_rot, exit_x, exit_y, exit_z, exit_rot FROM interiors WHERE id = $1',
[interiorId]
);
const interior = base.rows[0];
if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });
let exitInt = null;
try {
const extra = await db.query(
'SELECT exit_int_x, exit_int_y, exit_int_z FROM interiors WHERE id = $1',
[interiorId]
);
if (extra.rows[0] && (extra.rows[0].exit_int_x != null || extra.rows[0].exit_int_y != null || extra.rows[0].exit_int_z != null)) {
exitInt = {
x: extra.rows[0].exit_int_x,
y: extra.rows[0].exit_int_y,
z: extra.rows[0].exit_int_z
};
}
} catch (e2) {
// Колонки exit_int_* могут отсутствовать — это не ошибка для клиента
console.warn('exit_int_* columns are not available yet:', e2.message);
}
// Эффективная точка входа: если spawn_x/y/z не заданы (или равны 0),
// пробуем использовать внутреннюю точку выхода (exit_int_*),
// иначе в крайнем случае используем координаты внешнего выхода.
const rawSpawn = {
x: interior.spawn_x,
y: interior.spawn_y,
z: interior.spawn_z,
rot: interior.spawn_rot
};
const isZeroOrNull = v => v == null || Number(v) === 0;
const spawnLooksEmpty = isZeroOrNull(rawSpawn.x) && isZeroOrNull(rawSpawn.y) && isZeroOrNull(rawSpawn.z);
let effectiveSpawn = { ...rawSpawn };
if (spawnLooksEmpty) {
if (exitInt && typeof exitInt.x === 'number' && typeof exitInt.z === 'number') {
effectiveSpawn = { x: exitInt.x, y: exitInt.y ?? 0, z: exitInt.z, rot: rawSpawn.rot };
} else if (!isZeroOrNull(interior.exit_x) || !isZeroOrNull(interior.exit_y) || !isZeroOrNull(interior.exit_z)) {
// Фолбэк на внешние координаты выхода, если они заданы
effectiveSpawn = { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: rawSpawn.rot };
}
}
// Логируем вычисленные координаты для отладки
try {
console.log('[INTERIOR ENTER]', {
interiorId,
spawn: effectiveSpawn,
rawSpawn,
exit: { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: interior.exit_rot },
exitInt
});
} catch (_) {}
res.json({
spawn: effectiveSpawn,
exit: {
x: interior.exit_x,
y: interior.exit_y,
z: interior.exit_z,
rot: interior.exit_rot
},
exitInt
});
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Не удалось получить координаты интерьера' });
}
});
// server.js, после маршрута /api/city_objects/:objectId/interior
app.get('/api/interiors/:interiorId/definition', authenticate, async (req, res) => {
const interiorId = parseInt(req.params.interiorId, 10);
try {
const interior = (await db.query(
'SELECT glb_filename, pos_x, pos_y, pos_z, spawn_x, spawn_y, spawn_z, spawn_rot FROM interiors WHERE id = $1',
[interiorId]
)).rows[0];
if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });
const objects = (await db.query(
`SELECT type, model_url, x, y, z, rot_x, rot_y, rot_z, scale
FROM interior_objects
WHERE interior_id = $1
ORDER BY id`,
[interiorId]
)).rows;
res.json({
glb: `/models/interiors/${interior.glb_filename}`,
position: { x: interior.pos_x, y: interior.pos_y, z: interior.pos_z },
spawn: { x: interior.spawn_x, y: interior.spawn_y, z: interior.spawn_z, rot: interior.spawn_rot },
objects
});
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Не удалось загрузить определение интерьера' });
}
});
// Начало копи
app.post('/api/listen', authenticate, async (req, res) => {
try {
console.log('[API /api/listen] Request body:', req.body);
const { player_id: bodyPlayerId, json_filename } = req.body || {};
const authUser = req.user || {};
const effectivePlayerId = bodyPlayerId || authUser.email || authUser.id || authUser.userId;
if (!effectivePlayerId || !json_filename) {
return res.status(400).json({ success: false, error: 'player_id and json_filename are required' });
}
// Выбираем пул: если есть отдельный пул, берём его, иначе общий db
try {
// Создаём таблицу при необходимости
await virtualWorldPool.query(`
CREATE TABLE IF NOT EXISTS json_listened (
id SERIAL PRIMARY KEY,
player_id TEXT NOT NULL,
json_filename TEXT NOT NULL,
listened_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`);
} catch (e) {
console.warn('[API /api/listen] ensure table failed:', e.message);
}
const q = `INSERT INTO json_listened (player_id, json_filename, listened_at) VALUES ($1, $2, NOW())`;
await virtualWorldPool.query(q, [String(effectivePlayerId), String(json_filename)]);
console.log('[API /api/listen] Saved listened:', { player: effectivePlayerId, json: json_filename });
return res.status(200).json({ success: true });
} catch (err) {
console.error('[API /api/listen] error:', err);
return res.status(500).json({ success: false, error: 'Database operation failed', details: err.message });
}
});
//Конец копи
//Начало копи
function generateTransactions() {
const transactions = [];
const suspiciousCount = Math.min(3 + Math.floor(level / 3), 5);
const suspiciousIds = [];
while (suspiciousIds.length < suspiciousCount) {
const id = Math.floor(Math.random() * 15);
if (!suspiciousIds.includes(id)) {
suspiciousIds.push(id);
}
}
// Генерируем 15 транзакций
for (let i = 0; i < 15; i++) {
const isSuspicious = suspiciousIds.includes(i);
// ... остальной код генерации транзакции ...
}
return transactions;
}
// Завершение игры
app.post('/api/cleanup-game/finish', authenticate, async (req, res) => {
try {
const { success, markedTransactions, personalArchive } = req.body;
const userId = req.user.id;
// Здесь должна быть логика обновления прогресса игрока
// Например, увеличение репутации, разблокировка квестов и т.д.
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: 'Ошибка сохранения результата игры' });
}
});
// Вспомогательная функция для генерации транзакций
function getRandomCity() {
const cities = ["Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", "Казань", "Самара", "Омск", "Челябинск", "Ростов-на-Дону", "Уфа"];
return cities[Math.floor(Math.random() * cities.length)];
}
function getRandomIP() {
return `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`;
}
function getRandomDevice() {
const devices = ["Chrome Win", "Safari iOS", "Android", "Firefox Mac", "Edge Win", "Opera Win"];
return devices[Math.floor(Math.random() * devices.length)];
}
function getRandomPurpose() {
const purposes = [
"Покупка продуктов",
"Оплата услуг",
"Перевод другу",
"Оплата аренды",
"Покупка техники",
"Благотворительность",
"Образовательные курсы"
];
return purposes[Math.floor(Math.random() * purposes.length)];
}
function getRandomRecipient() {
const recipients = [
"Пятёрочка №17",
"ИП Сидоров И.И.",
"OOO 'Комплекс-С'",
"ИП Петрова А.А.",
"Магнит №45",
"Ашан Супермаркет",
"ООО 'ТехноПрофи'"
];
return recipients[Math.floor(Math.random() * recipients.length)];
}
function getSuspiciousIP() {
// Генерируем IP из известных VPN диапазонов или Tor exit nodes
const vpnRanges = [
"185.2.33.", "213.42.12.", "172.16.", "192.168.", "10.0."
];
const range = vpnRanges[Math.floor(Math.random() * vpnRanges.length)];
return range + Math.floor(Math.random() * 255);
}
function getSuspiciousDevice() {
// Подозрительные устройства - одинаковые для разных транзакций
const suspiciousDevices = [
"Tor Browser",
"Android 4.4.2 (старая версия)",
"iPhone 6 (iOS 10)",
"Emulator Android"
];
return suspiciousDevices[Math.floor(Math.random() * suspiciousDevices.length)];
}
// Обновленная функция генерации транзакций
function generateTransactions() {
const transactions = [];
const suspiciousIds = [0, 5, 10]; // Фиксированные индексы подозрительных транзакций
// Генерируем 15 транзакций
for (let i = 0; i < 15; i++) {
const isSuspicious = suspiciousIds.includes(i);
const baseDate = new Date(2023, 6, 25); // 25 июля 2023
let date, amount, purpose, ip, city, device, recipient;
let anomalyType = isSuspicious ? i % 3 : null; // 0, 1 или 2 для подозрительных
if (isSuspicious) {
date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000);
amount = `${Math.floor(200000 + Math.random() * 800000).toLocaleString()}`;
switch (anomalyType) {
case 0: // Географический прыжок
date.setHours(date.getHours() + 1);
ip = getSuspiciousIP();
city = i % 2 === 0 ? "Москва" : "Самара";
device = getRandomDevice();
purpose = getRandomPurpose();
recipient = getRandomRecipient();
break;
case 1: // Пустое назначение + VPN
ip = getSuspiciousIP();
city = getRandomCity();
device = getSuspiciousDevice();
purpose = '';
recipient = getRandomRecipient();
break;
case 2: // Повтор получателя
ip = getRandomIP();
city = getRandomCity();
device = getRandomDevice();
purpose = getRandomPurpose();
recipient = "ООО 'Сомнительные Переводы'";
break;
}
} else {
// Нормальные транзакции
date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000);
amount = `${Math.floor(1000 + Math.random() * 20000).toLocaleString()}`;
purpose = getRandomPurpose();
ip = getRandomIP();
city = getRandomCity();
device = getRandomDevice();
recipient = getRandomRecipient();
}
transactions.push({
id: i,
date: date.toLocaleDateString(),
time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
amount,
purpose: isSuspicious && Math.random() > 0.5 ? '' : purpose,
ip: isSuspicious && Math.random() > 0.7 ? '' : ip,
city,
device: isSuspicious && Math.random() > 0.5 ? '' : device,
recipient,
_realIp: isSuspicious ? getSuspiciousIP() : ip,
_realDevice: isSuspicious ? getSuspiciousDevice() : device,
_isSuspicious: isSuspicious,
_anomalyType: anomalyType
});
}
return transactions;
}
//Конец копи
//Начало копи
app.get('/api/quests/progress', authenticate, async (req, res) => {
console.log("Загрузка на сервере. ID пользователя:", req.user.id);
try {
// ВМЕСТО получения email, используем напрямую ID пользователя
const userId = req.user.id.toString(); // Преобразуем в строку для consistency
// Получаем список всех квестов с их JSON файлами
const questsQuery = await virtualWorldPool.query(`
SELECT q.id, q.title, qj.json_filename
FROM quests q
JOIN quest_jsons qj ON q.id = qj.quest_id
ORDER BY q.id
`);
// Получаем JSON файлы, которые прослушал игрок по его ID
const listenedQuery = await virtualWorldPool.query(`
SELECT json_filename FROM json_listened
WHERE player_id = $1
`, [userId]); // Используем ID вместо email
console.log("Результат запроса listenedQuery для ID", userId, ":", listenedQuery.rows);
const listenedFiles = new Set(listenedQuery.rows.map(row => row.json_filename));
console.log("Прослушанные файлы:", Array.from(listenedFiles));
// Остальной код без изменений...
const questsMap = new Map();
questsQuery.rows.forEach(row => {
if (!questsMap.has(row.id)) {
questsMap.set(row.id, {
id: row.id,
title: row.title,
total: 0,
completed: 0,
files: []
});
}
const quest = questsMap.get(row.id);
quest.total++;
quest.files.push(row.json_filename);
if (listenedFiles.has(row.json_filename)) {
quest.completed++;
}
});
const result = Array.from(questsMap.values()).map(quest => ({
id: quest.id,
title: quest.title,
progress: quest.total > 0 ? Math.round((quest.completed / quest.total) * 100) : 0,
completed: quest.completed,
total: quest.total
}));
console.log("Результат для клиента:", result);
res.json(result);
} catch (err) {
console.error('Ошибка получения прогресса квестов:', err);
res.status(500).json({ error: 'Ошибка получения прогресса квестов' });
}
});
app.get('/api/cleanup-game/data', authenticate, async (req, res) => {
try {
const level = parseInt(req.query.level) || 1;
// Генерируем транзакции с учетом уровня
const transactions = generateTransactions(level);
res.json({
success: true,
transactions: transactions,
level: level
});
} catch (e) {
console.error('Ошибка генерации данных игры:', e);
res.status(500).json({
success: false,
error: 'Ошибка генерации данных игры'
});
}
});
//Конец копи
// Список интерьеров с координатами для отображения на карте
app.get('/api/interiors', authenticate, async (req, res) => {
try {
const { rows } = await db.query(
'SELECT id, pos_x, pos_y, pos_z FROM interiors ORDER BY id'
);
res.json(rows);
} catch (e) {
console.error('Ошибка получения списка интерьеров', e);
res.status(500).json({ error: 'Не удалось получить список интерьеров' });
}
});
// Получить объекты интерьера
app.get('/api/interiors/:id/objects', authenticate, async (req, res) => {
const id = parseInt(req.params.id, 10);
try {
const { rows } = await db.query(
`SELECT id, model_url, x, y, z, rot_x, rot_y, rot_z, scale
FROM interior_objects
WHERE interior_id = $1
ORDER BY id`,
[id]
);
res.json(rows);
} catch (e) {
console.error('Ошибка получения объектов интерьера', e);
res.status(500).json({ error: 'Не удалось получить объекты интерьера' });
}
});
// Сохранить объекты интерьера в БД
app.post('/api/interiors/:id/save', authenticate, async (req, res) => {
const id = parseInt(req.params.id, 10);
const { objects = [], removedIds = [] } = req.body;
if (!Array.isArray(objects) || !Array.isArray(removedIds)) {
return res.status(400).json({ error: 'Invalid objects' });
}
try {
if (removedIds.length) {
await db.query(
'DELETE FROM interior_objects WHERE id = ANY($1::int[]) AND interior_id = $2',
[removedIds, id]
);
}
for (const obj of objects) {
if (obj.id) {
await db.query(
`UPDATE interior_objects
SET model_url=$1, x=$2, y=$3, z=$4,
rot_x=$5, rot_y=$6, rot_z=$7, scale=$8
WHERE id=$9 AND interior_id=$10`,
[
obj.model_url,
obj.x,
obj.y,
obj.z,
obj.rot_x,
obj.rot_y,
obj.rot_z,
obj.scale,
obj.id,
id
]
);
} else {
await db.query(
`INSERT INTO interior_objects
(interior_id, model_url, x, y, z, rot_x, rot_y, rot_z, scale)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[
id,
obj.model_url,
obj.x,
obj.y,
obj.z,
obj.rot_x,
obj.rot_y,
obj.rot_z,
obj.scale
]
);
}
}
res.json({ ok: true });
} catch (e) {
console.error('Ошибка сохранения интерьера', e);
res.status(500).json({ error: 'Не удалось сохранить интерьер' });
}
});
// Получить организацию по objectId
app.get('/api/organizations/by-object/:objectId', authenticate, async (req, res) => {
const objectId = parseInt(req.params.objectId, 10);
try {
const { rows } = await db.query(`
SELECT
o.id,
o.name,
os.menu,
os.work_hours
FROM city_objects AS co
JOIN organizations AS o
ON co.organization_id = o.id
JOIN organization_settings AS os
ON os.organization_id = o.id
WHERE co.id = $1
`, [objectId]);
if (rows.length === 0) {
return res.status(404).json({ error: 'Организация не найдена для этого объекта' });
}
res.json(rows[0]);
} catch (e) {
console.error('Ошибка в /api/organizations/by-object/:objectId:', e);
res.status(500).json({ error: 'Ошибка получения меню организации' });
}
});
// Сохранить текущую карту в текстовый файл
app.post('/api/save-map', authenticate, async (req, res) => {
const { cityId = 'unknown', objects, removedIds = [] } = req.body;
if (!Array.isArray(objects) || !Array.isArray(removedIds)) {
return res.status(400).json({ error: 'Invalid objects' });
}
try {
const dir = pathLib.join(__dirname, 'saves');
await fs.promises.mkdir(dir, { recursive: true });
const file = `city_${cityId}_${Date.now()}.txt`;
const filePath = pathLib.join(dir, file);
await fs.promises.writeFile(
filePath,
JSON.stringify({ objects, removedIds }, null, 2),
'utf8'
);
res.json({ ok: true, file });
} catch (e) {
console.error('Ошибка сохранения карты', e);
res.status(500).json({ error: 'Ошибка сохранения карты' });
}
});
// Получить список городов с названием и страной
app.get('/api/cities', authenticate, async (req, res) => {
try {
const { rows } = await db.query(`
SELECT cities.id, cities.name, countries.name AS country_name
FROM cities
JOIN countries ON cities.country_id = countries.id
ORDER BY countries.name, cities.name
`);
res.json(rows);
} catch (e) {
res.status(500).json({ error: 'Ошибка получения списка городов' });
}
});
app.use((req, res) => {
res.sendFile(pathLib.join(__dirname, 'build', 'index.html'));
});
const PORT = process.env.PORT || 4000;
http.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// Логирование всех маршрутов и middleware
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
const orig = app[method];
app[method] = function(path, ...args) {
if (typeof path === 'string') {
console.log(`Регистрируется ${method.toUpperCase()} маршрут:`, path);
} else if (typeof path === 'function') {
console.log(`Регистрируется middleware (без пути) через ${method}`);
}
return orig.call(this, path, ...args);
};
});
// После ensureMessagesTable();
async function ensureInteriorsSpawnColumns() {
try {
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_x NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_y NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_z NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_rot NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_x NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_y NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_z NUMERIC DEFAULT 0');
await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_rot NUMERIC DEFAULT 0');
} catch (e) {
console.error('Ошибка добавления spawn/exit-колонок в interiors', e);
}
}
ensureInteriorsSpawnColumns();
// Добавляем колонки масштаба для городских объектов, если ещё не созданы
async function ensureCityObjectsScaleColumns() {
try {
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_x NUMERIC DEFAULT 1');
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_y NUMERIC DEFAULT 1');
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_z NUMERIC DEFAULT 1');
} catch (e) {
console.error('Ошибка добавления scale колонок в city_objects', e);
}
}
ensureCityObjectsScaleColumns();