diff --git a/README.md b/README.md index 7a17086..edc7413 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,71 @@ Полнофункциональный Telegram бот для знакомств в стиле Tinder с инлайн-кнопками и красивым интерфейсом. Пользователи могут создавать профили, просматривать анкеты других пользователей, ставить лайки, получать матчи и общаться друг с другом. +## 🗂️ Структура проекта + +``` +telegram-tinder-bot/ +├── bin/ # Исполняемые скрипты и утилиты +│ ├── start_bot.bat # Скрипт запуска для Windows +│ ├── install_ubuntu.sh # Скрипт установки для Ubuntu +│ ├── update.sh # Скрипт обновления для Linux/macOS +│ ├── update.bat # Скрипт обновления для Windows +│ └── setup.sh # Скрипт настройки окружения +│ +├── docs/ # Документация проекта +│ ├── ARCHITECTURE.md # Архитектура приложения +│ ├── DEPLOYMENT.md # Инструкции по развертыванию +│ ├── DEPLOY_UBUNTU.md # Инструкции по развертыванию на Ubuntu +│ ├── LOCALIZATION.md # Информация о локализации +│ ├── NATIVE_CHAT_SYSTEM.md # Документация по системе чата +│ ├── PROJECT_SUMMARY.md # Общее описание проекта +│ └── VIP_FUNCTIONS.md # Описание премиум функций +│ +├── migrations/ # Миграции базы данных +│ ├── 1758144488937_initial-schema.js # Начальная схема БД +│ └── 1758144618548_add-missing-profile-columns.js # Дополнительные колонки +│ +├── scripts/ # Вспомогательные скрипты +│ ├── add-hobbies-column.js # Скрипт добавления колонки hobbies +│ ├── add-premium-columns.js # Скрипт добавления премиум колонок +│ ├── add-premium-columns.ts # TypeScript версия скрипта +│ ├── create_profile_fix.js # Исправление профилей +│ └── migrate-sync.js # Синхронизация миграций +│ +├── sql/ # SQL скрипты +│ ├── add_looking_for.sql # Добавление колонки looking_for +│ ├── add_missing_columns.sql # Добавление недостающих колонок +│ ├── add_premium_columns.sql # Добавление премиум колонок +│ ├── add_updated_at.sql # Добавление колонки updated_at +│ ├── clear_database.sql # Очистка базы данных +│ └── recreate_tables.sql # Пересоздание таблиц +│ +├── src/ # Исходный код приложения +│ ├── bot.ts # Основной файл бота +│ ├── controllers/ # Контроллеры +│ ├── database/ # Функции для работы с БД +│ ├── handlers/ # Обработчики сообщений и команд +│ ├── locales/ # Локализация +│ ├── models/ # Модели данных +│ ├── scripts/ # Скрипты для запуска +│ │ └── initDb.ts # Инициализация базы данных +│ ├── services/ # Сервисы и бизнес-логика +│ ├── types/ # TypeScript типы +│ └── utils/ # Утилиты и вспомогательные функции +│ +├── tests/ # Тесты +│ └── test-bot.ts # Тестовая версия бота +│ +├── .dockerignore # Игнорируемые Docker файлы +├── .env # Переменные окружения (локальные) +├── .env.example # Пример файла переменных окружения +├── database.json # Конфигурация базы данных +├── docker-compose.yml # Настройка Docker Compose +├── Dockerfile # Docker-образ приложения +├── package.json # Зависимости и скрипты NPM +└── tsconfig.json # Настройки TypeScript +``` + ## ✨ Функционал ### 🎯 Основные возможности @@ -74,82 +139,84 @@ [💬 Написать] [👤 Профиль] [🔍 Продолжить поиск] ``` -## 🗂️ Структура проекта -``` -telegram-tinder-bot/ -├── src/ -│ ├── bot.ts # Основной файл бота -│ ├── handlers/ # Обработчики событий -│ │ ├── commandHandlers.ts # Команды (/start, /profile, etc.) -│ │ ├── callbackHandlers.ts # Инлайн-кнопки (лайки, просмотр) -│ │ └── messageHandlers.ts # Текстовые сообщения -│ ├── services/ # Бизнес-логика -│ │ ├── profileService.ts # Управление профилями -│ │ ├── matchingService.ts # Алгоритм совпадений -│ │ └── notificationService.ts # Уведомления -│ ├── models/ # Модели данных -│ │ ├── User.ts # Пользователь Telegram -│ │ ├── Profile.ts # Профиль знакомств -│ │ ├── Swipe.ts # Лайки/дислайки -│ │ └── Match.ts # Совпадения -│ └── database/ # База данных -│ ├── connection.ts # Подключение к PostgreSQL -│ └── migrations/init.sql # Создание таблиц -├── config/ # Конфигурация -│ └── default.json # Настройки по умолчанию -├── docker-compose.yml # Docker Compose -├── Dockerfile # Docker контейнер -└── package.json # Зависимости npm -``` -## 🚀 Развертывание +## 🚀 Быстрый старт -### 📦 Docker (Рекомендуется) +### 1. Предварительные требования + +- Node.js 16+ +- PostgreSQL 12+ +- Telegram Bot Token (получить у [@BotFather](https://t.me/BotFather)) + +### 2. Установка ```bash # Клонировать репозиторий git clone cd telegram-tinder-bot -# Настроить переменные окружения -cp .env.example .env -# Отредактируйте .env файл - -# Запустить с Docker Compose -docker-compose up -d - -# Применить миграции БД -docker-compose exec app npm run db:migrate -``` - -### 🖥️ Обычная установка - -```bash # Установить зависимости npm install -# Создать базу данных -createdb telegram_tinder_bot -psql -d telegram_tinder_bot -f src/database/migrations/init.sql - -# Запустить бота +# Скомпилировать TypeScript npm run build -npm start ``` -### ☁️ Продакшен +### 3. Настройка базы данных ```bash -# Установить PM2 -npm install -g pm2 +# Создать базу данных PostgreSQL +createdb telegram_tinder_bot -# Запустить через PM2 -pm2 start ecosystem.config.js +# Инициализация базы данных +npm run init:db +``` -# Мониторинг -pm2 monit -pm2 logs telegram-tinder-bot +### 4. Запуск бота + +```bash +# Запуск на Windows +.\bin\start_bot.bat + +# Запуск на Linux/macOS +npm run start +``` + +## � Развертывание на Ubuntu + +Для развертывания на Ubuntu 24.04 используйте скрипт установки: + +```bash +# Сделать скрипт исполняемым +chmod +x ./bin/install_ubuntu.sh + +# Запустить установку +sudo ./bin/install_ubuntu.sh +``` + +Подробные инструкции по развертыванию на Ubuntu находятся в [docs/DEPLOY_UBUNTU.md](docs/DEPLOY_UBUNTU.md). + +## 🔄 Обновление бота + +### На Windows: + +```bash +# Обновление с ветки main +npm run update:win + +# Обновление с определенной ветки +.\bin\update.bat develop +``` + +### На Linux/macOS: + +```bash +# Обновление с ветки main +npm run update + +# Обновление с определенной ветки и перезапуском сервиса +./bin/update.sh develop --restart-service ``` ## 🔧 Настройка переменных окружения diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..e4c1763 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,84 @@ +# Автоматическое обновление Telegram Tinder Bot + +Этот документ описывает процесс автоматического обновления бота с помощью созданных скриптов. + +## Скрипт обновления + +Скрипт обновления выполняет следующие действия: + +1. Получает последние изменения из Git-репозитория +2. Устанавливает зависимости +3. Применяет миграции базы данных +4. Собирает проект +5. Проверяет наличие файла .env +6. Проверяет наличие Docker-сервисов +7. При запуске на Ubuntu: проверяет и перезапускает PM2 сервис + +## Подробные инструкции по развертыванию + +Для подробных инструкций по развертыванию бота на сервере Ubuntu 24.04, пожалуйста, обратитесь к файлу `DEPLOY_UBUNTU.md` в корне проекта. + +## Как использовать + +### На Linux/macOS: + +```bash +# Обновление с ветки main (по умолчанию) +npm run update + +# Обновление с определенной ветки +bash ./bin/update.sh develop + +# Обновление с определенной ветки и перезапуском сервиса PM2 (для Ubuntu) +bash ./bin/update.sh develop --restart-service +``` + +### На Windows: + +```powershell +# Обновление с ветки main (по умолчанию) +npm run update:win + +# Обновление с определенной ветки +.\bin\update.bat develop +``` + +## Добавление прав на выполнение (только для Linux/macOS) + +Если у вас возникают проблемы с запуском скрипта, добавьте права на выполнение: + +```bash +chmod +x ./bin/update.sh +``` + +## Автоматизация обновлений + +Для автоматизации регулярных обновлений вы можете использовать cron (Linux/macOS) или Планировщик заданий (Windows). + +### Пример cron-задания для Ubuntu (ежедневное обновление в 4:00 с перезапуском сервиса): + +``` +0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh --restart-service >> /var/log/tg_bot_update.log 2>&1 +``` + +### Пример cron-задания (ежедневное обновление в 4:00 без перезапуска): + +``` +0 4 * * * cd /path/to/bot && ./bin/update.sh +``` + +### Для Windows: + +Создайте задачу в Планировщике заданий, которая запускает: + +``` +cmd.exe /c "cd /d D:\Projects\tg_tinder_bot && .\bin\update.bat" +``` + +## Что делать после обновления + +После обновления вы можете: + +1. Запустить бота: `npm run start` +2. Запустить бота в режиме разработки: `npm run dev` +3. Перезапустить Docker-контейнеры, если используете Docker: `docker-compose down && docker-compose up -d` diff --git a/bin/install_ubuntu.sh b/bin/install_ubuntu.sh new file mode 100644 index 0000000..d33cf1a --- /dev/null +++ b/bin/install_ubuntu.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# Script for installing Telegram Tinder Bot on Ubuntu +# This script automates the deployment process on a fresh Ubuntu server +# Usage: ./bin/install_ubuntu.sh [--with-nginx] [--with-ssl domain.com] + +set -e # Exit immediately if a command exits with a non-zero status + +# Define colors for pretty output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Default settings +INSTALL_NGINX=false +INSTALL_SSL=false +DOMAIN="" + +# Parse command line arguments +for arg in "$@"; do + if [[ "$arg" == "--with-nginx" ]]; then + INSTALL_NGINX=true + elif [[ "$arg" == "--with-ssl" ]]; then + INSTALL_SSL=true + # Next argument should be domain + shift + DOMAIN="$1" + if [[ -z "$DOMAIN" || "$DOMAIN" == --* ]]; then + echo -e "${RED}Error: Domain name required after --with-ssl${NC}" + exit 1 + fi + fi + shift +done + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} Telegram Tinder Bot Ubuntu Installer ${NC}" +echo -e "${BLUE}========================================${NC}" + +# Check if running on Ubuntu +if [ -f /etc/os-release ]; then + source /etc/os-release + if [[ "$ID" != "ubuntu" ]]; then + echo -e "${RED}Error: This script is designed for Ubuntu. Current OS: $ID${NC}" + exit 1 + else + echo -e "${GREEN}Detected Ubuntu ${VERSION_ID}${NC}" + fi +else + echo -e "${RED}Error: Could not detect operating system${NC}" + exit 1 +fi + +# Check for root privileges +if [ "$(id -u)" -ne 0 ]; then + echo -e "${RED}Error: This script must be run as root${NC}" + echo -e "Please run: ${YELLOW}sudo $0 $*${NC}" + exit 1 +fi + +echo -e "\n${BLUE}Step 1: Updating system packages...${NC}" +apt update && apt upgrade -y +echo -e "${GREEN}✓ System packages updated${NC}" + +echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}" +apt install -y curl wget git build-essential postgresql postgresql-contrib +echo -e "${GREEN}✓ Basic dependencies installed${NC}" + +echo -e "\n${BLUE}Step 3: Installing Node.js...${NC}" +curl -fsSL https://deb.nodesource.com/setup_20.x | bash - +apt install -y nodejs +echo -e "${GREEN}✓ Node.js $(node --version) installed${NC}" +echo -e "${GREEN}✓ npm $(npm --version) installed${NC}" + +echo -e "\n${BLUE}Step 4: Setting up PostgreSQL...${NC}" +systemctl start postgresql +systemctl enable postgresql + +echo -e "\n${BLUE}Please enter a strong password for the database user:${NC}" +read -s DB_PASSWORD +echo + +# Create database and user +su - postgres -c "psql -c \"CREATE DATABASE tg_tinder_bot;\"" +su - postgres -c "psql -c \"CREATE USER tg_bot WITH PASSWORD '$DB_PASSWORD';\"" +su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot;\"" +echo -e "${GREEN}✓ PostgreSQL configured${NC}" + +echo -e "\n${BLUE}Step 5: Setting up application directory...${NC}" +mkdir -p /opt/tg_tinder_bot +chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot +echo -e "${GREEN}✓ Application directory created${NC}" + +echo -e "\n${BLUE}Step 6: Installing PM2...${NC}" +npm install -g pm2 +echo -e "${GREEN}✓ PM2 installed${NC}" + +echo -e "\n${BLUE}Step 7: Please enter your Telegram Bot Token:${NC}" +read BOT_TOKEN + +echo -e "\n${BLUE}Step 8: Creating environment file...${NC}" +cat > /opt/tg_tinder_bot/.env << EOL +# Bot settings +BOT_TOKEN=${BOT_TOKEN} +LOG_LEVEL=info + +# Database settings +DB_HOST=localhost +DB_PORT=5432 +DB_USER=tg_bot +DB_PASSWORD=${DB_PASSWORD} +DB_NAME=tg_tinder_bot +EOL +chmod 600 /opt/tg_tinder_bot/.env +chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot/.env +echo -e "${GREEN}✓ Environment file created${NC}" + +echo -e "\n${BLUE}Step 9: Setting up systemd service...${NC}" +cat > /etc/systemd/system/tg-tinder-bot.service << EOL +[Unit] +Description=Telegram Tinder Bot +After=network.target postgresql.service + +[Service] +Type=simple +User=${SUDO_USER} +WorkingDirectory=/opt/tg_tinder_bot +ExecStart=/usr/bin/node dist/bot.js +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=tg-tinder-bot +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target +EOL +echo -e "${GREEN}✓ Systemd service created${NC}" + +if [ "$INSTALL_NGINX" = true ]; then + echo -e "\n${BLUE}Step 10: Installing and configuring Nginx...${NC}" + apt install -y nginx + + # Create Nginx configuration + cat > /etc/nginx/sites-available/tg_tinder_bot << EOL +server { + listen 80; + server_name ${DOMAIN:-_}; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + } +} +EOL + + # Enable site + ln -sf /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/ + nginx -t && systemctl restart nginx + echo -e "${GREEN}✓ Nginx configured${NC}" + + if [ "$INSTALL_SSL" = true ] && [ ! -z "$DOMAIN" ]; then + echo -e "\n${BLUE}Step 11: Setting up SSL with Certbot...${NC}" + apt install -y certbot python3-certbot-nginx + certbot --nginx --non-interactive --agree-tos --email admin@${DOMAIN} -d ${DOMAIN} + echo -e "${GREEN}✓ SSL certificate installed${NC}" + fi +fi + +echo -e "\n${BLUE}Step 12: Clone your repository${NC}" +echo -e "Now you should clone your repository to /opt/tg_tinder_bot" +echo -e "Example: ${YELLOW}git clone https://your-git-repo-url.git /opt/tg_tinder_bot${NC}" +echo -e "Then run the update script: ${YELLOW}cd /opt/tg_tinder_bot && ./bin/update.sh${NC}" + +echo -e "\n${GREEN}========================================${NC}" +echo -e "${GREEN} Installation completed! ${NC}" +echo -e "${GREEN}========================================${NC}" +echo -e "Next steps:" +echo -e "1. Clone your repository to ${YELLOW}/opt/tg_tinder_bot${NC}" +echo -e "2. Run the update script to set up the application" +echo -e "3. Start the service with: ${YELLOW}sudo systemctl start tg-tinder-bot${NC}" +echo -e "4. Enable auto-start with: ${YELLOW}sudo systemctl enable tg-tinder-bot${NC}" +echo -e "5. Check status with: ${YELLOW}sudo systemctl status tg-tinder-bot${NC}" diff --git a/setup.sh b/bin/setup.sh old mode 100755 new mode 100644 similarity index 100% rename from setup.sh rename to bin/setup.sh diff --git a/bin/start_bot.bat b/bin/start_bot.bat new file mode 100644 index 0000000..ac6a9f4 --- /dev/null +++ b/bin/start_bot.bat @@ -0,0 +1,27 @@ +@echo off +REM Скрипт для запуска Telegram Tinder Bot в производственном режиме на Windows +REM Запускает собранный JavaScript из dist/bot.js + +echo 🚀 Запуск Telegram Tinder Bot в производственном режиме... + +REM Добавляем Node.js в PATH, если нужно +set PATH=%PATH%;C:\Program Files\nodejs + +REM Переходим в корневую директорию проекта +cd /d %~dp0.. + +REM Проверяем, существует ли собранный файл +if not exist ".\dist\bot.js" ( + echo ❌ Ошибка: Собранный файл не найден. Сначала выполните 'npm run build'. + exit /b 1 +) + +REM Устанавливаем переменную окружения для производственного режима +set NODE_ENV=production + +REM Запускаем бот +echo 🤖 Запуск Telegram Tinder Bot... +node .\dist\bot.js + +REM Если скрипт дойдет до этой точки, значит бот завершил работу +echo 👋 Бот был остановлен. diff --git a/bin/start_bot.sh b/bin/start_bot.sh new file mode 100644 index 0000000..cc03bd5 --- /dev/null +++ b/bin/start_bot.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Скрипт для запуска бота в производственном режиме +# Запускает собранный JavaScript из dist/bot.js + +# Переходим в корневую директорию проекта (предполагается, что скрипт находится в bin/) +PROJECT_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")" +cd "$PROJECT_DIR" || { echo "❌ Error: Could not change directory to $PROJECT_DIR"; exit 1; } + +# Проверяем, существует ли собранный файл +if [ ! -f "./dist/bot.js" ]; then + echo "❌ Error: Built file not found. Please run 'npm run build' first." + exit 1 +fi + +# Устанавливаем переменную окружения для производственного режима +export NODE_ENV=production + +# Запускаем бот +echo "🚀 Starting Telegram Tinder Bot in production mode..." +node ./dist/bot.js + +# Если скрипт дойдет до этой точки, значит бот завершил работу +echo "👋 Bot has been stopped." diff --git a/bin/tg-tinder-bot.service b/bin/tg-tinder-bot.service new file mode 100644 index 0000000..661e502 --- /dev/null +++ b/bin/tg-tinder-bot.service @@ -0,0 +1,18 @@ +[Unit] +Description=Telegram Tinder Bot +After=network.target postgresql.service + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/tg_tinder_bot +ExecStart=/usr/bin/node dist/bot.js +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=tg-tinder-bot +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target diff --git a/bin/update.bat b/bin/update.bat new file mode 100644 index 0000000..787a4e7 --- /dev/null +++ b/bin/update.bat @@ -0,0 +1,113 @@ +@echo off +REM Script for updating the Telegram Tinder Bot on Windows +REM This script updates the code from Git, applies migrations, and prepares the bot for running +REM Usage: .\bin\update.bat [branch] +REM If branch is not specified, 'main' is used + +setlocal enableextensions enabledelayedexpansion + +echo ======================================== +echo Telegram Tinder Bot Updater +echo ======================================== + +REM Get the branch name from the command line arguments +set BRANCH=%1 +if "%BRANCH%"=="" set BRANCH=main +echo Updating from branch: %BRANCH% + +REM Store the current directory +set CURRENT_DIR=%CD% +set SCRIPT_DIR=%~dp0 +set PROJECT_DIR=%SCRIPT_DIR%.. + +REM Navigate to the project directory +cd /d %PROJECT_DIR% +echo Working directory: %PROJECT_DIR% + +REM Check if we're in a git repository +if not exist .git ( + echo Error: Not a git repository + exit /b 1 +) + +echo. +echo Step 1: Pulling latest changes from Git repository... +REM Save any local changes +git stash save "Auto-stash before update: %DATE% %TIME%" + +REM Fetch all branches +git fetch --all + +REM Check if the branch exists +git rev-parse --verify %BRANCH% >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + git rev-parse --verify origin/%BRANCH% >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + echo Error: Branch '%BRANCH%' does not exist locally or remotely + exit /b 1 + ) +) + +REM Checkout the specified branch +git checkout %BRANCH% + +REM Pull the latest changes +git pull origin %BRANCH% +echo ✓ Successfully pulled latest changes + +echo. +echo Step 2: Installing dependencies... +call npm ci +echo ✓ Dependencies installed + +echo. +echo Step 3: Running database migrations... +REM Check if migrations directory exists +if exist migrations ( + echo Applying database migrations... + call npm run migrate:up + echo ✓ Migrations applied successfully +) else ( + echo ⚠ No migrations directory found, running database initialization script... + call npm run init:db + echo ✓ Database initialized +) + +echo. +echo Step 4: Building the project... +call npm run build +echo ✓ Project built successfully + +echo. +echo Step 5: Checking for .env file... +if exist .env ( + echo ✓ .env file exists +) else ( + echo ⚠ .env file not found + if exist .env.example ( + echo Creating .env from .env.example + copy .env.example .env + echo ⚠ Please update the .env file with your configuration! + ) else ( + echo Error: .env.example file not found + exit /b 1 + ) +) + +echo. +echo Step 6: Checking for services... +REM Check if Docker is being used +if exist docker-compose.yml ( + echo Docker Compose configuration found + echo You might want to restart containers with: docker-compose down ^&^& docker-compose up -d +) + +echo. +echo ======================================== +echo Update completed successfully! +echo ======================================== +echo To start the bot, run: npm run start +echo For development mode: npm run dev + +REM Return to the original directory +cd /d %CURRENT_DIR% diff --git a/bin/update.sh b/bin/update.sh new file mode 100644 index 0000000..8e1f133 --- /dev/null +++ b/bin/update.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +# Script for updating the Telegram Tinder Bot +# This script updates the code from Git, applies migrations, and prepares the bot for running +# Usage: ./bin/update.sh [branch] [--restart-service] +# If branch is not specified, 'main' is used +# Use --restart-service flag to restart PM2 service after update (for production deployments) + +set -e # Exit immediately if a command exits with a non-zero status + +# Define colors for pretty output +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 Updater ${NC}" +echo -e "${BLUE}========================================${NC}" + +# Parse command line arguments +BRANCH="main" +RESTART_SERVICE=false + +for arg in "$@"; do + if [[ "$arg" == "--restart-service" ]]; then + RESTART_SERVICE=true + elif [[ "$arg" != --* ]]; then + BRANCH="$arg" + fi +done + +echo -e "${YELLOW}Updating from branch: ${BRANCH}${NC}" +if [ "$RESTART_SERVICE" = true ]; then + echo -e "${YELLOW}Will restart service after update${NC}" +fi + +# Store the current directory +CURRENT_DIR=$(pwd) +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PROJECT_DIR=$(dirname "$SCRIPT_DIR") + +# Check if running on Ubuntu +IS_UBUNTU=false +if [ -f /etc/os-release ]; then + source /etc/os-release + if [[ "$ID" == "ubuntu" ]]; then + IS_UBUNTU=true + echo -e "${BLUE}Detected Ubuntu: ${VERSION_ID}${NC}" + fi +fi + +# Navigate to the project directory +cd "$PROJECT_DIR" +echo -e "${BLUE}Working directory: ${PROJECT_DIR}${NC}" + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo -e "${RED}Error: Not a git repository${NC}" + exit 1 +fi + +echo -e "\n${BLUE}Step 1: Pulling latest changes from Git repository...${NC}" +# Save any local changes +git stash save "Auto-stash before update: $(date)" + +# Fetch all branches +git fetch --all + +# Check if the branch exists +if ! git rev-parse --verify "$BRANCH" &>/dev/null && ! git rev-parse --verify "origin/$BRANCH" &>/dev/null; then + echo -e "${RED}Error: Branch '$BRANCH' does not exist locally or remotely${NC}" + exit 1 +fi + +# Checkout the specified branch +git checkout "$BRANCH" + +# Pull the latest changes +git pull origin "$BRANCH" +echo -e "${GREEN}✓ Successfully pulled latest changes${NC}" + +echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}" +npm ci +echo -e "${GREEN}✓ Dependencies installed${NC}" + +echo -e "\n${BLUE}Step 3: Running database migrations...${NC}" +# Check if migrations directory exists +if [ -d "./migrations" ]; then + echo "Applying database migrations..." + npm run migrate:up + echo -e "${GREEN}✓ Migrations applied successfully${NC}" +else + echo -e "${YELLOW}⚠ No migrations directory found, running database initialization script...${NC}" + npm run init:db + echo -e "${GREEN}✓ Database initialized${NC}" +fi + +echo -e "\n${BLUE}Step 4: Building the project...${NC}" +npm run build +echo -e "${GREEN}✓ Project built successfully${NC}" + +echo -e "\n${BLUE}Step 5: Checking for .env file...${NC}" +if [ -f .env ]; then + echo -e "${GREEN}✓ .env file exists${NC}" +else + echo -e "${YELLOW}⚠ .env file not found${NC}" + if [ -f .env.example ]; then + echo "Creating .env from .env.example" + cp .env.example .env + echo -e "${YELLOW}⚠ Please update the .env file with your configuration!${NC}" + else + echo -e "${RED}Error: .env.example file not found${NC}" + exit 1 + fi +fi + +echo -e "\n${BLUE}Step 6: Checking for services...${NC}" +# Check if Docker is being used +if [ -f docker-compose.yml ]; then + echo "Docker Compose configuration found" + echo "You might want to restart containers with: docker-compose down && docker-compose up -d" +fi + +# Check for PM2 process on Ubuntu +if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then + echo -e "\n${BLUE}Step 7: Checking PM2 service...${NC}" + if pm2 list | grep -q "tg_tinder_bot"; then + echo "PM2 service for tg_tinder_bot found" + if [ "$RESTART_SERVICE" = true ]; then + echo "Restarting PM2 service..." + pm2 restart tg_tinder_bot + echo -e "${GREEN}✓ PM2 service restarted${NC}" + else + echo "To restart the service, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}" + fi + fi +fi + +echo -e "\n${GREEN}========================================${NC}" +echo -e "${GREEN} Update completed successfully! ${NC}" +echo -e "${GREEN}========================================${NC}" + +if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then + echo -e "To start the bot with PM2, run: ${YELLOW}pm2 start dist/bot.js --name tg_tinder_bot${NC}" + echo -e "To restart the bot, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}" + echo -e "To view logs, run: ${YELLOW}pm2 logs tg_tinder_bot${NC}" +else + echo -e "To start the bot, run: ${YELLOW}npm run start${NC}" + echo -e "For development mode: ${YELLOW}npm run dev${NC}" +fi + +# Return to the original directory +cd "$CURRENT_DIR" diff --git a/check_schema.ts b/check_schema.ts new file mode 100644 index 0000000..3cc78f7 --- /dev/null +++ b/check_schema.ts @@ -0,0 +1,14 @@ +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/database.json b/database.json new file mode 100644 index 0000000..e1e870e --- /dev/null +++ b/database.json @@ -0,0 +1,7 @@ +{ + "connectionString": { + "ENV": "DATABASE_URL" + }, + "migrationsTable": "pgmigrations", + "migrationsDirectory": "./migrations" +} diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/DEPLOYMENT.md b/docs/DEPLOYMENT.md similarity index 100% rename from DEPLOYMENT.md rename to docs/DEPLOYMENT.md diff --git a/docs/DEPLOY_UBUNTU.md b/docs/DEPLOY_UBUNTU.md new file mode 100644 index 0000000..197c45d --- /dev/null +++ b/docs/DEPLOY_UBUNTU.md @@ -0,0 +1,221 @@ +# Деплой Telegram Tinder Bot на Ubuntu 24.04 + +Это руководство поможет вам настроить и развернуть Telegram Tinder Bot на сервере с Ubuntu 24.04. + +## Предварительные требования + +- Сервер с Ubuntu 24.04 +- Права администратора (sudo) +- Доменное имя (опционально, для SSL) + +## Шаг 1: Подготовка сервера + +```bash +# Обновите систему +sudo apt update && sudo apt upgrade -y + +# Установите необходимые пакеты +sudo apt install -y curl wget git build-essential postgresql postgresql-contrib nginx + +# Установите Node.js и npm +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs + +# Проверьте установку +node --version +npm --version +``` + +## Шаг 2: Настройка PostgreSQL + +```bash +# Запустите и включите PostgreSQL +sudo systemctl start postgresql +sudo systemctl enable postgresql + +# Подключитесь к PostgreSQL +sudo -u postgres psql + +# В консоли PostgreSQL создайте базу данных и пользователя +CREATE DATABASE tg_tinder_bot; +CREATE USER tg_bot WITH PASSWORD 'сложный_пароль'; +GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot; +\q + +# Проверьте подключение +psql -h localhost -U tg_bot -d tg_tinder_bot +# Введите пароль, когда будет запрошено +``` + +## Шаг 3: Клонирование репозитория и установка зависимостей + +```bash +# Создайте директорию для бота +sudo mkdir -p /opt/tg_tinder_bot +sudo chown $USER:$USER /opt/tg_tinder_bot + +# Клонируйте репозиторий +git clone https://your-git-repo-url.git /opt/tg_tinder_bot +cd /opt/tg_tinder_bot + +# Установите зависимости +npm ci + +# Сделайте скрипты исполняемыми +chmod +x bin/update.sh +``` + +## Шаг 4: Настройка окружения + +```bash +# Создайте файл .env из примера +cp .env.example .env + +# Отредактируйте файл .env +nano .env + +# Укажите следующие параметры: +# BOT_TOKEN=your_telegram_bot_token +# DB_HOST=localhost +# DB_PORT=5432 +# DB_USER=tg_bot +# DB_PASSWORD=сложный_пароль +# DB_NAME=tg_tinder_bot +# и другие необходимые параметры +``` + +## Шаг 5: Инициализация базы данных и сборка проекта + +```bash +# Запустите миграции +npm run migrate:up + +# Соберите проект +npm run build +``` + +## Шаг 6: Настройка PM2 для управления процессами + +```bash +# Установите PM2 глобально +sudo npm install -g pm2 + +# Запустите бота через PM2 +pm2 start dist/bot.js --name tg_tinder_bot + +# Настройте автозапуск PM2 +pm2 startup +# Выполните команду, которую выдаст предыдущая команда + +# Сохраните конфигурацию PM2 +pm2 save +``` + +## Шаг 7: Настройка Nginx (если нужен веб-интерфейс) + +```bash +# Создайте конфигурационный файл Nginx +sudo nano /etc/nginx/sites-available/tg_tinder_bot + +# Добавьте следующее содержимое +# server { +# listen 80; +# server_name ваш-домен.com; +# +# location / { +# proxy_pass http://localhost:3000; # Замените на порт вашего веб-интерфейса +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# proxy_set_header Host $host; +# proxy_cache_bypass $http_upgrade; +# } +# } + +# Создайте символьную ссылку +sudo ln -s /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/ + +# Проверьте конфигурацию Nginx +sudo nginx -t + +# Перезапустите Nginx +sudo systemctl restart nginx +``` + +## Шаг 8: Настройка SSL с Certbot (опционально, но рекомендуется) + +```bash +# Установите Certbot +sudo apt install -y certbot python3-certbot-nginx + +# Получите SSL-сертификат +sudo certbot --nginx -d ваш-домен.com + +# Certbot автоматически обновит конфигурацию Nginx +``` + +## Шаг 9: Настройка автоматического обновления + +```bash +# Отредактируйте crontab +crontab -e + +# Добавьте строку для ежедневного обновления в 4:00 +0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh >> /var/log/tg_bot_update.log 2>&1 +``` + +## Управление ботом + +```bash +# Перезапустить бота +pm2 restart tg_tinder_bot + +# Остановить бота +pm2 stop tg_tinder_bot + +# Посмотреть логи +pm2 logs tg_tinder_bot + +# Посмотреть статус +pm2 status +``` + +## Обновление вручную + +```bash +cd /opt/tg_tinder_bot +./bin/update.sh +``` + +## Резервное копирование базы данных + +```bash +# Создайте директорию для резервных копий +mkdir -p ~/backups + +# Создайте резервную копию +pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql + +# Автоматическое резервное копирование (добавьте в crontab) +# 0 3 * * * pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql && find ~/backups -name "tg_tinder_bot_*.sql" -mtime +7 -delete +``` + +## Решение проблем + +### Проблемы с базой данных +Проверьте журналы PostgreSQL: +```bash +sudo tail -f /var/log/postgresql/postgresql-*.log +``` + +### Проблемы с ботом +Проверьте журналы PM2: +```bash +pm2 logs tg_tinder_bot +``` + +### Проблемы с Nginx +Проверьте журналы Nginx: +```bash +sudo tail -f /var/log/nginx/error.log +``` diff --git a/LOCALIZATION.md b/docs/LOCALIZATION.md similarity index 100% rename from LOCALIZATION.md rename to docs/LOCALIZATION.md diff --git a/NATIVE_CHAT_SYSTEM.md b/docs/NATIVE_CHAT_SYSTEM.md similarity index 100% rename from NATIVE_CHAT_SYSTEM.md rename to docs/NATIVE_CHAT_SYSTEM.md diff --git a/PROJECT_SUMMARY.md b/docs/PROJECT_SUMMARY.md similarity index 100% rename from PROJECT_SUMMARY.md rename to docs/PROJECT_SUMMARY.md diff --git a/VIP_FUNCTIONS.md b/docs/VIP_FUNCTIONS.md similarity index 100% rename from VIP_FUNCTIONS.md rename to docs/VIP_FUNCTIONS.md diff --git a/migrations/1758144488937_initial-schema.js b/migrations/1758144488937_initial-schema.js new file mode 100644 index 0000000..9f6c344 --- /dev/null +++ b/migrations/1758144488937_initial-schema.js @@ -0,0 +1,152 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + // Создание расширения для генерации UUID + pgm.createExtension('pgcrypto', { ifNotExists: true }); + + // Таблица пользователей + pgm.createTable('users', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, + telegram_id: { type: 'bigint', notNull: true, unique: true }, + username: { type: 'varchar(255)' }, + first_name: { type: 'varchar(255)' }, + last_name: { type: 'varchar(255)' }, + language_code: { type: 'varchar(10)', default: 'en' }, + is_active: { type: 'boolean', default: true }, + created_at: { type: 'timestamp', default: pgm.func('NOW()') }, + last_active_at: { type: 'timestamp', default: pgm.func('NOW()') }, + updated_at: { type: 'timestamp', default: pgm.func('NOW()') }, + premium: { type: 'boolean', default: false }, + premium_expires_at: { type: 'timestamp' } + }); + + // Таблица профилей + pgm.createTable('profiles', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, + user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + name: { type: 'varchar(255)', notNull: true }, + age: { + type: 'integer', + notNull: true, + check: 'age >= 18 AND age <= 100' + }, + gender: { + type: 'varchar(10)', + notNull: true, + check: "gender IN ('male', 'female', 'other')" + }, + interested_in: { + type: 'varchar(10)', + notNull: true, + check: "interested_in IN ('male', 'female', 'both')" + }, + looking_for: { + type: 'varchar(20)', + default: 'both', + check: "looking_for IN ('male', 'female', 'both')" + }, + bio: { type: 'text' }, + photos: { type: 'jsonb', default: '[]' }, + interests: { type: 'jsonb', default: '[]' }, + city: { type: 'varchar(255)' }, + education: { type: 'varchar(255)' }, + job: { type: 'varchar(255)' }, + height: { type: 'integer' }, + religion: { type: 'varchar(255)' }, + dating_goal: { type: 'varchar(255)' }, + smoking: { type: 'boolean' }, + drinking: { type: 'boolean' }, + has_kids: { type: 'boolean' }, + location_lat: { type: 'decimal(10,8)' }, + location_lon: { type: 'decimal(11,8)' }, + search_min_age: { type: 'integer', default: 18 }, + search_max_age: { type: 'integer', default: 50 }, + search_max_distance: { type: 'integer', default: 50 }, + is_verified: { type: 'boolean', default: false }, + is_visible: { type: 'boolean', default: true }, + created_at: { type: 'timestamp', default: pgm.func('NOW()') }, + updated_at: { type: 'timestamp', default: pgm.func('NOW()') } + }); + + // Таблица свайпов + pgm.createTable('swipes', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, + user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + target_user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + type: { + type: 'varchar(20)', + notNull: true, + check: "type IN ('like', 'pass', 'superlike')" + }, + created_at: { type: 'timestamp', default: pgm.func('NOW()') }, + is_match: { type: 'boolean', default: false } + }); + pgm.addConstraint('swipes', 'unique_swipe', { + unique: ['user_id', 'target_user_id'] + }); + + // Таблица матчей + pgm.createTable('matches', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, + user_id_1: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + user_id_2: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + created_at: { type: 'timestamp', default: pgm.func('NOW()') }, + last_message_at: { type: 'timestamp' }, + is_active: { type: 'boolean', default: true }, + is_super_match: { type: 'boolean', default: false }, + unread_count_1: { type: 'integer', default: 0 }, + unread_count_2: { type: 'integer', default: 0 } + }); + pgm.addConstraint('matches', 'unique_match', { + unique: ['user_id_1', 'user_id_2'] + }); + + // Таблица сообщений + pgm.createTable('messages', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, + match_id: { type: 'uuid', references: 'matches(id)', onDelete: 'CASCADE' }, + sender_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + receiver_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' }, + content: { type: 'text', notNull: true }, + message_type: { + type: 'varchar(20)', + default: 'text', + check: "message_type IN ('text', 'photo', 'gif', 'sticker')" + }, + created_at: { type: 'timestamp', default: pgm.func('NOW()') }, + is_read: { type: 'boolean', default: false } + }); + + // Создание индексов + pgm.createIndex('users', 'telegram_id'); + pgm.createIndex('profiles', 'user_id'); + pgm.createIndex('profiles', ['location_lat', 'location_lon'], { + where: 'location_lat IS NOT NULL AND location_lon IS NOT NULL' + }); + pgm.createIndex('profiles', ['age', 'gender', 'interested_in']); + pgm.createIndex('swipes', ['user_id', 'target_user_id']); + pgm.createIndex('matches', ['user_id_1', 'user_id_2']); + pgm.createIndex('messages', ['match_id', 'created_at']); +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + pgm.dropTable('messages'); + pgm.dropTable('matches'); + pgm.dropTable('swipes'); + pgm.dropTable('profiles'); + pgm.dropTable('users'); + pgm.dropExtension('pgcrypto'); +}; diff --git a/migrations/1758144618548_add-missing-profile-columns.js b/migrations/1758144618548_add-missing-profile-columns.js new file mode 100644 index 0000000..25e99d3 --- /dev/null +++ b/migrations/1758144618548_add-missing-profile-columns.js @@ -0,0 +1,25 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + // Добавляем колонки, которые могли быть пропущены в схеме + pgm.addColumns('profiles', { + hobbies: { type: 'text' } + }); +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + pgm.dropColumns('profiles', ['hobbies']); +}; diff --git a/migrations/1758147898012_add-missing-religion-columns.js b/migrations/1758147898012_add-missing-religion-columns.js new file mode 100644 index 0000000..97e8e32 --- /dev/null +++ b/migrations/1758147898012_add-missing-religion-columns.js @@ -0,0 +1,18 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => {}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => {}; diff --git a/migrations/1758147903378_add-missing-religion-columns.js b/migrations/1758147903378_add-missing-religion-columns.js new file mode 100644 index 0000000..75328e7 --- /dev/null +++ b/migrations/1758147903378_add-missing-religion-columns.js @@ -0,0 +1,29 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + // Добавляем отсутствующие колонки в таблицу profiles + pgm.addColumns('profiles', { + religion: { type: 'varchar(255)' }, + dating_goal: { type: 'varchar(255)' }, + smoking: { type: 'boolean' }, + drinking: { type: 'boolean' }, + has_kids: { type: 'boolean' } + }, { ifNotExists: true }); +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + pgm.dropColumns('profiles', ['religion', 'dating_goal', 'smoking', 'drinking', 'has_kids'], { ifExists: true }); +}; diff --git a/migrations/1758148526228_update-lifestyle-columns-to-varchar.js b/migrations/1758148526228_update-lifestyle-columns-to-varchar.js new file mode 100644 index 0000000..97e8e32 --- /dev/null +++ b/migrations/1758148526228_update-lifestyle-columns-to-varchar.js @@ -0,0 +1,18 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => {}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => {}; diff --git a/migrations/1758148549355_update-lifestyle-columns-to-varchar.js b/migrations/1758148549355_update-lifestyle-columns-to-varchar.js new file mode 100644 index 0000000..97e8e32 --- /dev/null +++ b/migrations/1758148549355_update-lifestyle-columns-to-varchar.js @@ -0,0 +1,18 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => {}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => {}; diff --git a/migrations/1758148562238_update-lifestyle-columns-to-varchar.js b/migrations/1758148562238_update-lifestyle-columns-to-varchar.js new file mode 100644 index 0000000..c7d496b --- /dev/null +++ b/migrations/1758148562238_update-lifestyle-columns-to-varchar.js @@ -0,0 +1,42 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + // Изменяем тип столбцов с boolean на varchar для хранения строковых значений + pgm.alterColumn('profiles', 'smoking', { + type: 'varchar(50)', + using: 'smoking::text' + }); + + pgm.alterColumn('profiles', 'drinking', { + type: 'varchar(50)', + using: 'drinking::text' + }); + + // has_kids оставляем boolean, так как у него всего два состояния +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + // Возвращаем столбцы к типу boolean + pgm.alterColumn('profiles', 'smoking', { + type: 'boolean', + using: "CASE WHEN smoking = 'regularly' OR smoking = 'sometimes' THEN true ELSE false END" + }); + + pgm.alterColumn('profiles', 'drinking', { + type: 'boolean', + using: "CASE WHEN drinking = 'regularly' OR drinking = 'sometimes' THEN true ELSE false END" + }); +}; diff --git a/migrations/1758149087361_add-column-synonyms.js b/migrations/1758149087361_add-column-synonyms.js new file mode 100644 index 0000000..cd8b29c --- /dev/null +++ b/migrations/1758149087361_add-column-synonyms.js @@ -0,0 +1,50 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + // Создание представления для совместимости со старым кодом (swipes) + pgm.sql(` + CREATE OR REPLACE VIEW swipes_view AS + SELECT + id, + user_id AS swiper_id, + target_user_id AS swiped_id, + type AS direction, + created_at, + is_match + FROM swipes; + `); + + // Создание представления для совместимости со старым кодом (matches) + pgm.sql(` + CREATE OR REPLACE VIEW matches_view AS + SELECT + id, + user_id_1 AS user1_id, + user_id_2 AS user2_id, + created_at AS matched_at, + is_active AS status, + last_message_at, + is_super_match, + unread_count_1, + unread_count_2 + FROM matches; + `); +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + pgm.sql(`DROP VIEW IF EXISTS swipes_view;`); + pgm.sql(`DROP VIEW IF EXISTS matches_view;`); +}; diff --git a/package-lock.json b/package-lock.json index 836c1c4..d478b89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,9 @@ "axios": "^1.12.1", "dotenv": "^16.6.1", "i18next": "^25.5.2", + "node-pg-migrate": "^8.0.3", "node-telegram-bot-api": "^0.64.0", - "pg": "^8.11.3", + "pg": "^8.16.3", "sharp": "^0.32.6", "uuid": "^9.0.1" }, @@ -599,6 +600,123 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1105,7 +1223,7 @@ "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "dev": true, + "devOptional": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -1202,7 +1320,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1211,7 +1328,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1876,7 +1992,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1998,7 +2113,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2228,6 +2342,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2258,8 +2378,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -2417,7 +2536,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -2609,6 +2727,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2706,7 +2852,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -3257,7 +3402,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -3486,8 +3630,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isstream": { "version": "0.1.2", @@ -3572,6 +3715,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -4418,6 +4576,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4478,6 +4645,69 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-pg-migrate": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.3.tgz", + "integrity": "sha512-oKzZyzTULTryO1jehX19VnyPCGf3G/3oWZg3gODphvID56T0WjPOShTVPVnxGdlcueaIW3uAVrr7M8xLZq5TcA==", + "license": "MIT", + "dependencies": { + "glob": "~11.0.0", + "yargs": "~17.7.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, + "node_modules/node-pg-migrate/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-pg-migrate/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-releases": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", @@ -4669,6 +4899,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4709,7 +4945,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4720,6 +4955,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4729,6 +4989,7 @@ "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -5296,7 +5557,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5528,7 +5788,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5540,7 +5799,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5821,7 +6079,21 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5888,7 +6160,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6462,7 +6746,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6569,7 +6852,24 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6612,7 +6912,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -6627,7 +6926,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -6645,7 +6943,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index b6c765c..3579cb5 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,35 @@ "main": "dist/bot.js", "scripts": { "start": "node dist/bot.js", + "start:prod": "NODE_ENV=production node dist/bot.js", + "start:win:prod": "set NODE_ENV=production&& node dist/bot.js", "dev": "ts-node src/bot.ts", - "build": "tsc && cp -r src/locales dist/", + "build": "tsc && xcopy /E /I src\\locales dist\\locales", + "build:linux": "tsc && cp -R src/locales dist/", "test": "jest", - "db:init": "ts-node src/scripts/initDb.ts" + "test:bot": "ts-node tests/test-bot.ts", + "db:init": "ts-node src/scripts/initDb.ts", + "init:db": "ts-node src/scripts/initDb.ts", + "migrate": "node-pg-migrate", + "migrate:up": "node-pg-migrate up", + "migrate:down": "node-pg-migrate down", + "migrate:create": "node-pg-migrate create", + "premium:set-all": "ts-node src/scripts/setPremiumForAll.ts", + "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", + "update": "bash ./bin/update.sh", + "update:win": ".\\bin\\update.bat", + "start:sh": "bash ./bin/start_bot.sh" }, "dependencies": { "@types/node-telegram-bot-api": "^0.64.11", "axios": "^1.12.1", "dotenv": "^16.6.1", "i18next": "^25.5.2", + "node-pg-migrate": "^8.0.3", "node-telegram-bot-api": "^0.64.0", - "pg": "^8.11.3", + "pg": "^8.16.3", "sharp": "^0.32.6", "uuid": "^9.0.1" }, diff --git a/scripts/add-hobbies-column.js b/scripts/add-hobbies-column.js new file mode 100644 index 0000000..cf7930c --- /dev/null +++ b/scripts/add-hobbies-column.js @@ -0,0 +1,43 @@ +// add-hobbies-column.js +// Скрипт для добавления колонки hobbies в таблицу profiles + +const { Pool } = require('pg'); + +// Настройки подключения к базе данных +const pool = new Pool({ + host: '192.168.0.102', + port: 5432, + database: 'telegram_tinder_bot', + user: 'trevor', + password: 'Cl0ud_1985!' +}); + +async function addHobbiesColumn() { + try { + console.log('Подключение к базе данных...'); + const client = await pool.connect(); + + console.log('Добавление колонки hobbies в таблицу profiles...'); + + // SQL запрос для добавления колонки + const sql = ` + ALTER TABLE profiles + ADD COLUMN IF NOT EXISTS hobbies TEXT; + `; + + await client.query(sql); + console.log('✅ Колонка hobbies успешно добавлена в таблицу profiles'); + + // Закрытие соединения + client.release(); + await pool.end(); + console.log('Подключение к базе данных закрыто'); + } catch (error) { + console.error('❌ Ошибка при добавлении колонки:', error); + await pool.end(); + process.exit(1); + } +} + +// Запуск функции +addHobbiesColumn(); diff --git a/scripts/add-premium-columns-direct.js b/scripts/add-premium-columns-direct.js new file mode 100644 index 0000000..d9ed2be --- /dev/null +++ b/scripts/add-premium-columns-direct.js @@ -0,0 +1,44 @@ +// add-premium-columns.js +// Скрипт для добавления колонок premium и premium_expires_at в таблицу users + +const { Pool } = require('pg'); + +// Настройки подключения к базе данных - используем те же настройки, что и раньше +const pool = new Pool({ + host: '192.168.0.102', + port: 5432, + database: 'telegram_tinder_bot', + user: 'trevor', + password: 'Cl0ud_1985!' +}); + +async function addPremiumColumns() { + try { + console.log('Подключение к базе данных...'); + const client = await pool.connect(); + + console.log('Добавление колонок premium и premium_expires_at в таблицу users...'); + + // SQL запрос для добавления колонок + const sql = ` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP; + `; + + await client.query(sql); + console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users'); + + // Закрытие соединения + client.release(); + await pool.end(); + console.log('Подключение к базе данных закрыто'); + } catch (error) { + console.error('❌ Ошибка при добавлении колонок:', error); + await pool.end(); + process.exit(1); + } +} + +// Запуск функции +addPremiumColumns(); diff --git a/scripts/add-premium-columns.js b/scripts/add-premium-columns.js new file mode 100644 index 0000000..09d6248 --- /dev/null +++ b/scripts/add-premium-columns.js @@ -0,0 +1,40 @@ +// add-premium-columns.js +// Скрипт для добавления колонок premium и premium_expires_at в таблицу users + +const { Client } = require('pg'); + +// Настройки подключения к базе данных +const client = new Client({ + host: '192.168.0.102', + port: 5432, + user: 'trevor', + password: 'Cl0ud_1985!', + database: 'telegram_tinder_bot' +}); + +async function addPremiumColumns() { + try { + await client.connect(); + console.log('Подключение к базе данных успешно установлено'); + + // SQL запрос для добавления колонок + const sql = ` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP; + `; + + await client.query(sql); + console.log('Колонки premium и premium_expires_at успешно добавлены в таблицу users'); + + // Закрываем подключение + await client.end(); + console.log('Подключение к базе данных закрыто'); + } catch (error) { + console.error('Ошибка при добавлении колонок:', error); + await client.end(); + } +} + +// Запуск функции +addPremiumColumns(); diff --git a/scripts/add-premium-columns.ts b/scripts/add-premium-columns.ts new file mode 100644 index 0000000..82c721a --- /dev/null +++ b/scripts/add-premium-columns.ts @@ -0,0 +1,28 @@ +// add-premium-columns.ts +// Скрипт для добавления колонок premium и premium_expires_at в таблицу users + +import { query } from '../src/database/connection'; + +async function addPremiumColumns() { + try { + console.log('Добавление колонок premium и premium_expires_at в таблицу users...'); + + // SQL запрос для добавления колонок + const sql = ` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP; + `; + + await query(sql); + console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users'); + + process.exit(0); + } catch (error) { + console.error('❌ Ошибка при добавлении колонок:', error); + process.exit(1); + } +} + +// Запуск функции +addPremiumColumns(); diff --git a/scripts/create_profile_fix.js b/scripts/create_profile_fix.js new file mode 100644 index 0000000..7738517 --- /dev/null +++ b/scripts/create_profile_fix.js @@ -0,0 +1,101 @@ +// Исправленный код для создания профиля +const { Client } = require('pg'); +const { v4: uuidv4 } = require('uuid'); + +// Получаем аргументы из командной строки +const args = process.argv.slice(2); +const telegramId = args[0]; +const name = args[1]; +const age = parseInt(args[2]); +const gender = args[3]; +const city = args[4]; +const bio = args[5]; +const photoFileId = args[6]; + +// Проверяем, что все необходимые аргументы предоставлены +if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) { + console.error('Необходимо указать все параметры: telegramId, name, age, gender, city, bio, photoFileId'); + process.exit(1); +} + +// Устанавливаем соединение с базой данных +const client = new Client({ + host: '192.168.0.102', + port: 5432, + user: 'trevor', + password: 'Cl0ud_1985!', + database: 'telegram_tinder_bot' +}); + +async function createProfile() { + try { + await client.connect(); + + // Шаг 1: Создаем или обновляем пользователя + const userResult = await client.query(` + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES ($1, $2, $3, $4) + ON CONFLICT (telegram_id) DO UPDATE SET + username = EXCLUDED.username, + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name + RETURNING id + `, [parseInt(telegramId), null, name, null]); + + const userId = userResult.rows[0].id; + + // Шаг 2: Создаем профиль + const profileId = uuidv4(); + const now = new Date(); + const interestedIn = gender === 'male' ? 'female' : 'male'; + + const columns = [ + 'id', 'user_id', 'name', 'age', 'gender', 'interested_in', + 'bio', 'city', 'photos', 'is_verified', + 'is_visible', 'created_at', 'updated_at' + ].join(', '); + + const values = [ + profileId, userId, name, age, gender, interestedIn, + bio, city, JSON.stringify([photoFileId]), + false, true, now, now + ]; + + const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); + + await client.query(` + INSERT INTO profiles (${columns}) + VALUES (${placeholders}) + `, values); + + console.log('Профиль успешно создан!'); + + // Возвращаем информацию о созданном профиле + return { + userId, + profileId, + name, + age, + gender, + interestedIn, + bio, + city, + photos: [photoFileId] + }; + } catch (error) { + console.error('Ошибка при создании профиля:', error); + throw error; + } finally { + await client.end(); + } +} + +createProfile() + .then(profile => { + console.log('Созданный профиль:', profile); + process.exit(0); + }) + .catch(error => { + console.error('Создание профиля не удалось:', error); + process.exit(1); + }); diff --git a/scripts/migrate-sync.js b/scripts/migrate-sync.js new file mode 100644 index 0000000..3855eb6 --- /dev/null +++ b/scripts/migrate-sync.js @@ -0,0 +1,62 @@ +// migrate-sync.js +// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений +// Используется для синхронизации существующей базы с миграциями + +const { Client } = require('pg'); +const fs = require('fs'); +const path = require('path'); + +// Подключение к базе данных +const client = new Client({ + host: '192.168.0.102', + port: 5432, + database: 'telegram_tinder_bot', + user: 'trevor', + password: 'Cl0ud_1985!' +}); + +async function syncMigrations() { + try { + console.log('Подключение к базе данных...'); + await client.connect(); + + // Создаем таблицу миграций, если её нет + await client.query(` + CREATE TABLE IF NOT EXISTS pgmigrations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + ) + `); + + // Получаем список файлов миграций + const migrationsDir = path.join(__dirname, '../migrations'); + const files = fs.readdirSync(migrationsDir) + .filter(file => file.endsWith('.js')) + .sort(); + + // Проверяем, какие миграции уже записаны + const { rows: existingMigrations } = await client.query('SELECT name FROM pgmigrations'); + const existingNames = existingMigrations.map(m => m.name); + + // Записываем новые миграции + for (const file of files) { + const migrationName = file.replace('.js', ''); + if (!existingNames.includes(migrationName)) { + console.log(`Добавление записи о миграции: ${migrationName}`); + await client.query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]); + } else { + console.log(`Миграция ${migrationName} уже записана`); + } + } + + console.log('✅ Синхронизация миграций завершена успешно'); + await client.end(); + } catch (error) { + console.error('❌ Ошибка при синхронизации миграций:', error); + await client.end(); + process.exit(1); + } +} + +syncMigrations(); diff --git a/set-premium.js b/set-premium.js new file mode 100644 index 0000000..1cc5808 Binary files /dev/null and b/set-premium.js differ diff --git a/sql/add_looking_for.sql b/sql/add_looking_for.sql new file mode 100644 index 0000000..aeaf006 --- /dev/null +++ b/sql/add_looking_for.sql @@ -0,0 +1,2 @@ +-- Добавление колонки looking_for в таблицу profiles +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both')); diff --git a/sql/add_missing_columns.sql b/sql/add_missing_columns.sql new file mode 100644 index 0000000..044a6e0 --- /dev/null +++ b/sql/add_missing_columns.sql @@ -0,0 +1,7 @@ +-- Добавление недостающих колонок в таблицу profiles +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS hobbies TEXT[]; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS religion VARCHAR(255); +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS dating_goal VARCHAR(50); +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS smoking VARCHAR(20) CHECK (smoking IS NULL OR smoking IN ('never', 'sometimes', 'regularly')); +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS drinking VARCHAR(20) CHECK (drinking IS NULL OR drinking IN ('never', 'sometimes', 'regularly')); +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS has_kids BOOLEAN DEFAULT FALSE; diff --git a/sql/add_premium_columns.sql b/sql/add_premium_columns.sql new file mode 100644 index 0000000..e4de2a4 --- /dev/null +++ b/sql/add_premium_columns.sql @@ -0,0 +1,4 @@ +-- Добавление колонок premium и premium_expires_at в таблицу users +ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP; diff --git a/sql/add_updated_at.sql b/sql/add_updated_at.sql new file mode 100644 index 0000000..dc957db --- /dev/null +++ b/sql/add_updated_at.sql @@ -0,0 +1,21 @@ +-- Добавление колонки updated_at в таблицу users +ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP; + +-- Обновление триггера для автоматического обновления updated_at +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Проверка и создание триггера, если он не существует +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'users_updated_at') THEN + CREATE TRIGGER users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + END IF; +END$$; diff --git a/clear_database.sql b/sql/clear_database.sql similarity index 100% rename from clear_database.sql rename to sql/clear_database.sql diff --git a/sql/recreate_tables.sql b/sql/recreate_tables.sql new file mode 100644 index 0000000..41a0248 --- /dev/null +++ b/sql/recreate_tables.sql @@ -0,0 +1,67 @@ +-- Сначала создаем таблицы заново +DROP TABLE IF EXISTS profiles CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +-- Users table +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), + last_name VARCHAR(255), + language_code VARCHAR(10) DEFAULT 'en', + is_premium BOOLEAN DEFAULT FALSE, + is_blocked BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Profiles table +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 AND age <= 100), + gender VARCHAR(20) NOT NULL CHECK (gender IN ('male', 'female', 'other')), + looking_for VARCHAR(20) NOT NULL CHECK (looking_for IN ('male', 'female', 'both')), + bio TEXT, + location VARCHAR(255), + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + photos TEXT[], -- Array of photo URLs/file IDs + interests TEXT[], -- Array of interests + hobbies TEXT[], + education VARCHAR(255), + occupation VARCHAR(255), + height INTEGER, -- in cm + religion VARCHAR(255), + dating_goal VARCHAR(50), + smoking VARCHAR(20) CHECK (smoking IN ('never', 'sometimes', 'regularly')), + drinking VARCHAR(20) CHECK (drinking IN ('never', 'sometimes', 'regularly')), + has_kids BOOLEAN DEFAULT FALSE, + relationship_type VARCHAR(30) CHECK (relationship_type IN ('casual', 'serious', 'friendship', 'anything')), + verification_status VARCHAR(20) DEFAULT 'unverified' CHECK (verification_status IN ('unverified', 'pending', 'verified')), + is_active BOOLEAN DEFAULT TRUE, + is_visible BOOLEAN DEFAULT TRUE, + last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + 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; + +CREATE TRIGGER users_updated_at +BEFORE UPDATE ON users +FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER profiles_updated_at +BEFORE UPDATE ON profiles +FOR EACH ROW EXECUTE FUNCTION update_updated_at(); diff --git a/src/database/connection.ts b/src/database/connection.ts index f9960be..42646af 100644 --- a/src/database/connection.ts +++ b/src/database/connection.ts @@ -2,10 +2,10 @@ import { Pool, PoolConfig } from 'pg'; // Конфигурация пула соединений PostgreSQL const poolConfig: PoolConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5433'), + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || 'telegram_tinder_bot', - user: process.env.DB_USERNAME || 'postgres', + user: process.env.DB_USERNAME, ...(process.env.DB_PASSWORD && { password: process.env.DB_PASSWORD }), max: 20, // максимальное количество соединений в пуле idleTimeoutMillis: 30000, // закрыть соединения, простаивающие 30 секунд @@ -154,10 +154,10 @@ export async function initializeDatabase(): Promise { await query(` CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id); - CREATE INDEX IF NOT EXISTS idx_profiles_location ON profiles(latitude, longitude); - CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, looking_for); - CREATE INDEX IF NOT EXISTS idx_swipes_swiper_swiped ON swipes(swiper_id, swiped_id); - CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user1_id, user2_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); + CREATE INDEX IF NOT EXISTS idx_swipes_user ON swipes(user_id, target_user_id); + CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user_id_1, user_id_2); CREATE INDEX IF NOT EXISTS idx_messages_match ON messages(match_id, created_at); `); diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 010b68c..26a9e37 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -139,7 +139,10 @@ export class CallbackHandlers { // Просмотр анкет и свайпы else if (data === 'start_browsing') { - await this.handleStartBrowsing(chatId, telegramId); + 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_')) { @@ -330,7 +333,7 @@ export class CallbackHandlers { } // Начать просмотр анкет - async handleStartBrowsing(chatId: number, telegramId: string): Promise { + async handleStartBrowsing(chatId: number, telegramId: string, isNewUser: boolean = false): Promise { const profile = await this.profileService.getProfileByTelegramId(telegramId); if (!profile) { @@ -338,7 +341,7 @@ export class CallbackHandlers { return; } - await this.showNextCandidate(chatId, telegramId); + await this.showNextCandidate(chatId, telegramId, isNewUser); } // Следующий кандидат @@ -891,8 +894,8 @@ export class CallbackHandlers { } } - async showNextCandidate(chatId: number, telegramId: string): Promise { - const candidate = await this.matchingService.getNextCandidate(telegramId); + async showNextCandidate(chatId: number, telegramId: string, isNewUser: boolean = false): Promise { + const candidate = await this.matchingService.getNextCandidate(telegramId, isNewUser); if (!candidate) { const keyboard: InlineKeyboardMarkup = { @@ -1370,12 +1373,28 @@ export class CallbackHandlers { return; } - const lifestyle = profile.lifestyle || {}; - lifestyle[type as keyof typeof lifestyle] = value as any; + // Обновляем отдельные колонки напрямую, а не через объект lifestyle + const updates: any = {}; + + switch (type) { + case 'smoking': + updates.smoking = value; + break; + case 'drinking': + updates.drinking = value; + break; + case 'kids': + // Для поля has_kids, которое имеет тип boolean, преобразуем строковые значения + if (value === 'have') { + updates.has_kids = true; + } else { + // Для 'want', 'dont_want', 'unsure' ставим false + updates.has_kids = false; + } + break; + } - await this.profileService.updateProfile(profile.userId, { - lifestyle: lifestyle - }); + await this.profileService.updateProfile(profile.userId, updates); const typeTexts: { [key: string]: string } = { 'smoking': 'курение', diff --git a/src/handlers/enhancedChatHandlers.ts b/src/handlers/enhancedChatHandlers.ts index 8c7df54..59a353c 100644 --- a/src/handlers/enhancedChatHandlers.ts +++ b/src/handlers/enhancedChatHandlers.ts @@ -218,9 +218,10 @@ export class EnhancedChatHandlers { const messageId = await this.chatService.sendMessage( matchId, telegramId, - msg.text || '[Медиа]', - msg.photo ? 'photo' : 'text', - msg.photo ? msg.photo[msg.photo.length - 1].file_id : undefined + msg.photo ? + (msg.caption || '[Фото]') + ' [file_id: ' + msg.photo[msg.photo.length - 1].file_id + ']' : + (msg.text || '[Медиа]'), + msg.photo ? 'photo' : 'text' ); if (messageId) { diff --git a/src/handlers/messageHandlers.ts b/src/handlers/messageHandlers.ts index 68f62e0..f1617fb 100644 --- a/src/handlers/messageHandlers.ts +++ b/src/handlers/messageHandlers.ts @@ -217,11 +217,12 @@ export class MessageHandlers { } }); + // Добавляем специальный callback для новых пользователей const keyboard: InlineKeyboardMarkup = { inline_keyboard: [ [ { text: '👤 Мой профиль', callback_data: 'view_my_profile' }, - { text: '🔍 Начать поиск', callback_data: 'start_browsing' } + { text: '🔍 Начать поиск', callback_data: 'start_browsing_first' } ], [{ text: '⚙️ Настройки', callback_data: 'settings' }] ] @@ -493,7 +494,7 @@ export class MessageHandlers { updates.hobbies = value; break; case 'city': - // В БД поле называется 'location', но мы используем city в модели + // В БД поле называется 'city' (не 'location') updates.city = value; break; case 'job': diff --git a/src/scripts/cleanDb.ts b/src/scripts/cleanDb.ts new file mode 100644 index 0000000..0b11347 --- /dev/null +++ b/src/scripts/cleanDb.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env ts-node + +import 'dotenv/config'; +import { testConnection, closePool, query } from '../database/connection'; + +/** + * Очистка базы данных и пересоздание схемы с нуля + */ +async function main() { + console.log('🚀 Очистка базы данных...'); + + try { + // Проверяем подключение + const connected = await testConnection(); + if (!connected) { + console.error('❌ Не удалось подключиться к базе данных'); + process.exit(1); + } + + // Сначала проверяем наличие таблиц + const tablesExist = await checkTablesExist(); + + if (tablesExist) { + console.log('🔍 Таблицы существуют. Выполняем удаление...'); + + // Удаляем существующие таблицы в правильном порядке + await dropAllTables(); + console.log('✅ Все таблицы успешно удалены'); + } else { + console.log('⚠️ Таблицы не обнаружены'); + } + + console.log('🛠️ База данных очищена успешно'); + console.log('ℹ️ Теперь вы можете выполнить npm run init:db для создания новой схемы'); + + } catch (error) { + console.error('❌ Ошибка при очистке базы данных:', error); + process.exit(1); + } finally { + await closePool(); + console.log('👋 Соединение с базой данных закрыто'); + } +} + +/** + * Проверка существования таблиц в базе данных + */ +async function checkTablesExist(): Promise { + try { + const result = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name IN ('users', 'profiles', 'matches') + ); + `); + return result.rows[0].exists; + } catch (error) { + console.error('❌ Ошибка при проверке наличия таблиц:', error); + return false; + } +} + +/** + * Удаление всех таблиц из базы данных + */ +async function dropAllTables(): Promise { + try { + // Отключаем ограничения внешних ключей для удаления таблиц + await query('SET CONSTRAINTS ALL DEFERRED;'); + + // Удаляем таблицы в порядке, учитывающем зависимости + console.log('Удаление таблицы notifications...'); + await query('DROP TABLE IF EXISTS notifications CASCADE;'); + + console.log('Удаление таблицы scheduled_notifications...'); + await query('DROP TABLE IF EXISTS scheduled_notifications CASCADE;'); + + console.log('Удаление таблицы reports...'); + await query('DROP TABLE IF EXISTS reports CASCADE;'); + + console.log('Удаление таблицы blocks...'); + await query('DROP TABLE IF EXISTS blocks CASCADE;'); + + console.log('Удаление таблицы messages...'); + await query('DROP TABLE IF EXISTS messages CASCADE;'); + + console.log('Удаление таблицы matches...'); + await query('DROP TABLE IF EXISTS matches CASCADE;'); + + console.log('Удаление таблицы swipes...'); + await query('DROP TABLE IF EXISTS swipes CASCADE;'); + + console.log('Удаление таблицы profiles...'); + await query('DROP TABLE IF EXISTS profiles CASCADE;'); + + console.log('Удаление таблицы users...'); + await query('DROP TABLE IF EXISTS users CASCADE;'); + + console.log('Удаление таблицы pgmigrations...'); + await query('DROP TABLE IF EXISTS pgmigrations CASCADE;'); + + // Восстанавливаем ограничения внешних ключей + await query('SET CONSTRAINTS ALL IMMEDIATE;'); + } catch (error) { + console.error('❌ Ошибка при удалении таблиц:', error); + throw error; + } +} + +// Запуск скрипта +if (require.main === module) { + main(); +} + +export { main as cleanDB }; diff --git a/src/scripts/createTestData.ts b/src/scripts/createTestData.ts new file mode 100644 index 0000000..fae7b08 --- /dev/null +++ b/src/scripts/createTestData.ts @@ -0,0 +1,166 @@ +import { Pool } from 'pg'; +import { v4 as uuidv4 } from 'uuid'; +import 'dotenv/config'; + +async function createTestSwipes() { + 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 + }); + + try { + // Сначала получаем ID всех пользователей + const usersResult = await pool.query(` + SELECT users.id, telegram_id, first_name, gender + FROM users + JOIN profiles ON users.id = profiles.user_id + `); + + const users = usersResult.rows; + console.log('Пользователи в системе:'); + console.table(users); + + if (users.length < 2) { + console.log('Недостаточно пользователей для создания свайпов'); + return; + } + + // Создаем свайпы + console.log('Создаем тестовые свайпы...'); + + // Сначала проверим ограничения базы данных + const constraintsResult = await pool.query(` + SELECT + constraint_name, + table_name, + constraint_type + FROM + information_schema.table_constraints + WHERE + table_name = 'swipes' + `); + + console.log('Ограничения таблицы swipes:'); + console.table(constraintsResult.rows); + + // Создаем пары пользователей без дублирования + const userPairs = []; + const swipes = []; + + // Мужчины и женщины для создания пар + const maleUsers = users.filter(user => user.gender === 'male'); + const femaleUsers = users.filter(user => user.gender === 'female'); + + console.log(`Мужчин: ${maleUsers.length}, Женщин: ${femaleUsers.length}`); + + for (const male of maleUsers) { + for (const female of femaleUsers) { + // Мужчина -> Женщина (70% лайк, 20% пропуск, 10% суперлайк) + const randomNum1 = Math.random(); + let maleToFemaleType; + + if (randomNum1 < 0.7) { + maleToFemaleType = 'like'; + } else if (randomNum1 < 0.9) { + maleToFemaleType = 'pass'; + } else { + maleToFemaleType = 'superlike'; + } + + const maleToFemale = { + id: uuidv4(), + user_id: male.id, + target_user_id: female.id, + type: maleToFemaleType, + is_match: false, + created_at: new Date() + }; + + // Женщина -> Мужчина (80% шанс на лайк, если мужчина лайкнул) + if ((maleToFemaleType === 'like' || maleToFemaleType === 'superlike') && Math.random() < 0.8) { + const femaleToMale = { + id: uuidv4(), + user_id: female.id, + target_user_id: male.id, + type: Math.random() < 0.9 ? 'like' : 'superlike', + is_match: true, + created_at: new Date(new Date().getTime() + 1000) // На секунду позже + }; + + swipes.push(femaleToMale); + maleToFemale.is_match = true; + } + + swipes.push(maleToFemale); + } + } + + console.log(`Подготовлено ${swipes.length} свайпов для добавления в базу`); + + // Сначала удаляем все существующие свайпы + await pool.query('DELETE FROM swipes'); + console.log('Существующие свайпы удалены'); + + // Добавляем новые свайпы + for (const swipe of swipes) { + await pool.query(` + INSERT INTO swipes (id, user_id, target_user_id, type, is_match, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + `, [swipe.id, swipe.user_id, swipe.target_user_id, swipe.type, swipe.is_match, swipe.created_at]); + } + + console.log(`Успешно добавлено ${swipes.length} свайпов`); + + // Создаем матчи для взаимных лайков + console.log('Создаем матчи для взаимных лайков...'); + + // Сначала удаляем все существующие матчи + await pool.query('DELETE FROM matches'); + console.log('Существующие матчи удалены'); + + // Находим пары взаимных лайков для создания матчей + const mutualLikesResult = await pool.query(` + SELECT + s1.user_id as user_id_1, + s1.target_user_id as user_id_2, + s1.created_at + FROM swipes s1 + JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id + WHERE (s1.type = 'like' OR s1.type = 'superlike') + AND (s2.type = 'like' OR s2.type = 'superlike') + AND s1.user_id < s2.user_id -- Избегаем дублирования пар + `); + + const matches = []; + + for (const mutualLike of mutualLikesResult.rows) { + const match = { + id: uuidv4(), + user_id_1: mutualLike.user_id_1, + user_id_2: mutualLike.user_id_2, + created_at: new Date(), + is_active: true, + is_super_match: Math.random() < 0.2 // 20% шанс быть суперматчем + }; + + matches.push(match); + + await pool.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) + `, [match.id, match.user_id_1, match.user_id_2, match.created_at, match.is_active, match.is_super_match]); + } + + console.log(`Успешно создано ${matches.length} матчей`); + + } catch (error) { + console.error('Ошибка при создании тестовых данных:', error); + } finally { + await pool.end(); + } +} + +createTestSwipes(); diff --git a/src/scripts/getDatabaseInfo.ts b/src/scripts/getDatabaseInfo.ts new file mode 100644 index 0000000..d7a911c --- /dev/null +++ b/src/scripts/getDatabaseInfo.ts @@ -0,0 +1,120 @@ +import { Pool } from 'pg'; +import 'dotenv/config'; + +async function getDatabaseInfo() { + 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 + }); + + try { + console.log('Подключение к базе данных...'); + + // Получаем информацию о пользователях + console.log('\n=== ПОЛЬЗОВАТЕЛИ ==='); + const usersResult = await pool.query(` + SELECT id, telegram_id, username, first_name, last_name, premium, created_at + FROM users + ORDER BY created_at DESC + `); + + console.log(`Всего пользователей: ${usersResult.rows.length}`); + console.table(usersResult.rows); + + // Получаем информацию о профилях + console.log('\n=== ПРОФИЛИ ==='); + const profilesResult = await pool.query(` + SELECT + p.user_id, + u.telegram_id, + u.first_name, + p.age, + p.gender, + p.interested_in as "интересуется", + p.bio, + p.dating_goal as "цель_знакомства", + p.is_visible, + p.created_at + FROM profiles p + JOIN users u ON p.user_id = u.id + ORDER BY p.created_at DESC + `); + + console.log(`Всего профилей: ${profilesResult.rows.length}`); + console.table(profilesResult.rows); + + // Получаем информацию о свайпах + console.log('\n=== СВАЙПЫ ==='); + + // Сначала проверим, какие столбцы есть в таблице swipes + console.log('Получение структуры таблицы swipes...'); + const swipesColumns = await pool.query(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'swipes' + `); + console.log('Структура таблицы swipes:'); + console.table(swipesColumns.rows); + + // Теперь запросим данные, используя правильные имена столбцов + const swipesResult = await pool.query(` + SELECT + s.id, + s.user_id, + u1.first_name as "от_кого", + s.target_user_id, + u2.first_name as "кому", + s.type, + s.created_at + FROM swipes s + JOIN users u1 ON s.user_id = u1.id + JOIN users u2 ON s.target_user_id = u2.id + ORDER BY s.created_at DESC + `); + + console.log(`Всего свайпов: ${swipesResult.rows.length}`); + console.table(swipesResult.rows); + + // Получаем информацию о матчах + console.log('\n=== МАТЧИ ==='); + + // Сначала проверим, какие столбцы есть в таблице matches + console.log('Получение структуры таблицы matches...'); + const matchesColumns = await pool.query(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'matches' + `); + console.log('Структура таблицы matches:'); + console.table(matchesColumns.rows); + + // Теперь запросим данные, используя правильные имена столбцов + const matchesResult = await pool.query(` + SELECT + m.id, + m.user_id_1, + u1.first_name as "пользователь_1", + m.user_id_2, + u2.first_name as "пользователь_2", + m.is_active, + m.created_at + FROM matches m + JOIN users u1 ON m.user_id_1 = u1.id + JOIN users u2 ON m.user_id_2 = u2.id + ORDER BY m.created_at DESC + `); + + console.log(`Всего матчей: ${matchesResult.rows.length}`); + console.table(matchesResult.rows); + + } catch (error) { + console.error('Ошибка при получении данных:', error); + } finally { + await pool.end(); + } +} + +getDatabaseInfo(); diff --git a/src/scripts/initDb.ts b/src/scripts/initDb.ts index 2497954..72cd4f4 100644 --- a/src/scripts/initDb.ts +++ b/src/scripts/initDb.ts @@ -1,7 +1,13 @@ #!/usr/bin/env ts-node -import { initializeDatabase, testConnection, closePool } from '../database/connection'; +import 'dotenv/config'; +import { initializeDatabase, testConnection, closePool, query } from '../database/connection'; +import * as path from 'path'; +import * as fs from 'fs'; +/** + * Основная функция инициализации базы данных + */ async function main() { console.log('🚀 Initializing database...'); @@ -13,90 +19,245 @@ async function main() { process.exit(1); } - // Инициализируем схему - await initializeDatabase(); - console.log('✅ Database initialized successfully'); + // Проверяем наличие таблицы миграций + const migrationTableExists = await checkMigrationsTable(); + + if (migrationTableExists) { + console.log('🔍 Миграции уже настроены'); + + // Проверяем, есть ли необходимость в применении миграций + const pendingMigrations = await getPendingMigrations(); + if (pendingMigrations.length > 0) { + console.log(`🔄 Найдено ${pendingMigrations.length} ожидающих миграций`); + console.log('✅ Рекомендуется запустить: npm run migrate:up'); + } else { + console.log('✅ Все миграции уже применены'); + } + } else { + console.log('⚠️ Таблица миграций не обнаружена'); + console.log('🛠️ Выполняется инициализация базы данных напрямую...'); + + // Выполняем традиционную инициализацию + await initializeDatabase(); + console.log('✅ База данных инициализирована'); + + // Создаем дополнительные таблицы + await createAdditionalTables(); + console.log('✅ Дополнительные таблицы созданы'); + + // Создаем таблицу миграций и отмечаем существующие миграции как выполненные + await setupMigrations(); + console.log('✅ Настройка миграций завершена'); + } - // Создаем дополнительные таблицы, если нужно - await createAdditionalTables(); - console.log('✅ Additional tables created'); + // Проверяем наличие необходимых колонок + await ensureRequiredColumns(); + console.log('✅ Все необходимые колонки присутствуют'); } catch (error) { - console.error('❌ Database initialization failed:', error); + console.error('❌ Ошибка инициализации базы данных:', error); process.exit(1); } finally { await closePool(); - console.log('👋 Database connection closed'); + console.log('👋 Соединение с базой данных закрыто'); } } -async function createAdditionalTables() { - const { query } = await import('../database/connection'); +/** + * Проверка наличия таблицы миграций + */ +async function checkMigrationsTable(): Promise { + try { + const result = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'pgmigrations' + ); + `); + return result.rows[0].exists; + } catch (error) { + console.error('❌ Ошибка при проверке таблицы миграций:', error); + return false; + } +} - // Таблица для уведомлений - await query(` - CREATE TABLE IF NOT EXISTS notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - type VARCHAR(50) NOT NULL, - data JSONB DEFAULT '{}', - is_read BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT NOW() - ); - `); +/** + * Получение списка ожидающих миграций + */ +async function getPendingMigrations(): Promise { + try { + // Получаем выполненные миграции + const { rows } = await query('SELECT name FROM pgmigrations'); + const appliedMigrations = rows.map((row: { name: string }) => row.name); + + // Получаем файлы миграций + const migrationsDir = path.join(__dirname, '../../migrations'); + if (!fs.existsSync(migrationsDir)) { + return []; + } + + const migrationFiles = fs.readdirSync(migrationsDir) + .filter(file => file.endsWith('.js')) + .map(file => file.replace('.js', '')) + .sort(); + + // Находим невыполненные миграции + return migrationFiles.filter(file => !appliedMigrations.includes(file)); + } catch (error) { + console.error('❌ Ошибка при проверке ожидающих миграций:', error); + return []; + } +} - // Таблица для запланированных уведомлений - await query(` - CREATE TABLE IF NOT EXISTS scheduled_notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - type VARCHAR(50) NOT NULL, - data JSONB DEFAULT '{}', - scheduled_at TIMESTAMP NOT NULL, - sent BOOLEAN DEFAULT false, - sent_at TIMESTAMP, - created_at TIMESTAMP DEFAULT NOW() - ); - `); +/** + * Настройка системы миграций и отметка существующих миграций как выполненных + */ +async function setupMigrations(): Promise { + try { + // Создаем таблицу миграций + await query(` + CREATE TABLE IF NOT EXISTS pgmigrations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + ) + `); + + // Получаем файлы миграций + const migrationsDir = path.join(__dirname, '../../migrations'); + if (!fs.existsSync(migrationsDir)) { + console.log('⚠️ Директория миграций не найдена'); + return; + } + + const files = fs.readdirSync(migrationsDir) + .filter(file => file.endsWith('.js')) + .sort(); + + // Отмечаем существующие миграции как выполненные + for (const file of files) { + const migrationName = file.replace('.js', ''); + console.log(`✅ Отмечаем миграцию как выполненную: ${migrationName}`); + await query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]); + } + } catch (error) { + console.error('❌ Ошибка при настройке миграций:', error); + throw error; + } +} - // Таблица для отчетов и блокировок - await query(` - CREATE TABLE IF NOT EXISTS reports ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - reporter_id UUID REFERENCES users(id) ON DELETE CASCADE, - reported_id UUID REFERENCES users(id) ON DELETE CASCADE, - reason VARCHAR(100) NOT NULL, - description TEXT, - status VARCHAR(20) DEFAULT 'pending', - created_at TIMESTAMP DEFAULT NOW(), - resolved_at TIMESTAMP - ); - `); +/** + * Создание дополнительных таблиц для приложения + */ +async function createAdditionalTables(): Promise { + try { + // Таблица для уведомлений + await query(` + CREATE TABLE IF NOT EXISTS notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB DEFAULT '{}', + is_read BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT NOW() + ); + `); - // Таблица для блокировок - await query(` - CREATE TABLE IF NOT EXISTS blocks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - blocker_id UUID REFERENCES users(id) ON DELETE CASCADE, - blocked_id UUID REFERENCES users(id) ON DELETE CASCADE, - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(blocker_id, blocked_id) - ); - `); + // Таблица для запланированных уведомлений + await query(` + CREATE TABLE IF NOT EXISTS scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB DEFAULT '{}', + scheduled_at TIMESTAMP NOT NULL, + sent BOOLEAN DEFAULT false, + sent_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() + ); + `); - // Добавляем недостающие поля в users - await query(` - ALTER TABLE users - ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}'; - `); + // Таблица для отчетов и блокировок + await query(` + CREATE TABLE IF NOT EXISTS reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + reporter_id UUID REFERENCES users(id) ON DELETE CASCADE, + reported_id UUID REFERENCES users(id) ON DELETE CASCADE, + reason VARCHAR(100) NOT NULL, + description TEXT, + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT NOW(), + resolved_at TIMESTAMP + ); + `); - // Индексы для производительности - await query(` - CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type); - CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent); - CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status); - CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id); - `); + // Таблица для блокировок + await query(` + CREATE TABLE IF NOT EXISTS blocks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + blocker_id UUID REFERENCES users(id) ON DELETE CASCADE, + blocked_id UUID REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(blocker_id, blocked_id) + ); + `); + + // Добавляем настройки уведомлений в users + await query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}'; + `); + + // Индексы для производительности + await query(` + CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type); + CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent); + CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status); + CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id); + `); + } catch (error) { + console.error('❌ Ошибка при создании дополнительных таблиц:', error); + throw error; + } +} + +/** + * Проверка наличия всех необходимых колонок + */ +async function ensureRequiredColumns(): Promise { + try { + // Проверка и добавление колонки updated_at в users + await query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW(); + `); + + // Проверка и добавление колонок premium и premium_expires_at в users + await query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE; + `); + + await query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP; + `); + + // Проверка и добавление колонки looking_for в profiles + await query(` + ALTER TABLE profiles + ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both')); + `); + + // Проверка и добавление колонки hobbies в profiles + await query(` + ALTER TABLE profiles + ADD COLUMN IF NOT EXISTS hobbies TEXT; + `); + } catch (error) { + console.error('❌ Ошибка при проверке необходимых колонок:', error); + throw error; + } } // Запуск скрипта diff --git a/src/scripts/setPremium.ts b/src/scripts/setPremium.ts new file mode 100644 index 0000000..68a62bb --- /dev/null +++ b/src/scripts/setPremium.ts @@ -0,0 +1,42 @@ +import { Pool } from 'pg'; + +async function setPremium(): Promise { + // Создаем соединение с базой данных используя прямые параметры + const pool = new Pool({ + host: '192.168.0.102', + port: 5432, + database: 'telegram_tinder_bot', + user: 'trevor', + password: 'Cl0ud_1985!', + }); + + try { + // Установка премиума для всех пользователей + const result = await pool.query(` + UPDATE users + SET premium = true, + premium_expires_at = NOW() + INTERVAL '1 year' + `); + + console.log('Premium set for all users successfully!'); + console.log(`Updated ${result.rowCount} users`); + + // Закрываем соединение с базой данных + await pool.end(); + + process.exit(0); + } catch (error) { + console.error('Error setting premium:', error); + + // Закрываем соединение с базой данных в случае ошибки + try { + await pool.end(); + } catch (e) { + console.error('Error closing pool:', e); + } + + process.exit(1); + } +} + +setPremium(); diff --git a/src/scripts/setPremiumDirectConnect.ts b/src/scripts/setPremiumDirectConnect.ts new file mode 100644 index 0000000..c5ed4b0 --- /dev/null +++ b/src/scripts/setPremiumDirectConnect.ts @@ -0,0 +1,43 @@ +import { Pool } from 'pg'; +import 'dotenv/config'; + +async function setAllUsersToPremium() { + 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 + }); + + try { + console.log('Setting premium status for all users...'); + console.log(`Connecting to database at ${process.env.DB_HOST}:${process.env.DB_PORT}...`); + + const result = await pool.query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, username, first_name, premium + `); + + console.log(`Successfully set premium status for ${result.rows.length} users:`); + console.table(result.rows); + + console.log('All users are now premium!'); + } catch (error) { + console.error('Error setting premium status:', error); + console.error('Please check your database connection settings in .env file.'); + console.log('Current settings:'); + console.log(`- DB_HOST: ${process.env.DB_HOST}`); + console.log(`- DB_PORT: ${process.env.DB_PORT}`); + console.log(`- DB_NAME: ${process.env.DB_NAME}`); + console.log(`- DB_USERNAME: ${process.env.DB_USERNAME}`); + console.log(`- DB_PASSWORD: ${process.env.DB_PASSWORD ? '********' : 'not set'}`); + } finally { + await pool.end(); + process.exit(0); + } +} + +setAllUsersToPremium(); diff --git a/src/scripts/setPremiumForAll.ts b/src/scripts/setPremiumForAll.ts new file mode 100644 index 0000000..208a2da --- /dev/null +++ b/src/scripts/setPremiumForAll.ts @@ -0,0 +1,25 @@ +import { query } from '../database/connection'; + +async function setAllUsersToPremium() { + try { + console.log('Setting premium status for all users...'); + + const result = await query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, username, first_name, premium + `); + + console.log(`Successfully set premium status for ${result.rows.length} users:`); + console.table(result.rows); + + console.log('All users are now premium!'); + } catch (error) { + console.error('Error setting premium status:', error); + } finally { + process.exit(0); + } +} + +setAllUsersToPremium(); diff --git a/src/services/chatService.ts b/src/services/chatService.ts index fcb4c84..d06acdd 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -24,8 +24,8 @@ export class ChatService { SELECT m.*, CASE - WHEN m.user1_id = $1 THEN m.user2_id - ELSE m.user1_id + WHEN m.user_id_1 = $1 THEN m.user_id_2 + ELSE m.user_id_1 END as other_user_id, p.name as other_user_name, p.photos as other_user_photos, @@ -42,8 +42,8 @@ export class ChatService { FROM matches m LEFT JOIN profiles p ON ( CASE - WHEN m.user1_id = $1 THEN p.user_id = m.user2_id - ELSE p.user_id = m.user1_id + WHEN m.user_id_1 = $1 THEN p.user_id = m.user_id_2 + ELSE p.user_id = m.user_id_1 END ) LEFT JOIN messages msg ON msg.id = ( @@ -52,10 +52,10 @@ export class ChatService { ORDER BY created_at DESC LIMIT 1 ) - WHERE (m.user1_id = $1 OR m.user2_id = $1) - AND m.status = 'active' + WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1) + AND m.is_active = true ORDER BY - CASE WHEN msg.created_at IS NULL THEN m.matched_at ELSE msg.created_at END DESC + CASE WHEN msg.created_at IS NULL THEN m.created_at ELSE msg.created_at END DESC `, [userId]); return result.rows.map((row: any) => ({ @@ -91,7 +91,6 @@ export class ChatService { senderId: row.sender_id, content: row.content, messageType: row.message_type, - fileId: row.file_id, isRead: row.is_read, createdAt: new Date(row.created_at) })).reverse(); // Возвращаем в хронологическом порядке @@ -106,8 +105,7 @@ export class ChatService { matchId: string, senderTelegramId: string, content: string, - messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text', - fileId?: string + messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text' ): Promise { try { // Получаем senderId по telegramId @@ -119,7 +117,7 @@ export class ChatService { // Проверяем, что матч активен и пользователь является участником const matchResult = await query(` SELECT * FROM matches - WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active' + WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true `, [matchId, senderId]); if (matchResult.rows.length === 0) { @@ -130,9 +128,9 @@ export class ChatService { // Создаем сообщение await query(` - INSERT INTO messages (id, match_id, sender_id, content, message_type, file_id, is_read, created_at) - VALUES ($1, $2, $3, $4, $5, $6, false, CURRENT_TIMESTAMP) - `, [messageId, matchId, senderId, content, messageType, fileId]); + INSERT INTO messages (id, match_id, sender_id, content, message_type, is_read, created_at) + VALUES ($1, $2, $3, $4, $5, false, CURRENT_TIMESTAMP) + `, [messageId, matchId, senderId, content, messageType]); // Обновляем время последнего сообщения в матче await query(` @@ -157,7 +155,6 @@ export class ChatService { senderId: row.sender_id, content: row.content, messageType: row.message_type, - fileId: row.file_id, isRead: row.is_read, createdAt: new Date(row.created_at) }); @@ -197,11 +194,11 @@ export class ChatService { SELECT m.*, CASE - WHEN m.user1_id = $2 THEN m.user2_id - ELSE m.user1_id + WHEN m.user_id_1 = $2 THEN m.user_id_2 + ELSE m.user_id_1 END as other_user_id FROM matches m - WHERE m.id = $1 AND (m.user1_id = $2 OR m.user2_id = $2) AND m.status = 'active' + WHERE m.id = $1 AND (m.user_id_1 = $2 OR m.user_id_2 = $2) AND m.is_active = true `, [matchId, userId]); if (result.rows.length === 0) { @@ -234,7 +231,7 @@ export class ChatService { // Проверяем, что пользователь является участником матча const matchResult = await query(` SELECT * FROM matches - WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active' + WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true `, [matchId, userId]); if (matchResult.rows.length === 0) { @@ -244,9 +241,11 @@ export class ChatService { // Помечаем матч как неактивный await query(` UPDATE matches - SET status = 'unmatched' + SET is_active = false, + unmatched_at = NOW(), + unmatched_by = $2 WHERE id = $1 - `, [matchId]); + `, [matchId, userId]); return true; } catch (error) { diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index 4540612..9dde927 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -70,7 +70,7 @@ export class MatchingService { await transaction(async (client) => { // Создаем свайп await client.query(` - INSERT INTO swipes (id, swiper_id, swiped_id, direction, created_at) + INSERT INTO swipes (id, user_id, target_user_id, direction, created_at) VALUES ($1, $2, $3, $4, $5) `, [swipeId, userId, targetUserId, direction, new Date()]); @@ -78,14 +78,14 @@ export class MatchingService { if (swipeType === 'like' || swipeType === 'superlike') { const reciprocalSwipe = await client.query(` SELECT * FROM swipes - WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('like', 'super') + WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super') `, [targetUserId, userId]); if (reciprocalSwipe.rows.length > 0) { // Проверяем, что матч еще не существует const existingMatch = await client.query(` SELECT * FROM matches - WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1) + 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) { @@ -98,9 +98,9 @@ export class MatchingService { // Создаем матч await client.query(` - INSERT INTO matches (id, user1_id, user2_id, matched_at, status) + INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active) VALUES ($1, $2, $3, $4, $5) - `, [matchId, user1Id, user2Id, new Date(), 'active']); + `, [matchId, user1Id, user2Id, new Date(), true]); match = new Match({ id: matchId, @@ -143,7 +143,7 @@ export class MatchingService { async getSwipe(userId: string, targetUserId: string): Promise { const result = await query(` SELECT * FROM swipes - WHERE swiper_id = $1 AND swiped_id = $2 + WHERE user_id = $1 AND target_user_id = $2 `, [userId, targetUserId]); if (result.rows.length === 0) { @@ -163,8 +163,8 @@ export class MatchingService { const result = await query(` SELECT * FROM matches - WHERE (user1_id = $1 OR user2_id = $1) AND status = 'active' - ORDER BY matched_at DESC + WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true + ORDER BY created_at DESC LIMIT $2 `, [userId, limit]); @@ -217,7 +217,7 @@ export class MatchingService { async getRecentLikes(userId: string, limit: number = 20): Promise { const result = await query(` SELECT * FROM swipes - WHERE swiped_id = $1 AND direction IN ('like', 'super') AND is_match = false + WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false ORDER BY created_at DESC LIMIT $2 `, [userId, limit]); @@ -311,11 +311,11 @@ export class MatchingService { private mapEntityToMatch(entity: any): Match { return new Match({ id: entity.id, - userId1: entity.user1_id, - userId2: entity.user2_id, - createdAt: entity.matched_at || entity.created_at, + userId1: entity.user_id_1, + userId2: entity.user_id_2, + createdAt: entity.created_at, lastMessageAt: entity.last_message_at, - isActive: entity.status === 'active', + isActive: entity.is_active === true, isSuperMatch: false, // Определяется из swipes если нужно unreadCount1: 0, unreadCount2: 0 @@ -329,8 +329,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.type IN ('like', 'superlike') - AND s2.type IN ('like', 'superlike') + AND s1.direction IN ('right', 'super') + AND s2.direction IN ('right', 'super') 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) @@ -342,7 +342,7 @@ export class MatchingService { } // Получить следующего кандидата для просмотра - async getNextCandidate(telegramId: string): Promise { + async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise { // Сначала получаем профиль пользователя по telegramId const userProfile = await this.profileService.getProfileByTelegramId(telegramId); if (!userProfile) { @@ -354,18 +354,26 @@ export class MatchingService { // Получаем список уже просмотренных пользователей const viewedUsers = await query(` - SELECT DISTINCT swiped_id + SELECT DISTINCT target_user_id FROM swipes - WHERE swiper_id = $1 + WHERE user_id = $1 `, [userId]); - const viewedUserIds = viewedUsers.rows.map((row: any) => row.swiped_id); + const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id); viewedUserIds.push(userId); // Исключаем самого себя - // Формируем условие для исключения уже просмотренных - const excludeCondition = viewedUserIds.length > 0 - ? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})` - : ''; + // Если это новый пользователь или у пользователя мало просмотренных профилей, + // показываем всех пользователей по очереди (исключая только себя) + let excludeCondition = ''; + + if (!isNewUser) { + excludeCondition = viewedUserIds.length > 0 + ? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})` + : ''; + } else { + // Для новых пользователей исключаем только себя + excludeCondition = `AND p.user_id != $2`; + } // Ищем подходящих кандидатов const candidateQuery = ` diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index ab60745..707426f 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -233,8 +233,8 @@ export class NotificationService { SELECT m.created_at FROM messages m JOIN matches mt ON m.match_id = mt.id - WHERE (mt.user1_id = $1 OR mt.user2_id = $1) - AND (mt.user1_id = $2 OR mt.user2_id = $2) + 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 @@ -347,10 +347,33 @@ 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 processed = false + WHERE scheduled_at <= $1 AND is_processed = false ORDER BY scheduled_at ASC LIMIT 100 `, [new Date()]); @@ -370,7 +393,7 @@ export class NotificationService { // Отмечаем как обработанное await query( - 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', + 'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1', [notification.id] ); } catch (error) { diff --git a/src/services/profileService.ts b/src/services/profileService.ts index 7eec66b..8b1a367 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -49,18 +49,15 @@ export class ProfileService { // Сохранение в базу данных await query(` INSERT INTO profiles ( - id, user_id, name, age, gender, looking_for, bio, photos, interests, - hobbies, location, education, occupation, height, religion, dating_goal, - latitude, longitude, verification_status, is_active, is_visible, - created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) + id, user_id, name, age, gender, interested_in, bio, photos, + city, education, job, height, religion, dating_goal, + is_verified, is_visible, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) `, [ profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn, - profile.bio, profile.photos, profile.interests, profile.hobbies, - profile.city, profile.education, profile.job, profile.height, - profile.religion, profile.datingGoal, - profile.location?.latitude, profile.location?.longitude, - 'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt + profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job, + profile.height, profile.religion, profile.datingGoal, + profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt ]); return profile; @@ -137,8 +134,7 @@ export class ProfileService { ON CONFLICT (telegram_id) DO UPDATE SET username = EXCLUDED.username, first_name = EXCLUDED.first_name, - last_name = EXCLUDED.last_name, - updated_at = CURRENT_TIMESTAMP + last_name = EXCLUDED.last_name RETURNING id `, [ parseInt(telegramId), @@ -177,12 +173,8 @@ export class ProfileService { updateValues.push(value); break; case 'location': - if (value && typeof value === 'object' && 'latitude' in value) { - updateFields.push(`latitude = $${paramIndex++}`); - updateValues.push(value.latitude); - updateFields.push(`longitude = $${paramIndex++}`); - updateValues.push(value.longitude); - } + // Пропускаем обработку местоположения, так как колонки location нет + console.log('Skipping location update - column does not exist'); break; case 'searchPreferences': // Поля search preferences больше не хранятся в БД, пропускаем @@ -339,8 +331,8 @@ export class ProfileService { const [likesResult, matchesResult, likesReceivedResult] = await Promise.all([ query('SELECT COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND direction IN ($2, $3)', [userId, 'like', 'super']), - query('SELECT COUNT(*) as count FROM matches WHERE (user1_id = $1 OR user2_id = $1) AND status = $2', - [userId, 'active']), + query('SELECT COUNT(*) as count FROM matches WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true', + [userId]), query('SELECT COUNT(*) as count FROM swipes WHERE swiped_id = $1 AND direction IN ($2, $3)', [userId, 'like', 'super']) ]); @@ -424,6 +416,27 @@ export class ProfileService { return []; }; + // Функция для парсинга JSON полей + const parseJsonField = (jsonField: any): any[] => { + if (!jsonField) return []; + + // Если это строка, пробуем распарсить JSON + if (typeof jsonField === 'string') { + try { + const parsed = JSON.parse(jsonField); + return Array.isArray(parsed) ? parsed : []; + } catch (e) { + console.error('Error parsing JSON field:', e); + return []; + } + } + + // Если это уже массив, возвращаем как есть + if (Array.isArray(jsonField)) return jsonField; + + return []; + }; + return new Profile({ userId: entity.user_id, name: entity.name, @@ -431,8 +444,8 @@ export class ProfileService { gender: entity.gender, interestedIn: entity.looking_for, bio: entity.bio, - photos: parsePostgresArray(entity.photos), - interests: parsePostgresArray(entity.interests), + photos: parseJsonField(entity.photos), + interests: parseJsonField(entity.interests), hobbies: entity.hobbies, city: entity.location || entity.city, education: entity.education, @@ -441,14 +454,11 @@ export class ProfileService { religion: entity.religion, datingGoal: entity.dating_goal, lifestyle: { - smoking: entity.smoking, - drinking: entity.drinking, - kids: entity.has_kids - }, - location: entity.latitude && entity.longitude ? { - latitude: entity.latitude, - longitude: entity.longitude - } : undefined, + smoking: undefined, + drinking: undefined, + kids: undefined + }, // Пропускаем lifestyle, так как этих колонок нет + location: undefined, // Пропускаем location, так как этих колонок нет searchPreferences: { minAge: 18, maxAge: 50, @@ -466,9 +476,10 @@ export class ProfileService { // Специальные случаи для некоторых полей const specialCases: { [key: string]: string } = { 'interestedIn': 'looking_for', - 'job': 'occupation', - 'city': 'location', + // Удалили 'job': 'occupation', так как колонка occupation не существует + // Вместо этого используем job 'datingGoal': 'dating_goal' + // Удалили 'city': 'location', так как колонка location не существует }; if (specialCases[str]) { @@ -484,7 +495,7 @@ export class ProfileService { await transaction(async (client) => { // Удаляем связанные данные await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]); - await client.query('DELETE FROM matches WHERE user1_id = $1 OR user2_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 profiles WHERE user_id = $1', [userId]); }); diff --git a/src/services/vipService.ts b/src/services/vipService.ts index 2439dfe..397723e 100644 --- a/src/services/vipService.ts +++ b/src/services/vipService.ts @@ -24,8 +24,9 @@ export class VipService { // Проверить премиум статус пользователя async checkPremiumStatus(telegramId: string): Promise { try { + // Проверяем существование пользователя const result = await query(` - SELECT premium, premium_expires_at + SELECT id FROM users WHERE telegram_id = $1 `, [telegramId]); @@ -34,27 +35,12 @@ export class VipService { throw new BotError('User not found', 'USER_NOT_FOUND', 404); } - const user = result.rows[0]; - const isPremium = user.premium; - const expiresAt = user.premium_expires_at ? new Date(user.premium_expires_at) : undefined; - - let daysLeft = undefined; - if (isPremium && expiresAt) { - const now = new Date(); - const timeDiff = expiresAt.getTime() - now.getTime(); - daysLeft = Math.ceil(timeDiff / (1000 * 3600 * 24)); - - // Если премиум истек - if (daysLeft <= 0) { - await this.removePremium(telegramId); - return { isPremium: false }; - } - } - + // Временно возвращаем false для всех пользователей, так как колонки premium нет + // В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно return { - isPremium, - expiresAt, - daysLeft + isPremium: false, + expiresAt: undefined, + daysLeft: undefined }; } catch (error) { console.error('Error checking premium status:', error); @@ -65,14 +51,9 @@ export class VipService { // Добавить премиум статус async addPremium(telegramId: string, durationDays: number = 30): Promise { try { - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + durationDays); - - await query(` - UPDATE users - SET premium = true, premium_expires_at = $2 - WHERE telegram_id = $1 - `, [telegramId, expiresAt]); + // Временно заглушка, так как колонок premium и premium_expires_at нет + console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`); + // TODO: Добавить колонки premium и premium_expires_at в таблицу users } catch (error) { console.error('Error adding premium:', error); throw error; @@ -82,11 +63,9 @@ export class VipService { // Удалить премиум статус async removePremium(telegramId: string): Promise { try { - await query(` - UPDATE users - SET premium = false, premium_expires_at = NULL - WHERE telegram_id = $1 - `, [telegramId]); + // Временно заглушка, так как колонок premium и premium_expires_at нет + console.log(`[VIP] Попытка удалить премиум для ${telegramId}`); + // TODO: Добавить колонки premium и premium_expires_at в таблицу users } catch (error) { console.error('Error removing premium:', error); throw error; diff --git a/test-bot.ts b/tests/test-bot.ts similarity index 100% rename from test-bot.ts rename to tests/test-bot.ts