mass refactor
This commit is contained in:
181
README.md
181
README.md
@@ -2,6 +2,71 @@
|
||||
|
||||
Полнофункциональный Telegram бот для знакомств в стиле Tinder с инлайн-кнопками и красивым интерфейсом. Пользователи могут создавать профили, просматривать анкеты других пользователей, ставить лайки, получать матчи и общаться друг с другом.
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
telegram-tinder-bot/
|
||||
├── bin/ # Исполняемые скрипты и утилиты
|
||||
│ ├── start_bot.bat # Скрипт запуска для Windows
|
||||
│ ├── install_ubuntu.sh # Скрипт установки для Ubuntu
|
||||
│ ├── update.sh # Скрипт обновления для Linux/macOS
|
||||
│ ├── update.bat # Скрипт обновления для Windows
|
||||
│ └── setup.sh # Скрипт настройки окружения
|
||||
│
|
||||
├── docs/ # Документация проекта
|
||||
│ ├── ARCHITECTURE.md # Архитектура приложения
|
||||
│ ├── DEPLOYMENT.md # Инструкции по развертыванию
|
||||
│ ├── DEPLOY_UBUNTU.md # Инструкции по развертыванию на Ubuntu
|
||||
│ ├── LOCALIZATION.md # Информация о локализации
|
||||
│ ├── NATIVE_CHAT_SYSTEM.md # Документация по системе чата
|
||||
│ ├── PROJECT_SUMMARY.md # Общее описание проекта
|
||||
│ └── VIP_FUNCTIONS.md # Описание премиум функций
|
||||
│
|
||||
├── migrations/ # Миграции базы данных
|
||||
│ ├── 1758144488937_initial-schema.js # Начальная схема БД
|
||||
│ └── 1758144618548_add-missing-profile-columns.js # Дополнительные колонки
|
||||
│
|
||||
├── scripts/ # Вспомогательные скрипты
|
||||
│ ├── add-hobbies-column.js # Скрипт добавления колонки hobbies
|
||||
│ ├── add-premium-columns.js # Скрипт добавления премиум колонок
|
||||
│ ├── add-premium-columns.ts # TypeScript версия скрипта
|
||||
│ ├── create_profile_fix.js # Исправление профилей
|
||||
│ └── migrate-sync.js # Синхронизация миграций
|
||||
│
|
||||
├── sql/ # SQL скрипты
|
||||
│ ├── add_looking_for.sql # Добавление колонки looking_for
|
||||
│ ├── add_missing_columns.sql # Добавление недостающих колонок
|
||||
│ ├── add_premium_columns.sql # Добавление премиум колонок
|
||||
│ ├── add_updated_at.sql # Добавление колонки updated_at
|
||||
│ ├── clear_database.sql # Очистка базы данных
|
||||
│ └── recreate_tables.sql # Пересоздание таблиц
|
||||
│
|
||||
├── src/ # Исходный код приложения
|
||||
│ ├── bot.ts # Основной файл бота
|
||||
│ ├── controllers/ # Контроллеры
|
||||
│ ├── database/ # Функции для работы с БД
|
||||
│ ├── handlers/ # Обработчики сообщений и команд
|
||||
│ ├── locales/ # Локализация
|
||||
│ ├── models/ # Модели данных
|
||||
│ ├── scripts/ # Скрипты для запуска
|
||||
│ │ └── initDb.ts # Инициализация базы данных
|
||||
│ ├── services/ # Сервисы и бизнес-логика
|
||||
│ ├── types/ # TypeScript типы
|
||||
│ └── utils/ # Утилиты и вспомогательные функции
|
||||
│
|
||||
├── tests/ # Тесты
|
||||
│ └── test-bot.ts # Тестовая версия бота
|
||||
│
|
||||
├── .dockerignore # Игнорируемые Docker файлы
|
||||
├── .env # Переменные окружения (локальные)
|
||||
├── .env.example # Пример файла переменных окружения
|
||||
├── database.json # Конфигурация базы данных
|
||||
├── docker-compose.yml # Настройка Docker Compose
|
||||
├── Dockerfile # Docker-образ приложения
|
||||
├── package.json # Зависимости и скрипты NPM
|
||||
└── tsconfig.json # Настройки TypeScript
|
||||
```
|
||||
|
||||
## ✨ Функционал
|
||||
|
||||
### 🎯 Основные возможности
|
||||
@@ -74,82 +139,84 @@
|
||||
[💬 Написать] [👤 Профиль] [🔍 Продолжить поиск]
|
||||
```
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
telegram-tinder-bot/
|
||||
├── src/
|
||||
│ ├── bot.ts # Основной файл бота
|
||||
│ ├── handlers/ # Обработчики событий
|
||||
│ │ ├── commandHandlers.ts # Команды (/start, /profile, etc.)
|
||||
│ │ ├── callbackHandlers.ts # Инлайн-кнопки (лайки, просмотр)
|
||||
│ │ └── messageHandlers.ts # Текстовые сообщения
|
||||
│ ├── services/ # Бизнес-логика
|
||||
│ │ ├── profileService.ts # Управление профилями
|
||||
│ │ ├── matchingService.ts # Алгоритм совпадений
|
||||
│ │ └── notificationService.ts # Уведомления
|
||||
│ ├── models/ # Модели данных
|
||||
│ │ ├── User.ts # Пользователь Telegram
|
||||
│ │ ├── Profile.ts # Профиль знакомств
|
||||
│ │ ├── Swipe.ts # Лайки/дислайки
|
||||
│ │ └── Match.ts # Совпадения
|
||||
│ └── database/ # База данных
|
||||
│ ├── connection.ts # Подключение к PostgreSQL
|
||||
│ └── migrations/init.sql # Создание таблиц
|
||||
├── config/ # Конфигурация
|
||||
│ └── default.json # Настройки по умолчанию
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker контейнер
|
||||
└── package.json # Зависимости npm
|
||||
```
|
||||
|
||||
## 🚀 Развертывание
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 📦 Docker (Рекомендуется)
|
||||
### 1. Предварительные требования
|
||||
|
||||
- Node.js 16+
|
||||
- PostgreSQL 12+
|
||||
- Telegram Bot Token (получить у [@BotFather](https://t.me/BotFather))
|
||||
|
||||
### 2. Установка
|
||||
|
||||
```bash
|
||||
# Клонировать репозиторий
|
||||
git clone <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
84
bin/README.md
Normal 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
190
bin/install_ubuntu.sh
Normal 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
0
setup.sh → bin/setup.sh
Executable file → Normal file
27
bin/start_bot.bat
Normal file
27
bin/start_bot.bat
Normal 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
24
bin/start_bot.sh
Normal 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
18
bin/tg-tinder-bot.service
Normal 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
113
bin/update.bat
Normal 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
155
bin/update.sh
Normal 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
14
check_schema.ts
Normal 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
7
database.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"connectionString": {
|
||||
"ENV": "DATABASE_URL"
|
||||
},
|
||||
"migrationsTable": "pgmigrations",
|
||||
"migrationsDirectory": "./migrations"
|
||||
}
|
||||
221
docs/DEPLOY_UBUNTU.md
Normal file
221
docs/DEPLOY_UBUNTU.md
Normal 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
|
||||
```
|
||||
152
migrations/1758144488937_initial-schema.js
Normal file
152
migrations/1758144488937_initial-schema.js
Normal 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');
|
||||
};
|
||||
25
migrations/1758144618548_add-missing-profile-columns.js
Normal file
25
migrations/1758144618548_add-missing-profile-columns.js
Normal 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']);
|
||||
};
|
||||
18
migrations/1758147898012_add-missing-religion-columns.js
Normal file
18
migrations/1758147898012_add-missing-religion-columns.js
Normal 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) => {};
|
||||
29
migrations/1758147903378_add-missing-religion-columns.js
Normal file
29
migrations/1758147903378_add-missing-religion-columns.js
Normal 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 });
|
||||
};
|
||||
@@ -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) => {};
|
||||
@@ -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) => {};
|
||||
@@ -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"
|
||||
});
|
||||
};
|
||||
50
migrations/1758149087361_add-column-synonyms.js
Normal file
50
migrations/1758149087361_add-column-synonyms.js
Normal 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
345
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
23
package.json
23
package.json
@@ -5,18 +5,35 @@
|
||||
"main": "dist/bot.js",
|
||||
"scripts": {
|
||||
"start": "node dist/bot.js",
|
||||
"start:prod": "NODE_ENV=production node dist/bot.js",
|
||||
"start:win:prod": "set NODE_ENV=production&& node dist/bot.js",
|
||||
"dev": "ts-node src/bot.ts",
|
||||
"build": "tsc && cp -r src/locales dist/",
|
||||
"build": "tsc && xcopy /E /I src\\locales dist\\locales",
|
||||
"build:linux": "tsc && cp -R src/locales dist/",
|
||||
"test": "jest",
|
||||
"db:init": "ts-node src/scripts/initDb.ts"
|
||||
"test:bot": "ts-node tests/test-bot.ts",
|
||||
"db:init": "ts-node src/scripts/initDb.ts",
|
||||
"init:db": "ts-node src/scripts/initDb.ts",
|
||||
"migrate": "node-pg-migrate",
|
||||
"migrate:up": "node-pg-migrate up",
|
||||
"migrate:down": "node-pg-migrate down",
|
||||
"migrate:create": "node-pg-migrate create",
|
||||
"premium:set-all": "ts-node src/scripts/setPremiumForAll.ts",
|
||||
"premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts",
|
||||
"db:info": "ts-node src/scripts/getDatabaseInfo.ts",
|
||||
"db:test-data": "ts-node src/scripts/createTestData.ts",
|
||||
"update": "bash ./bin/update.sh",
|
||||
"update:win": ".\\bin\\update.bat",
|
||||
"start:sh": "bash ./bin/start_bot.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-telegram-bot-api": "^0.64.11",
|
||||
"axios": "^1.12.1",
|
||||
"dotenv": "^16.6.1",
|
||||
"i18next": "^25.5.2",
|
||||
"node-pg-migrate": "^8.0.3",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"pg": "^8.11.3",
|
||||
"pg": "^8.16.3",
|
||||
"sharp": "^0.32.6",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
|
||||
43
scripts/add-hobbies-column.js
Normal file
43
scripts/add-hobbies-column.js
Normal 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();
|
||||
44
scripts/add-premium-columns-direct.js
Normal file
44
scripts/add-premium-columns-direct.js
Normal 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();
|
||||
40
scripts/add-premium-columns.js
Normal file
40
scripts/add-premium-columns.js
Normal 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();
|
||||
28
scripts/add-premium-columns.ts
Normal file
28
scripts/add-premium-columns.ts
Normal 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();
|
||||
101
scripts/create_profile_fix.js
Normal file
101
scripts/create_profile_fix.js
Normal 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
62
scripts/migrate-sync.js
Normal 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
BIN
set-premium.js
Normal file
Binary file not shown.
2
sql/add_looking_for.sql
Normal file
2
sql/add_looking_for.sql
Normal 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'));
|
||||
7
sql/add_missing_columns.sql
Normal file
7
sql/add_missing_columns.sql
Normal 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;
|
||||
4
sql/add_premium_columns.sql
Normal file
4
sql/add_premium_columns.sql
Normal 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
21
sql/add_updated_at.sql
Normal 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
67
sql/recreate_tables.sql
Normal 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();
|
||||
@@ -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);
|
||||
`);
|
||||
|
||||
|
||||
@@ -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': 'курение',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
115
src/scripts/cleanDb.ts
Normal 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 };
|
||||
166
src/scripts/createTestData.ts
Normal file
166
src/scripts/createTestData.ts
Normal 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();
|
||||
120
src/scripts/getDatabaseInfo.ts
Normal file
120
src/scripts/getDatabaseInfo.ts
Normal 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();
|
||||
@@ -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
42
src/scripts/setPremium.ts
Normal 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();
|
||||
43
src/scripts/setPremiumDirectConnect.ts
Normal file
43
src/scripts/setPremiumDirectConnect.ts
Normal 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();
|
||||
25
src/scripts/setPremiumForAll.ts
Normal file
25
src/scripts/setPremiumForAll.ts
Normal 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();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user