759 lines
26 KiB
JavaScript
759 lines
26 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
const { authenticateModeration } = 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 config = require('../config');
|
||
|
||
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
|
||
|
||
const requireModerationAccess = async (req, res, next) => {
|
||
const username = normalizeUsername(req.user?.username);
|
||
const telegramId = req.user?.telegramId;
|
||
|
||
if (!username || !telegramId) {
|
||
return res.status(401).json({ error: 'Требуется авторизация' });
|
||
}
|
||
|
||
if (OWNER_USERNAMES.has(username) || req.user.role === 'admin') {
|
||
req.isModerationAdmin = true;
|
||
req.isOwner = true;
|
||
return next();
|
||
}
|
||
|
||
const allowed = await isModerationAdmin({ telegramId, username });
|
||
if (!allowed) {
|
||
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) => ({
|
||
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
|
||
});
|
||
|
||
router.post('/auth/verify', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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,
|
||
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.put('/posts/:id', authenticateModeration, requireModerationAccess, async (req, res) => {
|
||
const { content, hashtags, tags, isNSFW } = req.body;
|
||
|
||
const post = await Post.findById(req.params.id).populate('author');
|
||
if (!post) {
|
||
return res.status(404).json({ error: 'Пост не найден' });
|
||
}
|
||
|
||
// Проверить, может ли админ редактировать этот пост
|
||
// Админ может редактировать:
|
||
// 1. Любой пост, если он владелец (req.isOwner)
|
||
// 2. Только свои посты из канала (где adminNumber совпадает)
|
||
if (!req.isOwner) {
|
||
// Получить админа текущего пользователя
|
||
const admin = await ModerationAdmin.findOne({ telegramId: req.user.telegramId });
|
||
|
||
// Если это пост из канала, проверить, что админ - автор
|
||
if (post.publishedToChannel && post.adminNumber) {
|
||
if (!admin || admin.adminNumber !== post.adminNumber) {
|
||
return res.status(403).json({ error: 'Вы можете редактировать только свои посты из канала' });
|
||
}
|
||
}
|
||
// Если это обычный пост, владелец может редактировать любой, остальные админы - нет
|
||
// (это поведение можно изменить по необходимости)
|
||
}
|
||
|
||
if (content !== undefined) {
|
||
post.content = content;
|
||
post.hashtags = Array.isArray(hashtags)
|
||
? hashtags.map((tag) => tag.toLowerCase())
|
||
: post.hashtags;
|
||
}
|
||
|
||
if (tags !== undefined) {
|
||
post.tags = Array.isArray(tags) ? tags : post.tags;
|
||
}
|
||
|
||
if (isNSFW !== undefined) {
|
||
post.isNSFW = !!isNSFW;
|
||
}
|
||
|
||
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,
|
||
publishedToChannel: post.publishedToChannel,
|
||
adminNumber: post.adminNumber,
|
||
editedAt: post.editedAt,
|
||
createdAt: post.createdAt
|
||
}
|
||
});
|
||
});
|
||
|
||
router.delete('/posts/:id', authenticateModeration, 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);
|
||
}
|
||
|
||
await Post.deleteOne({ _id: post._id });
|
||
res.json({ success: true });
|
||
});
|
||
|
||
router.delete('/posts/:id/images/:index', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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', authenticateModeration, 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'
|
||
});
|
||
|
||
// Отправить код владельцу (req.user - это ты)
|
||
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 минут.`
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Код подтверждения отправлен вам в бот',
|
||
username: user.username
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка инициирования добавления админа:', error);
|
||
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
|
||
}
|
||
});
|
||
|
||
// Подтвердить добавление админа
|
||
router.post('/admins/confirm-add', authenticateModeration, 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', authenticateModeration, 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'
|
||
});
|
||
|
||
// Отправить код владельцу (req.user - это ты)
|
||
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 минут.`
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Код подтверждения отправлен вам в бот',
|
||
username: admin.username
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка инициирования удаления админа:', error);
|
||
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
|
||
}
|
||
});
|
||
|
||
// Подтвердить удаление админа
|
||
router.post('/admins/confirm-remove', authenticateModeration, 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',
|
||
authenticateModeration,
|
||
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;
|
||
|