mass refactor

This commit is contained in:
2025-09-18 08:31:14 +09:00
parent 856bf3ca2a
commit bdd7d0424f
58 changed files with 3009 additions and 291 deletions

181
README.md
View File

@@ -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 <repository-url>
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
```
## <20> Развертывание на 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
```
## 🔧 Настройка переменных окружения

84
bin/README.md Normal file
View File

@@ -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`

190
bin/install_ubuntu.sh Normal file
View File

@@ -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}"

0
setup.sh → bin/setup.sh Executable file → Normal file
View File

27
bin/start_bot.bat Normal file
View File

@@ -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 👋 Бот был остановлен.

24
bin/start_bot.sh Normal file
View File

@@ -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."

18
bin/tg-tinder-bot.service Normal file
View File

@@ -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

113
bin/update.bat Normal file
View File

@@ -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%

155
bin/update.sh Normal file
View File

@@ -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"

14
check_schema.ts Normal file
View File

@@ -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();

7
database.json Normal file
View File

@@ -0,0 +1,7 @@
{
"connectionString": {
"ENV": "DATABASE_URL"
},
"migrationsTable": "pgmigrations",
"migrationsDirectory": "./migrations"
}

221
docs/DEPLOY_UBUNTU.md Normal file
View File

@@ -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
```

View File

@@ -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> | 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> | void}
*/
export const down = (pgm) => {
pgm.dropTable('messages');
pgm.dropTable('matches');
pgm.dropTable('swipes');
pgm.dropTable('profiles');
pgm.dropTable('users');
pgm.dropExtension('pgcrypto');
};

View File

@@ -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> | 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> | void}
*/
export const down = (pgm) => {
pgm.dropColumns('profiles', ['hobbies']);
};

View File

@@ -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> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -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> | 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> | void}
*/
export const down = (pgm) => {
pgm.dropColumns('profiles', ['religion', 'dating_goal', 'smoking', 'drinking', 'has_kids'], { ifExists: true });
};

View File

@@ -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> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -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> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -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> | 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> | 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"
});
};

View File

@@ -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> | 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> | void}
*/
export const down = (pgm) => {
pgm.sql(`DROP VIEW IF EXISTS swipes_view;`);
pgm.sql(`DROP VIEW IF EXISTS matches_view;`);
};

345
package-lock.json generated
View File

@@ -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"
}

View File

@@ -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"
},

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);
});

62
scripts/migrate-sync.js Normal file
View File

@@ -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();

BIN
set-premium.js Normal file

Binary file not shown.

2
sql/add_looking_for.sql Normal file
View File

@@ -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'));

View File

@@ -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;

View File

@@ -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;

21
sql/add_updated_at.sql Normal file
View File

@@ -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$$;

67
sql/recreate_tables.sql Normal file
View File

@@ -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();

View File

@@ -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<void> {
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);
`);

View File

@@ -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<void> {
async handleStartBrowsing(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
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<void> {
const candidate = await this.matchingService.getNextCandidate(telegramId);
async showNextCandidate(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
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': 'курение',

View File

@@ -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) {

View File

@@ -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':

115
src/scripts/cleanDb.ts Normal file
View File

@@ -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<boolean> {
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<void> {
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 };

View File

@@ -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();

View File

@@ -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();

View File

@@ -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<boolean> {
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<string[]> {
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<void> {
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<void> {
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<void> {
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;
}
}
// Запуск скрипта

42
src/scripts/setPremium.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Pool } from 'pg';
async function setPremium(): Promise<void> {
// Создаем соединение с базой данных используя прямые параметры
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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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<Message | null> {
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) {

View File

@@ -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<Swipe | null> {
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<Swipe[]> {
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<Profile | null> {
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
// Сначала получаем профиль пользователя по 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 = `

View File

@@ -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<void> {
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) {

View File

@@ -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]);
});

View File

@@ -24,8 +24,9 @@ export class VipService {
// Проверить премиум статус пользователя
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
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<void> {
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<void> {
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;