diff --git a/.env.example b/.env.example index ef9a793..069ba3f 100644 --- a/.env.example +++ b/.env.example @@ -1,30 +1,58 @@ -# Telegram Bot Configuration +# Telegram Tinder Bot Configuration +# Rename this file to .env before starting the application + +# === REQUIRED SETTINGS === + +# Telegram Bot Token (Get from @BotFather) TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here # Database Configuration +# For local development (when running the bot directly) DB_HOST=localhost DB_PORT=5432 DB_NAME=telegram_tinder_bot DB_USERNAME=postgres DB_PASSWORD=your_password_here -# Application Settings +# === APPLICATION SETTINGS === + +# Environment (development, production) NODE_ENV=development + +# Port for health checks PORT=3000 -# Optional: Redis for caching (if using) -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= +# === FILE UPLOAD SETTINGS === -# Optional: File upload settings +# Path for storing uploaded files UPLOAD_PATH=./uploads + +# Maximum file size for uploads (in bytes, default: 5MB) MAX_FILE_SIZE=5242880 -# Optional: External services -GOOGLE_MAPS_API_KEY=your_google_maps_key -CLOUDINARY_URL=your_cloudinary_url +# === LOGGING === -# Security +# Log level (error, warn, info, debug) +LOG_LEVEL=info + +# Path for storing log files +LOG_PATH=./logs + +# === SECURITY === + +# Secret key for JWT tokens JWT_SECRET=your_jwt_secret_here + +# Encryption key for sensitive data ENCRYPTION_KEY=your_encryption_key_here + +# === ADVANCED SETTINGS === + +# Notification check interval in milliseconds (default: 60000 - 1 minute) +NOTIFICATION_CHECK_INTERVAL=60000 + +# Number of matches to show per page +MATCHES_PER_PAGE=10 + +# Number of profiles to load at once +PROFILES_BATCH_SIZE=5 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..879c206 --- /dev/null +++ b/.env.production @@ -0,0 +1,68 @@ +# Конфигурация Telegram Tinder Bot для Production + +# === НЕОБХОДИМЫЕ НАСТРОЙКИ === + +# Токен Telegram бота (получить у @BotFather) +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Настройки базы данных PostgreSQL +DB_HOST=db +DB_PORT=5432 +DB_NAME=telegram_tinder_bot +DB_USERNAME=postgres +DB_PASSWORD=your_secure_password_here + +# === НАСТРОЙКИ ПРИЛОЖЕНИЯ === + +# Окружение +NODE_ENV=production + +# Порт для проверок работоспособности +PORT=3000 + +# === НАСТРОЙКИ ЗАГРУЗКИ ФАЙЛОВ === + +# Путь для хранения загруженных файлов +UPLOAD_PATH=./uploads + +# Максимальный размер загружаемого файла (в байтах, по умолчанию: 5MB) +MAX_FILE_SIZE=5242880 + +# Разрешенные типы файлов +ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif + +# === ЛОГИРОВАНИЕ === + +# Уровень логирования (error, warn, info, debug) +LOG_LEVEL=info + +# Путь для хранения лог-файлов +LOG_PATH=./logs + +# === БЕЗОПАСНОСТЬ === + +# Секретный ключ для JWT токенов +JWT_SECRET=your_jwt_secret_here + +# Ключ шифрования для чувствительных данных +ENCRYPTION_KEY=your_encryption_key_here + +# === РАСШИРЕННЫЕ НАСТРОЙКИ === + +# Интервал проверки уведомлений в миллисекундах (по умолчанию: 60000 - 1 минута) +NOTIFICATION_CHECK_INTERVAL=60000 + +# Количество матчей для отображения на странице +MATCHES_PER_PAGE=10 + +# Количество профилей для загрузки за один раз +PROFILES_BATCH_SIZE=5 + +# === НАСТРОЙКИ DOCKER === + +# Имя хоста для доступа извне +EXTERNAL_HOSTNAME=your_domain.com + +# Настройки кеширования (Redis, если используется) +CACHE_HOST=redis +CACHE_PORT=6379 diff --git a/Dockerfile b/Dockerfile index dbb2620..05edabe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,12 @@ WORKDIR /app COPY package*.json ./ COPY tsconfig.json ./ -# Install dependencies -RUN npm ci --only=production && npm cache clean --force +# Install all dependencies (including devDependencies for build) +RUN npm ci && npm cache clean --force # Copy source code COPY src/ ./src/ +COPY .env.example ./ # Build the application RUN npm run build @@ -31,11 +32,19 @@ RUN npm ci --only=production && npm cache clean --force # Copy built application from builder stage COPY --from=builder /app/dist ./dist +COPY --from=builder /app/.env.example ./.env.example -# Copy configuration files -COPY config/ ./config/ +# Copy database migrations +COPY src/database/migrations/ ./dist/database/migrations/ -# Create uploads directory +# Copy locales +COPY src/locales/ ./dist/locales/ + +# Copy scripts +COPY scripts/startup.sh ./startup.sh +RUN chmod +x ./startup.sh + +# Create directories RUN mkdir -p uploads logs # Create non-root user for security @@ -53,7 +62,7 @@ EXPOSE 3000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 -# Start the application -CMD ["node", "dist/bot.js"] +# Start the application with migration script +CMD ["./startup.sh"] diff --git a/README.md b/README.md index edc7413..f465430 100644 --- a/README.md +++ b/README.md @@ -268,8 +268,32 @@ npm run dev - Node.js 16+ - PostgreSQL 12+ - Telegram Bot Token (получить у [@BotFather](https://t.me/BotFather)) +- Docker и Docker Compose (опционально) -### 2. Установка +### 2. Установка и запуск + +#### С использованием стартовых скриптов (рекомендуется) + +```bash +# Клонировать репозиторий +git clone +cd telegram-tinder-bot + +# На Windows: +.\start.bat + +# На Linux/macOS: +chmod +x start.sh +./start.sh +``` + +Скрипт автоматически: +- Проверит наличие файла .env и создаст его из шаблона при необходимости +- Предложит выбор между запуском с локальной БД или подключением к внешней +- Настроит все необходимые параметры окружения +- Запустит контейнеры Docker + +#### Без Docker ```bash # Клонировать репозиторий @@ -279,24 +303,17 @@ cd telegram-tinder-bot # Установить зависимости npm install -# Скомпилировать TypeScript -npm run build -``` +# Скопировать файл конфигурации +cp .env.example .env +# Отредактируйте файл .env и укажите свой TELEGRAM_BOT_TOKEN -### 3. Настройка базы данных - -```bash # Создать базу данных PostgreSQL createdb telegram_tinder_bot # Запустить миграции -psql -d telegram_tinder_bot -f src/database/migrations/init.sql -``` +npm run migrate:up -### 4. Запуск бота - -```bash -# Компиляция TypeScript +# Скомпилировать TypeScript npm run build # Запуск бота diff --git a/bin/backup_db.sh b/bin/backup_db.sh new file mode 100644 index 0000000..117d106 --- /dev/null +++ b/bin/backup_db.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# backup_db.sh - Script for backing up the PostgreSQL database + +echo "📦 Backing up PostgreSQL database..." + +# Default backup directory +BACKUP_DIR="${BACKUP_DIR:-/var/backups/tg_tinder_bot}" +BACKUP_FILENAME="tg_tinder_bot_$(date +%Y%m%d_%H%M%S).sql" +BACKUP_PATH="$BACKUP_DIR/$BACKUP_FILENAME" + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +# Check if running in docker-compose environment +if [ -f /.dockerenv ] || [ -f /proc/self/cgroup ] && grep -q docker /proc/self/cgroup; then + echo "🐳 Running in Docker environment, using docker-compose exec..." + docker-compose exec -T db pg_dump -U postgres telegram_tinder_bot > "$BACKUP_PATH" +else + # Check if PGPASSWORD is set in environment + if [ -z "$PGPASSWORD" ]; then + # If .env file exists, try to get password from there + if [ -f .env ]; then + DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d '=' -f2) + export PGPASSWORD="$DB_PASSWORD" + else + echo "⚠️ No DB_PASSWORD found in environment or .env file." + echo "Please enter PostgreSQL password:" + read -s PGPASSWORD + export PGPASSWORD + fi + fi + + echo "💾 Backing up database to $BACKUP_PATH..." + pg_dump -h localhost -U postgres -d telegram_tinder_bot > "$BACKUP_PATH" +fi + +# Check if backup was successful +if [ $? -eq 0 ]; then + echo "✅ Backup completed successfully: $BACKUP_PATH" + echo "📊 Backup size: $(du -h $BACKUP_PATH | cut -f1)" + + # Compress the backup + gzip -f "$BACKUP_PATH" + echo "🗜️ Compressed backup: $BACKUP_PATH.gz" + + # Keep only the last 7 backups + echo "🧹 Cleaning up old backups..." + find "$BACKUP_DIR" -name "tg_tinder_bot_*.sql.gz" -type f -mtime +7 -delete + + echo "🎉 Backup process completed!" +else + echo "❌ Backup failed!" + exit 1 +fi diff --git a/bin/create_release.sh b/bin/create_release.sh new file mode 100644 index 0000000..83397d1 --- /dev/null +++ b/bin/create_release.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Скрипт для создания релиза Telegram Tinder Bot + +# Получение версии из package.json +VERSION=$(grep -m1 "version" package.json | cut -d'"' -f4) +RELEASE_NAME="tg-tinder-bot-v$VERSION" +RELEASE_DIR="bin/releases/$RELEASE_NAME" + +echo "🚀 Создание релиза $RELEASE_NAME" + +# Создание директории релиза +mkdir -p "$RELEASE_DIR" + +# Очистка временных файлов +echo "🧹 Очистка временных файлов..." +rm -rf dist node_modules + +# Установка зависимостей +echo "📦 Установка зависимостей production..." +npm ci --only=production + +# Сборка проекта +echo "🔧 Сборка проекта..." +npm run build + +# Копирование файлов релиза +echo "📋 Копирование файлов..." +cp -r dist "$RELEASE_DIR/" +cp -r src/locales "$RELEASE_DIR/dist/" +cp package.json package-lock.json .env.example "$RELEASE_DIR/" +cp -r bin/start_bot.* bin/install_ubuntu.sh "$RELEASE_DIR/" +cp README.md LICENSE "$RELEASE_DIR/" 2>/dev/null || echo "Файлы документации не найдены" +cp sql/consolidated.sql "$RELEASE_DIR/" +cp docker-compose.yml Dockerfile "$RELEASE_DIR/" +cp deploy.sh "$RELEASE_DIR/" && chmod +x "$RELEASE_DIR/deploy.sh" + +# Создание README для релиза +cat > "$RELEASE_DIR/RELEASE.md" << EOL +# Telegram Tinder Bot v$VERSION + +Эта папка содержит релиз Telegram Tinder Bot версии $VERSION. + +## Содержимое + +- \`dist/\` - Скомпилированный код +- \`package.json\` - Зависимости и скрипты +- \`.env.example\` - Пример конфигурации +- \`docker-compose.yml\` и \`Dockerfile\` - Для запуска через Docker +- \`consolidated.sql\` - SQL-скрипт для инициализации базы данных +- \`deploy.sh\` - Скрипт для простого деплоя + +## Быстрый старт + +1. Создайте файл \`.env\` на основе \`.env.example\` +2. Запустите бота одним из способов: + - Через Docker: \`./deploy.sh\` + - Через Node.js: \`node dist/bot.js\` + +## Дата релиза + +$(date "+%d.%m.%Y %H:%M") +EOL + +# Архивирование релиза +echo "📦 Создание архива..." +cd bin/releases +zip -r "$RELEASE_NAME.zip" "$RELEASE_NAME" +cd ../.. + +echo "✅ Релиз создан успешно!" +echo "📂 Релиз доступен в: bin/releases/$RELEASE_NAME" +echo "📦 Архив релиза: bin/releases/$RELEASE_NAME.zip" diff --git a/bin/install_docker.sh b/bin/install_docker.sh new file mode 100644 index 0000000..3033222 --- /dev/null +++ b/bin/install_docker.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# install_docker.sh - Script for installing Docker and Docker Compose + +echo "🚀 Installing Docker and Docker Compose..." + +# Check if script is run as root +if [ "$(id -u)" -ne 0 ]; then + echo "❌ This script must be run as root. Please run with sudo." + exit 1 +fi + +# Update package lists +echo "📦 Updating package lists..." +apt update + +# Install required packages +echo "📦 Installing required packages..." +apt install -y apt-transport-https ca-certificates curl software-properties-common + +# Add Docker GPG key +echo "🔑 Adding Docker GPG key..." +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + +# Add Docker repository +echo "📁 Adding Docker repository..." +add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + +# Update package lists again +apt update + +# Install Docker +echo "🐳 Installing Docker..." +apt install -y docker-ce docker-ce-cli containerd.io + +# Enable and start Docker service +systemctl enable docker +systemctl start docker + +# Install Docker Compose +echo "🐳 Installing Docker Compose..." +curl -L "https://github.com/docker/compose/releases/download/v2.24.6/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose + +# Check versions +echo "✅ Installation complete!" +echo "Docker version:" +docker --version +echo "Docker Compose version:" +docker-compose --version + +# Add current user to docker group if not root +if [ -n "$SUDO_USER" ]; then + echo "👤 Adding user $SUDO_USER to docker group..." + usermod -aG docker $SUDO_USER + echo "⚠️ Please log out and log back in for group changes to take effect." +fi + +echo "🎉 Docker installation completed successfully!" diff --git a/check_schema.ts b/check_schema.ts deleted file mode 100644 index 3cc78f7..0000000 --- a/check_schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { query } from './src/database/connection'; - -async function checkSchema() { - try { - const result = await query('SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1', ['messages']); - console.log(result.rows); - process.exit(0); - } catch (error) { - console.error(error); - process.exit(1); - } -} - -checkSchema(); diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..7091bb8 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# deploy.sh - Скрипт для деплоя Telegram Tinder Bot + +echo "🚀 Деплой Telegram Tinder Bot..." + +# Проверяем наличие Docker +if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then + echo "❌ Docker и Docker Compose должны быть установлены!" + echo "Для установки на Ubuntu выполните:" + echo "sudo apt update && sudo apt install -y docker.io docker-compose" + exit 1 +fi + +# Определяем рабочую директорию +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +cd "$SCRIPT_DIR" + +# Получаем последние изменения +echo "📥 Получение последних изменений..." +git pull origin main + +# Проверяем наличие .env файла +if [ ! -f .env ]; then + echo "📝 Создание .env файла из .env.production..." + cp .env.production .env + echo "⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!" + exit 1 +fi + +# Запускаем Docker Compose +echo "🐳 Сборка и запуск контейнеров Docker..." +docker-compose down +docker-compose build +docker-compose up -d + +# Проверяем статус контейнеров +echo "🔍 Проверка статуса контейнеров..." +docker-compose ps + +echo "✅ Деплой успешно завершен! Бот должен быть доступен через Telegram." +echo "" +echo "📊 Полезные команды:" +echo "- Просмотр логов: docker-compose logs -f" +echo "- Перезапуск сервисов: docker-compose restart" +echo "- Остановка всех сервисов: docker-compose down" +echo "- Доступ к базе данных: docker-compose exec db psql -U postgres -d telegram_tinder_bot" +echo "- Проверка состояния бота: curl http://localhost:3000/health" +echo "" +echo "🌟 Для администрирования базы данных:" +echo "Adminer доступен по адресу: http://ваш_сервер:8080" +echo " - Система: PostgreSQL" +echo " - Сервер: db" +echo " - Пользователь: postgres" +echo " - Пароль: (из переменной DB_PASSWORD в .env)" +echo " - База данных: telegram_tinder_bot" diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..7ebf603 --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,69 @@ +# Используем версию Docker Compose для локальной разработки + +version: '3.8' + +services: + bot: + build: + context: . + dockerfile: Dockerfile + args: + - NODE_ENV=development + environment: + - NODE_ENV=development + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=telegram_tinder_bot + - DB_USERNAME=postgres + - DB_PASSWORD=dev_password + volumes: + # Монтируем исходный код для горячей перезагрузки + - ./src:/app/src + - ./dist:/app/dist + - ./.env:/app/.env + ports: + # Открываем порт для отладки + - "9229:9229" + command: npm run dev + networks: + - bot-network + depends_on: + - db + + db: + # Используем последнюю версию PostgreSQL для разработки + image: postgres:16-alpine + environment: + - POSTGRES_DB=telegram_tinder_bot + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=dev_password + volumes: + # Хранение данных локально для быстрого сброса + - postgres_data_dev:/var/lib/postgresql/data + # Монтируем скрипты инициализации + - ./sql:/docker-entrypoint-initdb.d + ports: + # Открываем порт для доступа к БД напрямую + - "5433:5432" + networks: + - bot-network + + adminer: + image: adminer:latest + ports: + - "8080:8080" + networks: + - bot-network + depends_on: + - db + environment: + - ADMINER_DEFAULT_SERVER=db + - ADMINER_DEFAULT_USER=postgres + - ADMINER_DEFAULT_PASSWORD=dev_password + +volumes: + postgres_data_dev: + +networks: + bot-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 8ddb875..272f4bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,19 +6,26 @@ services: container_name: telegram-tinder-bot restart: unless-stopped depends_on: - - db + db: + condition: service_healthy + env_file: .env environment: - NODE_ENV=production - DB_HOST=db - DB_PORT=5432 - DB_NAME=telegram_tinder_bot - DB_USERNAME=postgres - - DB_PASSWORD=${DB_PASSWORD} - - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} volumes: - ./uploads:/app/uploads + - ./logs:/app/logs networks: - bot-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s db: image: postgres:15-alpine @@ -27,14 +34,18 @@ services: environment: - POSTGRES_DB=telegram_tinder_bot - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password123 + - POSTGRES_PASSWORD=${DB_PASSWORD:-password123} volumes: - postgres_data:/var/lib/postgresql/data - - ./src/database/migrations/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "5433:5432" networks: - bot-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 adminer: image: adminer:latest diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md new file mode 100644 index 0000000..ac43818 --- /dev/null +++ b/docs/PRODUCTION_DEPLOYMENT.md @@ -0,0 +1,264 @@ +# Инструкция по развертыванию Telegram Tinder Bot в Production + +Это подробное руководство по развертыванию Telegram Tinder Bot в production-окружении с использованием Docker и Docker Compose. + +## 📋 Требования + +- **Операционная система**: Ubuntu 20.04 или выше (рекомендуется) / Windows Server с Docker +- **Программное обеспечение**: + - Docker (последняя версия) + - Docker Compose (последняя версия) + - Git + +## 🚀 Быстрое развертывание + +### 1. Клонирование репозитория + +```bash +git clone https://github.com/your-username/telegram-tinder-bot.git +cd telegram-tinder-bot +``` + +### 2. Настройка конфигурации + +```bash +# Создание файла конфигурации из шаблона +cp .env.production .env + +# Редактирование конфигурационного файла +nano .env +``` + +Важно указать следующие параметры: +- `TELEGRAM_BOT_TOKEN`: токен от @BotFather +- `DB_PASSWORD`: надежный пароль для базы данных +- `JWT_SECRET`: случайная строка для JWT +- `ENCRYPTION_KEY`: случайная строка для шифрования + +### 3. Запуск деплоя + +```bash +# Сделайте скрипт исполняемым +chmod +x deploy.sh + +# Запустите деплой +./deploy.sh +``` + +## 🔧 Подробное руководство по установке + +### Подготовка сервера Ubuntu + +```bash +# Обновление системы +sudo apt update && sudo apt upgrade -y + +# Установка необходимых пакетов +sudo apt install -y apt-transport-https ca-certificates curl software-properties-common git + +# Установка Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Добавление текущего пользователя в группу docker +sudo usermod -aG docker ${USER} + +# Установка Docker Compose +sudo apt install -y docker-compose +``` + +### Клонирование и настройка проекта + +```bash +# Создание директории для проекта +mkdir -p /opt/telegram-tinder +cd /opt/telegram-tinder + +# Клонирование репозитория +git clone https://github.com/your-username/telegram-tinder-bot.git . + +# Настройка .env файла +cp .env.production .env +nano .env + +# Создание директорий для данных и логов +mkdir -p uploads logs +chmod 777 uploads logs +``` + +### Запуск проекта + +```bash +# Запуск в фоновом режиме +docker-compose up -d + +# Проверка статуса контейнеров +docker-compose ps + +# Просмотр логов +docker-compose logs -f +``` + +## 🔄 Обновление бота + +Для обновления бота выполните: + +```bash +cd /путь/к/telegram-tinder-bot +./deploy.sh +``` + +Скрипт автоматически выполнит: +1. Получение последних изменений из репозитория +2. Перезапуск контейнеров с новой версией кода +3. Применение миграций базы данных + +## 🛡️ Обеспечение безопасности + +### Настройка файрвола + +```bash +# Разрешение только необходимых портов +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable +``` + +### Настройка HTTPS с Let's Encrypt (опционально) + +Для использования HTTPS с Let's Encrypt и Nginx: + +```bash +# Установка Certbot +sudo apt install -y certbot python3-certbot-nginx + +# Получение SSL-сертификата +sudo certbot --nginx -d your-domain.com +``` + +## 📊 Мониторинг и управление + +### Просмотр логов + +```bash +# Логи всех контейнеров +docker-compose logs -f + +# Логи конкретного контейнера (например, бота) +docker-compose logs -f bot + +# Последние 100 строк логов +docker-compose logs --tail=100 bot +``` + +### Управление сервисами + +```bash +# Остановка всех контейнеров +docker-compose down + +# Перезапуск всех контейнеров +docker-compose restart + +# Перезапуск только бота +docker-compose restart bot +``` + +### Доступ к базе данных + +```bash +# Вход в консоль PostgreSQL +docker-compose exec db psql -U postgres -d telegram_tinder_bot + +# Резервное копирование базы данных +docker-compose exec db pg_dump -U postgres telegram_tinder_bot > backup_$(date +%Y%m%d).sql + +# Восстановление базы из резервной копии +cat backup.sql | docker-compose exec -T db psql -U postgres -d telegram_tinder_bot +``` + +## 🔍 Устранение неполадок + +### Проверка работоспособности + +```bash +# Проверка API бота +curl http://localhost:3000/health + +# Проверка подключения к базе данных +docker-compose exec bot node -e "const { Client } = require('pg'); const client = new Client({ host: 'db', port: 5432, database: 'telegram_tinder_bot', user: 'postgres', password: process.env.DB_PASSWORD }); client.connect().then(() => { console.log('Connected to DB!'); client.end(); }).catch(e => console.error(e));" +``` + +### Общие проблемы и решения + +**Проблема**: Бот не отвечает в Telegram +**Решение**: +- Проверьте валидность токена бота +- Проверьте логи на наличие ошибок: `docker-compose logs -f bot` + +**Проблема**: Ошибки подключения к базе данных +**Решение**: +- Проверьте настройки подключения в `.env` +- Убедитесь, что контейнер с базой данных запущен: `docker-compose ps` +- Проверьте логи базы данных: `docker-compose logs db` + +**Проблема**: Недостаточно свободного места на диске +**Решение**: +- Очистите неиспользуемые Docker образы: `docker image prune -a` +- Очистите неиспользуемые Docker тома: `docker volume prune` + +## 🔁 Настройка автоматического обновления + +### Настройка автообновления через Cron + +```bash +# Редактирование crontab +crontab -e + +# Добавление задачи (обновление каждую ночь в 3:00) +0 3 * * * cd /путь/к/telegram-tinder-bot && ./deploy.sh > /tmp/tg-tinder-update.log 2>&1 +``` + +## 📝 Рекомендации по обслуживанию + +1. **Регулярное резервное копирование**: + ```bash + # Ежедневное резервное копирование через cron + 0 2 * * * docker-compose exec -T db pg_dump -U postgres telegram_tinder_bot > /path/to/backups/tg_$(date +\%Y\%m\%d).sql + ``` + +2. **Мониторинг использования ресурсов**: + ```bash + # Просмотр использования ресурсов контейнерами + docker stats + ``` + +3. **Обновление Docker образов**: + ```bash + # Обновление образов + docker-compose pull + docker-compose up -d + ``` + +4. **Проверка журналов на наличие ошибок**: + ```bash + # Поиск ошибок в логах + docker-compose logs | grep -i error + docker-compose logs | grep -i exception + ``` + +--- + +## 📋 Контрольный список деплоя + +- [ ] Установлены Docker и Docker Compose +- [ ] Клонирован репозиторий +- [ ] Настроен файл .env с реальными данными +- [ ] Запущены контейнеры через docker-compose +- [ ] Проверено подключение бота к Telegram API +- [ ] Настроено резервное копирование +- [ ] Настроен файрвол и безопасность сервера +- [ ] Проверены и настроены логи +- [ ] (Опционально) Настроен SSL для веб-интерфейса +- [ ] (Опционально) Настроено автоматическое обновление diff --git a/migrations/1631980000000_add_profile_views_table.ts b/migrations/1631980000000_add_profile_views_table.ts new file mode 100644 index 0000000..73b79c6 --- /dev/null +++ b/migrations/1631980000000_add_profile_views_table.ts @@ -0,0 +1,44 @@ +import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export async function up(pgm: MigrationBuilder): Promise { + // Создание таблицы profile_views для хранения информации о просмотренных профилях + pgm.createTable('profile_views', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('uuid_generate_v4()') }, + viewer_id: { + type: 'uuid', + notNull: true, + references: 'users', + onDelete: 'CASCADE' + }, + viewed_profile_id: { + type: 'uuid', + notNull: true, + references: 'profiles(user_id)', + onDelete: 'CASCADE' + }, + view_date: { type: 'timestamp', notNull: true, default: pgm.func('now()') }, + view_type: { type: 'varchar(20)', notNull: true, default: 'browse' }, // browse, match, like, etc. + }); + + // Создание индекса для быстрого поиска по паре (просмотревший - просмотренный) + pgm.createIndex('profile_views', ['viewer_id', 'viewed_profile_id'], { + unique: true, + name: 'profile_views_viewer_viewed_idx' + }); + + // Индекс для быстрого поиска по viewer_id + pgm.createIndex('profile_views', ['viewer_id'], { + name: 'profile_views_viewer_idx' + }); + + // Индекс для быстрого поиска по viewed_profile_id + pgm.createIndex('profile_views', ['viewed_profile_id'], { + name: 'profile_views_viewed_idx' + }); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropTable('profile_views', { cascade: true }); +} diff --git a/migrations/1758156426793_add-processed-column-to-notifications.js b/migrations/1758156426793_add-processed-column-to-notifications.js new file mode 100644 index 0000000..497729c --- /dev/null +++ b/migrations/1758156426793_add-processed-column-to-notifications.js @@ -0,0 +1,52 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = pgm => { + // Проверяем существование таблицы scheduled_notifications + pgm.sql(` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) THEN + -- Проверяем, нет ли уже столбца processed + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'scheduled_notifications' AND column_name = 'processed' + ) THEN + -- Добавляем столбец processed + ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE; + END IF; + ELSE + -- Создаем таблицу, если она не существует + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY, + user_id UUID REFERENCES users(id), + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ); + END IF; + END + $$; + `); +}; + +exports.down = pgm => { + pgm.sql(` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'scheduled_notifications' AND column_name = 'processed' + ) THEN + ALTER TABLE scheduled_notifications DROP COLUMN processed; + END IF; + END + $$; + `); +}; diff --git a/migrations/add_user_state_columns.sql b/migrations/add_user_state_columns.sql new file mode 100644 index 0000000..0c6c0cd --- /dev/null +++ b/migrations/add_user_state_columns.sql @@ -0,0 +1,14 @@ +-- Добавление столбцов state и state_data в таблицу users для обработки состояний пользователя + +-- Добавляем столбец state для хранения текущего состояния пользователя +ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL; + +-- Добавляем столбец state_data для хранения дополнительных данных о состоянии +ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; + +-- Добавляем индекс для быстрого поиска по state +CREATE INDEX IF NOT EXISTS idx_users_state ON users(state); + +-- Комментарий к столбцам +COMMENT ON COLUMN users.state IS 'Текущее состояние пользователя (например, ожидание ввода)'; +COMMENT ON COLUMN users.state_data IS 'Дополнительные данные о состоянии пользователя в формате JSON'; diff --git a/package.json b/package.json index 3579cb5..ef721e7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts", "db:info": "ts-node src/scripts/getDatabaseInfo.ts", "db:test-data": "ts-node src/scripts/createTestData.ts", + "enhance-notifications": "ts-node src/scripts/enhanceNotifications.ts", "update": "bash ./bin/update.sh", "update:win": ".\\bin\\update.bat", "start:sh": "bash ./bin/start_bot.sh" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..687bed5 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,49 @@ +# Структура скриптов в директории `/scripts` + +Эта директория содержит вспомогательные скрипты для работы с Telegram Tinder Bot. + +## Основные скрипты + +- `startup.sh` - Скрипт запуска бота в Docker-контейнере +- `migrate-sync.js` - Синхронизация миграций базы данных +- `createNotificationTables.js` - Создание таблиц для системы уведомлений +- `add-hobbies-column.js` - Добавление колонки интересов в профиль +- `create_profile_fix.js` - Исправление профилей пользователей +- `createProfileViewsTable.js` - Создание таблицы для учета просмотров профилей +- `update_bot_with_notifications.js` - Обновление бота с поддержкой уведомлений + +## Директории + +- `/legacy` - Устаревшие и тестовые скрипты, сохраненные для истории + +## Использование скриптов + +Скрипты JavaScript можно запускать с помощью Node.js: + +```bash +node scripts/script-name.js +``` + +Bash скрипты должны быть сделаны исполняемыми: + +```bash +chmod +x scripts/script-name.sh +./scripts/script-name.sh +``` + +## Добавление новых скриптов + +При добавлении новых скриптов соблюдайте следующие правила: +1. Используйте понятное имя файла, отражающее его назначение +2. Добавьте комментарии в начало файла с описанием его функциональности +3. Добавьте запись об этом скрипте в текущий файл README.md + +## Скрипты миграций + +Миграции базы данных следует создавать с помощью команды: + +```bash +npm run migrate:create your_migration_name +``` + +Это создаст файл миграции в директории `/migrations`. diff --git a/scripts/createNotificationTables.js b/scripts/createNotificationTables.js new file mode 100644 index 0000000..23ef00d --- /dev/null +++ b/scripts/createNotificationTables.js @@ -0,0 +1,259 @@ +const { Pool } = require('pg'); +const dotenv = require('dotenv'); +const uuid = require('uuid'); + +dotenv.config(); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +async function createNotificationTables() { + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + console.log('Creating UUID extension if not exists...'); + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + `); + + // Проверяем существование таблицы notifications + const notificationsExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + console.log('Creating notifications table...'); + await client.query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + console.log('Creating index on notifications...'); + await client.query(` + CREATE INDEX idx_notifications_user_id ON notifications (user_id); + CREATE INDEX idx_notifications_type ON notifications (type); + CREATE INDEX idx_notifications_created_at ON notifications (created_at); + `); + } else { + console.log('Notifications table already exists.'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + console.log('Creating scheduled_notifications table...'); + await client.query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + console.log('Creating index on scheduled_notifications...'); + await client.query(` + CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications (user_id); + CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications (scheduled_at); + CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications (processed); + `); + } else { + console.log('Scheduled_notifications table already exists.'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + console.log('Creating notification_templates table...'); + await client.query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + } else { + console.log('Notification_templates table already exists.'); + } + + // Проверяем наличие колонки notification_settings в таблице users + const settingsColumnExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + console.log('Adding notification_settings column to users table...'); + await client.query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + } else { + console.log('Notification_settings column already exists in users table.'); + } + + // Заполнение таблицы шаблонов уведомлений базовыми шаблонами + if (!templatesExists.rows[0].exists) { + console.log('Populating notification templates...'); + + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await client.query(` + INSERT INTO notification_templates (id, type, title, message_template, button_template) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuid.v4(), + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + } + + await client.query('COMMIT'); + console.log('Successfully created notification tables'); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Error creating notification tables:', err); + } finally { + client.release(); + pool.end(); + } +} + +createNotificationTables().catch(err => console.error('Failed to create notification tables:', err)); diff --git a/scripts/createProfileViewsTable.js b/scripts/createProfileViewsTable.js new file mode 100644 index 0000000..dc9899d --- /dev/null +++ b/scripts/createProfileViewsTable.js @@ -0,0 +1,86 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function createProfileViewsTable() { + const client = await pool.connect(); + try { + console.log('Creating profile_views table...'); + await client.query('BEGIN'); + + // Включаем расширение uuid-ossp, если оно еще не включено + await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`); + + // Создаем таблицу profile_views, если она не существует + await client.query(` + CREATE TABLE IF NOT EXISTS profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + view_date TIMESTAMP NOT NULL DEFAULT NOW(), + view_type VARCHAR(20) NOT NULL DEFAULT 'browse' + ) + `); + + // Создаем уникальный индекс для пары (просмотревший - просмотренный) + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewer_viewed_idx' + ) THEN + CREATE UNIQUE INDEX profile_views_viewer_viewed_idx + ON profile_views (viewer_id, viewed_profile_id); + END IF; + END $$; + `); + + // Создаем индекс для быстрого поиска по viewer_id + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewer_idx' + ) THEN + CREATE INDEX profile_views_viewer_idx + ON profile_views (viewer_id); + END IF; + END $$; + `); + + // Создаем индекс для быстрого поиска по viewed_profile_id + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewed_idx' + ) THEN + CREATE INDEX profile_views_viewed_idx + ON profile_views (viewed_profile_id); + END IF; + END $$; + `); + + await client.query('COMMIT'); + console.log('Table profile_views created successfully'); + } catch (e) { + await client.query('ROLLBACK'); + console.error('Error creating table:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем функцию создания таблицы +createProfileViewsTable(); diff --git a/scripts/legacy/checkCallbackHandlers.js b/scripts/legacy/checkCallbackHandlers.js new file mode 100644 index 0000000..31087aa --- /dev/null +++ b/scripts/legacy/checkCallbackHandlers.js @@ -0,0 +1,88 @@ +// Скрипт для анализа и отладки проблем с обработчиками коллбэков +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); + +function analyzeCallbackHandlers() { + const filePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts'); + const content = fs.readFileSync(filePath, 'utf-8'); + + // Проверяем наличие реализаций методов + const methodsToCheck = [ + 'handleCreateProfile', + 'handleGenderSelection', + 'handleViewMyProfile', + 'handleEditProfile', + 'handleManagePhotos', + 'handleStartBrowsing', + 'handleSettings' + ]; + + const issues = []; + let debugInfo = []; + + methodsToCheck.forEach(method => { + debugInfo.push(`Проверяем метод: ${method}`); + + // Проверяем наличие полной реализации метода (не только сигнатуры) + const methodSignatureRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise\\s*{`, 'g'); + const hasSignature = methodSignatureRegex.test(content); + + const methodBodyRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise\\s*{[\\s\\S]+?}`, 'g'); + const methodMatch = content.match(methodBodyRegex); + + debugInfo.push(` Сигнатура найдена: ${hasSignature}`); + debugInfo.push(` Реализация найдена: ${methodMatch !== null}`); + + if (methodMatch) { + const methodContent = methodMatch[0]; + debugInfo.push(` Длина метода: ${methodContent.length} символов`); + + // Проверяем, содержит ли метод только заглушку + const isStub = methodContent.includes('// Заглушка метода') || + (!methodContent.includes('await') && methodContent.split('\n').length <= 3); + + if (isStub) { + issues.push(`❌ Метод ${method} содержит только заглушку, нет реальной реализации`); + } else { + debugInfo.push(` Метод ${method} имеет полную реализацию`); + } + } else if (hasSignature) { + issues.push(`❌ Метод ${method} имеет только сигнатуру, но нет реализации`); + } else { + issues.push(`❌ Метод ${method} не найден в файле`); + } + }); + + // Проверяем регистрацию обработчиков для NotificationHandlers + const notificationHandlersRegex = /this\.notificationHandlers\s*=\s*new\s+NotificationHandlers\(bot\);/g; + const hasNotificationHandlers = notificationHandlersRegex.test(content); + debugInfo.push(`NotificationHandlers инициализирован: ${hasNotificationHandlers}`); + + // Проверяем обработку коллбэка notifications + const notificationsCallbackRegex = /if\s*\(data\s*===\s*['"]notifications['"].*?\)/g; + const hasNotificationsCallback = notificationsCallbackRegex.test(content); + debugInfo.push(`Обработчик для callback 'notifications' найден: ${hasNotificationsCallback}`); + + // Выводим результаты + console.log('\n=== Анализ CallbackHandlers.ts ===\n'); + if (issues.length > 0) { + console.log('НАЙДЕНЫ ПРОБЛЕМЫ:'); + issues.forEach(issue => console.log(issue)); + console.log('\nРЕКОМЕНДАЦИИ:'); + console.log('1. Восстановите оригинальные реализации методов вместо заглушек'); + console.log('2. Убедитесь, что методы содержат необходимую бизнес-логику'); + console.log('3. Проверьте, что все коллбэки правильно обрабатываются'); + } else { + console.log('✅ Проблем не обнаружено'); + } + + console.log('\n=== Отладочная информация ===\n'); + debugInfo.forEach(info => console.log(info)); + + // Проверяем количество методов в файле + const asyncMethodsCount = (content.match(/async\s+handle[A-Za-z]+\s*\(/g) || []).length; + console.log(`\nВсего async методов в файле: ${asyncMethodsCount}`); +} + +analyzeCallbackHandlers(); diff --git a/scripts/legacy/checkDatabase.js b/scripts/legacy/checkDatabase.js new file mode 100644 index 0000000..c0b65c0 --- /dev/null +++ b/scripts/legacy/checkDatabase.js @@ -0,0 +1,66 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function checkDatabase() { + const client = await pool.connect(); + try { + console.log('\n===== ПРОВЕРКА СОСТОЯНИЯ БАЗЫ ДАННЫХ ====='); + + // Проверка таблицы users + const usersResult = await client.query('SELECT COUNT(*) as count FROM users'); + console.log(`Пользователей в БД: ${usersResult.rows[0].count}`); + if (parseInt(usersResult.rows[0].count) > 0) { + const users = await client.query('SELECT id, telegram_id, username, first_name FROM users LIMIT 10'); + console.log('Последние пользователи:'); + users.rows.forEach(user => { + console.log(` - ID: ${user.id.substring(0, 8)}... | Telegram: ${user.telegram_id} | Имя: ${user.first_name || user.username}`); + }); + } + + // Проверка таблицы profiles + const profilesResult = await client.query('SELECT COUNT(*) as count FROM profiles'); + console.log(`\nПрофилей в БД: ${profilesResult.rows[0].count}`); + if (parseInt(profilesResult.rows[0].count) > 0) { + const profiles = await client.query(` + SELECT p.id, p.user_id, p.name, p.age, p.gender, p.interested_in, p.is_visible + FROM profiles p + ORDER BY p.created_at DESC + LIMIT 10 + `); + console.log('Последние профили:'); + profiles.rows.forEach(profile => { + console.log(` - ID: ${profile.id.substring(0, 8)}... | UserID: ${profile.user_id.substring(0, 8)}... | Имя: ${profile.name} | Возраст: ${profile.age} | Пол: ${profile.gender} | Интересы: ${profile.interested_in} | Виден: ${profile.is_visible}`); + }); + } + + // Проверка таблицы swipes + const swipesResult = await client.query('SELECT COUNT(*) as count FROM swipes'); + console.log(`\nСвайпов в БД: ${swipesResult.rows[0].count}`); + + // Проверка таблицы profile_views + const viewsResult = await client.query('SELECT COUNT(*) as count FROM profile_views'); + console.log(`Просмотров профилей в БД: ${viewsResult.rows[0].count}`); + + // Проверка таблицы matches + const matchesResult = await client.query('SELECT COUNT(*) as count FROM matches'); + console.log(`Матчей в БД: ${matchesResult.rows[0].count}`); + + console.log('\n===== ПРОВЕРКА ЗАВЕРШЕНА =====\n'); + } catch (e) { + console.error('Ошибка при проверке базы данных:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем проверку +checkDatabase(); diff --git a/scripts/legacy/checkProfileViews.js b/scripts/legacy/checkProfileViews.js new file mode 100644 index 0000000..22ebad7 --- /dev/null +++ b/scripts/legacy/checkProfileViews.js @@ -0,0 +1,64 @@ +// Скрипт для проверки таблицы profile_views +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function checkProfileViewsTable() { + const client = await pool.connect(); + try { + console.log('Проверка таблицы profile_views...'); + + // Проверяем наличие таблицы + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'profile_views' + ); + `); + + const tableExists = tableCheck.rows[0].exists; + console.log(`Таблица profile_views ${tableExists ? 'существует' : 'не существует'}`); + + if (tableExists) { + // Проверяем количество записей в таблице + const countResult = await client.query('SELECT COUNT(*) FROM profile_views'); + console.log(`Количество записей в таблице: ${countResult.rows[0].count}`); + + // Получаем данные из таблицы + const dataResult = await client.query(` + SELECT pv.*, + v.telegram_id as viewer_telegram_id, + vp.telegram_id as viewed_telegram_id + FROM profile_views pv + LEFT JOIN users v ON pv.viewer_id = v.id + LEFT JOIN users vp ON pv.viewed_profile_id = vp.id + LIMIT 10 + `); + + if (dataResult.rows.length > 0) { + console.log('Данные из таблицы profile_views:'); + dataResult.rows.forEach((row, index) => { + console.log(`${index + 1}. Просмотр: ${row.viewer_telegram_id || 'Неизвестно'} → ${row.viewed_telegram_id || 'Неизвестно'}, дата: ${row.view_date}`); + }); + } else { + console.log('Таблица profile_views пуста'); + } + } + } catch (error) { + console.error('Ошибка при проверке таблицы profile_views:', error); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем проверку +checkProfileViewsTable(); diff --git a/scripts/legacy/checkUserTable.js b/scripts/legacy/checkUserTable.js new file mode 100644 index 0000000..811b3a6 --- /dev/null +++ b/scripts/legacy/checkUserTable.js @@ -0,0 +1,74 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + user: process.env.DB_USER || 'postgres', + host: process.env.DB_HOST || 'localhost', + database: process.env.DB_NAME || 'telegram_tinder_db', + password: process.env.DB_PASSWORD || 'postgres', + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function checkUserTableStructure() { + try { + // Получаем информацию о структуре таблицы users + const result = await pool.query(` + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = 'users' + ORDER BY ordinal_position; + `); + + console.log('=== Структура таблицы users ==='); + console.table(result.rows); + + // Проверяем наличие столбцов state и state_data + const stateColumn = result.rows.find(row => row.column_name === 'state'); + const stateDataColumn = result.rows.find(row => row.column_name === 'state_data'); + + if (!stateColumn) { + console.log('❌ Столбец state отсутствует в таблице users'); + } else { + console.log('✅ Столбец state присутствует в таблице users'); + } + + if (!stateDataColumn) { + console.log('❌ Столбец state_data отсутствует в таблице users'); + } else { + console.log('✅ Столбец state_data присутствует в таблице users'); + } + + // Добавляем эти столбцы, если их нет + if (!stateColumn || !stateDataColumn) { + console.log('🔄 Добавление отсутствующих столбцов...'); + + await pool.query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; + `); + + console.log('✅ Столбцы успешно добавлены'); + } + + // Проверяем наличие других таблиц, связанных с уведомлениями + const tablesResult = await pool.query(` + SELECT tablename + FROM pg_catalog.pg_tables + WHERE schemaname = 'public' + AND tablename IN ('notifications', 'notification_settings', 'scheduled_notifications'); + `); + + console.log('\n=== Таблицы для уведомлений ==='); + console.table(tablesResult.rows); + + // Закрываем соединение + await pool.end(); + + } catch (error) { + console.error('Ошибка при проверке структуры таблицы:', error); + await pool.end(); + } +} + +checkUserTableStructure(); diff --git a/scripts/legacy/cleanDatabase.js b/scripts/legacy/cleanDatabase.js new file mode 100644 index 0000000..8ad6c9c --- /dev/null +++ b/scripts/legacy/cleanDatabase.js @@ -0,0 +1,55 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function cleanDatabase() { + const client = await pool.connect(); + try { + console.log('Очистка базы данных...'); + await client.query('BEGIN'); + + // Отключаем временно foreign key constraints + await client.query('SET CONSTRAINTS ALL DEFERRED'); + + // Очищаем таблицы в правильном порядке + console.log('Очистка таблицы messages...'); + await client.query('DELETE FROM messages'); + + console.log('Очистка таблицы profile_views...'); + await client.query('DELETE FROM profile_views'); + + console.log('Очистка таблицы matches...'); + await client.query('DELETE FROM matches'); + + console.log('Очистка таблицы swipes...'); + await client.query('DELETE FROM swipes'); + + console.log('Очистка таблицы profiles...'); + await client.query('DELETE FROM profiles'); + + console.log('Очистка таблицы users...'); + await client.query('DELETE FROM users'); + + // Возвращаем foreign key constraints + await client.query('SET CONSTRAINTS ALL IMMEDIATE'); + + await client.query('COMMIT'); + console.log('✅ База данных успешно очищена'); + } catch (e) { + await client.query('ROLLBACK'); + console.error('❌ Ошибка при очистке базы данных:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем функцию очистки +cleanDatabase(); diff --git a/scripts/legacy/clearDatabase.js b/scripts/legacy/clearDatabase.js new file mode 100644 index 0000000..28b17f3 --- /dev/null +++ b/scripts/legacy/clearDatabase.js @@ -0,0 +1,66 @@ +// Скрипт для очистки всех таблиц в базе данных +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения из .env файла +dotenv.config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function clearDatabase() { + const client = await pool.connect(); + try { + console.log('Начинаем очистку базы данных...'); + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Отключаем внешние ключи на время выполнения (если они используются) + // await client.query('SET session_replication_role = \'replica\''); + + // Очистка таблиц в порядке, учитывающем зависимости + console.log('Очистка таблицы сообщений...'); + await client.query('TRUNCATE TABLE messages CASCADE'); + + console.log('Очистка таблицы просмотров профилей...'); + await client.query('TRUNCATE TABLE profile_views CASCADE'); + + console.log('Очистка таблицы свайпов...'); + await client.query('TRUNCATE TABLE swipes CASCADE'); + + console.log('Очистка таблицы матчей...'); + await client.query('TRUNCATE TABLE matches CASCADE'); + + console.log('Очистка таблицы профилей...'); + await client.query('TRUNCATE TABLE profiles CASCADE'); + + console.log('Очистка таблицы пользователей...'); + await client.query('TRUNCATE TABLE users CASCADE'); + + // Возвращаем внешние ключи (если они использовались) + // await client.query('SET session_replication_role = \'origin\''); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Все таблицы успешно очищены!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при очистке базы данных:', error); + } finally { + // Освобождаем клиента + client.release(); + // Закрываем пул соединений + await pool.end(); + } +} + +// Запускаем функцию очистки +clearDatabase(); diff --git a/scripts/legacy/clearDatabase.mjs b/scripts/legacy/clearDatabase.mjs new file mode 100644 index 0000000..a0835f7 --- /dev/null +++ b/scripts/legacy/clearDatabase.mjs @@ -0,0 +1,81 @@ +// Скрипт для очистки всех таблиц в базе данных +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения из .env файла +dotenv.config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function clearDatabase() { + const client = await pool.connect(); + try { + console.log('Начинаем очистку базы данных...'); + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Получаем список существующих таблиц + const tablesResult = await client.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + `); + + const tables = tablesResult.rows.map(row => row.table_name); + console.log('Найдены таблицы:', tables.join(', ')); + + // Очистка таблиц в порядке, учитывающем зависимости + if (tables.includes('messages')) { + console.log('Очистка таблицы messages...'); + await client.query('TRUNCATE TABLE messages CASCADE'); + } + + if (tables.includes('swipes')) { + console.log('Очистка таблицы swipes...'); + await client.query('TRUNCATE TABLE swipes CASCADE'); + } + + if (tables.includes('matches')) { + console.log('Очистка таблицы matches...'); + await client.query('TRUNCATE TABLE matches CASCADE'); + } + + if (tables.includes('profiles')) { + console.log('Очистка таблицы profiles...'); + await client.query('TRUNCATE TABLE profiles CASCADE'); + } + + if (tables.includes('users')) { + console.log('Очистка таблицы users...'); + await client.query('TRUNCATE TABLE users CASCADE'); + } + + // Возвращаем внешние ключи (если они использовались) + // await client.query('SET session_replication_role = \'origin\''); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Все таблицы успешно очищены!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при очистке базы данных:', error); + } finally { + // Освобождаем клиента + client.release(); + // Закрываем пул соединений + await pool.end(); + } +} + +// Запускаем функцию очистки +clearDatabase(); diff --git a/scripts/legacy/clear_database.sql b/scripts/legacy/clear_database.sql new file mode 100644 index 0000000..f1071a9 --- /dev/null +++ b/scripts/legacy/clear_database.sql @@ -0,0 +1,26 @@ +-- Скрипт для очистки всех таблиц в базе данных +-- Важно: таблицы очищаются в порядке, учитывающем зависимости между ними + +-- Отключаем внешние ключи на время выполнения (если они используются) +-- SET session_replication_role = 'replica'; + +-- Очистка таблицы сообщений +TRUNCATE TABLE messages CASCADE; + +-- Очистка таблицы просмотров профилей +TRUNCATE TABLE profile_views CASCADE; + +-- Очистка таблицы свайпов +TRUNCATE TABLE swipes CASCADE; + +-- Очистка таблицы матчей +TRUNCATE TABLE matches CASCADE; + +-- Очистка таблицы профилей +TRUNCATE TABLE profiles CASCADE; + +-- Очистка таблицы пользователей +TRUNCATE TABLE users CASCADE; + +-- Возвращаем внешние ключи (если они использовались) +-- SET session_replication_role = 'origin'; diff --git a/scripts/legacy/createProfileViewsTable.ts b/scripts/legacy/createProfileViewsTable.ts new file mode 100644 index 0000000..101e274 --- /dev/null +++ b/scripts/legacy/createProfileViewsTable.ts @@ -0,0 +1,70 @@ +// Скрипт для создания таблицы profile_views + +// Функция для ручного запуска создания таблицы profile_views +async function createProfileViewsTable() { + const client = await require('../database/connection').pool.connect(); + try { + console.log('Создание таблицы profile_views...'); + + // Проверяем, существует ли уже таблица profile_views + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'profile_views' + ); + `); + + if (tableCheck.rows[0].exists) { + console.log('Таблица profile_views уже существует, пропускаем создание'); + return; + } + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Создаем таблицу profile_views + await client.query(` + CREATE TABLE profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + view_date TIMESTAMP NOT NULL DEFAULT NOW(), + view_type VARCHAR(20) NOT NULL DEFAULT 'browse' + ); + `); + + // Создаем индекс для быстрого поиска по паре (просмотревший - просмотренный) + await client.query(` + CREATE UNIQUE INDEX profile_views_viewer_viewed_idx ON profile_views (viewer_id, viewed_profile_id); + `); + + // Индекс для быстрого поиска по viewer_id + await client.query(` + CREATE INDEX profile_views_viewer_idx ON profile_views (viewer_id); + `); + + // Индекс для быстрого поиска по viewed_profile_id + await client.query(` + CREATE INDEX profile_views_viewed_idx ON profile_views (viewed_profile_id); + `); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Таблица profile_views успешно создана!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при создании таблицы profile_views:', error); + } finally { + // Освобождаем клиента + client.release(); + } +} + +// Запускаем функцию создания таблицы +createProfileViewsTable() + .then(() => console.log('Скрипт выполнен')) + .catch(err => console.error('Ошибка выполнения скрипта:', err)) + .finally(() => process.exit()); diff --git a/scripts/legacy/fixCallbackHandlers.js b/scripts/legacy/fixCallbackHandlers.js new file mode 100644 index 0000000..e5182cf --- /dev/null +++ b/scripts/legacy/fixCallbackHandlers.js @@ -0,0 +1,142 @@ +// Скрипт для восстановления оригинальной функциональности callbackHandlers.ts +const fs = require('fs'); +const path = require('path'); + +// Находим самую последнюю версию файла callbackHandlers.ts в репозитории +const { execSync } = require('child_process'); + +try { + console.log('Поиск оригинальной версии CallbackHandlers.ts с полной функциональностью...'); + + // Находим коммиты, содержащие значительные изменения в файле (более 1000 символов) + const commits = execSync('git log --format="%H" -- src/handlers/callbackHandlers.ts') + .toString() + .trim() + .split('\n'); + + console.log(`Найдено ${commits.length} коммитов с изменениями файла`); + + // Пробуем разные коммиты, начиная с последнего, чтобы найти полную реализацию + let foundFullImplementation = false; + let fullImplementationContent = ''; + + for (const commit of commits) { + console.log(`Проверяем коммит ${commit.substring(0, 8)}...`); + + try { + const fileContent = execSync(`git show ${commit}:src/handlers/callbackHandlers.ts`).toString(); + + // Проверяем, содержит ли файл полные реализации методов + const hasFullImplementations = !fileContent.includes('// Заглушка метода') && + fileContent.includes('await this.bot.sendMessage'); + + if (hasFullImplementations) { + console.log(`✅ Найдена полная реализация в коммите ${commit.substring(0, 8)}`); + fullImplementationContent = fileContent; + foundFullImplementation = true; + break; + } else { + console.log(`❌ Коммит ${commit.substring(0, 8)} не содержит полной реализации`); + } + } catch (error) { + console.error(`Ошибка при проверке коммита ${commit}:`, error.message); + } + } + + if (!foundFullImplementation) { + console.error('❌ Не удалось найти полную реализацию в истории коммитов'); + process.exit(1); + } + + // Теперь получаем текущую версию файла с поддержкой уведомлений + console.log('Получаем текущую версию с поддержкой уведомлений...'); + const currentFilePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts'); + const currentContent = fs.readFileSync(currentFilePath, 'utf-8'); + + // Сначала создаем бэкап текущего файла + const backupPath = currentFilePath + '.backup-' + Date.now(); + fs.writeFileSync(backupPath, currentContent); + console.log(`✅ Создан бэкап текущей версии: ${path.basename(backupPath)}`); + + // Извлекаем код для поддержки уведомлений из текущей версии + console.log('Извлекаем код для поддержки уведомлений...'); + + // Находим импорт NotificationHandlers + const notificationImportRegex = /import\s+{\s*NotificationHandlers\s*}\s*from\s*['"]\.\/notificationHandlers['"]\s*;/; + const notificationImport = currentContent.match(notificationImportRegex)?.[0] || ''; + + // Находим объявление поля notificationHandlers + const notificationFieldRegex = /private\s+notificationHandlers\?\s*:\s*NotificationHandlers\s*;/; + const notificationField = currentContent.match(notificationFieldRegex)?.[0] || ''; + + // Находим инициализацию notificationHandlers в конструкторе + const notificationInitRegex = /\/\/\s*Создаем экземпляр NotificationHandlers[\s\S]*?try\s*{[\s\S]*?this\.notificationHandlers\s*=\s*new\s*NotificationHandlers[\s\S]*?}\s*catch[\s\S]*?}/; + const notificationInit = currentContent.match(notificationInitRegex)?.[0] || ''; + + // Находим метод handleNotificationSettings + const notificationSettingsMethodRegex = /async\s+handleNotificationSettings[\s\S]*?}\s*}/; + const notificationSettingsMethod = currentContent.match(notificationSettingsMethodRegex)?.[0] || ''; + + // Находим обработку callback для notifications в handleCallback + const notificationCallbackRegex = /\/\/\s*Настройки уведомлений[\s\S]*?else\s+if\s*\(data\s*===\s*['"]notifications['"][\s\S]*?}\s*}/; + const notificationCallback = currentContent.match(notificationCallbackRegex)?.[0] || ''; + + // Получаем часть обработки коллбэков для уведомлений + const notificationToggleRegex = /\/\/\s*Обработка переключения настроек уведомлений[\s\S]*?else\s+if[\s\S]*?notif_[\s\S]*?}\s*}/; + const notificationToggle = currentContent.match(notificationToggleRegex)?.[0] || ''; + + console.log(`✅ Извлечены блоки кода для уведомлений`); + + // Интегрируем код уведомлений в оригинальную версию + console.log('Интегрируем код уведомлений в оригинальную версию...'); + + // 1. Добавляем импорт + let newContent = fullImplementationContent; + if (notificationImport) { + newContent = newContent.replace(/import\s*{[^}]*}\s*from\s*['"]\.\/messageHandlers['"]\s*;/, + match => match + '\n' + notificationImport); + } + + // 2. Добавляем объявление поля + if (notificationField) { + newContent = newContent.replace(/private\s+translationController\s*:\s*TranslationController\s*;/, + match => match + '\n ' + notificationField); + } + + // 3. Добавляем инициализацию в конструкторе + if (notificationInit) { + newContent = newContent.replace(/this\.translationController\s*=\s*new\s*TranslationController\(\);/, + match => match + '\n ' + notificationInit); + } + + // 4. Добавляем обработку коллбэков для уведомлений + if (notificationCallback) { + newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/, + match => notificationCallback + '\n ' + match); + } + + // 5. Добавляем обработку переключения настроек уведомлений + if (notificationToggle) { + newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/, + match => notificationToggle + '\n ' + match); + } + + // 6. Добавляем метод handleNotificationSettings в конец класса + if (notificationSettingsMethod) { + newContent = newContent.replace(/}(\s*)$/, notificationSettingsMethod + '\n}$1'); + } + + // Сохраняем обновленный файл + const outputPath = currentFilePath + '.fixed'; + fs.writeFileSync(outputPath, newContent); + console.log(`✅ Создана исправленная версия файла: ${path.basename(outputPath)}`); + + console.log('\nИнструкция по восстановлению:'); + console.log(`1. Проверьте файл ${path.basename(outputPath)}`); + console.log('2. Если все выглядит правильно, выполните команду:'); + console.log(` Move-Item -Force "${path.basename(outputPath)}" "${path.basename(currentFilePath)}"`); + console.log('3. Перезапустите бота'); + +} catch (error) { + console.error('Произошла ошибка:', error); +} diff --git a/scripts/legacy/fixDatabaseStructure.js b/scripts/legacy/fixDatabaseStructure.js new file mode 100644 index 0000000..9326b08 --- /dev/null +++ b/scripts/legacy/fixDatabaseStructure.js @@ -0,0 +1,170 @@ +// Скрипт для исправления проблемы с ботом +require('dotenv').config(); +const { Pool } = require('pg'); + +// Получаем данные подключения из .env +console.log('Параметры подключения к БД:'); +console.log('DB_USERNAME:', process.env.DB_USERNAME); +console.log('DB_HOST:', process.env.DB_HOST); +console.log('DB_NAME:', process.env.DB_NAME); +console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '[указан]' : '[не указан]'); +console.log('DB_PORT:', process.env.DB_PORT); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function fixDatabase() { + try { + console.log('Начинаем исправление базы данных...'); + + // Проверяем существование таблицы users + const tableResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'users' + ); + `); + + if (!tableResult.rows[0].exists) { + console.error('Таблица users не найдена!'); + return; + } + + console.log('✅ Таблица users существует'); + + // Проверяем и добавляем столбцы state и state_data, если они отсутствуют + console.log('Проверяем наличие столбцов state и state_data...'); + + const stateColumnResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'state' + ); + `); + + const stateDataColumnResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'state_data' + ); + `); + + if (!stateColumnResult.rows[0].exists) { + console.log('🔄 Добавляем столбец state...'); + await pool.query(`ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL;`); + console.log('✅ Столбец state успешно добавлен'); + } else { + console.log('✅ Столбец state уже существует'); + } + + if (!stateDataColumnResult.rows[0].exists) { + console.log('🔄 Добавляем столбец state_data...'); + await pool.query(`ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb;`); + console.log('✅ Столбец state_data успешно добавлен'); + } else { + console.log('✅ Столбец state_data уже существует'); + } + + // Проверка наличия таблиц для уведомлений + console.log('Проверяем наличие таблиц для уведомлений...'); + + const tablesCheck = await Promise.all([ + checkTableExists('notifications'), + checkTableExists('notification_settings'), + checkTableExists('scheduled_notifications') + ]); + + // Создаем отсутствующие таблицы + if (!tablesCheck[0]) { + console.log('🔄 Создаем таблицу notifications...'); + await pool.query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + is_read BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + CREATE INDEX idx_notifications_user_id ON notifications(user_id); + CREATE INDEX idx_notifications_type ON notifications(type); + CREATE INDEX idx_notifications_created_at ON notifications(created_at); + `); + console.log('✅ Таблица notifications успешно создана'); + } + + if (!tablesCheck[1]) { + console.log('🔄 Создаем таблицу notification_settings...'); + await pool.query(` + CREATE TABLE notification_settings ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + new_matches BOOLEAN DEFAULT true, + new_messages BOOLEAN DEFAULT true, + new_likes BOOLEAN DEFAULT true, + reminders BOOLEAN DEFAULT true, + daily_summary BOOLEAN DEFAULT false, + time_preference VARCHAR(20) DEFAULT 'evening', + do_not_disturb BOOLEAN DEFAULT false, + do_not_disturb_start TIME, + do_not_disturb_end TIME, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + `); + console.log('✅ Таблица notification_settings успешно создана'); + } + + if (!tablesCheck[2]) { + console.log('🔄 Создаем таблицу scheduled_notifications...'); + await pool.query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL, + processed BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications(user_id); + CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at); + CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications(processed); + `); + console.log('✅ Таблица scheduled_notifications успешно создана'); + } + + console.log('✅ Исправление базы данных завершено успешно'); + + } catch (error) { + console.error('Ошибка при исправлении базы данных:', error); + } finally { + await pool.end(); + } +} + +async function checkTableExists(tableName) { + const result = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + ); + `, [tableName]); + + const exists = result.rows[0].exists; + console.log(`${exists ? '✅' : '❌'} Таблица ${tableName} ${exists ? 'существует' : 'отсутствует'}`); + + return exists; +} + +fixDatabase(); diff --git a/scripts/legacy/fix_all_notifications.js b/scripts/legacy/fix_all_notifications.js new file mode 100644 index 0000000..57d1a2e --- /dev/null +++ b/scripts/legacy/fix_all_notifications.js @@ -0,0 +1,48 @@ +/** + * Комплексный скрипт для исправления всех проблем с уведомлениями + * Запускает последовательно оба скрипта исправления + */ + +const { exec } = require('child_process'); +const path = require('path'); + +console.log('🔧 Запуск комплексного исправления проблем с уведомлениями...'); + +// Путь к скриптам +const fixNotificationCallbacksScript = path.join(__dirname, 'fix_notification_callbacks.js'); +const updateBotWithNotificationsScript = path.join(__dirname, 'update_bot_with_notifications.js'); + +// Запуск первого скрипта для исправления таблиц и колонок +console.log('\n📊 Шаг 1/2: Проверка и исправление таблиц базы данных...'); +exec(`node ${fixNotificationCallbacksScript}`, (error, stdout, stderr) => { + if (error) { + console.error(`❌ Ошибка при запуске скрипта исправления таблиц: ${error}`); + return; + } + + console.log(stdout); + + if (stderr) { + console.error(`❌ Ошибки при выполнении скрипта: ${stderr}`); + } + + // Запуск второго скрипта для обновления bot.ts + console.log('\n📝 Шаг 2/2: Обновление файла bot.ts для регистрации обработчиков уведомлений...'); + exec(`node ${updateBotWithNotificationsScript}`, (error2, stdout2, stderr2) => { + if (error2) { + console.error(`❌ Ошибка при запуске скрипта обновления bot.ts: ${error2}`); + return; + } + + console.log(stdout2); + + if (stderr2) { + console.error(`❌ Ошибки при выполнении скрипта: ${stderr2}`); + } + + console.log('\n✅ Все исправления успешно выполнены!'); + console.log('🔄 Пожалуйста, перезапустите бота для применения изменений:'); + console.log(' npm run start'); + console.log('\n💡 Уведомления должны теперь работать корректно!'); + }); +}); diff --git a/scripts/legacy/fix_notification_callbacks.js b/scripts/legacy/fix_notification_callbacks.js new file mode 100644 index 0000000..a501a58 --- /dev/null +++ b/scripts/legacy/fix_notification_callbacks.js @@ -0,0 +1,332 @@ +/** + * Скрипт для проверки и исправления проблем с обработчиками уведомлений в боте + */ + +const { Client } = require('pg'); +const fs = require('fs'); + +// Конфигурация базы данных +const dbConfig = { + host: 'localhost', + port: 5432, + database: 'telegram_tinder', + user: 'postgres', + password: 'postgres' +}; + +// Подключение к базе данных +const client = new Client(dbConfig); + +async function main() { + try { + console.log('Подключение к базе данных...'); + await client.connect(); + console.log('Успешно подключено к базе данных'); + + // Шаг 1: Проверка существования необходимых таблиц для уведомлений + console.log('\n=== Проверка таблиц для уведомлений ==='); + + // Проверяем таблицу notifications + let notificationsTableExists = await checkTableExists('notifications'); + if (!notificationsTableExists) { + console.log('Таблица notifications не найдена. Создаем...'); + await createNotificationsTable(); + console.log('Таблица notifications успешно создана'); + } else { + console.log('Таблица notifications уже существует'); + } + + // Проверяем таблицу scheduled_notifications + let scheduledNotificationsTableExists = await checkTableExists('scheduled_notifications'); + if (!scheduledNotificationsTableExists) { + console.log('Таблица scheduled_notifications не найдена. Создаем...'); + await createScheduledNotificationsTable(); + console.log('Таблица scheduled_notifications успешно создана'); + } else { + console.log('Таблица scheduled_notifications уже существует'); + } + + // Проверяем таблицу notification_templates + let notificationTemplatesTableExists = await checkTableExists('notification_templates'); + if (!notificationTemplatesTableExists) { + console.log('Таблица notification_templates не найдена. Создаем...'); + await createNotificationTemplatesTable(); + console.log('Таблица notification_templates успешно создана'); + console.log('Заполняем таблицу базовыми шаблонами...'); + await populateDefaultTemplates(); + console.log('Шаблоны успешно добавлены'); + } else { + console.log('Таблица notification_templates уже существует'); + } + + // Шаг 2: Проверка существования столбца notification_settings в таблице users + console.log('\n=== Проверка столбца notification_settings в таблице users ==='); + + const notificationSettingsColumnExists = await checkColumnExists('users', 'notification_settings'); + if (!notificationSettingsColumnExists) { + console.log('Столбец notification_settings не найден. Добавляем...'); + await addNotificationSettingsColumn(); + console.log('Столбец notification_settings успешно добавлен'); + } else { + console.log('Столбец notification_settings уже существует'); + } + + // Шаг 3: Проверка существования столбцов state и state_data в таблице users + console.log('\n=== Проверка столбцов state и state_data в таблице users ==='); + + const stateColumnExists = await checkColumnExists('users', 'state'); + if (!stateColumnExists) { + console.log('Столбец state не найден. Добавляем...'); + await addStateColumn(); + console.log('Столбец state успешно добавлен'); + } else { + console.log('Столбец state уже существует'); + } + + const stateDataColumnExists = await checkColumnExists('users', 'state_data'); + if (!stateDataColumnExists) { + console.log('Столбец state_data не найден. Добавляем...'); + await addStateDataColumn(); + console.log('Столбец state_data успешно добавлен'); + } else { + console.log('Столбец state_data уже существует'); + } + + console.log('\nВсе таблицы и столбцы успешно проверены и созданы при необходимости.'); + console.log('Механизм уведомлений должен работать корректно.'); + + console.log('\n=== Проверка регистрации обработчиков уведомлений ==='); + console.log('Подсказка: убедитесь, что в файле bot.ts создается экземпляр NotificationHandlers и регистрируются его обработчики:'); + console.log(` + // Настройка обработчиков уведомлений + const notificationHandlers = new NotificationHandlers(bot); + notificationHandlers.register(); + + // Запуск обработчика запланированных уведомлений + setInterval(() => { + const notificationService = new NotificationService(bot); + notificationService.processScheduledNotifications(); + }, 60000); // Проверяем каждую минуту + `); + + } catch (error) { + console.error('Ошибка выполнения скрипта:', error); + } finally { + await client.end(); + console.log('\nСоединение с базой данных закрыто.'); + } +} + +async function checkTableExists(tableName) { + const query = ` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = $1 + ) as exists + `; + + const result = await client.query(query, [tableName]); + return result.rows[0].exists; +} + +async function checkColumnExists(tableName, columnName) { + const query = ` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = $1 AND column_name = $2 + ) as exists + `; + + const result = await client.query(query, [tableName, columnName]); + return result.rows[0].exists; +} + +async function createNotificationsTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function createScheduledNotificationsTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function createNotificationTemplatesTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function addNotificationSettingsColumn() { + await client.query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); +} + +async function addStateColumn() { + await client.query(` + ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL + `); +} + +async function addStateDataColumn() { + await client.query(` + ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb + `); +} + +async function populateDefaultTemplates() { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await client.query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } +} + +// Запуск скрипта +main(); diff --git a/scripts/legacy/testCallbacks.js b/scripts/legacy/testCallbacks.js new file mode 100644 index 0000000..dde2ed6 --- /dev/null +++ b/scripts/legacy/testCallbacks.js @@ -0,0 +1,85 @@ +// Скрипт для проверки работы callback-хэндлеров и уведомлений +require('dotenv').config(); +const TelegramBot = require('node-telegram-bot-api'); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +// Функция для имитации callback-запроса к боту +async function testCallback() { + try { + console.log('Начинаем тестирование callback-хэндлеров и уведомлений...'); + + // Используем последнего пользователя из базы данных + const userResult = await pool.query(` + SELECT * FROM users ORDER BY last_active_at DESC NULLS LAST LIMIT 1 + `); + + if (userResult.rows.length === 0) { + console.error('❌ Пользователи не найдены в базе данных'); + return; + } + + const user = userResult.rows[0]; + console.log(`Выбран тестовый пользователь: ${user.first_name || 'Без имени'} (ID: ${user.telegram_id})`); + + // Получаем токен бота из переменных окружения + const token = process.env.TELEGRAM_BOT_TOKEN; + if (!token) { + console.error('❌ Токен бота не найден в переменных окружения'); + return; + } + + // Создаем экземпляр бота + const bot = new TelegramBot(token); + + // Отправляем тестовое уведомление пользователю + console.log(`Отправляем тестовое уведомление пользователю ID: ${user.telegram_id}...`); + + try { + const result = await bot.sendMessage( + user.telegram_id, + `🔔 *Тестовое уведомление*\n\nЭто проверка работы уведомлений и callback-хэндлеров.\n\nВаш премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '🔔 Уведомления', callback_data: 'notification_settings' }, + { text: '❤️ Профиль', callback_data: 'view_profile' } + ], + [ + { text: '⚙️ Настройки', callback_data: 'settings' } + ] + ] + } + } + ); + + console.log('✅ Тестовое сообщение успешно отправлено!'); + console.log('Информация о сообщении:', JSON.stringify(result, null, 2)); + } catch (error) { + console.error('❌ Ошибка при отправке тестового сообщения:', error.message); + if (error.response && error.response.body) { + console.error('Детали ошибки:', JSON.stringify(error.response.body, null, 2)); + } + } + + } catch (error) { + console.error('❌ Ошибка при тестировании:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + console.log('Тестирование завершено!'); + } +} + +// Запускаем тестирование +testCallback(); diff --git a/scripts/legacy/testMatching.js b/scripts/legacy/testMatching.js new file mode 100644 index 0000000..3eaa217 --- /dev/null +++ b/scripts/legacy/testMatching.js @@ -0,0 +1,102 @@ +require('dotenv').config(); +const { MatchingService } = require('../dist/services/matchingService'); +const { ProfileService } = require('../dist/services/profileService'); + +// Функция для создания тестовых пользователей +async function createTestUsers() { + const profileService = new ProfileService(); + + console.log('Создание тестовых пользователей...'); + + // Создаем мужской профиль + const maleUserId = await profileService.ensureUser('123456', { + username: 'test_male', + first_name: 'Иван', + last_name: 'Тестов' + }); + + await profileService.createProfile(maleUserId, { + name: 'Иван', + age: 30, + gender: 'male', + interestedIn: 'female', + bio: 'Тестовый мужской профиль', + photos: ['photo1.jpg'], + city: 'Москва', + searchPreferences: { + minAge: 18, + maxAge: 45, + maxDistance: 50 + } + }); + console.log(`Создан мужской профиль: userId=${maleUserId}, telegramId=123456`); + + // Создаем женский профиль + const femaleUserId = await profileService.ensureUser('654321', { + username: 'test_female', + first_name: 'Анна', + last_name: 'Тестова' + }); + + await profileService.createProfile(femaleUserId, { + name: 'Анна', + age: 28, + gender: 'female', + interestedIn: 'male', + bio: 'Тестовый женский профиль', + photos: ['photo2.jpg'], + city: 'Москва', + searchPreferences: { + minAge: 25, + maxAge: 40, + maxDistance: 30 + } + }); + console.log(`Создан женский профиль: userId=${femaleUserId}, telegramId=654321`); + + console.log('Тестовые пользователи созданы успешно'); +} + +// Функция для тестирования подбора анкет +async function testMatching() { + console.log('\n===== ТЕСТИРОВАНИЕ ПОДБОРА АНКЕТ ====='); + + const matchingService = new MatchingService(); + + console.log('\nТест 1: Получение анкеты для мужского профиля (должна вернуться женская анкета)'); + const femaleProfile = await matchingService.getNextCandidate('123456', true); + if (femaleProfile) { + console.log(`✓ Получена анкета: ${femaleProfile.name}, возраст: ${femaleProfile.age}, пол: ${femaleProfile.gender}`); + } else { + console.log('✗ Анкета не найдена'); + } + + console.log('\nТест 2: Получение анкеты для женского профиля (должна вернуться мужская анкета)'); + const maleProfile = await matchingService.getNextCandidate('654321', true); + if (maleProfile) { + console.log(`✓ Получена анкета: ${maleProfile.name}, возраст: ${maleProfile.age}, пол: ${maleProfile.gender}`); + } else { + console.log('✗ Анкета не найдена'); + } + + console.log('\n===== ТЕСТИРОВАНИЕ ЗАВЕРШЕНО =====\n'); + + // Завершение работы скрипта + process.exit(0); +} + +// Главная функция +async function main() { + try { + // Создаем тестовых пользователей + await createTestUsers(); + + // Тестируем подбор анкет + await testMatching(); + } catch (error) { + console.error('Ошибка при выполнении тестов:', error); + process.exit(1); + } +} + +main(); diff --git a/scripts/legacy/testProfileViews.js b/scripts/legacy/testProfileViews.js new file mode 100644 index 0000000..55195b9 --- /dev/null +++ b/scripts/legacy/testProfileViews.js @@ -0,0 +1,98 @@ +// Тестирование работы с таблицей profile_views +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +// Функция для тестирования записи просмотра профиля +async function testRecordProfileView(viewerId, viewedProfileId) { + const client = await pool.connect(); + try { + console.log(`Запись просмотра профиля: ${viewerId} просмотрел ${viewedProfileId}`); + + // Получаем UUID пользователей + const viewerResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewerId]); + if (viewerResult.rows.length === 0) { + console.log(`Пользователь с telegram_id ${viewerId} не найден, создаём нового пользователя`); + const newUserResult = await client.query(` + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES ($1, $2, $3, $4) RETURNING id + `, [viewerId, `user_${viewerId}`, `Имя ${viewerId}`, `Фамилия ${viewerId}`]); + + var viewerUuid = newUserResult.rows[0].id; + } else { + var viewerUuid = viewerResult.rows[0].id; + } + + const viewedResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewedProfileId]); + if (viewedResult.rows.length === 0) { + console.log(`Пользователь с telegram_id ${viewedProfileId} не найден, создаём нового пользователя`); + const newUserResult = await client.query(` + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES ($1, $2, $3, $4) RETURNING id + `, [viewedProfileId, `user_${viewedProfileId}`, `Имя ${viewedProfileId}`, `Фамилия ${viewedProfileId}`]); + + var viewedUuid = newUserResult.rows[0].id; + } else { + var viewedUuid = viewedResult.rows[0].id; + } + + console.log(`UUID просматривающего: ${viewerUuid}`); + console.log(`UUID просматриваемого: ${viewedUuid}`); + + // Записываем просмотр + await client.query(` + INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type, view_date) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE + SET view_date = NOW(), view_type = $3 + `, [viewerUuid, viewedUuid, 'browse']); + + console.log('Просмотр профиля успешно записан'); + + // Получаем список просмотренных профилей + const viewedProfiles = await client.query(` + SELECT v.viewed_profile_id, v.view_date, u.telegram_id + FROM profile_views v + JOIN users u ON u.id = v.viewed_profile_id + WHERE v.viewer_id = $1 + ORDER BY v.view_date DESC + `, [viewerUuid]); + + console.log('Список просмотренных профилей:'); + viewedProfiles.rows.forEach((row, index) => { + console.log(`${index + 1}. ID: ${row.telegram_id}, просмотрен: ${row.view_date}`); + }); + + return true; + } catch (error) { + console.error('Ошибка записи просмотра профиля:', error); + return false; + } finally { + client.release(); + } +} + +// Запускаем тест +async function runTest() { + try { + // Тестируем запись просмотра профиля + await testRecordProfileView(123456, 789012); + await testRecordProfileView(123456, 345678); + await testRecordProfileView(789012, 123456); + + console.log('Тесты завершены успешно'); + } catch (error) { + console.error('Ошибка при выполнении тестов:', error); + } finally { + await pool.end(); + } +} + +runTest(); diff --git a/scripts/legacy/testVipMethod.js b/scripts/legacy/testVipMethod.js new file mode 100644 index 0000000..565288a --- /dev/null +++ b/scripts/legacy/testVipMethod.js @@ -0,0 +1,81 @@ +// Скрипт для тестирования метода checkPremiumStatus +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function testCheckPremiumMethod() { + try { + console.log('Тестирование метода checkPremiumStatus...'); + + // Получаем пользователя для тестирования + const userResult = await pool.query(` + SELECT id, telegram_id, first_name, username, premium + FROM users + ORDER BY last_active_at DESC NULLS LAST + LIMIT 1 + `); + + if (userResult.rows.length === 0) { + console.error('❌ Пользователи не найдены в базе данных'); + return; + } + + const user = userResult.rows[0]; + console.log(`Выбран тестовый пользователь: ${user.first_name || user.username || 'Без имени'} (Telegram ID: ${user.telegram_id})`); + console.log(`Текущий премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`); + + // Проверка работы метода checkPremiumStatus + console.log('\nЭмулируем вызов метода checkPremiumStatus из vipService:'); + const result = await pool.query(` + SELECT id, premium + FROM users + WHERE telegram_id = $1 + `, [user.telegram_id]); + + if (result.rows.length === 0) { + console.log('❌ Пользователь не найден'); + } else { + const isPremium = result.rows[0].premium || false; + console.log(`Результат метода: isPremium = ${isPremium ? '✅ true' : '❌ false'}`); + + if (!isPremium) { + console.log('\nПремиум-статус отсутствует. Устанавливаем премиум...'); + await pool.query(` + UPDATE users + SET premium = true + WHERE telegram_id = $1 + `, [user.telegram_id]); + + // Проверяем обновление + const updatedResult = await pool.query(` + SELECT premium + FROM users + WHERE telegram_id = $1 + `, [user.telegram_id]); + + const updatedPremium = updatedResult.rows[0].premium; + console.log(`Обновленный статус: isPremium = ${updatedPremium ? '✅ true' : '❌ false'}`); + } + } + + console.log('\n✅ Тестирование завершено'); + console.log('🔧 Теперь проверьте функциональность VIP поиска в боте'); + + } catch (error) { + console.error('❌ Ошибка при тестировании:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +// Запускаем тест +testCheckPremiumMethod(); diff --git a/scripts/legacy/testVipStatus.js b/scripts/legacy/testVipStatus.js new file mode 100644 index 0000000..16a7839 --- /dev/null +++ b/scripts/legacy/testVipStatus.js @@ -0,0 +1,75 @@ +// Скрипт для тестирования VIP функционала +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function testVipStatus() { + try { + console.log('Тестирование функционала VIP статуса...'); + + // Получаем список пользователей с информацией о premium статусе + const users = await pool.query(` + SELECT id, telegram_id, username, first_name, premium + FROM users + ORDER BY last_active_at DESC + LIMIT 5 + `); + + console.log('Список пользователей и их премиум статус:'); + users.rows.forEach(user => { + console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`); + }); + + // Если premium у всех false, устанавливаем premium = true + const nonPremiumUsers = users.rows.filter(user => !user.premium); + if (nonPremiumUsers.length > 0) { + console.log('\nОбнаружены пользователи без премиум статуса. Устанавливаем премиум...'); + + for (const user of nonPremiumUsers) { + await pool.query(` + UPDATE users + SET premium = true + WHERE id = $1 + RETURNING id, telegram_id, premium + `, [user.id]); + + console.log(`✅ Установлен премиум для пользователя ${user.first_name || user.username || user.telegram_id}`); + } + } else { + console.log('\nВсе пользователи уже имеют премиум-статус!'); + } + + // Проверяем результат + const updatedUsers = await pool.query(` + SELECT id, telegram_id, username, first_name, premium + FROM users + ORDER BY last_active_at DESC + LIMIT 5 + `); + + console.log('\nОбновленный список пользователей и их премиум статус:'); + updatedUsers.rows.forEach(user => { + console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`); + }); + + console.log('\n✅ Тестирование VIP функционала завершено'); + console.log('🔧 Проверьте доступность VIP поиска в боте через меню или команды'); + + } catch (error) { + console.error('❌ Ошибка при тестировании VIP статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +// Запускаем тест +testVipStatus(); diff --git a/scripts/startup.sh b/scripts/startup.sh new file mode 100644 index 0000000..9cb83ea --- /dev/null +++ b/scripts/startup.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# startup.sh - Script to run migrations and start the bot + +echo "🚀 Starting Telegram Tinder Bot..." + +# Wait for database to be ready +echo "⏳ Waiting for database to be ready..." +sleep 5 + +# Run database migrations +echo "🔄 Running database migrations..." +node dist/database/migrateOnStartup.js + +# Start the bot +echo "✅ Starting the bot..." +node dist/bot.js diff --git a/scripts/update_bot_with_notifications.js b/scripts/update_bot_with_notifications.js new file mode 100644 index 0000000..a4084de --- /dev/null +++ b/scripts/update_bot_with_notifications.js @@ -0,0 +1,104 @@ +/** + * Скрипт для проверки и исправления регистрации NotificationHandlers в bot.ts + */ + +const fs = require('fs'); +const path = require('path'); + +const botFilePath = path.join(__dirname, '../src/bot.ts'); + +// Проверка существования файла bot.ts +if (!fs.existsSync(botFilePath)) { + console.error(`❌ Файл ${botFilePath} не найден`); + process.exit(1); +} + +// Чтение содержимого файла bot.ts +let botContent = fs.readFileSync(botFilePath, 'utf8'); + +// Проверка импорта NotificationHandlers +if (!botContent.includes('import { NotificationHandlers }')) { + console.log('Добавляем импорт NotificationHandlers в bot.ts...'); + + // Находим последний импорт + const importRegex = /^import.*?;/gms; + const matches = [...botContent.matchAll(importRegex)]; + + if (matches.length > 0) { + const lastImport = matches[matches.length - 1][0]; + const lastImportIndex = botContent.lastIndexOf(lastImport) + lastImport.length; + + // Добавляем импорт NotificationHandlers + botContent = + botContent.slice(0, lastImportIndex) + + '\nimport { NotificationHandlers } from \'./handlers/notificationHandlers\';\n' + + botContent.slice(lastImportIndex); + + console.log('✅ Импорт NotificationHandlers добавлен'); + } else { + console.error('❌ Не удалось найти место для добавления импорта'); + } +} + +// Проверка объявления NotificationHandlers в классе +if (!botContent.includes('private notificationHandlers')) { + console.log('Добавляем объявление notificationHandlers в класс...'); + + const classPropertiesRegex = /class TelegramTinderBot {([^}]+?)constructor/s; + const classPropertiesMatch = botContent.match(classPropertiesRegex); + + if (classPropertiesMatch) { + const classProperties = classPropertiesMatch[1]; + const updatedProperties = classProperties + ' private notificationHandlers: NotificationHandlers;\n '; + + botContent = botContent.replace(classPropertiesRegex, `class TelegramTinderBot {${updatedProperties}constructor`); + + console.log('✅ Объявление notificationHandlers добавлено'); + } else { + console.error('❌ Не удалось найти место для добавления объявления notificationHandlers'); + } +} + +// Проверка создания экземпляра NotificationHandlers в конструкторе +if (!botContent.includes('this.notificationHandlers = new NotificationHandlers')) { + console.log('Добавляем инициализацию notificationHandlers в конструктор...'); + + const initializationRegex = /(this\.callbackHandlers = new CallbackHandlers[^;]+;)/; + const initializationMatch = botContent.match(initializationRegex); + + if (initializationMatch) { + const callbackHandlersInit = initializationMatch[1]; + const updatedInit = callbackHandlersInit + '\n this.notificationHandlers = new NotificationHandlers(this.bot);'; + + botContent = botContent.replace(initializationRegex, updatedInit); + + console.log('✅ Инициализация notificationHandlers добавлена'); + } else { + console.error('❌ Не удалось найти место для добавления инициализации notificationHandlers'); + } +} + +// Проверка регистрации notificationHandlers в методе registerHandlers +if (!botContent.includes('this.notificationHandlers.register()')) { + console.log('Добавляем регистрацию notificationHandlers...'); + + const registerHandlersRegex = /(private registerHandlers\(\): void {[^}]+?)}/s; + const registerHandlersMatch = botContent.match(registerHandlersRegex); + + if (registerHandlersMatch) { + const registerHandlersBody = registerHandlersMatch[1]; + const updatedBody = registerHandlersBody + '\n // Обработчики уведомлений\n this.notificationHandlers.register();\n }'; + + botContent = botContent.replace(registerHandlersRegex, updatedBody); + + console.log('✅ Регистрация notificationHandlers добавлена'); + } else { + console.error('❌ Не удалось найти место для добавления регистрации notificationHandlers'); + } +} + +// Запись обновленного содержимого в файл +fs.writeFileSync(botFilePath, botContent, 'utf8'); + +console.log('✅ Файл bot.ts успешно обновлен'); +console.log('🔔 Перезапустите бота для применения изменений'); diff --git a/set-premium.js b/set-premium.js deleted file mode 100644 index 1cc5808..0000000 Binary files a/set-premium.js and /dev/null differ diff --git a/sql/consolidated.sql b/sql/consolidated.sql new file mode 100644 index 0000000..a61b524 --- /dev/null +++ b/sql/consolidated.sql @@ -0,0 +1,404 @@ +# Consolidated SQL файл для миграции базы данных Telegram Tinder Bot +# Этот файл содержит все необходимые SQL-запросы для создания базы данных с нуля + +-- Создание расширения для UUID +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Создание перечислений +CREATE TYPE gender_type AS ENUM ('male', 'female', 'other'); +CREATE TYPE swipe_action AS ENUM ('like', 'dislike', 'superlike'); + +-- Создание таблицы пользователей +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + telegram_id BIGINT UNIQUE NOT NULL, + username VARCHAR(255), + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255), + language_code VARCHAR(10), + is_premium BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Создание таблицы профилей +CREATE TABLE IF NOT EXISTS profiles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + age INTEGER NOT NULL CHECK (age >= 18), + gender gender_type NOT NULL, + bio TEXT, + photos TEXT[], -- JSON array of photo file_ids + location VARCHAR(255), + job VARCHAR(255), + interests TEXT[], -- JSON array of interests + last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + is_completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_user_profile UNIQUE (user_id) +); + +-- Создание индекса для поиска по возрасту и полу +CREATE INDEX idx_profiles_age_gender ON profiles(age, gender); + +-- Создание таблицы предпочтений поиска +CREATE TABLE IF NOT EXISTS search_preferences ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + age_min INTEGER NOT NULL DEFAULT 18 CHECK (age_min >= 18), + age_max INTEGER NOT NULL DEFAULT 99 CHECK (age_max >= age_min), + looking_for gender_type NOT NULL, + distance_max INTEGER, -- max distance in km, NULL means no limit + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_user_preferences UNIQUE (user_id) +); + +-- Создание таблицы действий (лайки/дизлайки) +CREATE TABLE IF NOT EXISTS swipes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + action swipe_action NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_swipe UNIQUE (user_id, target_id) +); + +-- Создание индекса для быстрого поиска матчей +CREATE INDEX idx_swipes_user_target ON swipes(user_id, target_id); + +-- Создание таблицы матчей +CREATE TABLE IF NOT EXISTS matches ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id_1 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id_2 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + CONSTRAINT unique_match UNIQUE (user_id_1, user_id_2) +); + +-- Создание индекса для быстрого поиска матчей по пользователю +CREATE INDEX idx_matches_user_id_1 ON matches(user_id_1); +CREATE INDEX idx_matches_user_id_2 ON matches(user_id_2); + +-- Создание таблицы блокировок +CREATE TABLE IF NOT EXISTS blocks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_block UNIQUE (blocker_id, blocked_id) +); + +-- Создание таблицы сообщений +CREATE TABLE IF NOT EXISTS messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + match_id UUID NOT NULL REFERENCES matches(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + text TEXT NOT NULL, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Создание индекса для быстрого поиска сообщений +CREATE INDEX idx_messages_match_id ON messages(match_id); + +-- Создание таблицы уведомлений +CREATE TABLE IF NOT EXISTS notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, -- new_match, new_message, etc. + content TEXT NOT NULL, + is_read BOOLEAN DEFAULT FALSE, + reference_id UUID, -- Can reference a match_id, message_id, etc. + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Создание индекса для быстрого поиска уведомлений +CREATE INDEX idx_notifications_user_id ON notifications(user_id); + +-- Создание таблицы настроек +CREATE TABLE IF NOT EXISTS settings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + notifications_enabled BOOLEAN DEFAULT TRUE, + show_online_status BOOLEAN DEFAULT TRUE, + visibility BOOLEAN DEFAULT TRUE, -- whether profile is visible in search + theme VARCHAR(20) DEFAULT 'light', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_user_settings UNIQUE (user_id) +); + +-- Создание таблицы просмотров профиля +CREATE TABLE IF NOT EXISTS profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + view_count INTEGER DEFAULT 1, + last_viewed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_view UNIQUE (viewer_id, viewed_id) +); + +-- Создание таблицы для премиум-пользователей +CREATE TABLE IF NOT EXISTS premium_features ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + is_premium BOOLEAN DEFAULT FALSE, + superlike_quota INTEGER DEFAULT 1, + spotlight_quota INTEGER DEFAULT 0, + see_likes BOOLEAN DEFAULT FALSE, -- Can see who liked their profile + unlimited_likes BOOLEAN DEFAULT FALSE, + expires_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_user_premium UNIQUE (user_id) +); + +-- Функция для обновления поля updated_at +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггеры для обновления поля updated_at +CREATE TRIGGER update_users_updated_at +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_profiles_updated_at +BEFORE UPDATE ON profiles +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_search_preferences_updated_at +BEFORE UPDATE ON search_preferences +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_settings_updated_at +BEFORE UPDATE ON settings +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_premium_features_updated_at +BEFORE UPDATE ON premium_features +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +-- Индекс для поиска пользователей по Telegram ID (часто используемый запрос) +CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); + +-- Индекс для статуса профиля (активный/неактивный, завершенный/незавершенный) +CREATE INDEX IF NOT EXISTS idx_profiles_is_completed ON profiles(is_completed); + +-- Представление для статистики +CREATE OR REPLACE VIEW user_statistics AS +SELECT + u.id, + u.telegram_id, + (SELECT COUNT(*) FROM swipes WHERE user_id = u.id AND action = 'like') AS likes_given, + (SELECT COUNT(*) FROM swipes WHERE user_id = u.id AND action = 'dislike') AS dislikes_given, + (SELECT COUNT(*) FROM swipes WHERE target_id = u.id AND action = 'like') AS likes_received, + (SELECT COUNT(*) FROM matches WHERE user_id_1 = u.id OR user_id_2 = u.id) AS matches_count, + (SELECT COUNT(*) FROM messages WHERE sender_id = u.id) AS messages_sent, + (SELECT COUNT(*) FROM profile_views WHERE viewed_id = u.id) AS profile_views +FROM users u; + +-- Функция для создания матча при взаимных лайках +CREATE OR REPLACE FUNCTION create_match_on_mutual_like() +RETURNS TRIGGER AS $$ +DECLARE + reverse_like_exists BOOLEAN; +BEGIN + -- Check if there is a reverse like + SELECT EXISTS ( + SELECT 1 + FROM swipes + WHERE user_id = NEW.target_id + AND target_id = NEW.user_id + AND action = 'like' + ) INTO reverse_like_exists; + + -- If there is a reverse like, create a match + IF reverse_like_exists AND NEW.action = 'like' THEN + INSERT INTO matches (user_id_1, user_id_2) + VALUES ( + LEAST(NEW.user_id, NEW.target_id), + GREATEST(NEW.user_id, NEW.target_id) + ) + ON CONFLICT (user_id_1, user_id_2) DO NOTHING; + + -- Create notifications for both users + INSERT INTO notifications (user_id, type, content, reference_id) + VALUES ( + NEW.user_id, + 'new_match', + 'У вас новый матч!', + (SELECT id FROM matches WHERE + (user_id_1 = LEAST(NEW.user_id, NEW.target_id) AND user_id_2 = GREATEST(NEW.user_id, NEW.target_id)) + ) + ); + + INSERT INTO notifications (user_id, type, content, reference_id) + VALUES ( + NEW.target_id, + 'new_match', + 'У вас новый матч!', + (SELECT id FROM matches WHERE + (user_id_1 = LEAST(NEW.user_id, NEW.target_id) AND user_id_2 = GREATEST(NEW.user_id, NEW.target_id)) + ) + ); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггер для создания матча при взаимных лайках +CREATE TRIGGER create_match_trigger +AFTER INSERT ON swipes +FOR EACH ROW +EXECUTE FUNCTION create_match_on_mutual_like(); + +-- Функция для создания уведомления о новом сообщении +CREATE OR REPLACE FUNCTION notify_new_message() +RETURNS TRIGGER AS $$ +DECLARE + recipient_id UUID; + match_record RECORD; +BEGIN + -- Get the match record + SELECT * INTO match_record FROM matches WHERE id = NEW.match_id; + + -- Determine the recipient + IF match_record.user_id_1 = NEW.sender_id THEN + recipient_id := match_record.user_id_2; + ELSE + recipient_id := match_record.user_id_1; + END IF; + + -- Create notification + INSERT INTO notifications (user_id, type, content, reference_id) + VALUES ( + recipient_id, + 'new_message', + 'У вас новое сообщение!', + NEW.id + ); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггер для создания уведомления о новом сообщении +CREATE TRIGGER notify_new_message_trigger +AFTER INSERT ON messages +FOR EACH ROW +EXECUTE FUNCTION notify_new_message(); + +-- Функция для обновления времени последней активности пользователя +CREATE OR REPLACE FUNCTION update_last_active() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE profiles + SET last_active = CURRENT_TIMESTAMP + WHERE user_id = NEW.user_id; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггер для обновления времени последней активности при свайпах +CREATE TRIGGER update_last_active_on_swipe +AFTER INSERT ON swipes +FOR EACH ROW +EXECUTE FUNCTION update_last_active(); + +-- Триггер для обновления времени последней активности при отправке сообщений +CREATE TRIGGER update_last_active_on_message +AFTER INSERT ON messages +FOR EACH ROW +EXECUTE FUNCTION update_last_active(); + +-- Создание функции для автоматического создания профиля при создании пользователя +CREATE OR REPLACE FUNCTION create_initial_profile() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO profiles (user_id, name, age, gender) + VALUES (NEW.id, COALESCE(NEW.first_name, 'User'), 18, 'other'); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Триггер для автоматического создания профиля при создании пользователя +CREATE TRIGGER create_profile_trigger +AFTER INSERT ON users +FOR EACH ROW +EXECUTE FUNCTION create_initial_profile(); + +-- Индексы для оптимизации частых запросов +CREATE INDEX IF NOT EXISTS idx_profiles_last_active ON profiles(last_active); +CREATE INDEX IF NOT EXISTS idx_swipes_action ON swipes(action); +CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read); +CREATE INDEX IF NOT EXISTS idx_messages_is_read ON messages(is_read); + +-- Добавление ограничений для проверки возраста +ALTER TABLE profiles DROP CONSTRAINT IF EXISTS age_check; +ALTER TABLE profiles ADD CONSTRAINT age_check CHECK (age >= 18 AND age <= 99); + +-- Добавление ограничений для предпочтений +ALTER TABLE search_preferences DROP CONSTRAINT IF EXISTS age_range_check; +ALTER TABLE search_preferences ADD CONSTRAINT age_range_check CHECK (age_min >= 18 AND age_max >= age_min AND age_max <= 99); + +-- Комментарии к таблицам для документации +COMMENT ON TABLE users IS 'Таблица пользователей Telegram'; +COMMENT ON TABLE profiles IS 'Профили пользователей для знакомств'; +COMMENT ON TABLE search_preferences IS 'Предпочтения поиска пользователей'; +COMMENT ON TABLE swipes IS 'История лайков/дислайков'; +COMMENT ON TABLE matches IS 'Совпадения (матчи) между пользователями'; +COMMENT ON TABLE messages IS 'Сообщения между пользователями'; +COMMENT ON TABLE notifications IS 'Уведомления для пользователей'; +COMMENT ON TABLE settings IS 'Настройки пользователей'; +COMMENT ON TABLE profile_views IS 'История просмотров профилей'; +COMMENT ON TABLE premium_features IS 'Премиум-функции для пользователей'; + +-- Представление для быстрого получения активных матчей с информацией о пользователе +CREATE OR REPLACE VIEW active_matches AS +SELECT + m.id AS match_id, + m.created_at AS match_date, + CASE + WHEN m.user_id_1 = u1.id THEN u2.id + ELSE u1.id + END AS partner_id, + CASE + WHEN m.user_id_1 = u1.id THEN u2.telegram_id + ELSE u1.telegram_id + END AS partner_telegram_id, + CASE + WHEN m.user_id_1 = u1.id THEN p2.name + ELSE p1.name + END AS partner_name, + CASE + WHEN m.user_id_1 = u1.id THEN p2.photos[1] + ELSE p1.photos[1] + END AS partner_photo, + (SELECT COUNT(*) FROM messages WHERE match_id = m.id) AS message_count, + (SELECT COUNT(*) FROM messages WHERE match_id = m.id AND is_read = false AND sender_id != u1.id) AS unread_count, + m.is_active +FROM matches m +JOIN users u1 ON m.user_id_1 = u1.id +JOIN users u2 ON m.user_id_2 = u2.id +JOIN profiles p1 ON u1.id = p1.user_id +JOIN profiles p2 ON u2.id = p2.user_id +WHERE m.is_active = true; diff --git a/src/bot.ts b/src/bot.ts index 68711be..efea339 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -8,6 +8,8 @@ import LocalizationService from './services/localizationService'; import { CommandHandlers } from './handlers/commandHandlers'; import { CallbackHandlers } from './handlers/callbackHandlers'; import { MessageHandlers } from './handlers/messageHandlers'; +import { NotificationHandlers } from './handlers/notificationHandlers'; + class TelegramTinderBot { private bot: TelegramBot; @@ -18,7 +20,7 @@ class TelegramTinderBot { private commandHandlers: CommandHandlers; private callbackHandlers: CallbackHandlers; private messageHandlers: MessageHandlers; - + private notificationHandlers: NotificationHandlers; constructor() { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) { @@ -34,6 +36,7 @@ class TelegramTinderBot { this.commandHandlers = new CommandHandlers(this.bot); this.messageHandlers = new MessageHandlers(this.bot); this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers); + this.notificationHandlers = new NotificationHandlers(this.bot); this.setupErrorHandling(); this.setupPeriodicTasks(); @@ -78,6 +81,7 @@ class TelegramTinderBot { { command: 'browse', description: '💕 Смотреть анкеты' }, { command: 'matches', description: '💖 Мои матчи' }, { command: 'settings', description: '⚙️ Настройки' }, + { command: 'notifications', description: '🔔 Настройки уведомлений' }, { command: 'help', description: '❓ Помощь' } ]; @@ -94,6 +98,9 @@ class TelegramTinderBot { // Сообщения this.messageHandlers.register(); + + // Обработчики уведомлений + this.notificationHandlers.register(); } // Обработка ошибок @@ -137,14 +144,31 @@ class TelegramTinderBot { } }, 5 * 60 * 1000); - // Очистка старых данных каждый день + // Планирование периодических уведомлений раз в день в 00:05 setInterval(async () => { try { - await this.cleanupOldData(); + const now = new Date(); + if (now.getHours() === 0 && now.getMinutes() >= 5 && now.getMinutes() < 10) { + console.log('🔔 Scheduling periodic notifications...'); + await this.notificationService.schedulePeriodicNotifications(); + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + }, 5 * 60 * 1000); + + // Очистка старых данных каждый день в 03:00 + setInterval(async () => { + try { + const now = new Date(); + if (now.getHours() === 3 && now.getMinutes() < 5) { + console.log('🧹 Running scheduled cleanup...'); + await this.cleanupOldData(); + } } catch (error) { console.error('Error cleaning up old data:', error); } - }, 24 * 60 * 60 * 1000); + }, 5 * 60 * 1000); } // Очистка старых данных @@ -152,11 +176,18 @@ class TelegramTinderBot { console.log('🧹 Running cleanup tasks...'); try { - // Очистка старых уведомлений (старше 30 дней) - const notificationsResult = await query(` + // Очистка старых запланированных уведомлений (старше 30 дней или обработанных) + const scheduledNotificationsResult = await query(` DELETE FROM scheduled_notifications - WHERE processed = true - AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days' + WHERE (processed = true AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days') + OR (scheduled_at < CURRENT_TIMESTAMP - INTERVAL '7 days') + `); + console.log(`🗑️ Cleaned up ${scheduledNotificationsResult.rowCount} old scheduled notifications`); + + // Очистка старых уведомлений (старше 90 дней) + const notificationsResult = await query(` + DELETE FROM notifications + WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '90 days' `); console.log(`🗑️ Cleaned up ${notificationsResult.rowCount} old notifications`); @@ -186,7 +217,7 @@ class TelegramTinderBot { console.log(`💬 Cleaned up ${messagesResult.rowCount} old messages`); // Обновление статистики таблиц после очистки - await query('VACUUM ANALYZE scheduled_notifications, profile_views, swipes, messages'); + await query('VACUUM ANALYZE notifications, scheduled_notifications, profile_views, swipes, messages'); console.log('✅ Cleanup completed successfully'); } catch (error) { @@ -229,4 +260,4 @@ if (require.main === module) { }); } -export { TelegramTinderBot }; \ No newline at end of file +export { TelegramTinderBot }; diff --git a/src/database/migrateOnStartup.ts b/src/database/migrateOnStartup.ts new file mode 100644 index 0000000..55c58d3 --- /dev/null +++ b/src/database/migrateOnStartup.ts @@ -0,0 +1,108 @@ +// Script to run migrations on startup +import { Pool } from 'pg'; +import * as fs from 'fs'; +import * as path from 'path'; +import 'dotenv/config'; + +async function runMigrations() { + console.log('Starting database migration...'); + + // Create a connection pool + const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME || 'telegram_tinder_bot', + user: process.env.DB_USERNAME || 'postgres', + password: process.env.DB_PASSWORD, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }); + + try { + // Test connection + const testRes = await pool.query('SELECT NOW()'); + console.log(`Database connection successful at ${testRes.rows[0].now}`); + + // Create migrations table if not exists + await pool.query(` + CREATE TABLE IF NOT EXISTS migrations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Get list of executed migrations + const migrationRes = await pool.query('SELECT name FROM migrations'); + const executedMigrations = migrationRes.rows.map(row => row.name); + console.log(`Found ${executedMigrations.length} executed migrations`); + + // Get migration files + const migrationsPath = path.join(__dirname, 'migrations'); + let migrationFiles = []; + + try { + migrationFiles = fs.readdirSync(migrationsPath) + .filter(file => file.endsWith('.sql')) + .sort(); + console.log(`Found ${migrationFiles.length} migration files`); + } catch (error: any) { + console.error(`Error reading migrations directory: ${error.message}`); + console.log('Continuing with built-in consolidated migration...'); + + // If no external files found, use consolidated.sql + const consolidatedSQL = fs.readFileSync(path.join(__dirname, 'migrations', 'consolidated.sql'), 'utf8'); + + console.log('Executing consolidated migration...'); + await pool.query(consolidatedSQL); + + if (!executedMigrations.includes('consolidated.sql')) { + await pool.query( + 'INSERT INTO migrations (name) VALUES ($1)', + ['consolidated.sql'] + ); + } + + console.log('Consolidated migration completed successfully'); + return; + } + + // Run each migration that hasn't been executed yet + for (const file of migrationFiles) { + if (!executedMigrations.includes(file)) { + console.log(`Executing migration: ${file}`); + const sql = fs.readFileSync(path.join(migrationsPath, file), 'utf8'); + + try { + await pool.query('BEGIN'); + await pool.query(sql); + await pool.query( + 'INSERT INTO migrations (name) VALUES ($1)', + [file] + ); + await pool.query('COMMIT'); + console.log(`Migration ${file} completed successfully`); + } catch (error: any) { + await pool.query('ROLLBACK'); + console.error(`Error executing migration ${file}: ${error.message}`); + throw error; + } + } else { + console.log(`Migration ${file} already executed, skipping`); + } + } + + console.log('All migrations completed successfully!'); + } catch (error: any) { + console.error(`Migration failed: ${error.message}`); + process.exit(1); + } finally { + await pool.end(); + } +} + +runMigrations().catch((error: any) => { + console.error('Unhandled error during migration:', error); + process.exit(1); +}); diff --git a/src/database/migrations/consolidated.sql b/src/database/migrations/consolidated.sql new file mode 100644 index 0000000..2c24dff --- /dev/null +++ b/src/database/migrations/consolidated.sql @@ -0,0 +1,182 @@ +-- Consolidated migrations for Telegram Tinder Bot + +-- Create extension for UUID if not exists +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +---------------------------------------------- +-- Core Tables +---------------------------------------------- + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + telegram_id BIGINT UNIQUE NOT NULL, + username VARCHAR(255), + first_name VARCHAR(255), + last_name VARCHAR(255), + language_code VARCHAR(10) DEFAULT 'ru', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + last_active_at TIMESTAMP DEFAULT NOW(), + premium BOOLEAN DEFAULT FALSE, + state VARCHAR(255), + state_data JSONB DEFAULT '{}'::jsonb +); + +-- Profiles table +CREATE TABLE IF NOT EXISTS profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + age INTEGER NOT NULL CHECK (age >= 18 AND age <= 100), + gender VARCHAR(10) NOT NULL CHECK (gender IN ('male', 'female', 'other')), + interested_in VARCHAR(10) NOT NULL CHECK (interested_in IN ('male', 'female', 'both')), + bio TEXT, + photos JSONB DEFAULT '[]', + interests JSONB DEFAULT '[]', + city VARCHAR(255), + education VARCHAR(255), + job VARCHAR(255), + height INTEGER, + location_lat DECIMAL(10, 8), + location_lon DECIMAL(11, 8), + search_min_age INTEGER DEFAULT 18, + search_max_age INTEGER DEFAULT 50, + search_max_distance INTEGER DEFAULT 50, + is_verified BOOLEAN DEFAULT FALSE, + is_visible BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + religion VARCHAR(255), + dating_goal VARCHAR(50), + smoking VARCHAR(20), + drinking VARCHAR(20), + has_kids BOOLEAN DEFAULT FALSE +); + +-- Swipes table +CREATE TABLE IF NOT EXISTS swipes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + target_user_id UUID REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(20) NOT NULL CHECK (type IN ('like', 'pass', 'superlike')), + created_at TIMESTAMP DEFAULT NOW(), + is_match BOOLEAN DEFAULT FALSE, + UNIQUE(user_id, target_user_id) +); + +-- Matches table +CREATE TABLE IF NOT EXISTS matches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id_1 UUID REFERENCES users(id) ON DELETE CASCADE, + user_id_2 UUID REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + last_message_at TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + is_super_match BOOLEAN DEFAULT FALSE, + unread_count_1 INTEGER DEFAULT 0, + unread_count_2 INTEGER DEFAULT 0, + UNIQUE(user_id_1, user_id_2) +); + +-- Messages table +CREATE TABLE IF NOT EXISTS messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + sender_id UUID REFERENCES users(id) ON DELETE CASCADE, + receiver_id UUID REFERENCES users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + message_type VARCHAR(20) DEFAULT 'text' CHECK (message_type IN ('text', 'photo', 'gif', 'sticker')), + created_at TIMESTAMP DEFAULT NOW(), + is_read BOOLEAN DEFAULT FALSE +); + +---------------------------------------------- +-- Profile Views Table +---------------------------------------------- + +-- Table for tracking profile views +CREATE TABLE IF NOT EXISTS profile_views ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + viewer_id UUID REFERENCES users(id) ON DELETE CASCADE, + viewed_id UUID REFERENCES users(id) ON DELETE CASCADE, + view_type VARCHAR(20) DEFAULT 'browse' CHECK (view_type IN ('browse', 'search', 'recommended')), + viewed_at TIMESTAMP DEFAULT NOW(), + CONSTRAINT unique_profile_view UNIQUE (viewer_id, viewed_id, view_type) +); + +-- Index for profile views +CREATE INDEX IF NOT EXISTS idx_profile_views_viewer ON profile_views(viewer_id); +CREATE INDEX IF NOT EXISTS idx_profile_views_viewed ON profile_views(viewed_id); + +---------------------------------------------- +-- Notification Tables +---------------------------------------------- + +-- Notifications table +CREATE TABLE IF NOT EXISTS notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + is_read BOOLEAN DEFAULT FALSE, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Notification settings table +CREATE TABLE IF NOT EXISTS notification_settings ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + new_matches BOOLEAN DEFAULT TRUE, + new_messages BOOLEAN DEFAULT TRUE, + new_likes BOOLEAN DEFAULT TRUE, + reminders BOOLEAN DEFAULT TRUE, + daily_summary BOOLEAN DEFAULT FALSE, + time_preference VARCHAR(20) DEFAULT 'evening', + do_not_disturb BOOLEAN DEFAULT FALSE, + do_not_disturb_start TIME, + do_not_disturb_end TIME, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Scheduled notifications table +CREATE TABLE IF NOT EXISTS scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +---------------------------------------------- +-- Indexes for better performance +---------------------------------------------- + +-- User Indexes +CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); + +-- Profile Indexes +CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id); +CREATE INDEX IF NOT EXISTS idx_profiles_location ON profiles(location_lat, location_lon) + WHERE location_lat IS NOT NULL AND location_lon IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, interested_in); + +-- Swipe Indexes +CREATE INDEX IF NOT EXISTS idx_swipes_user ON swipes(user_id, target_user_id); + +-- Match Indexes +CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user_id_1, user_id_2); + +-- Message Indexes +CREATE INDEX IF NOT EXISTS idx_messages_match ON messages(match_id, created_at); + +-- Notification Indexes +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); +CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(type); +CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at); +CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_user_id ON scheduled_notifications(user_id); +CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at); +CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_processed ON scheduled_notifications(processed); diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 26a9e37..09c37b8 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -4,12 +4,14 @@ import { MatchingService } from '../services/matchingService'; import { ChatService } from '../services/chatService'; import { Profile } from '../models/Profile'; import { MessageHandlers } from './messageHandlers'; +import { NotificationHandlers } from './notificationHandlers'; import { ProfileEditController } from '../controllers/profileEditController'; import { EnhancedChatHandlers } from './enhancedChatHandlers'; import { VipController } from '../controllers/vipController'; import { VipService } from '../services/vipService'; import { TranslationController } from '../controllers/translationController'; import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; export class CallbackHandlers { private bot: TelegramBot; @@ -22,6 +24,8 @@ export class CallbackHandlers { private vipController: VipController; private vipService: VipService; private translationController: TranslationController; + private notificationHandlers?: NotificationHandlers; + private likeBackHandler: LikeBackHandler; constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { this.bot = bot; @@ -34,6 +38,13 @@ export class CallbackHandlers { this.vipController = new VipController(bot); this.vipService = new VipService(); this.translationController = new TranslationController(); + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } + this.likeBackHandler = new LikeBackHandler(bot); } register(): void { @@ -167,6 +178,12 @@ export class CallbackHandlers { await this.handleMorePhotos(chatId, telegramId, targetUserId); } + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + // Матчи и чаты else if (data === 'view_matches') { await this.handleViewMatches(chatId, telegramId); @@ -263,6 +280,41 @@ export class CallbackHandlers { await this.handleSettings(chatId, telegramId); } + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } else { await this.bot.answerCallbackQuery(query.id, { text: 'Функция в разработке!', @@ -385,9 +437,15 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, '👍 Лайк отправлен!'); await this.showNextCandidate(chatId, telegramId); } - } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка'); - console.error('Like error:', error); + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка'); + console.error('Like error:', error); + } } } @@ -402,9 +460,15 @@ export class CallbackHandlers { await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass'); await this.showNextCandidate(chatId, telegramId); - } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка'); - console.error('Dislike error:', error); + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка'); + console.error('Dislike error:', error); + } } } @@ -443,9 +507,73 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!'); await this.showNextCandidate(chatId, telegramId); } + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка'); + console.error('Superlike error:', error); + } + } + } + + // Обработка обратного лайка из уведомления + async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise { + try { + // Получаем информацию о пользователях + const targetProfile = await this.profileService.getProfileByUserId(targetUserId); + + if (!targetProfile) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль'); + return; + } + + // Получаем telegram ID целевого пользователя для свайпа + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId); + if (!targetTelegramId) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти пользователя'); + return; + } + + // Выполняем свайп + const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like'); + + if (result.isMatch) { + // Это матч! + await this.bot.sendMessage( + chatId, + '🎉 *Поздравляем! Это взаимно!*\n\n' + + `Вы и *${targetProfile.name}* понравились друг другу!\n` + + 'Теперь вы можете начать общение.', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }], + [{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }] + ] + } + } + ); + } else { + await this.bot.sendMessage( + chatId, + '❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' + + 'Если вы также понравитесь этому пользователю, будет создан матч.', + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }] + ] + } + } + ); + } } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка'); - console.error('Superlike error:', error); + console.error('Error in handleLikeBack:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка'); } } @@ -476,9 +604,28 @@ export class CallbackHandlers { return; } - for (let i = 1; i < targetProfile.photos.length; i++) { - const photoFileId = targetProfile.photos[i]; - await this.bot.sendPhoto(chatId, photoFileId); + // Отправляем фотографии в виде медиа-группы (коллажа) + // Создаем массив объектов медиа для группового отправления + const mediaGroup = targetProfile.photos.slice(1).map((photoFileId, index) => ({ + type: 'photo' as const, + media: photoFileId, + caption: index === 0 ? `📸 Дополнительные фото ${targetProfile.name}` : undefined + })); + + try { + // Отправляем все фото одним сообщением (медиа-группой) + await this.bot.sendMediaGroup(chatId, mediaGroup); + } catch (error) { + console.error('Error sending media group:', error); + + // Если не получилось отправить медиа-группой, отправляем по одной + for (let i = 1; i < targetProfile.photos.length; i++) { + try { + await this.bot.sendPhoto(chatId, targetProfile.photos[i]); + } catch (photoError) { + console.error(`Error sending photo ${i}:`, photoError); + } + } } const keyboard: InlineKeyboardMarkup = { @@ -766,13 +913,7 @@ export class CallbackHandlers { ); } - // Настройки уведомлений - async handleNotificationSettings(chatId: number, telegramId: string): Promise { - await this.bot.sendMessage( - chatId, - '🔔 Настройки уведомлений будут доступны в следующем обновлении!' - ); - } + // Настройки уведомлений - реализация перенесена в расширенную версию // Как это работает async handleHowItWorks(chatId: number): Promise { @@ -807,6 +948,7 @@ export class CallbackHandlers { // Вспомогательные методы async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise { + const hasMultiplePhotos = profile.photos.length > 1; const mainPhotoFileId = profile.photos[0]; // Первое фото - главное let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n'; @@ -876,20 +1018,43 @@ export class CallbackHandlers { if (hasValidPhoto) { try { - await this.bot.sendPhoto(chatId, mainPhotoFileId, { - caption: profileText, - reply_markup: keyboard - }); + if (hasMultiplePhotos) { + // Если есть несколько фото, отправляем их как медиа-группу (коллаж) + const mediaGroup = profile.photos.map((photoFileId, index) => ({ + type: 'photo' as const, + media: photoFileId, + caption: index === 0 ? profileText : undefined, + parse_mode: index === 0 ? 'Markdown' as const : undefined + })); + + // Сначала отправляем медиа-группу + await this.bot.sendMediaGroup(chatId, mediaGroup); + + // Затем отправляем отдельное сообщение с кнопками + await this.bot.sendMessage(chatId, '📸 Выберите действие:', { + reply_markup: keyboard + }); + } else { + // Если только одно фото, отправляем его с текстом + await this.bot.sendPhoto(chatId, mainPhotoFileId, { + caption: profileText, + reply_markup: keyboard, + parse_mode: 'Markdown' + }); + } } catch (error) { + console.error('Error sending profile photos:', error); // Если не удалось отправить фото, отправляем текст await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, { - reply_markup: keyboard + reply_markup: keyboard, + parse_mode: 'Markdown' }); } } else { // Отправляем как текстовое сообщение await this.bot.sendMessage(chatId, profileText, { - reply_markup: keyboard + reply_markup: keyboard, + parse_mode: 'Markdown' }); } } @@ -1450,7 +1615,7 @@ export class CallbackHandlers { try { // Проверяем VIP статус пользователя const user = await this.profileService.getUserByTelegramId(telegramId); - if (!user || !user.isPremium) { + if (!user || !user.premium) { // Изменено с isPremium на premium, чтобы соответствовать названию колонки в базе данных const keyboard = { inline_keyboard: [ [ @@ -2112,4 +2277,27 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, t('translation.error')); } } + + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Notification settings error:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений. Попробуйте позже.'); + } + } } diff --git a/src/handlers/callbackHandlers.ts.backup-1758166633763 b/src/handlers/callbackHandlers.ts.backup-1758166633763 new file mode 100644 index 0000000..4d537fa --- /dev/null +++ b/src/handlers/callbackHandlers.ts.backup-1758166633763 @@ -0,0 +1,606 @@ +import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; +import { ChatService } from '../services/chatService'; +import { Profile } from '../models/Profile'; +import { MessageHandlers } from './messageHandlers'; +import { ProfileEditController } from '../controllers/profileEditController'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; +import { VipController } from '../controllers/vipController'; +import { VipService } from '../services/vipService'; +import { TranslationController } from '../controllers/translationController'; +import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; +import { NotificationHandlers } from './notificationHandlers'; + +export class CallbackHandlers { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + private chatService: ChatService; + private messageHandlers: MessageHandlers; + private profileEditController: ProfileEditController; + private enhancedChatHandlers: EnhancedChatHandlers; + private vipController: VipController; + private vipService: VipService; + private translationController: TranslationController; + private likeBackHandler: LikeBackHandler; + private notificationHandlers?: NotificationHandlers; + + constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + this.chatService = new ChatService(); + this.messageHandlers = messageHandlers; + this.profileEditController = new ProfileEditController(this.profileService); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); + this.vipController = new VipController(bot); + this.vipService = new VipService(); + this.translationController = new TranslationController(); + this.likeBackHandler = new LikeBackHandler(bot); + + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } + } + + register(): void { + this.bot.on('callback_query', (query) => this.handleCallback(query)); + } + + async handleCallback(query: CallbackQuery): Promise { + if (!query.data || !query.from || !query.message) return; + + const telegramId = query.from.id.toString(); + const chatId = query.message.chat.id; + const data = query.data; + + try { + // Основные действия профиля + if (data === 'create_profile') { + await this.handleCreateProfile(chatId, telegramId); + } else if (data.startsWith('gender_')) { + const gender = data.replace('gender_', ''); + await this.handleGenderSelection(chatId, telegramId, gender); + } else if (data === 'view_my_profile') { + await this.handleViewMyProfile(chatId, telegramId); + } else if (data === 'edit_profile') { + await this.handleEditProfile(chatId, telegramId); + } else if (data === 'manage_photos') { + await this.handleManagePhotos(chatId, telegramId); + } else if (data === 'preview_profile') { + await this.handlePreviewProfile(chatId, telegramId); + } + + // Редактирование полей профиля + else if (data === 'edit_name') { + await this.handleEditName(chatId, telegramId); + } else if (data === 'edit_age') { + await this.handleEditAge(chatId, telegramId); + } else if (data === 'edit_bio') { + await this.handleEditBio(chatId, telegramId); + } else if (data === 'edit_hobbies') { + await this.handleEditHobbies(chatId, telegramId); + } else if (data === 'edit_city') { + await this.handleEditCity(chatId, telegramId); + } else if (data === 'edit_job') { + await this.handleEditJob(chatId, telegramId); + } else if (data === 'edit_education') { + await this.handleEditEducation(chatId, telegramId); + } else if (data === 'edit_height') { + await this.handleEditHeight(chatId, telegramId); + } else if (data === 'edit_religion') { + await this.handleEditReligion(chatId, telegramId); + } else if (data === 'edit_dating_goal') { + await this.handleEditDatingGoal(chatId, telegramId); + } else if (data === 'edit_lifestyle') { + await this.handleEditLifestyle(chatId, telegramId); + } else if (data === 'edit_search_preferences') { + await this.handleEditSearchPreferences(chatId, telegramId); + } + + // Управление фотографиями + else if (data === 'add_photo') { + await this.handleAddPhoto(chatId, telegramId); + } else if (data === 'delete_photo') { + await this.handleDeletePhoto(chatId, telegramId); + } else if (data === 'set_main_photo') { + await this.handleSetMainPhoto(chatId, telegramId); + } else if (data.startsWith('delete_photo_')) { + const photoIndex = parseInt(data.replace('delete_photo_', '')); + await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex); + } else if (data.startsWith('set_main_photo_')) { + const photoIndex = parseInt(data.replace('set_main_photo_', '')); + await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex); + } + + // Цели знакомства + else if (data.startsWith('set_dating_goal_')) { + const goal = data.replace('set_dating_goal_', ''); + await this.handleSetDatingGoal(chatId, telegramId, goal); + } + + // Образ жизни + else if (data === 'edit_smoking') { + await this.handleEditSmoking(chatId, telegramId); + } else if (data === 'edit_drinking') { + await this.handleEditDrinking(chatId, telegramId); + } else if (data === 'edit_kids') { + await this.handleEditKids(chatId, telegramId); + } else if (data.startsWith('set_smoking_')) { + const value = data.replace('set_smoking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'smoking', value); + } else if (data.startsWith('set_drinking_')) { + const value = data.replace('set_drinking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'drinking', value); + } else if (data.startsWith('set_kids_')) { + const value = data.replace('set_kids_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'kids', value); + } + + // Настройки поиска + else if (data === 'edit_age_range') { + await this.handleEditAgeRange(chatId, telegramId); + } else if (data === 'edit_distance') { + await this.handleEditDistance(chatId, telegramId); + } + + // Просмотр анкет и свайпы + else if (data === 'start_browsing') { + await this.handleStartBrowsing(chatId, telegramId, false); + } else if (data === 'start_browsing_first') { + // Показываем всех пользователей для нового пользователя + await this.handleStartBrowsing(chatId, telegramId, true); + } else if (data === 'vip_search') { + await this.handleVipSearch(chatId, telegramId); + } else if (data.startsWith('search_by_goal_')) { + const goal = data.replace('search_by_goal_', ''); + await this.handleSearchByGoal(chatId, telegramId, goal); + } else if (data === 'next_candidate') { + await this.handleNextCandidate(chatId, telegramId); + } else if (data.startsWith('like_')) { + const targetUserId = data.replace('like_', ''); + await this.handleLike(chatId, telegramId, targetUserId); + } else if (data.startsWith('dislike_')) { + const targetUserId = data.replace('dislike_', ''); + await this.handleDislike(chatId, telegramId, targetUserId); + } else if (data.startsWith('superlike_')) { + const targetUserId = data.replace('superlike_', ''); + await this.handleSuperlike(chatId, telegramId, targetUserId); + } else if (data.startsWith('view_profile_')) { + const targetUserId = data.replace('view_profile_', ''); + await this.handleViewProfile(chatId, telegramId, targetUserId); + } else if (data.startsWith('more_photos_')) { + const targetUserId = data.replace('more_photos_', ''); + await this.handleMorePhotos(chatId, telegramId, targetUserId); + } + + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + + // Матчи и чаты + else if (data === 'view_matches') { + await this.handleViewMatches(chatId, telegramId); + } else if (data === 'open_chats') { + await this.handleOpenChats(chatId, telegramId); + } else if (data === 'native_chats') { + await this.enhancedChatHandlers.showChatsNative(chatId, telegramId); + } else if (data.startsWith('open_native_chat_')) { + const matchId = data.replace('open_native_chat_', ''); + await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId); + } else if (data.startsWith('chat_history_')) { + const matchId = data.replace('chat_history_', ''); + await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId); + } else if (data.startsWith('chat_')) { + const matchId = data.replace('chat_', ''); + await this.handleOpenChat(chatId, telegramId, matchId); + } else if (data.startsWith('send_message_')) { + const matchId = data.replace('send_message_', ''); + await this.handleSendMessage(chatId, telegramId, matchId); + } else if (data.startsWith('view_chat_profile_')) { + const matchId = data.replace('view_chat_profile_', ''); + await this.handleViewChatProfile(chatId, telegramId, matchId); + } else if (data.startsWith('unmatch_')) { + const matchId = data.replace('unmatch_', ''); + await this.handleUnmatch(chatId, telegramId, matchId); + } else if (data.startsWith('confirm_unmatch_')) { + const matchId = data.replace('confirm_unmatch_', ''); + await this.handleConfirmUnmatch(chatId, telegramId, matchId); + } + + // Настройки + else if (data === 'settings') { + await this.handleSettings(chatId, telegramId); + } else if (data === 'search_settings') { + await this.handleSearchSettings(chatId, telegramId); + } else if (data === 'notification_settings') { + await this.handleNotificationSettings(chatId, telegramId); + } else if (data === 'view_stats') { + await this.handleViewStats(chatId, telegramId); + } else if (data === 'view_profile_viewers') { + await this.handleViewProfileViewers(chatId, telegramId); + } else if (data === 'hide_profile') { + await this.handleHideProfile(chatId, telegramId); + } else if (data === 'delete_profile') { + await this.handleDeleteProfile(chatId, telegramId); + } else if (data === 'main_menu') { + await this.handleMainMenu(chatId, telegramId); + } else if (data === 'confirm_delete_profile') { + await this.handleConfirmDeleteProfile(chatId, telegramId); + } + + // Информация + else if (data === 'how_it_works') { + await this.handleHowItWorks(chatId); + } else if (data === 'back_to_browsing') { + await this.handleStartBrowsing(chatId, telegramId); + } else if (data === 'get_vip') { + await this.vipController.showVipSearch(chatId, telegramId); + } + + // VIP функции + else if (data === 'vip_search') { + await this.vipController.showVipSearch(chatId, telegramId); + } else if (data === 'vip_quick_search') { + await this.vipController.performQuickVipSearch(chatId, telegramId); + } else if (data === 'vip_advanced_search') { + await this.vipController.startAdvancedSearch(chatId, telegramId); + } else if (data === 'vip_dating_goal_search') { + await this.vipController.showDatingGoalSearch(chatId, telegramId); + } else if (data.startsWith('vip_goal_')) { + const goal = data.replace('vip_goal_', ''); + await this.vipController.performDatingGoalSearch(chatId, telegramId, goal); + } else if (data.startsWith('vip_like_')) { + const targetTelegramId = data.replace('vip_like_', ''); + await this.handleVipLike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_superlike_')) { + const targetTelegramId = data.replace('vip_superlike_', ''); + await this.handleVipSuperlike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_dislike_')) { + const targetTelegramId = data.replace('vip_dislike_', ''); + await this.handleVipDislike(chatId, telegramId, targetTelegramId); + } + + // Настройки языка и переводы + else if (data === 'language_settings') { + await this.handleLanguageSettings(chatId, telegramId); + } else if (data.startsWith('set_language_')) { + const languageCode = data.replace('set_language_', ''); + await this.handleSetLanguage(chatId, telegramId, languageCode); + } else if (data.startsWith('translate_profile_')) { + const profileUserId = parseInt(data.replace('translate_profile_', '')); + await this.handleTranslateProfile(chatId, telegramId, profileUserId); + } else if (data === 'back_to_settings') { + await this.handleSettings(chatId, telegramId); + } + + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } + else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция в разработке!', + show_alert: false + }); + return; + } + + await this.bot.answerCallbackQuery(query.id); + + } catch (error) { + console.error('Callback handler error:', error); + await this.bot.answerCallbackQuery(query.id, { + text: 'Произошла ошибка. Попробуйте еще раз.', + show_alert: true + }); + } + } + + // Добавим все необходимые методы для обработки коллбэков + async handleCreateProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise { + // Заглушка метода + } + + async handleViewMyProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleManagePhotos(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handlePreviewProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditName(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditAge(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditBio(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHobbies(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditCity(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditJob(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditEducation(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHeight(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditReligion(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDatingGoal(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditLifestyle(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditSearchPreferences(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleAddPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetMainPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleEditSmoking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDrinking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditKids(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise { + // Заглушка метода + } + + async handleEditAgeRange(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDistance(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise { + // Заглушка метода + } + + async handleVipSearch(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleNextCandidate(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewMatches(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewStats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewProfileViewers(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHideProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleMainMenu(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHowItWorks(chatId: number): Promise { + // Заглушка метода + } + + async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleLanguageSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise { + // Заглушка метода + } + + async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise { + // Заглушка метода + } + + // Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers) + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Error handling notification settings:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.'); + } + } +} diff --git a/src/handlers/callbackHandlers.ts.original b/src/handlers/callbackHandlers.ts.original new file mode 100644 index 0000000..a1c6abf Binary files /dev/null and b/src/handlers/callbackHandlers.ts.original differ diff --git a/src/handlers/callbackHandlers.ts.stub b/src/handlers/callbackHandlers.ts.stub new file mode 100644 index 0000000..4d537fa --- /dev/null +++ b/src/handlers/callbackHandlers.ts.stub @@ -0,0 +1,606 @@ +import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; +import { ChatService } from '../services/chatService'; +import { Profile } from '../models/Profile'; +import { MessageHandlers } from './messageHandlers'; +import { ProfileEditController } from '../controllers/profileEditController'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; +import { VipController } from '../controllers/vipController'; +import { VipService } from '../services/vipService'; +import { TranslationController } from '../controllers/translationController'; +import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; +import { NotificationHandlers } from './notificationHandlers'; + +export class CallbackHandlers { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + private chatService: ChatService; + private messageHandlers: MessageHandlers; + private profileEditController: ProfileEditController; + private enhancedChatHandlers: EnhancedChatHandlers; + private vipController: VipController; + private vipService: VipService; + private translationController: TranslationController; + private likeBackHandler: LikeBackHandler; + private notificationHandlers?: NotificationHandlers; + + constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + this.chatService = new ChatService(); + this.messageHandlers = messageHandlers; + this.profileEditController = new ProfileEditController(this.profileService); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); + this.vipController = new VipController(bot); + this.vipService = new VipService(); + this.translationController = new TranslationController(); + this.likeBackHandler = new LikeBackHandler(bot); + + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } + } + + register(): void { + this.bot.on('callback_query', (query) => this.handleCallback(query)); + } + + async handleCallback(query: CallbackQuery): Promise { + if (!query.data || !query.from || !query.message) return; + + const telegramId = query.from.id.toString(); + const chatId = query.message.chat.id; + const data = query.data; + + try { + // Основные действия профиля + if (data === 'create_profile') { + await this.handleCreateProfile(chatId, telegramId); + } else if (data.startsWith('gender_')) { + const gender = data.replace('gender_', ''); + await this.handleGenderSelection(chatId, telegramId, gender); + } else if (data === 'view_my_profile') { + await this.handleViewMyProfile(chatId, telegramId); + } else if (data === 'edit_profile') { + await this.handleEditProfile(chatId, telegramId); + } else if (data === 'manage_photos') { + await this.handleManagePhotos(chatId, telegramId); + } else if (data === 'preview_profile') { + await this.handlePreviewProfile(chatId, telegramId); + } + + // Редактирование полей профиля + else if (data === 'edit_name') { + await this.handleEditName(chatId, telegramId); + } else if (data === 'edit_age') { + await this.handleEditAge(chatId, telegramId); + } else if (data === 'edit_bio') { + await this.handleEditBio(chatId, telegramId); + } else if (data === 'edit_hobbies') { + await this.handleEditHobbies(chatId, telegramId); + } else if (data === 'edit_city') { + await this.handleEditCity(chatId, telegramId); + } else if (data === 'edit_job') { + await this.handleEditJob(chatId, telegramId); + } else if (data === 'edit_education') { + await this.handleEditEducation(chatId, telegramId); + } else if (data === 'edit_height') { + await this.handleEditHeight(chatId, telegramId); + } else if (data === 'edit_religion') { + await this.handleEditReligion(chatId, telegramId); + } else if (data === 'edit_dating_goal') { + await this.handleEditDatingGoal(chatId, telegramId); + } else if (data === 'edit_lifestyle') { + await this.handleEditLifestyle(chatId, telegramId); + } else if (data === 'edit_search_preferences') { + await this.handleEditSearchPreferences(chatId, telegramId); + } + + // Управление фотографиями + else if (data === 'add_photo') { + await this.handleAddPhoto(chatId, telegramId); + } else if (data === 'delete_photo') { + await this.handleDeletePhoto(chatId, telegramId); + } else if (data === 'set_main_photo') { + await this.handleSetMainPhoto(chatId, telegramId); + } else if (data.startsWith('delete_photo_')) { + const photoIndex = parseInt(data.replace('delete_photo_', '')); + await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex); + } else if (data.startsWith('set_main_photo_')) { + const photoIndex = parseInt(data.replace('set_main_photo_', '')); + await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex); + } + + // Цели знакомства + else if (data.startsWith('set_dating_goal_')) { + const goal = data.replace('set_dating_goal_', ''); + await this.handleSetDatingGoal(chatId, telegramId, goal); + } + + // Образ жизни + else if (data === 'edit_smoking') { + await this.handleEditSmoking(chatId, telegramId); + } else if (data === 'edit_drinking') { + await this.handleEditDrinking(chatId, telegramId); + } else if (data === 'edit_kids') { + await this.handleEditKids(chatId, telegramId); + } else if (data.startsWith('set_smoking_')) { + const value = data.replace('set_smoking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'smoking', value); + } else if (data.startsWith('set_drinking_')) { + const value = data.replace('set_drinking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'drinking', value); + } else if (data.startsWith('set_kids_')) { + const value = data.replace('set_kids_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'kids', value); + } + + // Настройки поиска + else if (data === 'edit_age_range') { + await this.handleEditAgeRange(chatId, telegramId); + } else if (data === 'edit_distance') { + await this.handleEditDistance(chatId, telegramId); + } + + // Просмотр анкет и свайпы + else if (data === 'start_browsing') { + await this.handleStartBrowsing(chatId, telegramId, false); + } else if (data === 'start_browsing_first') { + // Показываем всех пользователей для нового пользователя + await this.handleStartBrowsing(chatId, telegramId, true); + } else if (data === 'vip_search') { + await this.handleVipSearch(chatId, telegramId); + } else if (data.startsWith('search_by_goal_')) { + const goal = data.replace('search_by_goal_', ''); + await this.handleSearchByGoal(chatId, telegramId, goal); + } else if (data === 'next_candidate') { + await this.handleNextCandidate(chatId, telegramId); + } else if (data.startsWith('like_')) { + const targetUserId = data.replace('like_', ''); + await this.handleLike(chatId, telegramId, targetUserId); + } else if (data.startsWith('dislike_')) { + const targetUserId = data.replace('dislike_', ''); + await this.handleDislike(chatId, telegramId, targetUserId); + } else if (data.startsWith('superlike_')) { + const targetUserId = data.replace('superlike_', ''); + await this.handleSuperlike(chatId, telegramId, targetUserId); + } else if (data.startsWith('view_profile_')) { + const targetUserId = data.replace('view_profile_', ''); + await this.handleViewProfile(chatId, telegramId, targetUserId); + } else if (data.startsWith('more_photos_')) { + const targetUserId = data.replace('more_photos_', ''); + await this.handleMorePhotos(chatId, telegramId, targetUserId); + } + + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + + // Матчи и чаты + else if (data === 'view_matches') { + await this.handleViewMatches(chatId, telegramId); + } else if (data === 'open_chats') { + await this.handleOpenChats(chatId, telegramId); + } else if (data === 'native_chats') { + await this.enhancedChatHandlers.showChatsNative(chatId, telegramId); + } else if (data.startsWith('open_native_chat_')) { + const matchId = data.replace('open_native_chat_', ''); + await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId); + } else if (data.startsWith('chat_history_')) { + const matchId = data.replace('chat_history_', ''); + await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId); + } else if (data.startsWith('chat_')) { + const matchId = data.replace('chat_', ''); + await this.handleOpenChat(chatId, telegramId, matchId); + } else if (data.startsWith('send_message_')) { + const matchId = data.replace('send_message_', ''); + await this.handleSendMessage(chatId, telegramId, matchId); + } else if (data.startsWith('view_chat_profile_')) { + const matchId = data.replace('view_chat_profile_', ''); + await this.handleViewChatProfile(chatId, telegramId, matchId); + } else if (data.startsWith('unmatch_')) { + const matchId = data.replace('unmatch_', ''); + await this.handleUnmatch(chatId, telegramId, matchId); + } else if (data.startsWith('confirm_unmatch_')) { + const matchId = data.replace('confirm_unmatch_', ''); + await this.handleConfirmUnmatch(chatId, telegramId, matchId); + } + + // Настройки + else if (data === 'settings') { + await this.handleSettings(chatId, telegramId); + } else if (data === 'search_settings') { + await this.handleSearchSettings(chatId, telegramId); + } else if (data === 'notification_settings') { + await this.handleNotificationSettings(chatId, telegramId); + } else if (data === 'view_stats') { + await this.handleViewStats(chatId, telegramId); + } else if (data === 'view_profile_viewers') { + await this.handleViewProfileViewers(chatId, telegramId); + } else if (data === 'hide_profile') { + await this.handleHideProfile(chatId, telegramId); + } else if (data === 'delete_profile') { + await this.handleDeleteProfile(chatId, telegramId); + } else if (data === 'main_menu') { + await this.handleMainMenu(chatId, telegramId); + } else if (data === 'confirm_delete_profile') { + await this.handleConfirmDeleteProfile(chatId, telegramId); + } + + // Информация + else if (data === 'how_it_works') { + await this.handleHowItWorks(chatId); + } else if (data === 'back_to_browsing') { + await this.handleStartBrowsing(chatId, telegramId); + } else if (data === 'get_vip') { + await this.vipController.showVipSearch(chatId, telegramId); + } + + // VIP функции + else if (data === 'vip_search') { + await this.vipController.showVipSearch(chatId, telegramId); + } else if (data === 'vip_quick_search') { + await this.vipController.performQuickVipSearch(chatId, telegramId); + } else if (data === 'vip_advanced_search') { + await this.vipController.startAdvancedSearch(chatId, telegramId); + } else if (data === 'vip_dating_goal_search') { + await this.vipController.showDatingGoalSearch(chatId, telegramId); + } else if (data.startsWith('vip_goal_')) { + const goal = data.replace('vip_goal_', ''); + await this.vipController.performDatingGoalSearch(chatId, telegramId, goal); + } else if (data.startsWith('vip_like_')) { + const targetTelegramId = data.replace('vip_like_', ''); + await this.handleVipLike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_superlike_')) { + const targetTelegramId = data.replace('vip_superlike_', ''); + await this.handleVipSuperlike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_dislike_')) { + const targetTelegramId = data.replace('vip_dislike_', ''); + await this.handleVipDislike(chatId, telegramId, targetTelegramId); + } + + // Настройки языка и переводы + else if (data === 'language_settings') { + await this.handleLanguageSettings(chatId, telegramId); + } else if (data.startsWith('set_language_')) { + const languageCode = data.replace('set_language_', ''); + await this.handleSetLanguage(chatId, telegramId, languageCode); + } else if (data.startsWith('translate_profile_')) { + const profileUserId = parseInt(data.replace('translate_profile_', '')); + await this.handleTranslateProfile(chatId, telegramId, profileUserId); + } else if (data === 'back_to_settings') { + await this.handleSettings(chatId, telegramId); + } + + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } + else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция в разработке!', + show_alert: false + }); + return; + } + + await this.bot.answerCallbackQuery(query.id); + + } catch (error) { + console.error('Callback handler error:', error); + await this.bot.answerCallbackQuery(query.id, { + text: 'Произошла ошибка. Попробуйте еще раз.', + show_alert: true + }); + } + } + + // Добавим все необходимые методы для обработки коллбэков + async handleCreateProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise { + // Заглушка метода + } + + async handleViewMyProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleManagePhotos(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handlePreviewProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditName(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditAge(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditBio(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHobbies(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditCity(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditJob(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditEducation(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHeight(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditReligion(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDatingGoal(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditLifestyle(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditSearchPreferences(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleAddPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetMainPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleEditSmoking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDrinking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditKids(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise { + // Заглушка метода + } + + async handleEditAgeRange(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDistance(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise { + // Заглушка метода + } + + async handleVipSearch(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleNextCandidate(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewMatches(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewStats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewProfileViewers(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHideProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleMainMenu(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHowItWorks(chatId: number): Promise { + // Заглушка метода + } + + async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleLanguageSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise { + // Заглушка метода + } + + async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise { + // Заглушка метода + } + + // Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers) + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Error handling notification settings:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.'); + } + } +} diff --git a/src/handlers/commandHandlers.ts b/src/handlers/commandHandlers.ts index c86032f..77d2ad3 100644 --- a/src/handlers/commandHandlers.ts +++ b/src/handlers/commandHandlers.ts @@ -3,16 +3,19 @@ import { ProfileService } from '../services/profileService'; import { MatchingService } from '../services/matchingService'; import { Profile } from '../models/Profile'; import { getUserTranslation } from '../services/localizationService'; +import { NotificationHandlers } from './notificationHandlers'; export class CommandHandlers { private bot: TelegramBot; private profileService: ProfileService; private matchingService: MatchingService; + private notificationHandlers: NotificationHandlers; constructor(bot: TelegramBot) { this.bot = bot; this.profileService = new ProfileService(); this.matchingService = new MatchingService(); + this.notificationHandlers = new NotificationHandlers(bot); } register(): void { @@ -23,6 +26,12 @@ export class CommandHandlers { this.bot.onText(/\/matches/, (msg: Message) => this.handleMatches(msg)); this.bot.onText(/\/settings/, (msg: Message) => this.handleSettings(msg)); this.bot.onText(/\/create_profile/, (msg: Message) => this.handleCreateProfile(msg)); + + // Регистрация обработчика настроек уведомлений + this.bot.onText(/\/notifications/, (msg: Message) => this.notificationHandlers.handleNotificationsCommand(msg)); + + // Регистрируем обработчики для уведомлений + this.notificationHandlers.register(); } async handleStart(msg: Message): Promise { @@ -44,7 +53,8 @@ export class CommandHandlers { { text: '⭐ VIP поиск', callback_data: 'vip_search' } ], [ - { text: '⚙️ Настройки', callback_data: 'settings' } + { text: '⚙️ Настройки', callback_data: 'settings' }, + { text: '🔔 Уведомления', callback_data: 'notifications' } ] ] }; @@ -84,6 +94,7 @@ export class CommandHandlers { /browse - Просмотр анкет /matches - Ваши матчи /settings - Настройки +/notifications - Настройки уведомлений /help - Эта справка � Как использовать: @@ -191,7 +202,7 @@ export class CommandHandlers { inline_keyboard: [ [ { text: '🔍 Настройки поиска', callback_data: 'search_settings' }, - { text: '🔔 Уведомления', callback_data: 'notification_settings' } + { text: '🔔 Уведомления', callback_data: 'notifications' } ], [ { text: '🚫 Скрыть профиль', callback_data: 'hide_profile' }, @@ -242,7 +253,10 @@ export class CommandHandlers { { text: '✏️ Редактировать', callback_data: 'edit_profile' }, { text: '📸 Фото', callback_data: 'manage_photos' } ], - [{ text: '🔍 Начать поиск', callback_data: 'start_browsing' }] + [ + { text: '🔍 Начать поиск', callback_data: 'start_browsing' }, + { text: '🔔 Уведомления', callback_data: 'notifications' } + ] ] } : { inline_keyboard: [ diff --git a/src/handlers/enhancedChatHandlers.ts b/src/handlers/enhancedChatHandlers.ts index 59a353c..72726b8 100644 --- a/src/handlers/enhancedChatHandlers.ts +++ b/src/handlers/enhancedChatHandlers.ts @@ -168,25 +168,24 @@ export class EnhancedChatHandlers { // ===== СИСТЕМА УВЕДОМЛЕНИЙ ===== - // Отправить уведомление о новом сообщении + // Отправить уведомление о новом сообщении - теперь используем NotificationService async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise { try { - const receiverChatId = parseInt(receiverTelegramId); + // Получаем идентификаторы пользователей для использования в NotificationService + const receiverUserId = await this.profileService.getUserIdByTelegramId(receiverTelegramId); + const sender = await this.chatService.getMatchInfo(matchId, receiverTelegramId); - await this.bot.sendMessage( - receiverChatId, - `💌 *Новое сообщение от ${senderName}*\n\n` + - `"${this.escapeMarkdown(messagePreview)}"\n\n` + - '👆 Нажмите "Открыть чат" для ответа', - { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [ - [{ text: '💬 Открыть чат', callback_data: `open_native_chat_${matchId}` }], - [{ text: '📱 Все чаты', callback_data: 'native_chats' }] - ] - } - } + if (!receiverUserId || !sender?.otherUserId) { + console.error('Failed to get user IDs for notification'); + return; + } + + // Используем сервис уведомлений для отправки более красивого уведомления + await this.notificationService.sendMessageNotification( + receiverUserId, + sender.otherUserId, + messagePreview, + matchId ); } catch (error) { console.error('Error sending message notification:', error); diff --git a/src/handlers/likeBackHandler.ts b/src/handlers/likeBackHandler.ts new file mode 100644 index 0000000..583d1f5 --- /dev/null +++ b/src/handlers/likeBackHandler.ts @@ -0,0 +1,76 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; + +export class LikeBackHandler { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + + constructor(bot: TelegramBot) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + } + + // Функция для обработки обратного лайка из уведомления + async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise { + try { + // Получаем информацию о пользователях + const [userId, targetProfile] = await Promise.all([ + this.profileService.getUserIdByTelegramId(telegramId), + this.profileService.getProfileByUserId(targetUserId) + ]); + + if (!userId || !targetProfile) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль'); + return; + } + + // Проверяем, есть ли уже свайп + const existingSwipe = await this.matchingService.getSwipeBetweenUsers(userId, targetUserId); + if (existingSwipe) { + await this.bot.sendMessage(chatId, '❓ Вы уже оценили этот профиль ранее.'); + return; + } + + // Создаем свайп (лайк) + const result = await this.matchingService.createSwipe(userId, targetUserId, 'like'); + + if (result.isMatch) { + // Это матч! + await this.bot.sendMessage( + chatId, + '🎉 *Поздравляем! Это взаимно!*\n\n' + + `Вы и *${targetProfile.name}* понравились друг другу!\n` + + 'Теперь вы можете начать общение.', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }], + [{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }] + ] + } + } + ); + } else { + await this.bot.sendMessage( + chatId, + '❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' + + 'Если вы также понравитесь этому пользователю, будет создан матч.', + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }] + ] + } + } + ); + } + } catch (error) { + console.error('Error in handleLikeBack:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка'); + } + } +} diff --git a/src/handlers/notificationHandlers.ts b/src/handlers/notificationHandlers.ts new file mode 100644 index 0000000..3c6866e --- /dev/null +++ b/src/handlers/notificationHandlers.ts @@ -0,0 +1,644 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { v4 as uuidv4 } from 'uuid'; +import { query } from '../database/connection'; +import { NotificationService } from '../services/notificationService'; + +interface NotificationSettings { + newMatches: boolean; + newMessages: boolean; + newLikes: boolean; + reminders: boolean; + dailySummary: boolean; + timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; + doNotDisturb: boolean; + doNotDisturbStart?: string; + doNotDisturbEnd?: string; +} + +export class NotificationHandlers { + private bot: TelegramBot; + private notificationService: NotificationService; + + constructor(bot: TelegramBot) { + this.bot = bot; + this.notificationService = new NotificationService(bot); + } + + // Метод для получения экземпляра сервиса уведомлений + getNotificationService(): NotificationService { + return this.notificationService; + } + + // Обработка команды /notifications + async handleNotificationsCommand(msg: TelegramBot.Message): Promise { + const telegramId = msg.from?.id.toString(); + if (!telegramId) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + const settings = await this.notificationService.getNotificationSettings(userId); + await this.sendNotificationSettings(msg.chat.id, settings as NotificationSettings); + } catch (error) { + console.error('Error handling notifications command:', error); + await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при загрузке настроек уведомлений.'); + } + } + + // Отправка меню настроек уведомлений + async sendNotificationSettings(chatId: number, settings: NotificationSettings): Promise { + const message = ` +🔔 *Настройки уведомлений* + +Выберите, какие уведомления вы хотите получать: + +${settings.newMatches ? '✅' : '❌'} Новые матчи +${settings.newMessages ? '✅' : '❌'} Новые сообщения +${settings.newLikes ? '✅' : '❌'} Новые лайки +${settings.reminders ? '✅' : '❌'} Напоминания +${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки + +⏰ Предпочтительное время: ${this.getTimePreferenceText(settings.timePreference)} + +${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить": ${settings.doNotDisturb ? 'Включен' : 'Выключен'} +${settings.doNotDisturb && settings.doNotDisturbStart && settings.doNotDisturbEnd ? + `с ${settings.doNotDisturbStart} до ${settings.doNotDisturbEnd}` : ''} + +Нажмите на кнопку, чтобы изменить настройку: +`; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: `${settings.newMatches ? '✅' : '❌'} Новые матчи`, callback_data: 'notif_toggle:newMatches' }, + { text: `${settings.newMessages ? '✅' : '❌'} Новые сообщения`, callback_data: 'notif_toggle:newMessages' } + ], + [ + { text: `${settings.newLikes ? '✅' : '❌'} Новые лайки`, callback_data: 'notif_toggle:newLikes' }, + { text: `${settings.reminders ? '✅' : '❌'} Напоминания`, callback_data: 'notif_toggle:reminders' } + ], + [ + { text: `${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки`, callback_data: 'notif_toggle:dailySummary' } + ], + [ + { text: `⏰ Время: ${this.getTimePreferenceText(settings.timePreference)}`, callback_data: 'notif_time' } + ], + [ + { text: `${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить"`, callback_data: 'notif_dnd' } + ], + [ + { text: '↩️ Назад', callback_data: 'settings' } + ] + ] + } + }); + } + + // Обработка переключения настройки уведомления + async handleNotificationToggle(callbackQuery: TelegramBot.CallbackQuery): Promise { + const telegramId = callbackQuery.from?.id.toString(); + if (!telegramId || !callbackQuery.message) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + // notif_toggle:settingName + const settingName = callbackQuery.data?.split(':')[1]; + if (!settingName) return; + + const settings = await this.notificationService.getNotificationSettings(userId); + let updatedSettings: Partial = { ...settings }; + + // Инвертируем значение настройки + if (settingName in updatedSettings) { + switch(settingName) { + case 'newMatches': + updatedSettings.newMatches = !updatedSettings.newMatches; + break; + case 'newMessages': + updatedSettings.newMessages = !updatedSettings.newMessages; + break; + case 'newLikes': + updatedSettings.newLikes = !updatedSettings.newLikes; + break; + case 'reminders': + updatedSettings.reminders = !updatedSettings.reminders; + break; + case 'dailySummary': + updatedSettings.dailySummary = !updatedSettings.dailySummary; + break; + } + } + + // Обновляем настройки + await this.notificationService.updateNotificationSettings(userId, updatedSettings); + + // Отправляем обновленные настройки + await this.bot.answerCallbackQuery(callbackQuery.id, { + text: `✅ Настройка "${this.getSettingName(settingName)}" ${updatedSettings[settingName as keyof NotificationSettings] ? 'включена' : 'отключена'}` + }); + + await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings as NotificationSettings); + } catch (error) { + console.error('Error handling notification toggle:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении настроек.' }); + } + } + + // Обработка выбора времени для уведомлений + async handleTimePreference(callbackQuery: TelegramBot.CallbackQuery): Promise { + if (!callbackQuery.message) return; + + await this.bot.editMessageText('⏰ *Выберите предпочтительное время для уведомлений:*', { + chat_id: callbackQuery.message.chat.id, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '🌅 Утро (9:00)', callback_data: 'notif_time_set:morning' }, + { text: '☀️ День (13:00)', callback_data: 'notif_time_set:afternoon' } + ], + [ + { text: '🌆 Вечер (19:00)', callback_data: 'notif_time_set:evening' }, + { text: '🌙 Ночь (22:00)', callback_data: 'notif_time_set:night' } + ], + [ + { text: '↩️ Назад', callback_data: 'notifications' } + ] + ] + } + }); + + await this.bot.answerCallbackQuery(callbackQuery.id); + } + + // Обработка установки времени для уведомлений + async handleTimePreferenceSet(callbackQuery: TelegramBot.CallbackQuery): Promise { + const telegramId = callbackQuery.from?.id.toString(); + if (!telegramId || !callbackQuery.message) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + // notif_time_set:timePreference + const timePreference = callbackQuery.data?.split(':')[1] as 'morning' | 'afternoon' | 'evening' | 'night'; + if (!timePreference) return; + + const settings = await this.notificationService.getNotificationSettings(userId); + // Копируем существующие настройки и обновляем нужные поля + const existingSettings = settings as NotificationSettings; + const updatedSettings: NotificationSettings = { + ...existingSettings, + timePreference + }; + + // Обновляем настройки + await this.notificationService.updateNotificationSettings(userId, updatedSettings); + + // Отправляем обновленные настройки + await this.bot.answerCallbackQuery(callbackQuery.id, { + text: `✅ Время уведомлений установлено на ${this.getTimePreferenceText(timePreference)}` + }); + + await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); + } catch (error) { + console.error('Error handling time preference set:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении времени уведомлений.' }); + } + } + + // Обработка режима "Не беспокоить" + async handleDndMode(callbackQuery: TelegramBot.CallbackQuery): Promise { + if (!callbackQuery.message) return; + + await this.bot.editMessageText('🔕 *Режим "Не беспокоить"*\n\nВыберите действие:', { + chat_id: callbackQuery.message.chat.id, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '✅ Включить', callback_data: 'notif_dnd_set:on' }, + { text: '❌ Выключить', callback_data: 'notif_dnd_set:off' } + ], + [ + { text: '⏰ Настроить время', callback_data: 'notif_dnd_time' } + ], + [ + { text: '↩️ Назад', callback_data: 'notifications' } + ] + ] + } + }); + + await this.bot.answerCallbackQuery(callbackQuery.id); + } + + // Обработка установки режима "Не беспокоить" + async handleDndModeSet(callbackQuery: TelegramBot.CallbackQuery): Promise { + const telegramId = callbackQuery.from?.id.toString(); + if (!telegramId || !callbackQuery.message) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + // notif_dnd_set:on/off + const mode = callbackQuery.data?.split(':')[1]; + if (!mode) return; + + const settings = await this.notificationService.getNotificationSettings(userId); + // Копируем существующие настройки и обновляем нужное поле + const existingSettings = settings as NotificationSettings; + let updatedSettings: NotificationSettings = { + ...existingSettings, + doNotDisturb: mode === 'on' + }; + + // Если включаем режим "Не беспокоить", но не задано время, ставим дефолтные значения + if (mode === 'on' && (!updatedSettings.doNotDisturbStart || !updatedSettings.doNotDisturbEnd)) { + updatedSettings.doNotDisturbStart = '23:00'; + updatedSettings.doNotDisturbEnd = '08:00'; + } + + // Обновляем настройки + await this.notificationService.updateNotificationSettings(userId, updatedSettings); + + // Отправляем обновленные настройки + await this.bot.answerCallbackQuery(callbackQuery.id, { + text: `✅ Режим "Не беспокоить" ${mode === 'on' ? 'включен' : 'выключен'}` + }); + + await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); + } catch (error) { + console.error('Error handling DND mode set:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении режима "Не беспокоить".' }); + } + } + + // Настройка времени для режима "Не беспокоить" + async handleDndTimeSetup(callbackQuery: TelegramBot.CallbackQuery): Promise { + if (!callbackQuery.message) return; + + await this.bot.editMessageText('⏰ *Настройка времени для режима "Не беспокоить"*\n\nВыберите один из предустановленных вариантов или введите свой:', { + chat_id: callbackQuery.message.chat.id, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '🌙 23:00 - 08:00', callback_data: 'notif_dnd_time_set:23:00:08:00' } + ], + [ + { text: '🌙 22:00 - 07:00', callback_data: 'notif_dnd_time_set:22:00:07:00' } + ], + [ + { text: '🌙 00:00 - 09:00', callback_data: 'notif_dnd_time_set:00:00:09:00' } + ], + [ + { text: '✏️ Ввести свой вариант', callback_data: 'notif_dnd_time_custom' } + ], + [ + { text: '↩️ Назад', callback_data: 'notif_dnd' } + ] + ] + } + }); + + await this.bot.answerCallbackQuery(callbackQuery.id); + } + + // Установка предустановленного времени для режима "Не беспокоить" + async handleDndTimeSet(callbackQuery: TelegramBot.CallbackQuery): Promise { + const telegramId = callbackQuery.from?.id.toString(); + if (!telegramId || !callbackQuery.message) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + // notif_dnd_time_set:startTime:endTime + const parts = callbackQuery.data?.split(':'); + if (parts && parts.length >= 4) { + const startTime = `${parts[2]}:${parts[3]}`; + const endTime = `${parts[4]}:${parts[5]}`; + + const settings = await this.notificationService.getNotificationSettings(userId); + // Копируем существующие настройки и обновляем нужные поля + const existingSettings = settings as NotificationSettings; + const updatedSettings: NotificationSettings = { + ...existingSettings, + doNotDisturb: true, + doNotDisturbStart: startTime, + doNotDisturbEnd: endTime + }; + + // Обновляем настройки + await this.notificationService.updateNotificationSettings(userId, updatedSettings); + + // Отправляем обновленные настройки + await this.bot.answerCallbackQuery(callbackQuery.id, { + text: `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}` + }); + + await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); + } + } catch (error) { + console.error('Error handling DND time set:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при настройке времени "Не беспокоить".' }); + } + } + + // Запрос пользовательского времени для режима "Не беспокоить" + async handleDndTimeCustom(callbackQuery: TelegramBot.CallbackQuery): Promise { + if (!callbackQuery.message) return; + + // Устанавливаем ожидание пользовательского ввода + const userId = callbackQuery.from?.id.toString(); + if (userId) { + await this.setUserState(userId, 'waiting_dnd_time'); + } + + await this.bot.editMessageText('⏰ *Введите время для режима "Не беспокоить"*\n\nУкажите время в формате:\n`с [ЧЧ:ММ] до [ЧЧ:ММ]`\n\nНапример: `с 23:30 до 07:00`', { + chat_id: callbackQuery.message.chat.id, + message_id: callbackQuery.message.message_id, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '↩️ Отмена', callback_data: 'notif_dnd_time' } + ] + ] + } + }); + + await this.bot.answerCallbackQuery(callbackQuery.id); + } + + // Обработка пользовательского ввода времени для режима "Не беспокоить" + async handleDndTimeInput(msg: TelegramBot.Message): Promise { + const telegramId = msg.from?.id.toString(); + if (!telegramId) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Очищаем состояние ожидания + await this.clearUserState(telegramId); + + // Парсим введенное время + const timeRegex = /с\s+(\d{1,2}[:\.]\d{2})\s+до\s+(\d{1,2}[:\.]\d{2})/i; + const match = msg.text?.match(timeRegex); + + if (match && match.length >= 3) { + let startTime = match[1].replace('.', ':'); + let endTime = match[2].replace('.', ':'); + + // Проверяем и форматируем время + if (this.isValidTime(startTime) && this.isValidTime(endTime)) { + startTime = this.formatTime(startTime); + endTime = this.formatTime(endTime); + + const settings = await this.notificationService.getNotificationSettings(userId); + // Копируем существующие настройки и обновляем нужные поля + const existingSettings = settings as NotificationSettings; + const updatedSettings: NotificationSettings = { + ...existingSettings, + doNotDisturb: true, + doNotDisturbStart: startTime, + doNotDisturbEnd: endTime + }; + + // Обновляем настройки + await this.notificationService.updateNotificationSettings(userId, updatedSettings); + + await this.bot.sendMessage(msg.chat.id, `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`); + await this.sendNotificationSettings(msg.chat.id, updatedSettings); + } else { + await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат времени. Пожалуйста, используйте формат ЧЧ:ММ (например, 23:30).'); + } + } else { + await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат ввода. Пожалуйста, введите время в формате "с [ЧЧ:ММ] до [ЧЧ:ММ]" (например, "с 23:30 до 07:00").'); + } + } catch (error) { + console.error('Error handling DND time input:', error); + await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при настройке времени "Не беспокоить".'); + } + } + + // Проверка валидности времени + private isValidTime(time: string): boolean { + const regex = /^(\d{1,2}):(\d{2})$/; + const match = time.match(regex); + + if (match) { + const hours = parseInt(match[1]); + const minutes = parseInt(match[2]); + return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; + } + + return false; + } + + // Форматирование времени в формат ЧЧ:ММ + private formatTime(time: string): string { + const [hours, minutes] = time.split(':').map(Number); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + } + + // Получение текстового представления времени + private getTimePreferenceText(preference: string): string { + switch (preference) { + case 'morning': return 'Утро (9:00)'; + case 'afternoon': return 'День (13:00)'; + case 'evening': return 'Вечер (19:00)'; + case 'night': return 'Ночь (22:00)'; + default: return 'Вечер (19:00)'; + } + } + + // Получение названия настройки + private getSettingName(setting: string): string { + switch (setting) { + case 'newMatches': return 'Новые матчи'; + case 'newMessages': return 'Новые сообщения'; + case 'newLikes': return 'Новые лайки'; + case 'reminders': return 'Напоминания'; + case 'dailySummary': return 'Ежедневные сводки'; + default: return setting; + } + } + + // Получение ID пользователя по Telegram ID + private async getUserIdByTelegramId(telegramId: string): Promise { + try { + const result = await query( + 'SELECT id FROM users WHERE telegram_id = $1', + [parseInt(telegramId)] + ); + return result.rows.length > 0 ? result.rows[0].id : null; + } catch (error) { + console.error('Error getting user by telegram ID:', error); + return null; + } + } + + // Установка состояния ожидания пользователя + private async setUserState(telegramId: string, state: string): Promise { + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) return; + + // Сначала проверяем, существуют ли столбцы state и state_data + const checkColumnResult = await query(` + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'state' + `); + + if (checkColumnResult.rows.length === 0) { + console.log('Adding state and state_data columns to users table...'); + // Добавляем столбцы, если их нет + await query(` + ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL; + ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; + `); + } + + // Теперь устанавливаем состояние + await query( + `UPDATE users + SET state = $1, + state_data = jsonb_set(COALESCE(state_data, '{}'::jsonb), '{timestamp}', to_jsonb(NOW())) + WHERE telegram_id = $2`, + [state, parseInt(telegramId)] + ); + } catch (error) { + console.error('Error setting user state:', error); + } + } + + // Очистка состояния ожидания пользователя + private async clearUserState(telegramId: string): Promise { + try { + await query( + 'UPDATE users SET state = NULL WHERE telegram_id = $1', + [parseInt(telegramId)] + ); + } catch (error) { + console.error('Error clearing user state:', error); + } + } + + // Регистрация обработчиков уведомлений + register(): void { + // Команда настройки уведомлений + this.bot.onText(/\/notifications/, this.handleNotificationsCommand.bind(this)); + + // Обработчик для кнопки настроек уведомлений в меню настроек + this.bot.on('callback_query', async (callbackQuery) => { + if (callbackQuery.data === 'notifications') { + const telegramId = callbackQuery.from?.id.toString(); + if (!telegramId || !callbackQuery.message) return; + + try { + const userId = await this.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationService.getNotificationSettings(userId); + await this.sendNotificationSettings(callbackQuery.message.chat.id, settings as NotificationSettings); + await this.bot.answerCallbackQuery(callbackQuery.id); + } catch (error) { + console.error('Error handling notifications callback:', error); + await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при загрузке настроек уведомлений.' }); + } + } + else if (callbackQuery.data?.startsWith('notif_toggle:')) { + await this.handleNotificationToggle(callbackQuery); + } + else if (callbackQuery.data === 'notif_time') { + await this.handleTimePreference(callbackQuery); + } + else if (callbackQuery.data?.startsWith('notif_time_set:')) { + await this.handleTimePreferenceSet(callbackQuery); + } + else if (callbackQuery.data === 'notif_dnd') { + await this.handleDndMode(callbackQuery); + } + else if (callbackQuery.data?.startsWith('notif_dnd_set:')) { + await this.handleDndModeSet(callbackQuery); + } + else if (callbackQuery.data === 'notif_dnd_time') { + await this.handleDndTimeSetup(callbackQuery); + } + else if (callbackQuery.data?.startsWith('notif_dnd_time_set:')) { + await this.handleDndTimeSet(callbackQuery); + } + else if (callbackQuery.data === 'notif_dnd_time_custom') { + await this.handleDndTimeCustom(callbackQuery); + } + }); + + // Обработчик пользовательского ввода для времени "Не беспокоить" + this.bot.on('message', async (msg) => { + if (!msg.text) return; + + const telegramId = msg.from?.id.toString(); + if (!telegramId) return; + + try { + // Сначала проверяем, существует ли столбец state + const checkColumnResult = await query(` + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'state' + `); + + if (checkColumnResult.rows.length === 0) { + console.log('State column does not exist in users table. Skipping state check.'); + return; + } + + // Теперь проверяем состояние пользователя + const result = await query( + 'SELECT state FROM users WHERE telegram_id = $1', + [parseInt(telegramId)] + ); + + if (result.rows.length > 0 && result.rows[0].state === 'waiting_dnd_time') { + await this.handleDndTimeInput(msg); + } + } catch (error) { + console.error('Error checking user state:', error); + } + }); + } +} diff --git a/src/premium/README.md b/src/premium/README.md new file mode 100644 index 0000000..aa34eef --- /dev/null +++ b/src/premium/README.md @@ -0,0 +1,62 @@ +# Модуль премиум-функций Telegram Tinder Bot + +Этот каталог содержит модули и скрипты для управления премиум-функциями бота. + +## Содержимое + +- `add-premium-columns.js` - Добавление колонок для премиум-функций в базу данных (версия JavaScript) +- `add-premium-columns.ts` - Добавление колонок для премиум-функций в базу данных (версия TypeScript) +- `add-premium-columns-direct.js` - Прямое добавление премиум-колонок без миграций +- `addPremiumColumn.js` - Добавление отдельной колонки премиум в таблицу пользователей +- `setPremiumStatus.js` - Обновление статуса премиум для пользователей + +## Премиум-функции + +В боте реализованы следующие премиум-функции: + +1. **Неограниченные лайки** - снятие дневного лимита на количество лайков +2. **Супер-лайки** - возможность отправлять супер-лайки (повышенный приоритет) +3. **Просмотр лайков** - возможность видеть, кто поставил лайк вашему профилю +4. **Скрытый режим** - возможность скрывать свою активность +5. **Расширенные фильтры** - дополнительные параметры для поиска + +## Использование + +### Добавление премиум-колонок в базу данных + +```bash +node src/premium/add-premium-columns.js +``` + +### Изменение премиум-статуса пользователя + +```typescript +import { PremiumService } from '../services/premiumService'; + +// Установка премиум-статуса для пользователя +const premiumService = new PremiumService(); +await premiumService.setPremiumStatus(userId, true, 30); // 30 дней премиума +``` + +## Интеграция в основной код + +Проверка премиум-статуса должна выполняться следующим образом: + +```typescript +// В классах контроллеров +const isPremium = await this.premiumService.checkUserPremium(userId); + +if (isPremium) { + // Предоставить премиум-функцию +} else { + // Сообщить о необходимости премиум-подписки +} +``` + +## Период действия премиум-статуса + +По умолчанию премиум-статус устанавливается на 30 дней. Для изменения срока используйте третий параметр в методе `setPremiumStatus`. + +## Дополнительная информация + +Более подробная информация о премиум-функциях содержится в документации проекта в каталоге `docs/VIP_FUNCTIONS.md`. diff --git a/scripts/add-premium-columns-direct.js b/src/premium/add-premium-columns-direct.js similarity index 100% rename from scripts/add-premium-columns-direct.js rename to src/premium/add-premium-columns-direct.js diff --git a/scripts/add-premium-columns.js b/src/premium/add-premium-columns.js similarity index 100% rename from scripts/add-premium-columns.js rename to src/premium/add-premium-columns.js diff --git a/scripts/add-premium-columns.ts b/src/premium/add-premium-columns.ts similarity index 100% rename from scripts/add-premium-columns.ts rename to src/premium/add-premium-columns.ts diff --git a/src/premium/addPremiumColumn.js b/src/premium/addPremiumColumn.js new file mode 100644 index 0000000..1d3a247 --- /dev/null +++ b/src/premium/addPremiumColumn.js @@ -0,0 +1,58 @@ +// Скрипт для добавления колонки premium в таблицу users и установки premium для всех пользователей +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function setAllUsersToPremium() { + try { + console.log('Проверяем наличие столбца premium в таблице users...'); + + const result = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!result.rows[0].exists) { + console.log('🔄 Добавляем столбец premium...'); + await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('✅ Столбец premium успешно добавлен'); + } else { + console.log('✅ Столбец premium уже существует'); + } + + console.log('Устанавливаем премиум-статус для всех пользователей...'); + + const updateResult = await pool.query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, premium + `); + + console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`); + updateResult.rows.forEach(row => { + console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`); + }); + + console.log('🎉 Все пользователи теперь имеют премиум-статус!'); + } catch (error) { + console.error('Ошибка при установке премиум-статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +setAllUsersToPremium(); diff --git a/src/premium/setPremiumStatus.js b/src/premium/setPremiumStatus.js new file mode 100644 index 0000000..420ef4e --- /dev/null +++ b/src/premium/setPremiumStatus.js @@ -0,0 +1,73 @@ +// Скрипт для установки премиум-статуса всем пользователям +require('dotenv').config(); +const { Pool } = require('pg'); + +// Проверяем и выводим параметры подключения +console.log('Параметры подключения к БД:'); +console.log('DB_USERNAME:', process.env.DB_USERNAME); +console.log('DB_HOST:', process.env.DB_HOST); +console.log('DB_NAME:', process.env.DB_NAME); +console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '[указан]' : '[не указан]'); +console.log('DB_PORT:', process.env.DB_PORT); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function setAllUsersToPremium() { + try { + console.log('Устанавливаем премиум-статус для всех пользователей...'); + + // Проверка соединения с БД + console.log('Проверка соединения с БД...'); + const testResult = await pool.query('SELECT NOW()'); + console.log('✅ Соединение успешно:', testResult.rows[0].now); + + // Проверка наличия столбца premium + console.log('Проверяем наличие столбца premium в таблице users...'); + + const checkResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!checkResult.rows[0].exists) { + console.log('🔄 Добавляем столбец premium...'); + await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('✅ Столбец premium успешно добавлен'); + } else { + console.log('✅ Столбец premium уже существует'); + } + + // Устанавливаем premium=true для всех пользователей + const updateResult = await pool.query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, premium + `); + + console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`); + updateResult.rows.forEach(row => { + console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`); + }); + + console.log('🎉 Все пользователи теперь имеют премиум-статус!'); + } catch (error) { + console.error('❌ Ошибка при установке премиум-статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +setAllUsersToPremium(); diff --git a/src/scripts/enhanceNotifications.ts b/src/scripts/enhanceNotifications.ts new file mode 100644 index 0000000..440d737 --- /dev/null +++ b/src/scripts/enhanceNotifications.ts @@ -0,0 +1,133 @@ +import { query } from '../database/connection'; +import { v4 as uuidv4 } from 'uuid'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения +dotenv.config(); + +/** + * Скрипт для обновления механизма уведомлений + */ +export async function enhanceNotifications() { + try { + console.log('Enhancing notifications system...'); + console.log('DB Connection Details:'); + console.log(`- Host: ${process.env.DB_HOST}`); + console.log(`- Port: ${process.env.DB_PORT}`); + console.log(`- Database: ${process.env.DB_NAME}`); + + // 1. Создаем расширение для генерации UUID, если его нет + await query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp" + `); + + // 2. Создаем таблицу для хранения типов уведомлений и шаблонов сообщений, если её нет + await query(` + CREATE TABLE IF NOT EXISTS notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + // 2. Вставляем базовые шаблоны для различных типов уведомлений + const templates = [ + { + id: uuidv4(), + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + }) + }, + { + id: uuidv4(), + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + }) + }, + { + id: uuidv4(), + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'native_chats' } + ] + ] + }) + }, + { + id: uuidv4(), + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'native_chats' } + ] + ] + }) + } + ]; + + // Вставляем шаблоны с проверкой на конфликты + for (const template of templates) { + await query(` + INSERT INTO notification_templates + (id, type, title, message_template, button_template, created_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (type) DO UPDATE + SET title = $3, + message_template = $4, + button_template = $5 + `, [template.id, template.type, template.title, template.message_template, template.button_template]); + } + + console.log('✅ Notification templates updated successfully'); + + return true; + } catch (error) { + console.error('❌ Error enhancing notifications:', error); + return false; + } +} + +// Если запускаем файл напрямую +if (require.main === module) { + enhanceNotifications().then(() => { + console.log('Notification system enhancement completed'); + process.exit(0); + }).catch(error => { + console.error('Error:', error); + process.exit(1); + }); +} diff --git a/src/scripts/setPremiumForAll.ts b/src/scripts/setPremiumForAll.ts index 208a2da..e502286 100644 --- a/src/scripts/setPremiumForAll.ts +++ b/src/scripts/setPremiumForAll.ts @@ -1,9 +1,33 @@ -import { query } from '../database/connection'; +import { query, testConnection } from '../database/connection'; async function setAllUsersToPremium() { try { console.log('Setting premium status for all users...'); + // Проверка соединения с базой данных + console.log('Testing database connection...'); + const dbConnected = await testConnection(); + if (!dbConnected) { + throw new Error('Failed to connect to database. Please check your database settings.'); + } + console.log('Database connection successful!'); + + // Проверка наличия столбца premium + const checkResult = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!checkResult.rows[0].exists) { + console.log('Adding premium column to users table...'); + await query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('Premium column added successfully'); + } + const result = await query(` UPDATE users SET premium = true diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index 9dde927..ff570d7 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -17,24 +17,6 @@ export class MatchingService { } // Выполнить свайп - // Конвертация типов свайпов между API и БД - private convertSwipeTypeToDirection(swipeType: SwipeType): string { - switch (swipeType) { - case 'like': return 'right'; - case 'pass': return 'left'; - case 'superlike': return 'super'; - default: return 'left'; - } - } - - private convertDirectionToSwipeType(direction: string): SwipeType { - switch (direction) { - case 'right': return 'like'; - case 'left': return 'pass'; - case 'super': return 'superlike'; - default: return 'pass'; - } - } async performSwipe(telegramId: string, targetTelegramId: string, swipeType: SwipeType): Promise<{ swipe: Swipe; @@ -63,22 +45,21 @@ export class MatchingService { } const swipeId = uuidv4(); - const direction = this.convertSwipeTypeToDirection(swipeType); let isMatch = false; let match: Match | undefined; await transaction(async (client) => { // Создаем свайп await client.query(` - INSERT INTO swipes (id, user_id, target_user_id, direction, created_at) + INSERT INTO swipes (id, user_id, target_user_id, type, created_at) VALUES ($1, $2, $3, $4, $5) - `, [swipeId, userId, targetUserId, direction, new Date()]); + `, [swipeId, userId, targetUserId, swipeType, new Date()]); // Если это лайк или суперлайк, проверяем взаимность if (swipeType === 'like' || swipeType === 'superlike') { const reciprocalSwipe = await client.query(` SELECT * FROM swipes - WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super') + WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike') `, [targetUserId, userId]); if (reciprocalSwipe.rows.length > 0) { @@ -91,7 +72,7 @@ export class MatchingService { if (existingMatch.rows.length === 0) { isMatch = true; const matchId = uuidv4(); - const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super'; + const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike'; // Упорядочиваем пользователей для консистентности const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId]; @@ -152,6 +133,89 @@ export class MatchingService { return this.mapEntityToSwipe(result.rows[0]); } + + // Получить свайп между двумя пользователями (псевдоним для getSwipe) + async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise { + return this.getSwipe(userId, targetUserId); + } + + // Создать свайп (лайк, дислайк или суперлайк) + async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{ + swipe: Swipe; + isMatch: boolean; + match?: Match; + }> { + const swipeId = uuidv4(); + let isMatch = false; + let match: Match | undefined; + + await transaction(async (client) => { + // Создаем свайп + await client.query(` + INSERT INTO swipes (id, user_id, target_user_id, type, created_at) + VALUES ($1, $2, $3, $4, $5) + `, [swipeId, userId, targetUserId, swipeType, new Date()]); + + // Если это лайк или суперлайк, проверяем взаимность + if (swipeType === 'like' || swipeType === 'superlike') { + const reciprocalSwipe = await client.query(` + SELECT * FROM swipes + WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike') + `, [targetUserId, userId]); + + if (reciprocalSwipe.rows.length > 0) { + // Проверяем, что матч еще не существует + const existingMatch = await client.query(` + SELECT * FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + `, [userId, targetUserId]); + + if (existingMatch.rows.length === 0) { + isMatch = true; + const matchId = uuidv4(); + const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike'; + + // Упорядочиваем пользователей для консистентности + const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId]; + + // Создаем матч + await client.query(` + INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match) + VALUES ($1, $2, $3, $4, $5, $6) + `, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]); + + match = new Match({ + id: matchId, + userId1: user1Id, + userId2: user2Id, + createdAt: new Date(), + isActive: true, + isSuperMatch: isSuperMatch, + unreadCount1: 0, + unreadCount2: 0 + }); + + // Обновляем свайпы, отмечая что они образуют матч + await client.query(` + UPDATE swipes SET is_match = true + WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1) + `, [userId, targetUserId]); + } + } + } + }); + + const swipe = new Swipe({ + id: swipeId, + userId, + targetUserId, + type: swipeType, + timestamp: new Date(), + isMatch + }); + + return { swipe, isMatch, match }; + } // Получить все матчи пользователя по telegram ID async getUserMatches(telegramId: string, limit: number = 50): Promise { @@ -217,7 +281,7 @@ export class MatchingService { async getRecentLikes(userId: string, limit: number = 20): Promise { const result = await query(` SELECT * FROM swipes - WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false + WHERE target_user_id = $1 AND type IN ('like', 'superlike') AND is_match = false ORDER BY created_at DESC LIMIT $2 `, [userId, limit]); @@ -236,10 +300,10 @@ export class MatchingService { today.setHours(0, 0, 0, 0); const result = await query(` - SELECT direction, COUNT(*) as count + SELECT type, COUNT(*) as count FROM swipes - WHERE swiper_id = $1 AND created_at >= $2 - GROUP BY direction + WHERE user_id = $1 AND created_at >= $2 + GROUP BY type `, [userId, today]); const stats = { @@ -253,11 +317,11 @@ export class MatchingService { const count = parseInt(row.count); stats.total += count; - switch (row.direction) { + switch (row.type) { case 'like': stats.likes = count; break; - case 'super': + case 'superlike': stats.superlikes = count; break; case 'pass': @@ -299,9 +363,9 @@ export class MatchingService { private mapEntityToSwipe(entity: any): Swipe { return new Swipe({ id: entity.id, - userId: entity.swiper_id, - targetUserId: entity.swiped_id, - type: this.convertDirectionToSwipeType(entity.direction), + userId: entity.user_id || entity.swiper_id, + targetUserId: entity.target_user_id || entity.swiped_id, + type: entity.type || 'pass', timestamp: entity.created_at, isMatch: entity.is_match }); @@ -329,8 +393,8 @@ export class MatchingService { FROM swipes s1 JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id WHERE s1.user_id = $1 - AND s1.direction IN ('right', 'super') - AND s2.direction IN ('right', 'super') + AND s1.type IN ('like', 'superlike') + AND s2.type IN ('like', 'superlike') AND NOT EXISTS ( SELECT 1 FROM matches m WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id) @@ -343,73 +407,156 @@ export class MatchingService { // Получить следующего кандидата для просмотра async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise { + console.log(`[DEBUG] getNextCandidate вызван для telegramId=${telegramId}, isNewUser=${isNewUser}`); + // Сначала получаем профиль пользователя по telegramId const userProfile = await this.profileService.getProfileByTelegramId(telegramId); if (!userProfile) { + console.log(`[ERROR] Профиль пользователя с telegramId=${telegramId} не найден`); throw new BotError('User profile not found', 'PROFILE_NOT_FOUND'); } + console.log(`[DEBUG] Найден профиль пользователя:`, JSON.stringify({ + userId: userProfile.userId, + gender: userProfile.gender, + interestedIn: userProfile.interestedIn, + minAge: userProfile.searchPreferences?.minAge, + maxAge: userProfile.searchPreferences?.maxAge + })); // Получаем UUID пользователя const userId = userProfile.userId; - - // Получаем список уже просмотренных пользователей - const viewedUsers = await query(` - SELECT DISTINCT target_user_id - FROM swipes - WHERE user_id = $1 - `, [userId]); - - const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id); - viewedUserIds.push(userId); // Исключаем самого себя - - // Если это новый пользователь или у пользователя мало просмотренных профилей, - // показываем всех пользователей по очереди (исключая только себя) - let excludeCondition = ''; - if (!isNewUser) { - excludeCondition = viewedUserIds.length > 0 - ? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})` - : ''; + // Определяем, каким должен быть пол показываемых профилей + let targetGender: string; + if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') { + targetGender = userProfile.interestedIn; } else { - // Для новых пользователей исключаем только себя - excludeCondition = `AND p.user_id != $2`; + // Если "both" или другое значение, показываем противоположный пол + targetGender = userProfile.gender === 'male' ? 'female' : 'male'; + } + + console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`); + + // Получаем список просмотренных профилей из новой таблицы profile_views + // и добавляем также профили из свайпов для полной совместимости + console.log(`[DEBUG] Запрашиваем просмотренные и свайпнутые профили для userId=${userId}`); + const [viewedProfilesResult, swipedProfilesResult] = await Promise.all([ + query(` + SELECT DISTINCT viewed_profile_id + FROM profile_views + WHERE viewer_id = $1 + `, [userId]), + query(` + SELECT DISTINCT target_user_id + FROM swipes + WHERE user_id = $1 + `, [userId]) + ]); + console.log(`[DEBUG] Найдено ${viewedProfilesResult.rows.length} просмотренных и ${swipedProfilesResult.rows.length} свайпнутых профилей`); + + // Объединяем просмотренные и свайпнутые профили в один список + const viewedUserIds = [ + ...viewedProfilesResult.rows.map((row: any) => row.viewed_profile_id), + ...swipedProfilesResult.rows.map((row: any) => row.target_user_id) + ]; + + // Всегда добавляем самого пользователя в список исключений + viewedUserIds.push(userId); + + // Удаляем дубликаты + const uniqueViewedIds = [...new Set(viewedUserIds)]; + console.log(`[DEBUG] Всего ${uniqueViewedIds.length} уникальных исключаемых профилей`); + + // Формируем параметры запроса + let params: any[] = []; + let excludeCondition: string = ''; + + // Для новых пользователей исключаем только себя + if (isNewUser || uniqueViewedIds.length <= 1) { + params = [userId]; + excludeCondition = 'AND p.user_id != $1'; + console.log(`[DEBUG] Режим нового пользователя: исключаем только самого себя`); + } else { + // Для остальных исключаем все просмотренные профили + params = [...uniqueViewedIds]; + const placeholders = uniqueViewedIds.map((_: any, i: number) => `$${i + 1}`).join(', '); + excludeCondition = `AND p.user_id NOT IN (${placeholders})`; + console.log(`[DEBUG] Стандартный режим: исключаем ${uniqueViewedIds.length} профилей`); } - // Ищем подходящих кандидатов + // Выполним предварительный запрос для проверки наличия доступных анкет + const countQuery = ` + SELECT COUNT(*) as count + FROM profiles p + JOIN users u ON p.user_id = u.id + WHERE p.is_visible = true + AND p.gender = '${targetGender}' + ${excludeCondition} + `; + + console.log(`[DEBUG] Проверка наличия подходящих анкет...`); + console.log(`[DEBUG] SQL запрос count: ${countQuery}`); + console.log(`[DEBUG] Параметры count: ${JSON.stringify(params)}`); + const countResult = await query(countQuery, params); + const availableProfilesCount = parseInt(countResult.rows[0]?.count || '0'); + console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`); + + // Используем определенный ранее targetGender для поиска + console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`); + const candidateQuery = ` SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name FROM profiles p JOIN users u ON p.user_id = u.id WHERE p.is_visible = true - AND p.gender = $1 + AND p.gender = '${targetGender}' AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge} ${excludeCondition} ORDER BY RANDOM() LIMIT 1 `; + console.log(`[DEBUG] SQL запрос: ${candidateQuery}`); + console.log(`[DEBUG] Параметры: ${JSON.stringify(params)}`); - const params = [userProfile.interestedIn, ...viewedUserIds]; const result = await query(candidateQuery, params); + console.log(`[DEBUG] Результаты запроса: найдено ${result.rows.length} профилей`); if (result.rows.length === 0) { + console.log(`[DEBUG] Подходящие кандидаты не найдены`); return null; } const candidateData = result.rows[0]; + console.log(`[DEBUG] Найден подходящий кандидат: ${candidateData.name}, возраст: ${candidateData.age}`); + + // Записываем просмотр профиля в новую таблицу profile_views + try { + const viewerTelegramId = telegramId; + const viewedTelegramId = candidateData.telegram_id.toString(); + + console.log(`[DEBUG] Записываем просмотр профиля: viewer=${viewerTelegramId}, viewed=${viewedTelegramId}`); + // Асинхронно записываем просмотр, но не ждем завершения + this.profileService.recordProfileView(viewerTelegramId, viewedTelegramId, 'browse') + .catch(err => console.error(`[ERROR] Ошибка записи просмотра профиля:`, err)); + } catch (err) { + console.error(`[ERROR] Ошибка записи просмотра профиля:`, err); + } // Используем ProfileService для правильного маппинга данных - return this.profileService.mapEntityToProfile(candidateData); + const profile = this.profileService.mapEntityToProfile(candidateData); + console.log(`[DEBUG] Профиль преобразован и возвращается клиенту`); + return profile; } // VIP функция: поиск кандидатов по цели знакомства async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise { const swipedUsersResult = await query(` - SELECT swiped_id + SELECT target_user_id FROM swipes - WHERE swiper_id = $1 + WHERE user_id = $1 `, [userProfile.userId]); - const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id); + const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.target_user_id); swipedUserIds.push(userProfile.userId); // Исключаем себя let candidateQuery = ` @@ -417,7 +564,7 @@ export class MatchingService { FROM profiles p JOIN users u ON p.user_id = u.id WHERE p.is_visible = true - AND p.is_active = true + AND u.is_active = true AND p.gender = $1 AND p.dating_goal = $2 AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')}) diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 707426f..833c86c 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -1,15 +1,27 @@ import TelegramBot from 'node-telegram-bot-api'; -import { query } from '../database/connection'; +import { query, transaction } from '../database/connection'; import { ProfileService } from './profileService'; -import config from '../../config/default.json'; +import { v4 as uuidv4 } from 'uuid'; export interface NotificationData { userId: string; - type: 'new_match' | 'new_message' | 'new_like' | 'super_like'; + type: 'new_match' | 'new_message' | 'new_like' | 'super_like' | 'match_reminder' | 'inactive_matches' | 'like_summary'; data: Record; scheduledAt?: Date; } +export interface NotificationSettings { + newMatches: boolean; + newMessages: boolean; + newLikes: boolean; + reminders: boolean; + dailySummary: boolean; + timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; + doNotDisturb: boolean; + doNotDisturbStart?: string; // Формат: "HH:MM" + doNotDisturbEnd?: string; // Формат: "HH:MM" +} + export class NotificationService { private bot?: TelegramBot; private profileService: ProfileService; @@ -17,6 +29,403 @@ export class NotificationService { constructor(bot?: TelegramBot) { this.bot = bot; this.profileService = new ProfileService(); + + // Создаем таблицу уведомлений, если её еще нет + this.ensureNotificationTablesExist().catch(err => + console.error('Failed to create notification tables:', err) + ); + } + + // Проверка и создание таблиц для уведомлений + private async ensureNotificationTablesExist(): Promise { + try { + // Проверяем существование таблицы notifications + const notificationsExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + await query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notifications table'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + await query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created scheduled_notifications table'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + await query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notification_templates table'); + + // Заполняем таблицу базовыми шаблонами + await this.populateDefaultTemplates(); + } + + // Проверяем, есть ли колонка notification_settings в таблице users + const settingsColumnExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + await query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + console.log('Added notification_settings column to users table'); + } + + } catch (error) { + console.error('Error ensuring notification tables exist:', error); + throw error; + } + } + + // Заполнение таблицы шаблонов уведомлений + private async populateDefaultTemplates(): Promise { + try { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + + console.log('Populated notification templates'); + } catch (error) { + console.error('Error populating default templates:', error); + } + } + + // Получить шаблон уведомления из базы данных или использовать встроенный + private async getNotificationTemplate(type: string): Promise<{ + title: string; + messageTemplate: string; + buttonTemplate: any; + }> { + try { + // Попытка получить шаблон из базы данных + const result = await query(` + SELECT title, message_template, button_template + FROM notification_templates + WHERE type = $1 + `, [type]); + + if (result.rows.length > 0) { + return { + title: result.rows[0].title, + messageTemplate: result.rows[0].message_template, + buttonTemplate: result.rows[0].button_template + }; + } + } catch (error: any) { + console.log('Using default template as database is not available:', error.message); + } + + // Если не удалось получить из базы или произошла ошибка, используем встроенные шаблоны + const defaultTemplates: Record = { + 'new_like': { + title: 'Новый лайк!', + messageTemplate: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'super_like': { + title: 'Супер-лайк!', + messageTemplate: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'new_match': { + title: 'Новый матч!', + messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + 'new_message': { + title: 'Новое сообщение!', + messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + 'match_reminder': { + title: 'Напоминание о матче', + messageTemplate: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + } + }; + + return defaultTemplates[type] || { + title: 'Уведомление', + messageTemplate: 'Новое уведомление', + buttonTemplate: { inline_keyboard: [] } + }; + } + + // Применить данные к шаблону + private applyTemplateData(template: string, data: Record): string { + return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { + return data[key] !== undefined ? data[key] : ''; + }); + } + + // Применить данные к шаблону кнопок + private applyTemplateDataToButtons(buttonTemplate: any, data: Record): any { + const result = JSON.parse(JSON.stringify(buttonTemplate)); // глубокая копия + + // Рекурсивная функция для замены в любой вложенности + const replaceInObject = (obj: any): any => { + if (typeof obj === 'string') { + return this.applyTemplateData(obj, data); + } else if (Array.isArray(obj)) { + return obj.map(item => replaceInObject(item)); + } else if (obj !== null && typeof obj === 'object') { + const newObj: Record = {}; + for (const key in obj) { + newObj[key] = replaceInObject(obj[key]); + } + return newObj; + } + return obj; + }; + + return replaceInObject(result); + } + + // Проверка режима "Не беспокоить" + private async shouldSendNotification(userId: string): Promise { + try { + const settings = await this.getNotificationSettings(userId); + + if (!settings.doNotDisturb) { + return true; // Режим "Не беспокоить" выключен + } + + // Если нет указанного времени, по умолчанию: не беспокоить с 23:00 до 8:00 + const startTime = settings.doNotDisturbStart || '23:00'; + const endTime = settings.doNotDisturbEnd || '08:00'; + + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const currentMinutes = currentHour * 60 + currentMinute; + const startMinutes = startHour * 60 + startMinute; + const endMinutes = endHour * 60 + endMinute; + + // Если время окончания меньше времени начала, значит период охватывает полночь + if (endMinutes < startMinutes) { + // Не отправлять, если текущее время между начальным и конечным временем (например, 23:30-07:00) + return !(currentMinutes >= startMinutes || currentMinutes <= endMinutes); + } else { + // Не отправлять, если текущее время между начальным и конечным временем (например, 13:00-15:00) + return !(currentMinutes >= startMinutes && currentMinutes <= endMinutes); + } + } catch (error) { + console.error('Error checking notification settings:', error); + return true; // В случае ошибки отправляем уведомление + } } // Отправить уведомление о новом лайке @@ -27,28 +436,91 @@ export class NotificationService { this.profileService.getProfileByTelegramId(likerTelegramId) ]); - if (!targetUser || !likerProfile || !this.bot) { + if (!targetUser || !likerProfile) { + console.log(`Couldn't send like notification: user or profile not found`); return; } - - const message = isSuperLike - ? `⭐ ${likerProfile.name} отправил вам суперлайк!` - : `💖 ${likerProfile.name} поставил вам лайк!`; - - await this.bot.sendMessage(targetUser.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '👀 Посмотреть профиль', callback_data: `view_profile:${likerProfile.userId}` }, - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' } - ]] - } - }); + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(targetUser.id); + if (!settings.newLikes) { + console.log(`Like notifications disabled for user ${targetUser.id}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(targetUser.id))) { + console.log(`Do not disturb mode active for user ${targetUser.id}`); + + // Логируем уведомление и запланируем отправку позже + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Получаем шаблон уведомления + const templateType = isSuperLike ? 'super_like' : 'new_like'; + const template = await this.getNotificationTemplate(templateType); + + // Подготовка данных для шаблона + const templateData = { + name: likerProfile.name, + age: likerProfile.age.toString(), + city: likerProfile.city || '', + userId: likerProfile.userId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(targetUser.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent ${templateType} notification to user ${targetUser.id}`); + } // Логируем уведомление await this.logNotification({ userId: targetUser.id, - type: isSuperLike ? 'super_like' : 'new_like', - data: { likerUserId: likerProfile.userId, likerName: likerProfile.name } + type: templateType, + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } }); } catch (error) { console.error('Error sending like notification:', error); @@ -63,135 +535,535 @@ export class NotificationService { this.profileService.getProfileByUserId(matchedUserId) ]); - if (!user || !matchedProfile || !this.bot) { + if (!user || !matchedProfile) { + console.log(`Couldn't send match notification: user or profile not found`); return; } - const message = `🎉 У вас новый матч с ${matchedProfile.name}!\n\nТеперь вы можете начать общение.`; + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.newMatches) { + console.log(`Match notifications disabled for user ${userId}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + console.log(`Do not disturb mode active for user ${userId}`); + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + + // Запланируем отправку напоминания о матче позже + await this.scheduleMatchReminder(userId, matchedUserId); + return; + } - await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💬 Написать сообщение', callback_data: `start_chat:${matchedUserId}` }, - { text: '👀 Посмотреть профиль', callback_data: `view_profile:${matchedUserId}` } - ]] - } - }); + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_match'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + age: matchedProfile.age.toString(), + city: matchedProfile.city || '', + userId: matchedProfile.userId, + matchId: matchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_match notification to user ${userId}`); + } // Логируем уведомление await this.logNotification({ userId, type: 'new_match', - data: { matchedUserId, matchedName: matchedProfile.name } + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city, + matchId + } }); + + // Если это новый матч, запланируем напоминание через 24 часа, если пользователь не начнет общение + if (matchId) { + const reminderDate = new Date(); + reminderDate.setHours(reminderDate.getHours() + 24); + + await this.scheduleNotification({ + userId, + type: 'match_reminder', + data: { + matchedUserId, + matchId + }, + scheduledAt: reminderDate + }); + } } catch (error) { console.error('Error sending match notification:', error); } } // Отправить уведомление о новом сообщении - async sendMessageNotification(receiverId: string, senderId: string, messageContent: string): Promise { + async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise { try { const [receiver, senderProfile] = await Promise.all([ this.getUserByUserId(receiverId), this.profileService.getProfileByUserId(senderId) ]); - if (!receiver || !senderProfile || !this.bot) { + if (!receiver || !senderProfile) { + console.log(`Couldn't send message notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(receiverId); + if (!settings.newMessages) { + console.log(`Message notifications disabled for user ${receiverId}`); return; } // Проверяем, не в чате ли пользователь сейчас const isUserActive = await this.isUserActiveInChat(receiverId, senderId); if (isUserActive) { + console.log(`User ${receiverId} is active in chat with ${senderId}, skipping notification`); return; // Не отправляем уведомление, если пользователь активен в чате } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(receiverId))) { + console.log(`Do not disturb mode active for user ${receiverId}`); + return; + } + // Если matchId не передан, пытаемся его получить + let actualMatchId = matchId; + if (!actualMatchId) { + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [receiverId, senderId]); + + actualMatchId = matchResult.rows[0]?.id; + } + const truncatedMessage = messageContent.length > 50 ? messageContent.substring(0, 50) + '...' : messageContent; - - const message = `💬 Новое сообщение от ${senderProfile.name}:\n\n${truncatedMessage}`; - - await this.bot.sendMessage(receiver.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💬 Ответить', callback_data: `open_chat:${senderId}` } - ]] - } - }); + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_message'); + + // Подготовка данных для шаблона + const templateData = { + name: senderProfile.name, + message: truncatedMessage, + userId: senderProfile.userId, + matchId: actualMatchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(receiver.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_message notification to user ${receiverId}`); + } // Логируем уведомление await this.logNotification({ userId: receiverId, type: 'new_message', - data: { senderId, senderName: senderProfile.name, messageContent: truncatedMessage } + data: { + senderId, + senderName: senderProfile.name, + messageContent: truncatedMessage, + matchId: actualMatchId + } }); } catch (error) { console.error('Error sending message notification:', error); } } - // Отправить напоминание о неактивности - async sendInactivityReminder(userId: string): Promise { + // Отправить напоминание о матче без сообщений + async sendMatchReminder(userId: string, matchedUserId: string, matchId: string): Promise { try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('match_reminder'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + userId: matchedProfile.userId, + matchId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent match reminder to user ${userId} about match with ${matchedUserId}`); + } catch (error) { + console.error('Error sending match reminder:', error); + } + } + + // Отправить сводку по неактивным матчам + async sendInactiveMatchesSummary(userId: string, count: number): Promise { + try { + const user = await this.getUserByUserId(userId); + if (!user || !this.bot || count === 0) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('inactive_matches'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent inactive matches summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending inactive matches summary:', error); + } + } + + // Отправить сводку полученных лайков + async sendLikesSummary(userId: string): Promise { + try { + // Получаем количество непрочитанных лайков + const result = await query(` + SELECT COUNT(*) as count + FROM notifications + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + const count = parseInt(result.rows[0].count); + if (count === 0) { + return; // Нет новых лайков для отображения + } + const user = await this.getUserByUserId(userId); if (!user || !this.bot) { return; } - const message = `👋 Давно не виделись!\n\nВозможно, ваш идеальный матч уже ждет. Давайте найдем кого-то особенного?`; - - await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' }, - { text: '⚙️ Настройки', callback_data: 'settings' } - ]] - } - }); - } catch (error) { - console.error('Error sending inactivity reminder:', error); - } - } - - // Отправить уведомление о новых лайках (сводка) - async sendLikesSummary(userId: string, likesCount: number): Promise { - try { - const user = await this.getUserByUserId(userId); - if (!user || !this.bot || likesCount === 0) { + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.dailySummary) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { return; } - const message = likesCount === 1 - ? `💖 У вас 1 новый лайк! Посмотрите, кто это может быть.` - : `💖 У вас ${likesCount} новых лайков! Посмотрите, кто проявил к вам интерес.`; - + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('like_summary'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '👀 Посмотреть лайки', callback_data: 'view_likes' }, - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' } - ]] - } + parse_mode: 'Markdown', + reply_markup: keyboard }); + + // Отмечаем уведомления как прочитанные + await query(` + UPDATE notifications + SET is_read = true + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + console.log(`Sent likes summary to user ${userId}, count: ${count}`); } catch (error) { console.error('Error sending likes summary:', error); } } + // Запланировать отправку сводки лайков + async scheduleLikesSummary(userId: string): Promise { + try { + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Проверяем, есть ли уже запланированная сводка лайков + const existingResult = await query(` + SELECT id FROM scheduled_notifications + WHERE user_id = $1 AND type = 'like_summary' AND processed = false + AND scheduled_at > NOW() + `, [userId]); + + if (existingResult.rows.length > 0) { + console.log(`Like summary already scheduled for user ${userId}`); + return; // Уже есть запланированная сводка + } + + // Запланировать отправку сводки + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, scheduled_at) + VALUES ($1, $2, $3, $4) + `, [uuidv4(), userId, 'like_summary', scheduledAt]); + + console.log(`Scheduled likes summary for user ${userId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling likes summary:', error); + } + } + + // Запланировать напоминание о матче + async scheduleMatchReminder(userId: string, matchedUserId: string): Promise { + try { + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + if (!matchId) { + console.log(`No active match found between users ${userId} and ${matchedUserId}`); + return; + } + + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Запланировать напоминание + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + userId, + 'match_reminder', + JSON.stringify({ matchId, matchedUserId }), + scheduledAt + ]); + + console.log(`Scheduled match reminder for user ${userId} about match with ${matchedUserId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling match reminder:', error); + } + } + + // Получить предпочтительное время для отправки уведомлений + private getPreferredScheduleTime(preference: string): Date { + const now = new Date(); + const scheduledAt = new Date(); + + // Если текущее время после полуночи, планируем на сегодня + // Иначе планируем на следующий день + if (now.getHours() < 12) { + // Сегодня + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(19, 0, 0, 0); // По умолчанию вечер + } + } else { + // Завтра + scheduledAt.setDate(scheduledAt.getDate() + 1); + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(9, 0, 0, 0); // По умолчанию утро + } + } + + // Если запланированное время уже прошло, добавляем еще один день + if (scheduledAt <= now) { + scheduledAt.setDate(scheduledAt.getDate() + 1); + } + + return scheduledAt; + } + + // Запланировать уведомление + async scheduleNotification(notificationData: NotificationData): Promise { + try { + if (!notificationData.scheduledAt) { + notificationData.scheduledAt = new Date(); + } + + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + notificationData.scheduledAt + ]); + + console.log(`Scheduled ${notificationData.type} notification for user ${notificationData.userId} at ${notificationData.scheduledAt}`); + } catch (error) { + console.error('Error scheduling notification:', error); + } + } + // Логирование уведомлений private async logNotification(notificationData: NotificationData): Promise { try { await query(` - INSERT INTO notifications (user_id, type, data, created_at) - VALUES ($1, $2, $3, $4) + INSERT INTO notifications (id, user_id, type, data, created_at) + VALUES ($1, $2, $3, $4, $5) `, [ + uuidv4(), notificationData.userId, notificationData.type, JSON.stringify(notificationData.data), new Date() ]); + + console.log(`Logged ${notificationData.type} notification for user ${notificationData.userId}`); } catch (error) { console.error('Error logging notification:', error); } @@ -248,53 +1120,16 @@ export class NotificationService { const now = new Date(); const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60); - // Считаем активным если последнее сообщение было менее 24 часов назад - return hoursSinceLastMessage < 24; + // Считаем активным если последнее сообщение было менее 10 минут назад + return hoursSinceLastMessage < (10 / 60); } catch (error) { console.error('Error checking user activity:', error); return false; } } - // Отправить пуш-уведомление (для будущего использования) - async sendPushNotification(userId: string, title: string, body: string, data?: any): Promise { - try { - // Логируем уведомление - console.log(`📱 Push notification prepared for user ${userId}:`); - console.log(`📋 Title: ${title}`); - console.log(`💬 Body: ${body}`); - if (data) { - console.log(`📊 Data:`, JSON.stringify(data, null, 2)); - } - - // В будущем здесь будет интеграция с Firebase Cloud Messaging - // или другим сервисом пуш-уведомлений: - /* - const message = { - notification: { - title, - body - }, - data: data ? JSON.stringify(data) : undefined, - token: await this.getUserPushToken(userId) - }; - - await admin.messaging().send(message); - console.log(`✅ Push notification sent to user ${userId}`); - */ - - } catch (error) { - console.error(`❌ Error preparing push notification for user ${userId}:`, error); - } - } - // Получить настройки уведомлений пользователя - async getNotificationSettings(userId: string): Promise<{ - newMatches: boolean; - newMessages: boolean; - newLikes: boolean; - reminders: boolean; - }> { + async getNotificationSettings(userId: string): Promise { try { const result = await query( 'SELECT notification_settings FROM users WHERE id = $1', @@ -306,15 +1141,26 @@ export class NotificationService { newMatches: true, newMessages: true, newLikes: true, - reminders: true + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false }; } - return result.rows[0].notification_settings || { - newMatches: true, - newMessages: true, - newLikes: true, - reminders: true + const settings = result.rows[0].notification_settings || {}; + + // Возвращаем настройки с дефолтными значениями для отсутствующих свойств + return { + newMatches: settings.newMatches !== undefined ? settings.newMatches : true, + newMessages: settings.newMessages !== undefined ? settings.newMessages : true, + newLikes: settings.newLikes !== undefined ? settings.newLikes : true, + reminders: settings.reminders !== undefined ? settings.reminders : true, + dailySummary: settings.dailySummary !== undefined ? settings.dailySummary : true, + timePreference: settings.timePreference || 'evening', + doNotDisturb: settings.doNotDisturb || false, + doNotDisturbStart: settings.doNotDisturbStart, + doNotDisturbEnd: settings.doNotDisturbEnd }; } catch (error) { console.error('Error getting notification settings:', error); @@ -322,23 +1168,32 @@ export class NotificationService { newMatches: true, newMessages: true, newLikes: true, - reminders: true + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false }; } } // Обновить настройки уведомлений - async updateNotificationSettings(userId: string, settings: { - newMatches?: boolean; - newMessages?: boolean; - newLikes?: boolean; - reminders?: boolean; - }): Promise { + async updateNotificationSettings(userId: string, settings: Partial): Promise { try { + // Получаем текущие настройки + const currentSettings = await this.getNotificationSettings(userId); + + // Обновляем только переданные поля + const updatedSettings = { + ...currentSettings, + ...settings + }; + await query( 'UPDATE users SET notification_settings = $1 WHERE id = $2', - [JSON.stringify(settings), userId] + [JSON.stringify(updatedSettings), userId] ); + + console.log(`Updated notification settings for user ${userId}`); } catch (error) { console.error('Error updating notification settings:', error); } @@ -347,53 +1202,62 @@ export class NotificationService { // Планировщик уведомлений (вызывается периодически) async processScheduledNotifications(): Promise { try { - // Проверим, существует ли таблица scheduled_notifications - const tableCheck = await query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'scheduled_notifications' - ) as exists - `); - - if (!tableCheck.rows[0].exists) { - // Если таблицы нет, создаем её - await query(` - CREATE TABLE IF NOT EXISTS scheduled_notifications ( - id UUID PRIMARY KEY, - user_id UUID REFERENCES users(id), - type VARCHAR(50) NOT NULL, - data JSONB, - scheduled_at TIMESTAMP NOT NULL, - is_processed BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT NOW() - ) - `); - } - // Получаем запланированные уведомления const result = await query(` SELECT * FROM scheduled_notifications - WHERE scheduled_at <= $1 AND is_processed = false + WHERE scheduled_at <= $1 AND processed = false ORDER BY scheduled_at ASC LIMIT 100 `, [new Date()]); + console.log(`Processing ${result.rows.length} scheduled notifications`); + for (const notification of result.rows) { try { + const data = notification.data || {}; + switch (notification.type) { - case 'inactivity_reminder': - await this.sendInactivityReminder(notification.user_id); + case 'match_reminder': + if (data.matchId && data.matchedUserId) { + // Проверяем, что матч всё еще активен и нет сообщений + const matchCheck = await query(` + SELECT m.id + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE m.id = $1 AND m.is_active = true + AND msg.id IS NULL + `, [data.matchId]); + + if (matchCheck.rows.length > 0) { + await this.sendMatchReminder(notification.user_id, data.matchedUserId, data.matchId); + } + } break; - case 'likes_summary': - const likesCount = notification.data?.likesCount || 0; - await this.sendLikesSummary(notification.user_id, likesCount); + case 'like_summary': + await this.sendLikesSummary(notification.user_id); + break; + case 'inactive_matches': + // Получаем количество неактивных матчей + const matchesResult = await query(` + SELECT COUNT(*) as count + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1) + AND m.is_active = true + AND msg.id IS NULL + AND m.created_at < NOW() - INTERVAL '3 days' + `, [notification.user_id]); + + const count = parseInt(matchesResult.rows[0].count); + if (count > 0) { + await this.sendInactiveMatchesSummary(notification.user_id, count); + } break; - // Добавить другие типы уведомлений } // Отмечаем как обработанное await query( - 'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1', + 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', [notification.id] ); } catch (error) { @@ -404,4 +1268,49 @@ export class NotificationService { console.error('Error processing scheduled notifications:', error); } } -} \ No newline at end of file + + // Планирование периодических уведомлений для всех пользователей + async schedulePeriodicNotifications(): Promise { + try { + // Получаем список активных пользователей + const usersResult = await query(` + SELECT u.id, u.notification_settings + FROM users u + JOIN profiles p ON u.id = p.user_id + WHERE p.is_visible = true + AND u.created_at < NOW() - INTERVAL '1 day' + `); + + for (const user of usersResult.rows) { + try { + const settings = user.notification_settings || {}; + + // Проверяем настройку ежедневных сводок + if (settings.dailySummary !== false) { + // Планируем сводку лайков + await this.scheduleLikesSummary(user.id); + + // Планируем проверку неактивных матчей раз в неделю + if (settings.reminders !== false) { + const dayOfWeek = new Date().getDay(); + if (dayOfWeek === 1) { // Понедельник + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference || 'evening'); + + await this.scheduleNotification({ + userId: user.id, + type: 'inactive_matches', + data: {}, + scheduledAt + }); + } + } + } + } catch (error) { + console.error(`Error scheduling periodic notifications for user ${user.id}:`, error); + } + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + } +} diff --git a/src/services/notificationService.ts.new b/src/services/notificationService.ts.new new file mode 100644 index 0000000..833c86c --- /dev/null +++ b/src/services/notificationService.ts.new @@ -0,0 +1,1316 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { query, transaction } from '../database/connection'; +import { ProfileService } from './profileService'; +import { v4 as uuidv4 } from 'uuid'; + +export interface NotificationData { + userId: string; + type: 'new_match' | 'new_message' | 'new_like' | 'super_like' | 'match_reminder' | 'inactive_matches' | 'like_summary'; + data: Record; + scheduledAt?: Date; +} + +export interface NotificationSettings { + newMatches: boolean; + newMessages: boolean; + newLikes: boolean; + reminders: boolean; + dailySummary: boolean; + timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; + doNotDisturb: boolean; + doNotDisturbStart?: string; // Формат: "HH:MM" + doNotDisturbEnd?: string; // Формат: "HH:MM" +} + +export class NotificationService { + private bot?: TelegramBot; + private profileService: ProfileService; + + constructor(bot?: TelegramBot) { + this.bot = bot; + this.profileService = new ProfileService(); + + // Создаем таблицу уведомлений, если её еще нет + this.ensureNotificationTablesExist().catch(err => + console.error('Failed to create notification tables:', err) + ); + } + + // Проверка и создание таблиц для уведомлений + private async ensureNotificationTablesExist(): Promise { + try { + // Проверяем существование таблицы notifications + const notificationsExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + await query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notifications table'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + await query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created scheduled_notifications table'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + await query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notification_templates table'); + + // Заполняем таблицу базовыми шаблонами + await this.populateDefaultTemplates(); + } + + // Проверяем, есть ли колонка notification_settings в таблице users + const settingsColumnExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + await query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + console.log('Added notification_settings column to users table'); + } + + } catch (error) { + console.error('Error ensuring notification tables exist:', error); + throw error; + } + } + + // Заполнение таблицы шаблонов уведомлений + private async populateDefaultTemplates(): Promise { + try { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + + console.log('Populated notification templates'); + } catch (error) { + console.error('Error populating default templates:', error); + } + } + + // Получить шаблон уведомления из базы данных или использовать встроенный + private async getNotificationTemplate(type: string): Promise<{ + title: string; + messageTemplate: string; + buttonTemplate: any; + }> { + try { + // Попытка получить шаблон из базы данных + const result = await query(` + SELECT title, message_template, button_template + FROM notification_templates + WHERE type = $1 + `, [type]); + + if (result.rows.length > 0) { + return { + title: result.rows[0].title, + messageTemplate: result.rows[0].message_template, + buttonTemplate: result.rows[0].button_template + }; + } + } catch (error: any) { + console.log('Using default template as database is not available:', error.message); + } + + // Если не удалось получить из базы или произошла ошибка, используем встроенные шаблоны + const defaultTemplates: Record = { + 'new_like': { + title: 'Новый лайк!', + messageTemplate: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'super_like': { + title: 'Супер-лайк!', + messageTemplate: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'new_match': { + title: 'Новый матч!', + messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + 'new_message': { + title: 'Новое сообщение!', + messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + 'match_reminder': { + title: 'Напоминание о матче', + messageTemplate: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + } + }; + + return defaultTemplates[type] || { + title: 'Уведомление', + messageTemplate: 'Новое уведомление', + buttonTemplate: { inline_keyboard: [] } + }; + } + + // Применить данные к шаблону + private applyTemplateData(template: string, data: Record): string { + return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { + return data[key] !== undefined ? data[key] : ''; + }); + } + + // Применить данные к шаблону кнопок + private applyTemplateDataToButtons(buttonTemplate: any, data: Record): any { + const result = JSON.parse(JSON.stringify(buttonTemplate)); // глубокая копия + + // Рекурсивная функция для замены в любой вложенности + const replaceInObject = (obj: any): any => { + if (typeof obj === 'string') { + return this.applyTemplateData(obj, data); + } else if (Array.isArray(obj)) { + return obj.map(item => replaceInObject(item)); + } else if (obj !== null && typeof obj === 'object') { + const newObj: Record = {}; + for (const key in obj) { + newObj[key] = replaceInObject(obj[key]); + } + return newObj; + } + return obj; + }; + + return replaceInObject(result); + } + + // Проверка режима "Не беспокоить" + private async shouldSendNotification(userId: string): Promise { + try { + const settings = await this.getNotificationSettings(userId); + + if (!settings.doNotDisturb) { + return true; // Режим "Не беспокоить" выключен + } + + // Если нет указанного времени, по умолчанию: не беспокоить с 23:00 до 8:00 + const startTime = settings.doNotDisturbStart || '23:00'; + const endTime = settings.doNotDisturbEnd || '08:00'; + + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const currentMinutes = currentHour * 60 + currentMinute; + const startMinutes = startHour * 60 + startMinute; + const endMinutes = endHour * 60 + endMinute; + + // Если время окончания меньше времени начала, значит период охватывает полночь + if (endMinutes < startMinutes) { + // Не отправлять, если текущее время между начальным и конечным временем (например, 23:30-07:00) + return !(currentMinutes >= startMinutes || currentMinutes <= endMinutes); + } else { + // Не отправлять, если текущее время между начальным и конечным временем (например, 13:00-15:00) + return !(currentMinutes >= startMinutes && currentMinutes <= endMinutes); + } + } catch (error) { + console.error('Error checking notification settings:', error); + return true; // В случае ошибки отправляем уведомление + } + } + + // Отправить уведомление о новом лайке + async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise { + try { + const [targetUser, likerProfile] = await Promise.all([ + this.getUserByTelegramId(targetTelegramId), + this.profileService.getProfileByTelegramId(likerTelegramId) + ]); + + if (!targetUser || !likerProfile) { + console.log(`Couldn't send like notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(targetUser.id); + if (!settings.newLikes) { + console.log(`Like notifications disabled for user ${targetUser.id}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(targetUser.id))) { + console.log(`Do not disturb mode active for user ${targetUser.id}`); + + // Логируем уведомление и запланируем отправку позже + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Получаем шаблон уведомления + const templateType = isSuperLike ? 'super_like' : 'new_like'; + const template = await this.getNotificationTemplate(templateType); + + // Подготовка данных для шаблона + const templateData = { + name: likerProfile.name, + age: likerProfile.age.toString(), + city: likerProfile.city || '', + userId: likerProfile.userId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(targetUser.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent ${templateType} notification to user ${targetUser.id}`); + } + + // Логируем уведомление + await this.logNotification({ + userId: targetUser.id, + type: templateType, + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + } catch (error) { + console.error('Error sending like notification:', error); + } + } + + // Отправить уведомление о новом матче + async sendMatchNotification(userId: string, matchedUserId: string): Promise { + try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile) { + console.log(`Couldn't send match notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.newMatches) { + console.log(`Match notifications disabled for user ${userId}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + console.log(`Do not disturb mode active for user ${userId}`); + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + + // Запланируем отправку напоминания о матче позже + await this.scheduleMatchReminder(userId, matchedUserId); + return; + } + + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_match'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + age: matchedProfile.age.toString(), + city: matchedProfile.city || '', + userId: matchedProfile.userId, + matchId: matchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_match notification to user ${userId}`); + } + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city, + matchId + } + }); + + // Если это новый матч, запланируем напоминание через 24 часа, если пользователь не начнет общение + if (matchId) { + const reminderDate = new Date(); + reminderDate.setHours(reminderDate.getHours() + 24); + + await this.scheduleNotification({ + userId, + type: 'match_reminder', + data: { + matchedUserId, + matchId + }, + scheduledAt: reminderDate + }); + } + } catch (error) { + console.error('Error sending match notification:', error); + } + } + + // Отправить уведомление о новом сообщении + async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise { + try { + const [receiver, senderProfile] = await Promise.all([ + this.getUserByUserId(receiverId), + this.profileService.getProfileByUserId(senderId) + ]); + + if (!receiver || !senderProfile) { + console.log(`Couldn't send message notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(receiverId); + if (!settings.newMessages) { + console.log(`Message notifications disabled for user ${receiverId}`); + return; + } + + // Проверяем, не в чате ли пользователь сейчас + const isUserActive = await this.isUserActiveInChat(receiverId, senderId); + if (isUserActive) { + console.log(`User ${receiverId} is active in chat with ${senderId}, skipping notification`); + return; // Не отправляем уведомление, если пользователь активен в чате + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(receiverId))) { + console.log(`Do not disturb mode active for user ${receiverId}`); + return; + } + + // Если matchId не передан, пытаемся его получить + let actualMatchId = matchId; + if (!actualMatchId) { + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [receiverId, senderId]); + + actualMatchId = matchResult.rows[0]?.id; + } + + const truncatedMessage = messageContent.length > 50 + ? messageContent.substring(0, 50) + '...' + : messageContent; + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_message'); + + // Подготовка данных для шаблона + const templateData = { + name: senderProfile.name, + message: truncatedMessage, + userId: senderProfile.userId, + matchId: actualMatchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(receiver.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_message notification to user ${receiverId}`); + } + + // Логируем уведомление + await this.logNotification({ + userId: receiverId, + type: 'new_message', + data: { + senderId, + senderName: senderProfile.name, + messageContent: truncatedMessage, + matchId: actualMatchId + } + }); + } catch (error) { + console.error('Error sending message notification:', error); + } + } + + // Отправить напоминание о матче без сообщений + async sendMatchReminder(userId: string, matchedUserId: string, matchId: string): Promise { + try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('match_reminder'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + userId: matchedProfile.userId, + matchId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent match reminder to user ${userId} about match with ${matchedUserId}`); + } catch (error) { + console.error('Error sending match reminder:', error); + } + } + + // Отправить сводку по неактивным матчам + async sendInactiveMatchesSummary(userId: string, count: number): Promise { + try { + const user = await this.getUserByUserId(userId); + if (!user || !this.bot || count === 0) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('inactive_matches'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent inactive matches summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending inactive matches summary:', error); + } + } + + // Отправить сводку полученных лайков + async sendLikesSummary(userId: string): Promise { + try { + // Получаем количество непрочитанных лайков + const result = await query(` + SELECT COUNT(*) as count + FROM notifications + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + const count = parseInt(result.rows[0].count); + if (count === 0) { + return; // Нет новых лайков для отображения + } + + const user = await this.getUserByUserId(userId); + if (!user || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.dailySummary) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('like_summary'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + // Отмечаем уведомления как прочитанные + await query(` + UPDATE notifications + SET is_read = true + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + console.log(`Sent likes summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending likes summary:', error); + } + } + + // Запланировать отправку сводки лайков + async scheduleLikesSummary(userId: string): Promise { + try { + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Проверяем, есть ли уже запланированная сводка лайков + const existingResult = await query(` + SELECT id FROM scheduled_notifications + WHERE user_id = $1 AND type = 'like_summary' AND processed = false + AND scheduled_at > NOW() + `, [userId]); + + if (existingResult.rows.length > 0) { + console.log(`Like summary already scheduled for user ${userId}`); + return; // Уже есть запланированная сводка + } + + // Запланировать отправку сводки + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, scheduled_at) + VALUES ($1, $2, $3, $4) + `, [uuidv4(), userId, 'like_summary', scheduledAt]); + + console.log(`Scheduled likes summary for user ${userId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling likes summary:', error); + } + } + + // Запланировать напоминание о матче + async scheduleMatchReminder(userId: string, matchedUserId: string): Promise { + try { + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + if (!matchId) { + console.log(`No active match found between users ${userId} and ${matchedUserId}`); + return; + } + + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Запланировать напоминание + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + userId, + 'match_reminder', + JSON.stringify({ matchId, matchedUserId }), + scheduledAt + ]); + + console.log(`Scheduled match reminder for user ${userId} about match with ${matchedUserId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling match reminder:', error); + } + } + + // Получить предпочтительное время для отправки уведомлений + private getPreferredScheduleTime(preference: string): Date { + const now = new Date(); + const scheduledAt = new Date(); + + // Если текущее время после полуночи, планируем на сегодня + // Иначе планируем на следующий день + if (now.getHours() < 12) { + // Сегодня + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(19, 0, 0, 0); // По умолчанию вечер + } + } else { + // Завтра + scheduledAt.setDate(scheduledAt.getDate() + 1); + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(9, 0, 0, 0); // По умолчанию утро + } + } + + // Если запланированное время уже прошло, добавляем еще один день + if (scheduledAt <= now) { + scheduledAt.setDate(scheduledAt.getDate() + 1); + } + + return scheduledAt; + } + + // Запланировать уведомление + async scheduleNotification(notificationData: NotificationData): Promise { + try { + if (!notificationData.scheduledAt) { + notificationData.scheduledAt = new Date(); + } + + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + notificationData.scheduledAt + ]); + + console.log(`Scheduled ${notificationData.type} notification for user ${notificationData.userId} at ${notificationData.scheduledAt}`); + } catch (error) { + console.error('Error scheduling notification:', error); + } + } + + // Логирование уведомлений + private async logNotification(notificationData: NotificationData): Promise { + try { + await query(` + INSERT INTO notifications (id, user_id, type, data, created_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + new Date() + ]); + + console.log(`Logged ${notificationData.type} notification for user ${notificationData.userId}`); + } catch (error) { + console.error('Error logging notification:', error); + } + } + + // Получить пользователя по ID + private async getUserByUserId(userId: string): Promise { + try { + const result = await query( + 'SELECT * FROM users WHERE id = $1', + [userId] + ); + return result.rows[0] || null; + } catch (error) { + console.error('Error getting user:', error); + return null; + } + } + + // Получить пользователя по Telegram ID + private async getUserByTelegramId(telegramId: string): Promise { + try { + const result = await query( + 'SELECT * FROM users WHERE telegram_id = $1', + [parseInt(telegramId)] + ); + return result.rows[0] || null; + } catch (error) { + console.error('Error getting user by telegram ID:', error); + return null; + } + } + + // Проверить, активен ли пользователь в чате + private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise { + try { + // Проверяем последнее сообщение пользователя в чате + const result = await query(` + SELECT m.created_at + FROM messages m + JOIN matches mt ON m.match_id = mt.id + WHERE (mt.user_id_1 = $1 OR mt.user_id_2 = $1) + AND (mt.user_id_1 = $2 OR mt.user_id_2 = $2) + AND m.sender_id = $1 + ORDER BY m.created_at DESC + LIMIT 1 + `, [userId, chatWithUserId]); + + if (result.rows.length === 0) { + return false; // Нет сообщений - не активен + } + + const lastMessageTime = new Date(result.rows[0].created_at); + const now = new Date(); + const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60); + + // Считаем активным если последнее сообщение было менее 10 минут назад + return hoursSinceLastMessage < (10 / 60); + } catch (error) { + console.error('Error checking user activity:', error); + return false; + } + } + + // Получить настройки уведомлений пользователя + async getNotificationSettings(userId: string): Promise { + try { + const result = await query( + 'SELECT notification_settings FROM users WHERE id = $1', + [userId] + ); + + if (result.rows.length === 0) { + return { + newMatches: true, + newMessages: true, + newLikes: true, + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false + }; + } + + const settings = result.rows[0].notification_settings || {}; + + // Возвращаем настройки с дефолтными значениями для отсутствующих свойств + return { + newMatches: settings.newMatches !== undefined ? settings.newMatches : true, + newMessages: settings.newMessages !== undefined ? settings.newMessages : true, + newLikes: settings.newLikes !== undefined ? settings.newLikes : true, + reminders: settings.reminders !== undefined ? settings.reminders : true, + dailySummary: settings.dailySummary !== undefined ? settings.dailySummary : true, + timePreference: settings.timePreference || 'evening', + doNotDisturb: settings.doNotDisturb || false, + doNotDisturbStart: settings.doNotDisturbStart, + doNotDisturbEnd: settings.doNotDisturbEnd + }; + } catch (error) { + console.error('Error getting notification settings:', error); + return { + newMatches: true, + newMessages: true, + newLikes: true, + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false + }; + } + } + + // Обновить настройки уведомлений + async updateNotificationSettings(userId: string, settings: Partial): Promise { + try { + // Получаем текущие настройки + const currentSettings = await this.getNotificationSettings(userId); + + // Обновляем только переданные поля + const updatedSettings = { + ...currentSettings, + ...settings + }; + + await query( + 'UPDATE users SET notification_settings = $1 WHERE id = $2', + [JSON.stringify(updatedSettings), userId] + ); + + console.log(`Updated notification settings for user ${userId}`); + } catch (error) { + console.error('Error updating notification settings:', error); + } + } + + // Планировщик уведомлений (вызывается периодически) + async processScheduledNotifications(): Promise { + try { + // Получаем запланированные уведомления + const result = await query(` + SELECT * FROM scheduled_notifications + WHERE scheduled_at <= $1 AND processed = false + ORDER BY scheduled_at ASC + LIMIT 100 + `, [new Date()]); + + console.log(`Processing ${result.rows.length} scheduled notifications`); + + for (const notification of result.rows) { + try { + const data = notification.data || {}; + + switch (notification.type) { + case 'match_reminder': + if (data.matchId && data.matchedUserId) { + // Проверяем, что матч всё еще активен и нет сообщений + const matchCheck = await query(` + SELECT m.id + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE m.id = $1 AND m.is_active = true + AND msg.id IS NULL + `, [data.matchId]); + + if (matchCheck.rows.length > 0) { + await this.sendMatchReminder(notification.user_id, data.matchedUserId, data.matchId); + } + } + break; + case 'like_summary': + await this.sendLikesSummary(notification.user_id); + break; + case 'inactive_matches': + // Получаем количество неактивных матчей + const matchesResult = await query(` + SELECT COUNT(*) as count + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1) + AND m.is_active = true + AND msg.id IS NULL + AND m.created_at < NOW() - INTERVAL '3 days' + `, [notification.user_id]); + + const count = parseInt(matchesResult.rows[0].count); + if (count > 0) { + await this.sendInactiveMatchesSummary(notification.user_id, count); + } + break; + } + + // Отмечаем как обработанное + await query( + 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', + [notification.id] + ); + } catch (error) { + console.error(`Error processing notification ${notification.id}:`, error); + } + } + } catch (error) { + console.error('Error processing scheduled notifications:', error); + } + } + + // Планирование периодических уведомлений для всех пользователей + async schedulePeriodicNotifications(): Promise { + try { + // Получаем список активных пользователей + const usersResult = await query(` + SELECT u.id, u.notification_settings + FROM users u + JOIN profiles p ON u.id = p.user_id + WHERE p.is_visible = true + AND u.created_at < NOW() - INTERVAL '1 day' + `); + + for (const user of usersResult.rows) { + try { + const settings = user.notification_settings || {}; + + // Проверяем настройку ежедневных сводок + if (settings.dailySummary !== false) { + // Планируем сводку лайков + await this.scheduleLikesSummary(user.id); + + // Планируем проверку неактивных матчей раз в неделю + if (settings.reminders !== false) { + const dayOfWeek = new Date().getDay(); + if (dayOfWeek === 1) { // Понедельник + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference || 'evening'); + + await this.scheduleNotification({ + userId: user.id, + type: 'inactive_matches', + data: {}, + scheduledAt + }); + } + } + } + } catch (error) { + console.error(`Error scheduling periodic notifications for user ${user.id}:`, error); + } + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + } +} diff --git a/src/services/profileService.ts b/src/services/profileService.ts index 8b1a367..fe35dd6 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -169,8 +169,8 @@ export class ProfileService { case 'photos': case 'interests': updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`); - // Для PostgreSQL массивы передаем как есть, не как JSON строки - updateValues.push(value); + // Для PostgreSQL массивы должны быть преобразованы в JSON-строку + updateValues.push(JSON.stringify(value)); break; case 'location': // Пропускаем обработку местоположения, так как колонки location нет @@ -496,7 +496,7 @@ export class ProfileService { // Удаляем связанные данные await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]); await client.query('DELETE FROM matches WHERE user_id_1 = $1 OR user_id_2 = $1', [userId]); - await client.query('DELETE FROM swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]); + await client.query('DELETE FROM swipes WHERE user_id = $1 OR target_user_id = $1', [userId]); await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]); }); return true; @@ -526,16 +526,38 @@ export class ProfileService { // Записать просмотр профиля async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise { try { + // Преобразуем строковые ID в числа для запросов + const viewerTelegramId = typeof viewerId === 'string' ? parseInt(viewerId) : viewerId; + const viewedTelegramId = typeof viewedProfileId === 'string' ? parseInt(viewedProfileId) : viewedProfileId; + + // Получаем внутренние ID пользователей + const viewerIdResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewerTelegramId]); + if (viewerIdResult.rows.length === 0) { + throw new Error(`User with telegram_id ${viewerId} not found`); + } + + const viewedUserResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewedTelegramId]); + if (viewedUserResult.rows.length === 0) { + throw new Error(`User with telegram_id ${viewedProfileId} not found`); + } + + const viewerUuid = viewerIdResult.rows[0].id; + const viewedUuid = viewedUserResult.rows[0].id; + + // Не записываем просмотры своего профиля + if (viewerUuid === viewedUuid) { + console.log('Skipping self-view record'); + return; + } + await query(` - INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type) - VALUES ( - (SELECT id FROM users WHERE telegram_id = $1), - (SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)), - $3 - ) + INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type, view_date) + VALUES ($1, $2, $3, NOW()) ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE - SET viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type - `, [viewerId, viewedProfileId, viewType]); + SET view_date = NOW(), view_type = $3 + `, [viewerUuid, viewedUuid, viewType]); + + console.log(`Recorded profile view: ${viewerId} viewed ${viewedProfileId}`); } catch (error) { console.error('Error recording profile view:', error); } @@ -547,8 +569,7 @@ export class ProfileService { const result = await query(` SELECT COUNT(*) as count FROM profile_views pv - JOIN profiles p ON pv.viewed_profile_id = p.id - WHERE p.user_id = $1 + WHERE viewed_profile_id = $1 `, [userId]); return parseInt(result.rows[0].count) || 0; @@ -562,14 +583,12 @@ export class ProfileService { async getProfileViewers(userId: string, limit: number = 10): Promise { try { const result = await query(` - SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name + SELECT DISTINCT p.* FROM profile_views pv - JOIN profiles target_p ON pv.viewed_profile_id = target_p.id JOIN users viewer_u ON pv.viewer_id = viewer_u.id JOIN profiles p ON viewer_u.id = p.user_id - JOIN users u ON p.user_id = u.id - WHERE target_p.user_id = $1 - ORDER BY pv.viewed_at DESC + WHERE pv.viewed_profile_id = $1 + ORDER BY pv.view_date DESC LIMIT $2 `, [userId, limit]); @@ -579,4 +598,22 @@ export class ProfileService { return []; } } + + // Получить список просмотренных профилей + async getViewedProfiles(userId: string, limit: number = 50): Promise { + try { + const result = await query(` + SELECT viewed_profile_id + FROM profile_views + WHERE viewer_id = $1 + ORDER BY view_date DESC + LIMIT $2 + `, [userId, limit]); + + return result.rows.map((row: any) => row.viewed_profile_id); + } catch (error) { + console.error('Error getting viewed profiles:', error); + return []; + } + } } \ No newline at end of file diff --git a/src/services/vipService.ts b/src/services/vipService.ts index 397723e..3d209a4 100644 --- a/src/services/vipService.ts +++ b/src/services/vipService.ts @@ -24,9 +24,9 @@ export class VipService { // Проверить премиум статус пользователя async checkPremiumStatus(telegramId: string): Promise { try { - // Проверяем существование пользователя + // Проверяем существование пользователя и получаем его премиум статус const result = await query(` - SELECT id + SELECT id, premium FROM users WHERE telegram_id = $1 `, [telegramId]); @@ -35,12 +35,13 @@ export class VipService { throw new BotError('User not found', 'USER_NOT_FOUND', 404); } - // Временно возвращаем false для всех пользователей, так как колонки premium нет - // В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно + // Получаем актуальное значение премиум статуса из базы данных + const isPremium = result.rows[0].premium || false; + return { - isPremium: false, - expiresAt: undefined, - daysLeft: undefined + isPremium: isPremium, + expiresAt: undefined, // Пока не используем дату истечения + daysLeft: undefined // Пока не используем количество дней }; } catch (error) { console.error('Error checking premium status:', error); @@ -51,9 +52,17 @@ export class VipService { // Добавить премиум статус async addPremium(telegramId: string, durationDays: number = 30): Promise { try { - // Временно заглушка, так как колонок premium и premium_expires_at нет - console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`); - // TODO: Добавить колонки premium и premium_expires_at в таблицу users + console.log(`[VIP] Добавление премиум для ${telegramId} на ${durationDays} дней`); + + // Обновляем статус premium в базе данных + await query(` + UPDATE users + SET premium = true + WHERE telegram_id = $1 + RETURNING id, telegram_id, premium + `, [telegramId]); + + console.log(`[VIP] Премиум успешно добавлен для пользователя ${telegramId}`); } catch (error) { console.error('Error adding premium:', error); throw error; @@ -63,9 +72,17 @@ export class VipService { // Удалить премиум статус async removePremium(telegramId: string): Promise { try { - // Временно заглушка, так как колонок premium и premium_expires_at нет - console.log(`[VIP] Попытка удалить премиум для ${telegramId}`); - // TODO: Добавить колонки premium и premium_expires_at в таблицу users + console.log(`[VIP] Удаление премиум для ${telegramId}`); + + // Обновляем статус premium в базе данных + await query(` + UPDATE users + SET premium = false + WHERE telegram_id = $1 + RETURNING id, telegram_id, premium + `, [telegramId]); + + console.log(`[VIP] Премиум успешно удален для пользователя ${telegramId}`); } catch (error) { console.error('Error removing premium:', error); throw error; diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..6ef2b25 --- /dev/null +++ b/start.bat @@ -0,0 +1,241 @@ +@echo off +:: start.bat - Скрипт для запуска Telegram Tinder Bot на Windows +:: Позволяет выбрать между локальной БД в контейнере или внешней БД + +echo ================================================ +echo Запуск Telegram Tinder Bot +echo ================================================ + +:: Проверка наличия Docker и Docker Compose +WHERE docker >nul 2>&1 +IF %ERRORLEVEL% NEQ 0 ( + echo [31mОШИБКА: Docker не установлен![0m + echo Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/ + exit /b 1 +) + +:: Проверяем наличие .env файла +IF NOT EXIST .env ( + echo [33mФайл .env не найден. Создаем из шаблона...[0m + IF EXIST .env.example ( + copy .env.example .env + echo [32mФайл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.[0m + ) ELSE ( + echo [31mОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.[0m + exit /b 1 + ) +) + +:: Спрашиваем про запуск базы данных +set /p use_container_db="Запустить базу данных PostgreSQL в контейнере? (y/n): " + +:: Функции для работы с docker-compose +IF /I "%use_container_db%" NEQ "y" ( + :: Запрашиваем параметры подключения к внешней БД + echo [36mВведите параметры подключения к внешней базе данных:[0m + set /p db_host="Хост (например, localhost): " + set /p db_port="Порт (например, 5432): " + set /p db_name="Имя базы данных: " + set /p db_user="Имя пользователя: " + set /p db_password="Пароль: " + + :: Модифицируем docker-compose.yml + echo [33mМодифицируем docker-compose.yml для работы с внешней базой данных...[0m + + :: Сохраняем оригинальную версию файла + copy docker-compose.yml docker-compose.yml.bak + + :: Создаем временный файл с модифицированным содержимым + ( + echo version: '3.8' + echo. + echo services: + echo bot: + echo build: . + echo container_name: telegram-tinder-bot + echo restart: unless-stopped + echo env_file: .env + echo environment: + echo - NODE_ENV=production + echo - DB_HOST=%db_host% + echo - DB_PORT=%db_port% + echo - DB_NAME=%db_name% + echo - DB_USERNAME=%db_user% + echo - DB_PASSWORD=%db_password% + echo volumes: + echo - ./uploads:/app/uploads + echo - ./logs:/app/logs + echo networks: + echo - bot-network + echo healthcheck: + echo test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + echo interval: 30s + echo timeout: 5s + echo retries: 3 + echo start_period: 10s + echo. + echo adminer: + echo image: adminer:latest + echo container_name: adminer-tinder + echo restart: unless-stopped + echo ports: + echo - "8080:8080" + echo networks: + echo - bot-network + echo. + echo volumes: + echo postgres_data: + echo. + echo networks: + echo bot-network: + echo driver: bridge + ) > docker-compose.temp.yml + + :: Заменяем оригинальный файл + move /y docker-compose.temp.yml docker-compose.yml + + echo [32mdocker-compose.yml обновлен для работы с внешней базой данных[0m + + :: Обновляем .env файл + echo [33mОбновляем файл .env с параметрами внешней базы данных...[0m + + :: Создаем временный файл + type NUL > .env.temp + + :: Читаем .env построчно и заменяем нужные строки + for /f "tokens=*" %%a in (.env) do ( + set line=%%a + set line=!line:DB_HOST=*! + if "!line:~0,1!" == "*" ( + echo DB_HOST=%db_host%>> .env.temp + ) else ( + set line=!line:DB_PORT=*! + if "!line:~0,1!" == "*" ( + echo DB_PORT=%db_port%>> .env.temp + ) else ( + set line=!line:DB_NAME=*! + if "!line:~0,1!" == "*" ( + echo DB_NAME=%db_name%>> .env.temp + ) else ( + set line=!line:DB_USERNAME=*! + if "!line:~0,1!" == "*" ( + echo DB_USERNAME=%db_user%>> .env.temp + ) else ( + set line=!line:DB_PASSWORD=*! + if "!line:~0,1!" == "*" ( + echo DB_PASSWORD=%db_password%>> .env.temp + ) else ( + echo %%a>> .env.temp + ) + ) + ) + ) + ) + ) + + :: Заменяем оригинальный файл + move /y .env.temp .env + + echo [32mФайл .env обновлен с параметрами внешней базы данных[0m + + :: Запускаем только контейнер с ботом + echo [36mЗапускаем Telegram Bot без контейнера базы данных...[0m + docker-compose up -d bot adminer + + echo [32mБот запущен и использует внешнюю базу данных: %db_host%:%db_port%/%db_name%[0m + echo [33mAdminer доступен по адресу: http://localhost:8080/[0m + echo [33mДанные для входа в Adminer:[0m + echo [33mСистема: PostgreSQL[0m + echo [33mСервер: %db_host%[0m + echo [33mПользователь: %db_user%[0m + echo [33mПароль: (введенный вами)[0m + echo [33mБаза данных: %db_name%[0m +) ELSE ( + :: Восстанавливаем оригинальный docker-compose.yml если есть бэкап + if exist docker-compose.yml.bak ( + move /y docker-compose.yml.bak docker-compose.yml + echo [32mdocker-compose.yml восстановлен из резервной копии[0m + ) + + echo [36mЗапускаем Telegram Bot с контейнером базы данных...[0m + + :: Проверка, запущены ли контейнеры + docker ps -q -f name=telegram-tinder-bot > tmp_containers.txt + set /p containers= .env.temp + + :: Читаем .env построчно и заменяем строку с паролем + for /f "tokens=*" %%a in (.env) do ( + set line=%%a + set line=!line:DB_PASSWORD=*! + if "!line:~0,1!" == "*" ( + echo DB_PASSWORD=%random_password%>> .env.temp + ) else ( + echo %%a>> .env.temp + ) + ) + + :: Заменяем оригинальный файл + move /y .env.temp .env + + echo [33mСгенерирован случайный пароль для базы данных и сохранен в .env[0m + ) + + echo [32mTelegram Bot запущен с локальной базой данных[0m + echo [33mAdminer доступен по адресу: http://localhost:8080/[0m + echo [33mДанные для входа в Adminer:[0m + echo [33mСистема: PostgreSQL[0m + echo [33mСервер: db[0m + echo [33mПользователь: postgres[0m + echo [33mПароль: (из переменной DB_PASSWORD в .env)[0m + echo [33mБаза данных: telegram_tinder_bot[0m +) + +:: Проверка статуса контейнеров +echo [36mПроверка статуса контейнеров:[0m +docker-compose ps + +echo ================================================ +echo [32mПроцесс запуска Telegram Tinder Bot завершен![0m +echo ================================================ +echo [33mДля просмотра логов используйте: docker-compose logs -f bot[0m +echo [33mДля остановки: docker-compose down[0m + +pause diff --git a/start.ps1 b/start.ps1 new file mode 100644 index 0000000..dd2674e --- /dev/null +++ b/start.ps1 @@ -0,0 +1,229 @@ +function createModifiedDockerCompose { + param ( + [string]$dbHost, + [string]$dbPort, + [string]$dbName, + [string]$dbUser, + [string]$dbPassword + ) + + $dockerComposeContent = @" +version: '3.8' + +services: + bot: + build: . + container_name: telegram-tinder-bot + restart: unless-stopped + env_file: .env + environment: + - NODE_ENV=production + - DB_HOST=$dbHost + - DB_PORT=$dbPort + - DB_NAME=$dbName + - DB_USERNAME=$dbUser + - DB_PASSWORD=$dbPassword + volumes: + - ./uploads:/app/uploads + - ./logs:/app/logs + networks: + - bot-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + adminer: + image: adminer:latest + container_name: adminer-tinder + restart: unless-stopped + ports: + - "8080:8080" + networks: + - bot-network + +volumes: + postgres_data: + +networks: + bot-network: + driver: bridge +"@ + + return $dockerComposeContent +} + +function restoreDockerCompose { + if (Test-Path -Path "docker-compose.yml.bak") { + Copy-Item -Path "docker-compose.yml.bak" -Destination "docker-compose.yml" -Force + Write-Host "docker-compose.yml восстановлен из резервной копии" -ForegroundColor Green + } +} + +function updateEnvFile { + param ( + [string]$dbHost, + [string]$dbPort, + [string]$dbName, + [string]$dbUser, + [string]$dbPassword + ) + + $envContent = Get-Content -Path ".env" -Raw + + $envContent = $envContent -replace "DB_HOST=.*", "DB_HOST=$dbHost" + $envContent = $envContent -replace "DB_PORT=.*", "DB_PORT=$dbPort" + $envContent = $envContent -replace "DB_NAME=.*", "DB_NAME=$dbName" + $envContent = $envContent -replace "DB_USERNAME=.*", "DB_USERNAME=$dbUser" + $envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$dbPassword" + + Set-Content -Path ".env" -Value $envContent + + Write-Host "Файл .env обновлен с параметрами внешней базы данных" -ForegroundColor Green +} + +function generateRandomPassword { + $length = 16 + $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + $bytes = New-Object Byte[] $length + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + + $password = "" + for ($i = 0; $i -lt $length; $i++) { + $password += $chars[$bytes[$i] % $chars.Length] + } + + return $password +} + +# Начало основного скрипта +Write-Host "==================================================" -ForegroundColor Cyan +Write-Host " Запуск Telegram Tinder Bot" -ForegroundColor Cyan +Write-Host "==================================================" -ForegroundColor Cyan + +# Проверка наличия Docker +try { + docker --version | Out-Null +} catch { + Write-Host "ОШИБКА: Docker не установлен!" -ForegroundColor Red + Write-Host "Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/" + exit +} + +# Проверяем наличие .env файла +if (-not (Test-Path -Path ".env")) { + Write-Host "Файл .env не найден. Создаем из шаблона..." -ForegroundColor Yellow + + if (Test-Path -Path ".env.example") { + Copy-Item -Path ".env.example" -Destination ".env" + Write-Host "Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками." -ForegroundColor Green + } else { + Write-Host "ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную." -ForegroundColor Red + exit + } +} + +# Спрашиваем про запуск базы данных +$useContainerDb = Read-Host "Запустить базу данных PostgreSQL в контейнере? (y/n)" + +if ($useContainerDb -ne "y") { + # Запрашиваем параметры подключения к внешней БД + Write-Host "Введите параметры подключения к внешней базе данных:" -ForegroundColor Cyan + $dbHost = Read-Host "Хост (например, localhost)" + $dbPort = Read-Host "Порт (например, 5432)" + $dbName = Read-Host "Имя базы данных" + $dbUser = Read-Host "Имя пользователя" + $dbPassword = Read-Host "Пароль" -AsSecureString + $dbPasswordText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($dbPassword)) + + # Модифицируем docker-compose.yml + Write-Host "Модифицируем docker-compose.yml для работы с внешней базой данных..." -ForegroundColor Yellow + + # Сохраняем оригинальную версию файла + Copy-Item -Path "docker-compose.yml" -Destination "docker-compose.yml.bak" -Force + + # Создаем модифицированный docker-compose.yml + $dockerComposeContent = createModifiedDockerCompose -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText + Set-Content -Path "docker-compose.yml" -Value $dockerComposeContent + + Write-Host "docker-compose.yml обновлен для работы с внешней базой данных" -ForegroundColor Green + + # Обновляем .env файл + Write-Host "Обновляем файл .env с параметрами внешней базы данных..." -ForegroundColor Yellow + updateEnvFile -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText + + # Запускаем только контейнер с ботом + Write-Host "Запускаем Telegram Bot без контейнера базы данных..." -ForegroundColor Cyan + docker-compose up -d bot adminer + + Write-Host "Бот запущен и использует внешнюю базу данных: $dbHost`:$dbPort/$dbName" -ForegroundColor Green + Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow + Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow + Write-Host "Система: PostgreSQL" -ForegroundColor Yellow + Write-Host "Сервер: $dbHost" -ForegroundColor Yellow + Write-Host "Пользователь: $dbUser" -ForegroundColor Yellow + Write-Host "Пароль: (введенный вами)" -ForegroundColor Yellow + Write-Host "База данных: $dbName" -ForegroundColor Yellow +} else { + # Восстанавливаем оригинальный docker-compose.yml если есть бэкап + restoreDockerCompose + + Write-Host "Запускаем Telegram Bot с контейнером базы данных..." -ForegroundColor Cyan + + # Проверка, запущены ли контейнеры + $containers = docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder + + if ($containers) { + $restartContainers = Read-Host "Контейнеры уже запущены. Перезапустить? (y/n)" + if ($restartContainers -eq "y") { + docker-compose down + docker-compose up -d + Write-Host "Контейнеры перезапущены" -ForegroundColor Green + } else { + Write-Host "Продолжаем работу с уже запущенными контейнерами" -ForegroundColor Cyan + } + } else { + docker-compose up -d + Write-Host "Контейнеры запущены" -ForegroundColor Green + } + + # Проверка наличия пароля для БД в .env + $envContent = Get-Content -Path ".env" -Raw + $match = [Regex]::Match($envContent, "DB_PASSWORD=(.*)(\r?\n|$)") + $dbPassword = if ($match.Success) { $match.Groups[1].Value.Trim() } else { "" } + + if ([string]::IsNullOrWhiteSpace($dbPassword)) { + # Генерируем случайный пароль + $randomPassword = generateRandomPassword + + # Обновляем .env файл + $envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$randomPassword" + Set-Content -Path ".env" -Value $envContent + + Write-Host "Сгенерирован случайный пароль для базы данных и сохранен в .env" -ForegroundColor Yellow + } + + Write-Host "Telegram Bot запущен с локальной базой данных" -ForegroundColor Green + Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow + Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow + Write-Host "Система: PostgreSQL" -ForegroundColor Yellow + Write-Host "Сервер: db" -ForegroundColor Yellow + Write-Host "Пользователь: postgres" -ForegroundColor Yellow + Write-Host "Пароль: (из переменной DB_PASSWORD в .env)" -ForegroundColor Yellow + Write-Host "База данных: telegram_tinder_bot" -ForegroundColor Yellow +} + +# Проверка статуса контейнеров +Write-Host "Проверка статуса контейнеров:" -ForegroundColor Cyan +docker-compose ps + +Write-Host "==================================================" -ForegroundColor Cyan +Write-Host "Процесс запуска Telegram Tinder Bot завершен!" -ForegroundColor Green +Write-Host "==================================================" -ForegroundColor Cyan +Write-Host "Для просмотра логов используйте: docker-compose logs -f bot" -ForegroundColor Yellow +Write-Host "Для остановки: docker-compose down" -ForegroundColor Yellow + +Read-Host "Нажмите Enter для выхода" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..b458b66 --- /dev/null +++ b/start.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# start.sh - Скрипт для запуска Telegram Tinder Bot +# Позволяет выбрать между локальной БД в контейнере или внешней БД + +# Цвета для вывода +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}==================================================${NC}" +echo -e "${BLUE} Запуск Telegram Tinder Bot ${NC}" +echo -e "${BLUE}==================================================${NC}" + +# Проверка наличия Docker и Docker Compose +if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then + echo -e "${RED}ОШИБКА: Docker и/или Docker Compose не установлены!${NC}" + echo -e "Для установки Docker следуйте инструкции на: https://docs.docker.com/get-docker/" + exit 1 +fi + +# Проверяем наличие .env файла +if [ ! -f .env ]; then + echo -e "${YELLOW}Файл .env не найден. Создаем из шаблона...${NC}" + if [ -f .env.example ]; then + cp .env.example .env + echo -e "${GREEN}Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.${NC}" + else + echo -e "${RED}ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.${NC}" + exit 1 + fi +fi + +# Спрашиваем про запуск базы данных +read -p "Запустить базу данных PostgreSQL в контейнере? (y/n): " use_container_db + +# Функция для изменения docker-compose.yml +modify_docker_compose() { + local host=$1 + local port=$2 + local user=$3 + local password=$4 + local db_name=$5 + + echo -e "${YELLOW}Модифицируем docker-compose.yml для работы с внешней базой данных...${NC}" + + # Сохраняем оригинальную версию файла + cp docker-compose.yml docker-compose.yml.bak + + # Создаем временный файл с модифицированным содержимым + cat > docker-compose.temp.yml << EOL +version: '3.8' + +services: + bot: + build: . + container_name: telegram-tinder-bot + restart: unless-stopped + env_file: .env + environment: + - NODE_ENV=production + - DB_HOST=${host} + - DB_PORT=${port} + - DB_NAME=${db_name} + - DB_USERNAME=${user} + - DB_PASSWORD=${password} + volumes: + - ./uploads:/app/uploads + - ./logs:/app/logs + networks: + - bot-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + adminer: + image: adminer:latest + container_name: adminer-tinder + restart: unless-stopped + ports: + - "8080:8080" + networks: + - bot-network + +volumes: + postgres_data: + +networks: + bot-network: + driver: bridge +EOL + + # Заменяем оригинальный файл + mv docker-compose.temp.yml docker-compose.yml + + echo -e "${GREEN}docker-compose.yml обновлен для работы с внешней базой данных${NC}" +} + +# Функция для восстановления docker-compose.yml +restore_docker_compose() { + if [ -f docker-compose.yml.bak ]; then + mv docker-compose.yml.bak docker-compose.yml + echo -e "${GREEN}docker-compose.yml восстановлен из резервной копии${NC}" + fi +} + +# Обработка выбора +if [[ "$use_container_db" =~ ^[Nn]$ ]]; then + # Запрашиваем параметры подключения к внешней БД + echo -e "${BLUE}Введите параметры подключения к внешней базе данных:${NC}" + read -p "Хост (например, localhost): " db_host + read -p "Порт (например, 5432): " db_port + read -p "Имя базы данных: " db_name + read -p "Имя пользователя: " db_user + read -p "Пароль: " db_password + + # Модифицируем docker-compose.yml + modify_docker_compose "$db_host" "$db_port" "$db_user" "$db_password" "$db_name" + + # Обновляем .env файл + echo -e "${YELLOW}Обновляем файл .env с параметрами внешней базы данных...${NC}" + + # Используем sed для замены переменных в .env + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS требует другой синтаксис для sed + sed -i '' "s/DB_HOST=.*/DB_HOST=${db_host}/" .env + sed -i '' "s/DB_PORT=.*/DB_PORT=${db_port}/" .env + sed -i '' "s/DB_NAME=.*/DB_NAME=${db_name}/" .env + sed -i '' "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env + sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env + else + # Linux и другие системы + sed -i "s/DB_HOST=.*/DB_HOST=${db_host}/" .env + sed -i "s/DB_PORT=.*/DB_PORT=${db_port}/" .env + sed -i "s/DB_NAME=.*/DB_NAME=${db_name}/" .env + sed -i "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env + sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env + fi + + echo -e "${GREEN}Файл .env обновлен с параметрами внешней базы данных${NC}" + + # Запускаем только контейнер с ботом + echo -e "${BLUE}Запускаем Telegram Bot без контейнера базы данных...${NC}" + docker-compose up -d bot adminer + + echo -e "${GREEN}Бот запущен и использует внешнюю базу данных: ${db_host}:${db_port}/${db_name}${NC}" + echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}" + echo -e "${YELLOW}Данные для входа в Adminer:${NC}" + echo -e "${YELLOW}Система: PostgreSQL${NC}" + echo -e "${YELLOW}Сервер: ${db_host}${NC}" + echo -e "${YELLOW}Пользователь: ${db_user}${NC}" + echo -e "${YELLOW}Пароль: (введенный вами)${NC}" + echo -e "${YELLOW}База данных: ${db_name}${NC}" +else + # Восстанавливаем оригинальный docker-compose.yml если есть бэкап + restore_docker_compose + + echo -e "${BLUE}Запускаем Telegram Bot с контейнером базы данных...${NC}" + + # Проверка, запущены ли контейнеры + containers=$(docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder) + if [ -n "$containers" ]; then + echo -e "${YELLOW}Контейнеры уже запущены. Перезапустить? (y/n): ${NC}" + read restart_containers + if [[ "$restart_containers" =~ ^[Yy]$ ]]; then + docker-compose down + docker-compose up -d + echo -e "${GREEN}Контейнеры перезапущены${NC}" + else + echo -e "${BLUE}Продолжаем работу с уже запущенными контейнерами${NC}" + fi + else + docker-compose up -d + echo -e "${GREEN}Контейнеры запущены${NC}" + fi + + # Проверка наличия пароля для БД в .env + db_password=$(grep DB_PASSWORD .env | cut -d '=' -f2) + if [ -z "$db_password" ]; then + # Генерируем случайный пароль + random_password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16) + + # Обновляем .env файл + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS требует другой синтаксис для sed + sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env + else + # Linux и другие системы + sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env + fi + + echo -e "${YELLOW}Сгенерирован случайный пароль для базы данных и сохранен в .env${NC}" + fi + + echo -e "${GREEN}Telegram Bot запущен с локальной базой данных${NC}" + echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}" + echo -e "${YELLOW}Данные для входа в Adminer:${NC}" + echo -e "${YELLOW}Система: PostgreSQL${NC}" + echo -e "${YELLOW}Сервер: db${NC}" + echo -e "${YELLOW}Пользователь: postgres${NC}" + echo -e "${YELLOW}Пароль: (из переменной DB_PASSWORD в .env)${NC}" + echo -e "${YELLOW}База данных: telegram_tinder_bot${NC}" +fi + +# Проверка статуса контейнеров +echo -e "${BLUE}Проверка статуса контейнеров:${NC}" +docker-compose ps + +echo -e "${BLUE}==================================================${NC}" +echo -e "${GREEN}Процесс запуска Telegram Tinder Bot завершен!${NC}" +echo -e "${BLUE}==================================================${NC}" +echo -e "${YELLOW}Для просмотра логов используйте: docker-compose logs -f bot${NC}" +echo -e "${YELLOW}Для остановки: docker-compose down${NC}"