nakama/backend/routes/modApp.js

881 lines
30 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.

const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { authenticateModerationFlexible } = require('../middleware/auth');
const { logSecurityEvent } = require('../middleware/logger');
const { uploadChannelMedia, cleanupOnError } = require('../middleware/upload');
const { deleteFile } = require('../utils/minio');
const User = require('../models/User');
const Post = require('../models/Post');
const Report = require('../models/Report');
const ModerationAdmin = require('../models/ModerationAdmin');
const AdminConfirmation = require('../models/AdminConfirmation');
const { listAdmins, isModerationAdmin, normalizeUsername } = require('../services/moderationAdmin');
const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
const { sendAdminConfirmationCode } = require('../utils/email');
const config = require('../config');
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
// Проверка доступа к модерации (только для модераторов и админов)
const requireModerationAccess = async (req, res, next) => {
if (!req.user) {
return res.status(403).json({ error: 'Недостаточно прав для модерации' });
}
// Для JWT авторизации (без Telegram) - проверяем роль
if (!req.user.telegramId) {
if (!['moderator', 'admin'].includes(req.user.role)) {
return res.status(403).json({ error: 'Недостаточно прав для модерации' });
}
req.isModerationAdmin = true;
req.isOwner = req.user.role === 'admin';
return next();
}
// Для Telegram авторизации - проверяем через isModerationAdmin
const username = normalizeUsername(req.user?.username);
const telegramId = req.user?.telegramId;
// Владелец или админ по роли - всегда доступ
if (OWNER_USERNAMES.has(username) || req.user.role === 'admin') {
req.isModerationAdmin = true;
req.isOwner = true;
return next();
}
// Проверить, является ли пользователь модератором через ModerationAdmin
const allowed = await isModerationAdmin({ telegramId, username });
if (!allowed) {
// Если не модератор по базе, но роль moderator или admin - разрешить
if (['moderator', 'admin'].includes(req.user.role)) {
req.isModerationAdmin = true;
req.isOwner = req.user.role === 'admin';
return next();
}
return res.status(403).json({ error: 'Недостаточно прав для модерации' });
}
req.isModerationAdmin = true;
req.isOwner = false;
return next();
};
const requireOwner = (req, res, next) => {
if (!req.isOwner) {
return res.status(403).json({ error: 'Требуются права владельца' });
}
next();
};
const serializeUser = (user) => {
if (!user) return null;
return {
id: user._id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
banned: user.banned,
bannedUntil: user.bannedUntil,
lastActiveAt: user.lastActiveAt,
createdAt: user.createdAt,
referralsCount: user.referralsCount || 0,
// passwordHash никогда не возвращается (уже select: false в модели)
// email не возвращается для безопасности
};
};
router.post('/auth/verify', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const admins = await listAdmins();
res.json({
success: true,
user: {
id: req.user._id,
username: req.user.username,
firstName: req.user.firstName,
lastName: req.user.lastName,
role: req.user.role,
telegramId: req.user.telegramId
},
admins
});
});
router.get('/users', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { filter = 'active', page = 1, limit = 50 } = req.query;
const pageNum = Math.max(parseInt(page, 10) || 1, 1);
const limitNum = Math.min(Math.max(parseInt(limit, 10) || 50, 1), 200);
const skip = (pageNum - 1) * limitNum;
const threshold = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
let query = {};
if (filter === 'active') {
query = { lastActiveAt: { $gte: threshold } };
} else if (filter === 'inactive') {
query = {
$or: [
{ lastActiveAt: { $lt: threshold } },
{ lastActiveAt: { $exists: false } }
],
banned: { $ne: true }
};
} else if (filter === 'banned') {
query = { banned: true };
}
const [users, total] = await Promise.all([
User.find(query)
.sort({ lastActiveAt: -1 })
.skip(skip)
.limit(limitNum)
.lean(),
User.countDocuments(query)
]);
res.json({
users: users.map(serializeUser),
total,
totalPages: Math.ceil(total / limitNum),
currentPage: pageNum
});
});
router.put('/users/:id/ban', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { banned, days } = req.body;
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
user.banned = !!banned;
if (user.banned) {
const durationDays = Math.max(parseInt(days, 10) || 7, 1);
user.bannedUntil = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000);
} else {
user.bannedUntil = null;
}
await user.save();
res.json({ user: serializeUser(user) });
});
router.get('/posts', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { page = 1, limit = 20, author, tag } = req.query;
const pageNum = Math.max(parseInt(page, 10) || 1, 1);
const limitNum = Math.min(Math.max(parseInt(limit, 10) || 20, 1), 100);
const skip = (pageNum - 1) * limitNum;
const query = {};
if (author) {
query.author = author;
}
if (tag) {
query.tags = tag;
}
const [posts, total] = await Promise.all([
Post.find(query)
.populate('author', 'username firstName lastName role banned bannedUntil lastActiveAt')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitNum)
.lean(),
Post.countDocuments(query)
]);
const serialized = posts.map((post) => ({
id: post._id,
author: post.author ? serializeUser(post.author) : null,
content: post.content,
hashtags: post.hashtags,
tags: post.tags,
images: post.images || (post.imageUrl ? [post.imageUrl] : []),
commentsCount: post.comments?.length || 0,
likesCount: post.likes?.length || 0,
isNSFW: post.isNSFW,
isArt: post.isArt,
publishedToChannel: post.publishedToChannel,
adminNumber: post.adminNumber,
editedAt: post.editedAt,
createdAt: post.createdAt
}));
res.json({
posts: serialized,
total,
totalPages: Math.ceil(total / limitNum),
currentPage: pageNum
});
});
// Получить пост с комментариями
router.get('/posts/:id', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username firstName lastName photoUrl')
.populate('comments.author', 'username firstName lastName photoUrl')
.exec();
if (!post) {
return res.status(404).json({ error: 'Пост не найден' });
}
res.json({
post: {
id: post._id,
author: post.author ? serializeUser(post.author) : null,
content: post.content,
hashtags: post.hashtags,
tags: post.tags,
images: post.images || (post.imageUrl ? [post.imageUrl] : []),
comments: post.comments || [],
likesCount: post.likes?.length || 0,
isNSFW: post.isNSFW,
createdAt: post.createdAt
}
});
} catch (error) {
console.error('Ошибка получения поста:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Удалить комментарий (модераторский интерфейс)
router.delete('/posts/:postId/comments/:commentId', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
try {
const post = await Post.findById(req.params.postId);
if (!post) {
return res.status(404).json({ error: 'Пост не найден' });
}
const comment = post.comments.id(req.params.commentId);
if (!comment) {
return res.status(404).json({ error: 'Комментарий не найден' });
}
post.comments.pull(req.params.commentId);
await post.save();
await post.populate('comments.author', 'username firstName lastName photoUrl');
res.json({ comments: post.comments });
} catch (error) {
console.error('Ошибка удаления комментария:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
router.put('/posts/:id', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { content, hashtags, tags, isNSFW, isArt } = req.body;
const post = await Post.findById(req.params.id).populate('author');
if (!post) {
return res.status(404).json({ error: 'Пост не найден' });
}
// Модераторы и админы могут редактировать любой пост через панель модерации
// Доступ к панели модерации уже проверен через requireModerationAccess
if (content !== undefined) {
post.content = content;
// Обновить хэштеги из контента, если hashtags не переданы явно
if (hashtags !== undefined) {
post.hashtags = Array.isArray(hashtags)
? hashtags.map((tag) => tag.toLowerCase())
: post.hashtags;
} else {
// Извлечь хэштеги из контента
const { extractHashtags } = require('../utils/hashtags');
post.hashtags = extractHashtags(content);
}
}
if (tags !== undefined) {
post.tags = Array.isArray(tags) ? tags : post.tags;
}
if (isNSFW !== undefined) {
post.isNSFW = !!isNSFW;
}
// Обработка метки "арт"
const wasArt = post.isArt || false;
if (isArt !== undefined) {
post.isArt = !!isArt;
// Если арт только что помечен (было false, стало true), начислить билеты
if (!wasArt && post.isArt && post.author) {
const { awardArtModeration } = require('../utils/tickets');
await awardArtModeration(post.author._id);
}
}
post.editedAt = new Date();
await post.save();
await post.populate('author', 'username firstName lastName role banned bannedUntil');
// Если пост был опубликован в канале, обновить его там
if (post.publishedToChannel && post.channelMessageId) {
try {
const { updateChannelMessage } = require('../bots/serverMonitor');
if (updateChannelMessage) {
await updateChannelMessage(post.channelMessageId, post.content, post.hashtags);
}
} catch (error) {
console.error('Не удалось обновить сообщение в канале:', error);
// Продолжаем выполнение, даже если не удалось обновить в канале
}
}
res.json({
post: {
id: post._id,
author: post.author ? serializeUser(post.author) : null,
content: post.content,
hashtags: post.hashtags,
tags: post.tags,
images: post.images,
isNSFW: post.isNSFW,
isArt: post.isArt,
publishedToChannel: post.publishedToChannel,
adminNumber: post.adminNumber,
editedAt: post.editedAt,
createdAt: post.createdAt
}
});
});
router.delete('/posts/:id', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Пост не найден' });
}
// Удалить изображения из MinIO
try {
const { deleteFiles } = require('../utils/minio');
const filesToDelete = [];
if (post.images && post.images.length > 0) {
post.images.forEach(imageUrl => {
// Извлекаем имя файла из URL
const match = imageUrl.match(/nakama-media\/(.+)$/);
if (match) {
filesToDelete.push(match[1]);
}
});
} else if (post.imageUrl) {
const match = post.imageUrl.match(/nakama-media\/(.+)$/);
if (match) {
filesToDelete.push(match[1]);
}
}
if (filesToDelete.length > 0) {
await deleteFiles(filesToDelete);
console.log(`✅ Удалено ${filesToDelete.length} файлов из MinIO (modApp)`);
}
} catch (error) {
console.error('❌ Ошибка удаления файлов из MinIO:', error);
}
// Списать все билеты, связанные с удаленным постом (только если пост создан сегодня)
const { deductPostDeletion } = require('../utils/tickets');
await deductPostDeletion(post);
await Post.deleteOne({ _id: post._id });
res.json({ success: true });
});
router.delete('/posts/:id/images/:index', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { id, index } = req.params;
const idx = parseInt(index, 10);
const post = await Post.findById(id);
if (!post) {
return res.status(404).json({ error: 'Пост не найден' });
}
if (!Array.isArray(post.images) || idx < 0 || idx >= post.images.length) {
return res.status(400).json({ error: 'Неверный индекс изображения' });
}
const [removed] = post.images.splice(idx, 1);
post.imageUrl = post.images[0] || null;
await post.save();
// Удалить изображение из MinIO
if (removed) {
try {
const match = removed.match(/nakama-media\/(.+)$/);
if (match) {
await deleteFile(match[1]);
console.log(`✅ Удалено изображение из MinIO: ${match[1]}`);
}
} catch (error) {
console.error('❌ Ошибка удаления изображения из MinIO:', error);
}
}
res.json({ images: post.images });
});
router.post('/posts/:id/ban', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { id } = req.params;
const { days = 7 } = req.body;
const post = await Post.findById(id).populate('author');
if (!post || !post.author) {
return res.status(404).json({ error: 'Пост или автор не найден' });
}
const durationDays = Math.max(parseInt(days, 10) || 7, 1);
post.author.banned = true;
post.author.bannedUntil = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000);
await post.author.save();
res.json({ user: serializeUser(post.author) });
});
router.get('/reports', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { page = 1, limit = 30, status = 'pending' } = req.query;
const pageNum = Math.max(parseInt(page, 10) || 1, 1);
const limitNum = Math.min(Math.max(parseInt(limit, 10) || 30, 1), 100);
const skip = (pageNum - 1) * limitNum;
const query = status === 'all' ? {} : { status };
const [reports, total] = await Promise.all([
Report.find(query)
.populate('reporter', 'username firstName lastName telegramId')
.populate({
path: 'post',
populate: {
path: 'author',
select: 'username firstName lastName telegramId banned bannedUntil'
}
})
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitNum)
.lean(),
Report.countDocuments(query)
]);
res.json({
reports: reports.map((report) => ({
id: report._id,
reporter: report.reporter ? serializeUser(report.reporter) : null,
status: report.status,
reason: report.reason || 'Не указана',
createdAt: report.createdAt,
post: report.post
? {
id: report.post._id,
content: report.post.content,
images: report.post.images || (report.post.imageUrl ? [report.post.imageUrl] : []),
author: report.post.author ? serializeUser(report.post.author) : null
}
: null
})),
total,
totalPages: Math.ceil(total / limitNum),
currentPage: pageNum
});
});
router.put('/reports/:id', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
const { status = 'reviewed' } = req.body;
const report = await Report.findById(req.params.id);
if (!report) {
return res.status(404).json({ error: 'Репорт не найден' });
}
report.status = status;
report.reviewedBy = req.user._id;
await report.save();
res.json({ success: true });
});
// ========== УПРАВЛЕНИЕ АДМИНАМИ ==========
// Получить список всех админов
router.get('/admins', authenticateModerationFlexible, requireModerationAccess, async (req, res) => {
try {
const admins = await ModerationAdmin.find().sort({ adminNumber: 1 });
res.json({
admins: admins.map(admin => ({
id: admin._id,
telegramId: admin.telegramId,
username: admin.username,
firstName: admin.firstName,
lastName: admin.lastName,
adminNumber: admin.adminNumber,
addedBy: admin.addedBy,
createdAt: admin.createdAt
}))
});
} catch (error) {
console.error('Ошибка получения списка админов:', error);
res.status(500).json({ error: 'Ошибка получения списка админов' });
}
});
// Инициировать добавление админа (только для владельца)
router.post('/admins/initiate-add', authenticateModerationFlexible, requireModerationAccess, requireOwner, async (req, res) => {
try {
const { userId, adminNumber } = req.body;
if (!userId || !adminNumber) {
return res.status(400).json({ error: 'Не указан ID пользователя или номер админа' });
}
if (adminNumber < 1 || adminNumber > 10) {
return res.status(400).json({ error: 'Номер админа должен быть от 1 до 10' });
}
// Проверить, не занят ли номер
const existingAdmin = await ModerationAdmin.findOne({ adminNumber });
if (existingAdmin) {
return res.status(400).json({ error: 'Номер админа уже занят' });
}
// Проверить, существует ли пользователь
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
// Проверить, не является ли пользователь уже админом
const isAlreadyAdmin = await ModerationAdmin.findOne({ telegramId: user.telegramId });
if (isAlreadyAdmin) {
return res.status(400).json({ error: 'Пользователь уже является админом' });
}
// Генерировать 6-значный код
const code = crypto.randomInt(100000, 999999).toString();
// Сохранить код подтверждения
await AdminConfirmation.create({
userId: user.telegramId,
code,
adminNumber,
action: 'add'
});
// Отправить код на email владельца
try {
await sendAdminConfirmationCode(code, 'add', {
username: user.username,
firstName: user.firstName,
adminNumber: adminNumber
});
} catch (emailError) {
console.error('Ошибка отправки кода на email:', emailError);
// Fallback - отправить в Telegram если email не работает
try {
await sendMessageToUser(
req.user.telegramId,
`<b>Подтверждение назначения админом</b>\n\n` +
`Назначаете пользователя @${user.username} (${user.firstName}) админом.\n` +
`Номер админа: <b>${adminNumber}</b>\n\n` +
`Код подтверждения:\n` +
`<code>${code}</code>\n\n` +
`Код действителен 5 минут.`
);
} catch (telegramError) {
console.error('Ошибка отправки кода в Telegram:', telegramError);
}
}
res.json({
success: true,
message: 'Код подтверждения отправлен на email владельца',
username: user.username
});
} catch (error) {
console.error('Ошибка инициирования добавления админа:', error);
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
}
});
// Подтвердить добавление админа
router.post('/admins/confirm-add', authenticateModerationFlexible, requireModerationAccess, requireOwner, async (req, res) => {
try {
const { userId, code } = req.body;
if (!userId || !code) {
return res.status(400).json({ error: 'Не указан ID пользователя или код' });
}
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
// Найти код подтверждения
const confirmation = await AdminConfirmation.findOne({
userId: user.telegramId,
code,
action: 'add'
});
if (!confirmation) {
return res.status(400).json({ error: 'Неверный код подтверждения' });
}
// Проверить, не занят ли номер
const existingAdmin = await ModerationAdmin.findOne({ adminNumber: confirmation.adminNumber });
if (existingAdmin) {
await AdminConfirmation.deleteOne({ _id: confirmation._id });
return res.status(400).json({ error: 'Номер админа уже занят' });
}
// Добавить админа
const newAdmin = await ModerationAdmin.create({
telegramId: user.telegramId,
username: normalizeUsername(user.username),
firstName: user.firstName,
lastName: user.lastName,
adminNumber: confirmation.adminNumber,
addedBy: normalizeUsername(req.user.username)
});
// Удалить код подтверждения
await AdminConfirmation.deleteOne({ _id: confirmation._id });
// Уведомить пользователя
try {
await sendMessageToUser(
user.telegramId,
`<b>✅ Вы назначены администратором модерации!</b>\n\n` +
`Ваш номер: <b>${confirmation.adminNumber}</b>\n` +
`Теперь вы можете использовать модераторское приложение.`
);
} catch (error) {
console.error('Не удалось отправить уведомление пользователю:', error);
}
res.json({
success: true,
admin: {
id: newAdmin._id,
telegramId: newAdmin.telegramId,
username: newAdmin.username,
firstName: newAdmin.firstName,
lastName: newAdmin.lastName,
adminNumber: newAdmin.adminNumber
}
});
} catch (error) {
console.error('Ошибка подтверждения добавления админа:', error);
res.status(500).json({ error: 'Ошибка добавления админа' });
}
});
// Инициировать удаление админа (только для владельца)
router.post('/admins/initiate-remove', authenticateModerationFlexible, requireModerationAccess, requireOwner, async (req, res) => {
try {
const { adminId } = req.body;
if (!adminId) {
return res.status(400).json({ error: 'Не указан ID админа' });
}
const admin = await ModerationAdmin.findById(adminId);
if (!admin) {
return res.status(404).json({ error: 'Администратор не найден' });
}
// Генерировать 6-значный код
const code = crypto.randomInt(100000, 999999).toString();
// Сохранить код подтверждения
await AdminConfirmation.create({
userId: admin.telegramId,
code,
adminNumber: admin.adminNumber,
action: 'remove'
});
// Отправить код на email владельца
try {
await sendAdminConfirmationCode(code, 'remove', {
username: admin.username,
firstName: admin.firstName,
adminNumber: admin.adminNumber
});
} catch (emailError) {
console.error('Ошибка отправки кода на email:', emailError);
// Fallback - отправить в Telegram если email не работает
try {
await sendMessageToUser(
req.user.telegramId,
`<b>Подтверждение снятия админа</b>\n\n` +
`Снимаете пользователя @${admin.username} (${admin.firstName}) с должности админа.\n` +
`Номер админа: <b>${admin.adminNumber}</b>\n\n` +
`Код подтверждения:\n` +
`<code>${code}</code>\n\n` +
`Код действителен 5 минут.`
);
} catch (telegramError) {
console.error('Ошибка отправки кода в Telegram:', telegramError);
}
}
res.json({
success: true,
message: 'Код подтверждения отправлен на email владельца',
username: admin.username
});
} catch (error) {
console.error('Ошибка инициирования удаления админа:', error);
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
}
});
// Подтвердить удаление админа
router.post('/admins/confirm-remove', authenticateModerationFlexible, requireModerationAccess, requireOwner, async (req, res) => {
try {
const { adminId, code } = req.body;
if (!adminId || !code) {
return res.status(400).json({ error: 'Не указан ID админа или код' });
}
const admin = await ModerationAdmin.findById(adminId);
if (!admin) {
return res.status(404).json({ error: 'Администратор не найден' });
}
// Найти код подтверждения
const confirmation = await AdminConfirmation.findOne({
userId: admin.telegramId,
code,
action: 'remove'
});
if (!confirmation) {
return res.status(400).json({ error: 'Неверный код подтверждения' });
}
// Удалить админа
await ModerationAdmin.deleteOne({ _id: admin._id });
// Удалить код подтверждения
await AdminConfirmation.deleteOne({ _id: confirmation._id });
// Уведомить пользователя
try {
await sendMessageToUser(
admin.telegramId,
`<b>❌ Вы сняты с должности администратора модерации</b>\n\n` +
`Доступ к модераторскому приложению прекращён.`
);
} catch (error) {
console.error('Не удалось отправить уведомление пользователю:', error);
}
res.json({ success: true });
} catch (error) {
console.error('Ошибка подтверждения удаления админа:', error);
res.status(500).json({ error: 'Ошибка удаления админа' });
}
});
// ========== ПУБЛИКАЦИЯ В КАНАЛ ==========
router.post(
'/channel/publish',
authenticateModerationFlexible,
requireModerationAccess,
uploadChannelMedia,
async (req, res) => {
const { description = '', tags } = req.body;
const files = req.files || [];
if (!files.length) {
return res.status(400).json({ error: 'Загрузите хотя бы одно изображение' });
}
// Получить номер админа из базы
const admin = await ModerationAdmin.findOne({ telegramId: req.user.telegramId });
// Проверить, что админ имеет номер от 1 до 10
if (!admin || !admin.adminNumber || admin.adminNumber < 1 || admin.adminNumber > 10) {
return res.status(403).json({
error: 'Публиковать в канал могут только админы с номерами от 1 до 10. Обратитесь к владельцу для назначения номера.'
});
}
const slotNumber = admin.adminNumber;
let tagsArray = [];
if (typeof tags === 'string' && tags.trim()) {
try {
tagsArray = JSON.parse(tags);
} catch {
tagsArray = tags.split(/[,\s]+/).filter(Boolean);
}
} else if (Array.isArray(tags)) {
tagsArray = tags;
}
const formattedTags = tagsArray
.map((tag) => tag.trim())
.filter(Boolean)
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`));
if (!formattedTags.includes(`#a${slotNumber}`)) {
formattedTags.push(`#a${slotNumber}`);
}
const captionLines = [];
if (description) {
captionLines.push(description);
}
if (formattedTags.length) {
captionLines.push('', formattedTags.join(' '));
}
const caption = captionLines.join('\n');
try {
// Отправить в канал и получить message_id
const messageId = await sendChannelMediaGroup(files, caption);
// Создать пост в базе данных
const newPost = new Post({
author: req.user._id,
content: description,
hashtags: tagsArray.map(tag => tag.replace('#', '').toLowerCase()),
images: [], // Медиа хранится в Telegram
tags: ['other'], // Можно настроить определение типа контента
publishedToChannel: true,
channelMessageId: messageId,
adminNumber: slotNumber,
isNSFW: false
});
await newPost.save();
res.json({ success: true, postId: newPost._id, messageId });
} catch (error) {
logSecurityEvent('CHANNEL_PUBLISH_FAILED', req, { error: error.message });
res.status(500).json({ error: 'Не удалось опубликовать в канал' });
}
}
);
module.exports = router;