From af063ecc7d1010688a5ea3bcc294d31df663c18d Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Thu, 4 Dec 2025 20:44:05 +0300 Subject: [PATCH] Update files --- backend/bots/mainBot.js | 199 +++++++++++++++++++++++++++ backend/middleware/auth.js | 17 ++- backend/models/User.js | 24 ++++ backend/routes/auth.js | 8 ++ backend/routes/bot.js | 31 +++++ backend/routes/modApp.js | 3 +- backend/routes/posts.js | 11 ++ backend/server.js | 2 + frontend/src/components/PostCard.jsx | 36 ++++- frontend/src/pages/Profile.css | 90 ++++++++++++ frontend/src/pages/Profile.jsx | 60 +++++++- moderation/frontend/src/App.jsx | 1 + 12 files changed, 476 insertions(+), 6 deletions(-) create mode 100644 backend/bots/mainBot.js diff --git a/backend/bots/mainBot.js b/backend/bots/mainBot.js new file mode 100644 index 0000000..7274b1b --- /dev/null +++ b/backend/bots/mainBot.js @@ -0,0 +1,199 @@ +const axios = require('axios'); +const config = require('../config'); +const { log, logError } = require('../middleware/logger'); +const User = require('../models/User'); + +const BOT_TOKEN = config.telegramBotToken; +const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null; + +let isPolling = false; +let offset = 0; + +const sendMessage = async (chatId, text, options = {}) => { + if (!TELEGRAM_API) { + log('warn', 'TELEGRAM_BOT_TOKEN не установлен, отправка сообщения невозможна'); + return null; + } + + try { + const response = await axios.post(`${TELEGRAM_API}/sendMessage`, { + chat_id: chatId, + text, + parse_mode: 'HTML', + ...options + }); + return response.data; + } catch (error) { + logError('Ошибка отправки сообщения', error, { chatId, text: text.substring(0, 50) }); + return null; + } +}; + +const sendMessageToAllUsers = async (messageText) => { + if (!TELEGRAM_API) { + throw new Error('TELEGRAM_BOT_TOKEN не установлен'); + } + + try { + const users = await User.find({ banned: { $ne: true } }).select('telegramId'); + let sent = 0; + let failed = 0; + + for (const user of users) { + try { + await sendMessage(user.telegramId, messageText); + sent++; + // Небольшая задержка, чтобы не превысить лимиты API + await new Promise(resolve => setTimeout(resolve, 50)); + } catch (error) { + failed++; + logError('Ошибка отправки сообщения пользователю', error, { telegramId: user.telegramId }); + } + } + + return { sent, failed, total: users.length }; + } catch (error) { + logError('Ошибка массовой отправки сообщений', error); + throw error; + } +}; + +const getStartMessage = () => { + return `👋 Добро пожаловать в Nakama! + +📱 Nakama — социальная сеть для фурри и аниме сообщества. + +Основные возможности: +• Создание постов с текстом и изображениями +• Поиск контента через e621 и Gelbooru +• Комментарии и лайки +• Подписки на пользователей +• Система уведомлений +• Фильтры и теги + +Как начать: +1. Нажмите кнопку "Войти" ниже, чтобы запустить приложение +2. Создайте свой первый пост +3. Подписывайтесь на интересных пользователей + +Поддержка: +Если возникли проблемы, напишите @NakamaReportbot + +Приятного использования!`; +}; + +const handleCommand = async (message) => { + const chatId = message.chat.id; + const text = (message.text || '').trim(); + const args = text.split(/\s+/); + const command = args[0].toLowerCase(); + + if (command === '/start') { + const startParam = message.text.split(' ')[1] || ''; + + // Если есть start_param (например, post_12345 или ref_ABC123) + // Это обрабатывается при открытии миниаппа, здесь просто показываем инструкцию + const startMessage = getStartMessage(); + + // Добавить кнопку для открытия миниаппа + let botUsername = 'NakamaSpaceBot'; + if (config.telegramBotToken) { + try { + const botInfo = await axios.get(`${TELEGRAM_API}/getMe`); + botUsername = botInfo.data.result.username || 'NakamaSpaceBot'; + } catch (error) { + log('warn', 'Не удалось получить имя бота', { error: error.message }); + } + } + + await sendMessage(chatId, startMessage, { + reply_markup: { + inline_keyboard: [[ + { + text: '🚀 Открыть Nakama', + web_app: { + url: `https://t.me/${botUsername}` + } + } + ]] + } + }); + return; + } + + // Игнорируем неизвестные команды +}; + +const processUpdate = async (update) => { + const message = update.message || update.edited_message; + if (!message || !message.text) { + return; + } + + try { + await handleCommand(message); + } catch (error) { + logError('Ошибка обработки команды основного бота', error, { + chatId: message.chat.id, + text: message.text?.substring(0, 50) + }); + } +}; + +const pollUpdates = async () => { + if (!TELEGRAM_API) { + log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен'); + return; + } + + if (isPolling) { + return; + } + + isPolling = true; + log('info', 'Основной бот запущен, опрос обновлений...'); + + const poll = async () => { + try { + const response = await axios.get(`${TELEGRAM_API}/getUpdates`, { + params: { + offset, + timeout: 30, + allowed_updates: ['message'] + } + }); + + const updates = response.data.result || []; + + for (const update of updates) { + offset = update.update_id + 1; + await processUpdate(update); + } + + // Продолжить опрос + setTimeout(poll, 1000); + } catch (error) { + logError('Ошибка опроса Telegram для основного бота', error); + // Переподключиться через 5 секунд + setTimeout(poll, 5000); + } + }; + + poll(); +}; + +const startMainBot = () => { + if (!BOT_TOKEN) { + log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен'); + return; + } + + pollUpdates(); +}; + +module.exports = { + startMainBot, + sendMessageToAllUsers, + sendMessage +}; + diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 292ad78..2bd5ff5 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -159,6 +159,7 @@ const authenticate = async (req, res, next) => { } const telegramUser = payload.user; + const startParam = payload.start_param || payload.startParam; if (!validateTelegramId(telegramUser.id)) { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id }); @@ -171,14 +172,28 @@ const authenticate = async (req, res, next) => { let user = await User.findOne({ telegramId: normalizedUser.id.toString() }); if (!user) { + // Обработка реферального кода из start_param + let referredBy = null; + if (startParam && startParam.startsWith('ref_')) { + const referralCode = startParam; + const referrer = await User.findOne({ referralCode }); + if (referrer) { + referredBy = referrer._id; + } + } + user = new User({ telegramId: normalizedUser.id.toString(), username: normalizedUser.username || normalizedUser.firstName || 'user', firstName: normalizedUser.firstName, lastName: normalizedUser.lastName, - photoUrl: normalizedUser.photoUrl + photoUrl: normalizedUser.photoUrl, + referredBy: referredBy }); await user.save(); + + // Счетчик рефералов увеличивается только когда пользователь создаст первый пост + // (см. routes/posts.js) } else { // Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями if (normalizedUser.username) { diff --git a/backend/models/User.js b/backend/models/User.js index 468e1cd..fbbdba5 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -55,11 +55,35 @@ const UserSchema = new mongoose.Schema({ default: false }, bannedUntil: Date, + // Реферальная система + referralCode: { + type: String, + unique: true, + sparse: true + }, + referredBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + referralsCount: { + type: Number, + default: 0 + }, createdAt: { type: Date, default: Date.now } }); +// Генерировать реферальный код перед сохранением +UserSchema.pre('save', async function(next) { + if (!this.referralCode) { + // Генерировать уникальный код на основе telegramId + const code = `ref_${this.telegramId.slice(-8)}${Math.random().toString(36).substring(2, 6)}`.toUpperCase(); + this.referralCode = code; + } + next(); +}); + module.exports = mongoose.model('User', UserSchema); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 6df5556..67a968e 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -55,6 +55,8 @@ const respondWithUser = async (user, res) => { role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, + referralCode: populatedUser.referralCode, + referralsCount: populatedUser.referralsCount || 0, settings, banned: populatedUser.banned } @@ -232,6 +234,8 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => { role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, + referralCode: populatedUser.referralCode, + referralsCount: populatedUser.referralsCount || 0, settings, banned: populatedUser.banned } @@ -277,6 +281,8 @@ router.post('/session', async (req, res) => { { path: 'following', select: 'username firstName lastName photoUrl' } ]); + const settings = normalizeUserSettings(populatedUser.settings); + res.json({ success: true, user: { @@ -290,6 +296,8 @@ router.post('/session', async (req, res) => { role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, + referralCode: populatedUser.referralCode, + referralsCount: populatedUser.referralsCount || 0, settings, banned: populatedUser.banned } diff --git a/backend/routes/bot.js b/backend/routes/bot.js index 3d70d45..e2ae2dc 100644 --- a/backend/routes/bot.js +++ b/backend/routes/bot.js @@ -57,5 +57,36 @@ router.post('/send-photos', authenticate, async (req, res) => { } }); +// Отправить сообщение всем пользователям (только админы) +router.post('/broadcast', authenticate, async (req, res) => { + try { + // Проверка прав админа + if (req.user.role !== 'admin') { + return res.status(403).json({ error: 'Требуются права администратора' }); + } + + const { message } = req.body; + + if (!message || !message.trim()) { + return res.status(400).json({ error: 'Сообщение обязательно' }); + } + + const { sendMessageToAllUsers } = require('../bots/mainBot'); + const result = await sendMessageToAllUsers(message); + + res.json({ + success: true, + message: `Сообщение отправлено ${result.sent} пользователям`, + result + }); + } catch (error) { + console.error('Ошибка рассылки:', error); + res.status(500).json({ + error: 'Ошибка отправки сообщений', + details: error.message + }); + } +}); + module.exports = router; diff --git a/backend/routes/modApp.js b/backend/routes/modApp.js index fd1c4e8..abaab74 100644 --- a/backend/routes/modApp.js +++ b/backend/routes/modApp.js @@ -58,7 +58,8 @@ const serializeUser = (user) => ({ banned: user.banned, bannedUntil: user.bannedUntil, lastActiveAt: user.lastActiveAt, - createdAt: user.createdAt + createdAt: user.createdAt, + referralsCount: user.referralsCount || 0 }); router.post('/auth/verify', authenticateModeration, requireModerationAccess, async (req, res) => { diff --git a/backend/routes/posts.js b/backend/routes/posts.js index 0b2b3ee..e5d9be7 100644 --- a/backend/routes/posts.js +++ b/backend/routes/posts.js @@ -153,6 +153,17 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa await post.save(); await post.populate('author', 'username firstName lastName photoUrl'); + // Проверка первого поста для реферальной системы + // Счетчик рефералов увеличивается только когда приглашенный пользователь создал первый пост + const userPostsCount = await Post.countDocuments({ author: req.user._id }); + if (userPostsCount === 1 && req.user.referredBy) { + // Это первый пост пользователя, который был приглашен по реферальной ссылке + const User = require('../models/User'); + await User.findByIdAndUpdate(req.user.referredBy, { + $inc: { referralsCount: 1 } + }); + } + // Создать уведомления для упомянутых пользователей if (post.mentionedUsers.length > 0) { const notifications = post.mentionedUsers.map(userId => ({ diff --git a/backend/server.js b/backend/server.js index 148a27d..e9e7d14 100644 --- a/backend/server.js +++ b/backend/server.js @@ -262,6 +262,8 @@ initWebSocket(server); // Автообновление аватарок отключено - обновление происходит только при перезаходе // scheduleAvatarUpdates(); startServerMonitorBot(); +const { startMainBot } = require('./bots/mainBot'); +startMainBot(); // Обработка необработанных ошибок process.on('uncaughtException', (error) => { diff --git a/frontend/src/components/PostCard.jsx b/frontend/src/components/PostCard.jsx index 4ef4dad..2591659 100644 --- a/frontend/src/components/PostCard.jsx +++ b/frontend/src/components/PostCard.jsx @@ -1,8 +1,8 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn } from 'lucide-react' +import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react' import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api' -import { hapticFeedback, showConfirm } from '../utils/telegram' +import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram' import './PostCard.css' const TAG_COLORS = { @@ -114,6 +114,27 @@ export default function PostCard({ post, currentUser, onUpdate }) { } } + const handleRepost = () => { + try { + hapticFeedback('light') + + // Получить имя бота из переменных окружения или использовать дефолтное + const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot' + + // Создать deeplink для открытия поста в миниапп + const deeplink = `https://t.me/${botName}?startapp=post_${post._id}` + + // Открыть нативное окно "Поделиться" в Telegram + const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(deeplink)}&text=${encodeURIComponent('Смотри пост в Nakama!')}` + + openTelegramLink(shareUrl) + hapticFeedback('success') + } catch (error) { + console.error('Ошибка репоста:', error) + hapticFeedback('error') + } + } + return (
{/* Хедер поста */} @@ -236,6 +257,17 @@ export default function PostCard({ post, currentUser, onUpdate }) { {post.comments.length} + + {images.length > 0 && (
+ {/* Реферальная ссылка */} + {user.referralCode && ( +
+
+
+ +
+
+

Пригласи друзей

+

Получи +1 к счетчику, когда приглашенный создаст первый пост

+
+ Приглашено: {user.referralsCount || 0} +
+
+
+
+
+ {`https://t.me/${import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'}?startapp=${user.referralCode}`} +
+ +
+
+ )} +
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx index 73018ea..84cbc3f 100644 --- a/moderation/frontend/src/App.jsx +++ b/moderation/frontend/src/App.jsx @@ -496,6 +496,7 @@ export default function App() {
Роль: {u.role} Активность: {formatDate(u.lastActiveAt)} + {u.referralsCount > 0 && Рефералов: {u.referralsCount}} {u.banned && Бан до {formatDate(u.bannedUntil)}}