236 строки
5.5 KiB
JavaScript
236 строки
5.5 KiB
JavaScript
import Fastify from 'fastify';
|
|
import cors from '@fastify/cors';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import websocket from '@fastify/websocket';
|
|
import { config } from './config.js';
|
|
import {
|
|
createVisitorMessage,
|
|
getOrCreateOpenConversation,
|
|
getSiteByWidgetKey,
|
|
getStats,
|
|
getTelegramSettings,
|
|
listRecentMessages,
|
|
migrate,
|
|
touchVisitor,
|
|
updateTelegramSettings
|
|
} from './db.js';
|
|
import {
|
|
getTelegramBridgeStatus,
|
|
notifyOperatorsAboutVisitorMessage,
|
|
restartTelegramBridge,
|
|
startTelegramBridge
|
|
} from './telegram.js';
|
|
|
|
migrate();
|
|
|
|
const app = Fastify({
|
|
logger: {
|
|
transport: config.app.env === 'development' ? { target: 'pino-pretty' } : undefined
|
|
},
|
|
trustProxy: config.app.trustProxy
|
|
});
|
|
|
|
await app.register(cors, {
|
|
origin(origin, callback) {
|
|
if (!origin) return callback(null, true);
|
|
const allowed = new Set([config.cors.adminOrigin, config.cors.widgetOrigin]);
|
|
callback(null, allowed.has(origin));
|
|
},
|
|
credentials: true
|
|
});
|
|
|
|
await app.register(rateLimit, {
|
|
max: 120,
|
|
timeWindow: '1 minute'
|
|
});
|
|
|
|
await app.register(websocket);
|
|
|
|
const clients = new Map();
|
|
|
|
function validateSocks5Settings(settings) {
|
|
const proxy = settings.proxy || {};
|
|
|
|
if (!proxy.enabled) {
|
|
return {
|
|
ok: true,
|
|
status: 'disabled',
|
|
message: 'SOCKS5 выключен. Telegram будет подключаться напрямую.'
|
|
};
|
|
}
|
|
|
|
if (proxy.type !== 'socks5') {
|
|
return {
|
|
ok: false,
|
|
status: 'invalid',
|
|
message: 'Сейчас поддерживается только socks5.'
|
|
};
|
|
}
|
|
|
|
if (!proxy.host) {
|
|
return {
|
|
ok: false,
|
|
status: 'invalid',
|
|
message: 'Укажите host SOCKS5 прокси.'
|
|
};
|
|
}
|
|
|
|
if (!Number.isInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) {
|
|
return {
|
|
ok: false,
|
|
status: 'invalid',
|
|
message: 'Порт SOCKS5 должен быть от 1 до 65535.'
|
|
};
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
status: 'configured',
|
|
message: `SOCKS5 включен: ${proxy.host}:${proxy.port}. Telegram bridge сможет использовать этот прокси.`
|
|
};
|
|
}
|
|
|
|
app.get('/health', async () => ({
|
|
ok: true,
|
|
service: 'wschat-backend',
|
|
env: config.app.env,
|
|
telegram: getTelegramBridgeStatus()
|
|
}));
|
|
|
|
app.get('/api/config/public', async () => ({
|
|
apiUrl: config.app.publicApiUrl,
|
|
wsUrl: config.app.publicWsUrl
|
|
}));
|
|
|
|
app.get('/api/admin/stats', async () => ({
|
|
ok: true,
|
|
stats: getStats(),
|
|
telegram: getTelegramBridgeStatus()
|
|
}));
|
|
|
|
app.get('/api/admin/messages', async (request) => {
|
|
const limit = Math.min(Math.max(Number(request.query?.limit || 50), 1), 200);
|
|
return {
|
|
ok: true,
|
|
messages: listRecentMessages(limit)
|
|
};
|
|
});
|
|
|
|
app.get('/api/admin/telegram/settings', async () => ({
|
|
ok: true,
|
|
settings: getTelegramSettings(),
|
|
bridge: getTelegramBridgeStatus()
|
|
}));
|
|
|
|
app.post('/api/admin/telegram/settings', async (request, reply) => {
|
|
try {
|
|
const settings = updateTelegramSettings(request.body || {});
|
|
const bridge = await restartTelegramBridge({ logger: app.log });
|
|
|
|
return {
|
|
ok: true,
|
|
settings,
|
|
bridge
|
|
};
|
|
} catch (error) {
|
|
reply.code(400);
|
|
return {
|
|
ok: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
});
|
|
|
|
app.post('/api/admin/telegram/restart', async () => ({
|
|
ok: true,
|
|
bridge: await restartTelegramBridge({ logger: app.log })
|
|
}));
|
|
|
|
app.post('/api/admin/telegram/test-proxy', async () => {
|
|
const settings = getTelegramSettings({ revealSecrets: true });
|
|
const result = validateSocks5Settings(settings);
|
|
|
|
return {
|
|
...result,
|
|
settings: getTelegramSettings(),
|
|
bridge: getTelegramBridgeStatus()
|
|
};
|
|
});
|
|
|
|
app.post('/api/widget/message', async (request) => {
|
|
const body = request.body || {};
|
|
const siteId = String(body.siteId || body.widgetKey || '');
|
|
const visitorKey = String(body.visitorId || '').trim();
|
|
const messageBody = String(body.message || '').trim();
|
|
|
|
if (!siteId || !visitorKey || !messageBody) {
|
|
return { ok: false, error: 'siteId, visitorId and message are required' };
|
|
}
|
|
|
|
if (messageBody.length > 5000) {
|
|
return { ok: false, error: 'message is too long' };
|
|
}
|
|
|
|
const site = getSiteByWidgetKey(siteId);
|
|
if (!site) {
|
|
return { ok: false, error: 'unknown or inactive site' };
|
|
}
|
|
|
|
const visitor = touchVisitor({
|
|
siteId: site.id,
|
|
visitorKey,
|
|
userAgent: request.headers['user-agent'] || '',
|
|
ip: request.ip
|
|
});
|
|
|
|
const conversation = getOrCreateOpenConversation({
|
|
siteId: site.id,
|
|
visitorId: visitor.id
|
|
});
|
|
|
|
const message = createVisitorMessage({
|
|
conversationId: conversation.id,
|
|
siteId: site.id,
|
|
visitorId: visitor.id,
|
|
body: messageBody
|
|
});
|
|
|
|
await notifyOperatorsAboutVisitorMessage({
|
|
site,
|
|
visitor,
|
|
conversation,
|
|
message,
|
|
logger: request.log
|
|
});
|
|
|
|
request.log.info({ siteId: site.id, visitorId: visitor.id, conversationId: conversation.id, messageId: message.id }, 'Widget message saved');
|
|
|
|
return {
|
|
ok: true,
|
|
status: 'saved',
|
|
conversationId: conversation.id,
|
|
messageId: message.id,
|
|
telegram: getTelegramBridgeStatus()
|
|
};
|
|
});
|
|
|
|
app.get('/ws', { websocket: true }, (connection, request) => {
|
|
const visitorId = request.query?.visitorId || crypto.randomUUID();
|
|
clients.set(visitorId, connection);
|
|
|
|
connection.on('close', () => {
|
|
clients.delete(visitorId);
|
|
});
|
|
|
|
connection.send(JSON.stringify({ type: 'connected', visitorId }));
|
|
});
|
|
|
|
await startTelegramBridge({ logger: app.log });
|
|
|
|
try {
|
|
await app.listen({ host: config.app.host, port: config.app.port });
|
|
} catch (error) {
|
|
app.log.error(error);
|
|
process.exit(1);
|
|
}
|