From d83399e5f94ade62a6a415ca40c689f9ce9ea2c4 Mon Sep 17 00:00:00 2001
From: glpshchn <464976@niuitmo.ru>
Date: Tue, 11 Nov 2025 03:33:22 +0300
Subject: [PATCH] Update files
---
ADMIN_MANAGEMENT_UPDATE.md | 145 ++++++++++++++
backend/bots/serverMonitor.js | 22 +++
backend/models/AdminConfirmation.js | 32 ++++
backend/models/ModerationAdmin.js | 7 +
backend/routes/modApp.js | 287 +++++++++++++++++++++++++++-
backend/server.js | 26 ++-
backend/websocket.js | 18 +-
moderation/frontend/src/App.jsx | 11 +-
8 files changed, 529 insertions(+), 19 deletions(-)
create mode 100644 ADMIN_MANAGEMENT_UPDATE.md
create mode 100644 backend/models/AdminConfirmation.js
diff --git a/ADMIN_MANAGEMENT_UPDATE.md b/ADMIN_MANAGEMENT_UPDATE.md
new file mode 100644
index 0000000..e04fefd
--- /dev/null
+++ b/ADMIN_MANAGEMENT_UPDATE.md
@@ -0,0 +1,145 @@
+# Обновление: Управление админами и исправления
+
+## ✅ Что сделано
+
+### 1. Убран суффикс "Сообщите об ошибке" из специфичных ошибок
+- Обновлён `backend/server.js`
+- Суффикс не добавляется к ошибкам валидации, публикации и других операционных сообщений
+- Список исключений: "Загрузите хотя бы одно изображение", "Не удалось опубликовать в канал", "Требуется авторизация", и др.
+
+### 2. Добавлено управление админами через Mini App
+**Новые модели:**
+- `backend/models/AdminConfirmation.js` - хранение кодов подтверждения (TTL 5 минут)
+- Обновлена `backend/models/ModerationAdmin.js` - добавлено поле `adminNumber` (1-10)
+
+**Новые API endpoints в `/api/mod-app`:**
+- `GET /admins` - получить список всех админов
+- `POST /admins/initiate-add` - инициировать добавление админа (только для @glpshchn00)
+- `POST /admins/confirm-add` - подтвердить добавление по коду
+- `POST /admins/initiate-remove` - инициировать удаление админа (только для @glpshchn00)
+- `POST /admins/confirm-remove` - подтвердить удаление по коду
+
+**Как работает:**
+1. Владелец (@glpshchn00) видит кнопки "Назначить" и "Снять" у пользователей
+2. При нажатии выбирается номер админа (1-10)
+3. Система генерирует 6-значный код и отправляет пользователю в личку бота
+4. Пользователь вводит код в Mini App
+5. После подтверждения админ добавляется/удаляется
+
+### 3. Номера админов (1-10)
+- Каждому админу присваивается уникальный номер от 1 до 10
+- Номер выбирается владельцем при назначении
+- Номер используется автоматически при публикации постов (теперь НЕ нужно выбирать слот)
+
+### 4. Убран выбор слота из публикации
+- В `backend/routes/modApp.js` роут `/channel/publish` обновлён
+- Теперь автоматически берётся `adminNumber` из базы данных
+- Поле `slot` больше не требуется в запросе
+
+### 5. Исправлен live chat
+- Обновлён `backend/websocket.js`
+- Владелец (@glpshchn00) теперь может подключаться к чату
+- Добавлена проверка `config.moderationOwnerUsernames`
+- Улучшено логирование подключений
+
+## 📦 Деплой
+
+### На сервере:
+
+```bash
+cd /var/www/nakama
+
+# 1. Обновить код (если через git)
+git pull
+
+# 2. Установить зависимости (если добавились новые)
+npm install --production
+
+# 3. Перезапустить бекэнд
+pm2 restart nakama-backend --update-env
+
+# 4. Проверить логи
+pm2 logs nakama-backend --lines 50
+```
+
+### Обновление существующих админов:
+
+Если у тебя уже есть админы в базе БЕЗ `adminNumber`, нужно добавить номера вручную:
+
+```bash
+mongosh nakama
+```
+
+```javascript
+// Посмотреть текущих админов
+db.moderationadmins.find()
+
+// Назначить номера вручную (замени ID и номера)
+db.moderationadmins.updateOne(
+ { _id: ObjectId("...") },
+ { $set: { adminNumber: 1 } }
+)
+
+db.moderationadmins.updateOne(
+ { _id: ObjectId("...") },
+ { $set: { adminNumber: 2 } }
+)
+
+// И так далее для каждого админа
+```
+
+Или удалить всех и добавить заново через Mini App:
+
+```javascript
+db.moderationadmins.deleteMany({})
+```
+
+## 🎯 Следующие шаги
+
+Нужно обновить фронтенд модерации (`moderation/frontend/src/App.jsx`), чтобы добавить:
+
+1. **Новую вкладку "Админы"** с:
+ - Списком всех админов с номерами
+ - Кнопками "Назначить" и "Снять" (только для @glpshchn00)
+ - Модальным окном для ввода кода подтверждения
+ - Выбором номера админа (1-10)
+
+2. **Убрать выбор слота** из вкладки "Публикация":
+ - Удалить dropdown со слотами
+ - Показывать текущий номер админа из базы
+
+3. **Тестирование:**
+ - Проверить live chat
+ - Проверить добавление/удаление админов
+ - Проверить публикацию с автоматическим слотом
+
+## 🔒 Безопасность
+
+- Все операции с админами требуют авторизации через `authenticateModeration`
+- Добавление/удаление доступно только владельцу через middleware `requireOwner`
+- Коды подтверждения удаляются автоматически через 5 минут (MongoDB TTL)
+- Коды одноразовые - удаляются сразу после использования
+- Боту нужны права отправки сообщений пользователям
+
+## ⚠️ Важно
+
+**Перед запуском на проде убедись:**
+1. `MODERATION_BOT_TOKEN` правильно настроен в `.env`
+2. Бот может отправлять сообщения пользователям (они должны начать диалог с ботом)
+3. Владелец (@glpshchn00) правильно указан в `MODERATION_OWNER_USERNAMES`
+4. MongoDB доступна и работает
+
+## 🐛 Возможные проблемы
+
+**"Бот не отправляет код":**
+- Проверь, что пользователь написал боту `/start`
+- Проверь `MODERATION_BOT_TOKEN` в логах
+
+**"Номер админа уже занят":**
+- Проверь `db.moderationadmins.find()` - возможно есть дубликаты
+- Очисти базу: `db.moderationadmins.deleteMany({})`
+
+**"Live chat не подключается":**
+- Проверь, что владелец указан в `MODERATION_OWNER_USERNAMES`
+- Посмотри логи WebSocket подключения
+
diff --git a/backend/bots/serverMonitor.js b/backend/bots/serverMonitor.js
index dc57a2b..6a1cc92 100644
--- a/backend/bots/serverMonitor.js
+++ b/backend/bots/serverMonitor.js
@@ -394,9 +394,31 @@ const sendChannelMediaGroup = async (files, caption) => {
}
};
+/**
+ * Отправить сообщение пользователю
+ */
+const sendMessageToUser = async (userId, message) => {
+ if (!moderationBot) {
+ throw new Error('Бот модерации не инициализирован');
+ }
+
+ try {
+ await axios.post(`${TELEGRAM_API}/sendMessage`, {
+ chat_id: userId,
+ text: message,
+ parse_mode: 'HTML'
+ });
+ log('info', 'Сообщение отправлено пользователю', { userId });
+ } catch (error) {
+ log('error', 'Не удалось отправить сообщение пользователю', { userId, error: error.response?.data || error.message });
+ throw error;
+ }
+};
+
module.exports = {
startServerMonitorBot,
sendChannelMediaGroup,
+ sendMessageToUser,
isModerationAdmin
};
diff --git a/backend/models/AdminConfirmation.js b/backend/models/AdminConfirmation.js
new file mode 100644
index 0000000..80ca669
--- /dev/null
+++ b/backend/models/AdminConfirmation.js
@@ -0,0 +1,32 @@
+const mongoose = require('mongoose');
+
+const AdminConfirmationSchema = new mongoose.Schema({
+ userId: {
+ type: String,
+ required: true,
+ index: true
+ },
+ code: {
+ type: String,
+ required: true
+ },
+ adminNumber: {
+ type: Number,
+ required: true,
+ min: 1,
+ max: 10
+ },
+ action: {
+ type: String,
+ enum: ['add', 'remove'],
+ required: true
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now,
+ expires: 300 // Удаляется через 5 минут
+ }
+});
+
+module.exports = mongoose.model('AdminConfirmation', AdminConfirmationSchema);
+
diff --git a/backend/models/ModerationAdmin.js b/backend/models/ModerationAdmin.js
index 03716d5..a10a1a4 100644
--- a/backend/models/ModerationAdmin.js
+++ b/backend/models/ModerationAdmin.js
@@ -15,6 +15,13 @@ const ModerationAdminSchema = new mongoose.Schema({
},
firstName: String,
lastName: String,
+ adminNumber: {
+ type: Number,
+ required: true,
+ min: 1,
+ max: 10,
+ unique: true
+ },
addedBy: {
type: String,
lowercase: true,
diff --git a/backend/routes/modApp.js b/backend/routes/modApp.js
index f713459..e757745 100644
--- a/backend/routes/modApp.js
+++ b/backend/routes/modApp.js
@@ -3,13 +3,16 @@ const router = express.Router();
const fs = require('fs');
const path = require('path');
const multer = require('multer');
+const crypto = require('crypto');
const { authenticateModeration } = require('../middleware/auth');
const { logSecurityEvent } = require('../middleware/logger');
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 } = require('../bots/serverMonitor');
+const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
const config = require('../config');
const TEMP_DIR = path.join(__dirname, '../uploads/mod-channel');
@@ -44,6 +47,7 @@ const requireModerationAccess = async (req, res, next) => {
if (OWNER_USERNAMES.has(username) || req.user.role === 'admin') {
req.isModerationAdmin = true;
+ req.isOwner = true;
return next();
}
@@ -53,9 +57,17 @@ const requireModerationAccess = async (req, res, next) => {
}
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,
@@ -360,20 +372,289 @@ router.put('/reports/:id', authenticateModeration, requireModerationAccess, asyn
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'
+ });
+
+ // Отправить код пользователю
+ await sendMessageToUser(
+ user.telegramId,
+ `Подтверждение назначения админом\n\n` +
+ `Вас назначают администратором модерации.\n` +
+ `Номер админа: ${adminNumber}\n\n` +
+ `Для подтверждения введите код в приложении:\n` +
+ `${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,
+ `✅ Вы назначены администратором модерации!\n\n` +
+ `Ваш номер: ${confirmation.adminNumber}\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'
+ });
+
+ // Отправить код пользователю
+ await sendMessageToUser(
+ admin.telegramId,
+ `Подтверждение снятия с должности админа\n\n` +
+ `Вас снимают с должности администратора модерации.\n\n` +
+ `Для подтверждения введите код в приложении:\n` +
+ `${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,
+ `❌ Вы сняты с должности администратора модерации\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,
upload.array('images', 10),
async (req, res) => {
- const { description = '', tags, slot } = req.body;
+ const { description = '', tags } = req.body;
const files = req.files || [];
if (!files.length) {
return res.status(400).json({ error: 'Загрузите хотя бы одно изображение' });
}
- const slotNumber = Math.max(Math.min(parseInt(slot, 10) || 1, 10), 1);
+ // Получить номер админа из базы
+ 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()) {
diff --git a/backend/server.js b/backend/server.js
index e7fd724..ab1665d 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -80,20 +80,40 @@ app.use((req, res, next) => {
return;
}
- if (typeof obj.error === 'string' && !obj.error.includes(ERROR_SUPPORT_SUFFIX)) {
+ // Список ошибок, к которым НЕ нужно добавлять суффикс
+ const skipSuffixMessages = [
+ 'Загрузите хотя бы одно изображение',
+ 'Не удалось опубликовать в канал',
+ 'Публиковать в канал могут только админы',
+ 'Требуется авторизация',
+ 'Требуются права',
+ 'Неверный код подтверждения',
+ 'Код подтверждения истёк',
+ 'Номер админа уже занят',
+ 'Пользователь не найден',
+ 'Администратор не найден'
+ ];
+
+ const shouldSkipSuffix = (text) => {
+ if (!text || typeof text !== 'string') return false;
+ return skipSuffixMessages.some(msg => text.includes(msg));
+ };
+
+ if (typeof obj.error === 'string' && !obj.error.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.error)) {
obj.error += ERROR_SUPPORT_SUFFIX;
}
- if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX)) {
+ if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.message)) {
obj.message += ERROR_SUPPORT_SUFFIX;
}
if (Array.isArray(obj.errors)) {
obj.errors = obj.errors.map((item) => {
if (typeof item === 'string') {
+ if (shouldSkipSuffix(item)) return item;
return item.includes(ERROR_SUPPORT_SUFFIX) ? item : `${item}${ERROR_SUPPORT_SUFFIX}`;
}
- if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX)) {
+ if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(item.message)) {
return { ...item, message: `${item.message}${ERROR_SUPPORT_SUFFIX}` };
}
return item;
diff --git a/backend/websocket.js b/backend/websocket.js
index 24e54d5..58d6d68 100644
--- a/backend/websocket.js
+++ b/backend/websocket.js
@@ -68,12 +68,20 @@ function registerModerationChat() {
const telegramId = payload.telegramId;
if (!username || !telegramId) {
+ log('warn', 'Mod chat auth failed: no username/telegramId', { username, telegramId });
socket.emit('unauthorized');
return socket.disconnect(true);
}
- const allowed = await isModerationAdmin({ username, telegramId });
- if (!allowed) {
+ // Проверить, является ли владельцем
+ const ownerUsernames = config.moderationOwnerUsernames || [];
+ const isOwner = ownerUsernames.includes(username);
+
+ // Проверить, является ли админом
+ const isAdmin = await isModerationAdmin({ username, telegramId });
+
+ if (!isOwner && !isAdmin) {
+ log('warn', 'Mod chat access denied', { username, telegramId });
socket.emit('unauthorized');
return socket.disconnect(true);
}
@@ -81,11 +89,15 @@ function registerModerationChat() {
socket.data.authorized = true;
socket.data.username = username;
socket.data.telegramId = telegramId;
+ socket.data.isOwner = isOwner;
+
connectedModerators.set(socket.id, {
username,
- telegramId
+ telegramId,
+ isOwner
});
+ log('info', 'Mod chat auth success', { username, isOwner, isAdmin });
socket.emit('ready');
broadcastOnline();
});
diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx
index f567367..5636165 100644
--- a/moderation/frontend/src/App.jsx
+++ b/moderation/frontend/src/App.jsx
@@ -131,10 +131,7 @@ export default function App() {
} finally {
if (!cancelled) {
setLoading(false);
- const telegramApp = window.Telegram?.WebApp;
- telegramApp?.MainButton?.setText?.('Закрыть');
- telegramApp?.MainButton?.show?.();
- telegramApp?.MainButton?.onClick?.(() => telegramApp.close());
+ // Убрана кнопка "Закрыть"
}
}
};
@@ -143,7 +140,6 @@ export default function App() {
return () => {
cancelled = true;
- window.Telegram?.WebApp?.MainButton?.hide?.();
};
}, []);
@@ -645,11 +641,6 @@ export default function App() {