Compare commits

9 Commits

Author SHA1 Message Date
bdd7d0424f mass refactor 2025-09-18 08:31:14 +09:00
856bf3ca2a Merge branch 'main' of ssh://git.smartsoltech.kr:2222/trevor/tg_tinder_bot 2025-09-13 15:26:54 +09:00
e3baa9be63 Удалён файл с неправильным именем, содержащим команду PostgreSQL 2025-09-13 15:26:45 +09:00
a3fb88e91e Удалить sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \d profiles 2025-09-13 06:25:52 +00:00
c5a0593222 Merge pull request 'localization' (#2) from localization into main
Reviewed-on: #2
2025-09-13 06:17:17 +00:00
1eb7d1c9bc localization 2025-09-13 15:16:05 +09:00
e81725e4d5 feat: Complete multilingual interface with 10 languages including Korean
🌍 Added complete translation files:
- 🇪🇸 Spanish (es.json) - Español
- 🇫🇷 French (fr.json) - Français
- 🇩🇪 German (de.json) - Deutsch
- 🇮🇹 Italian (it.json) - Italiano
- 🇵🇹 Portuguese (pt.json) - Português
- 🇨🇳 Chinese (zh.json) - 中文
- 🇯🇵 Japanese (ja.json) - 日本語

🔧 Updated LocalizationService:
- All 10 languages loaded and initialized
- Updated supported languages list
- Enhanced language detection

��️ Enhanced UI:
- Extended language selection menu with all 10 languages
- Updated language names mapping in controllers
- Proper flag emojis for each language

💡 Features:
- Native translations for all UI elements
- Cultural appropriate pricing displays
- Proper date/currency formatting per locale
- Korean language support with proper hangul characters

Ready for global deployment with comprehensive language support!
2025-09-13 09:19:13 +09:00
edddd52589 feat: Complete localization system with i18n and DeepSeek AI translation
🌐 Interface Localization:
- Added i18next for multi-language interface support
- Created LocalizationService with language detection
- Added translation files for Russian and English
- Implemented language selection in user settings

🤖 AI Profile Translation (Premium feature):
- Integrated DeepSeek API for profile translation
- Added TranslationController for translation management
- Premium-only access to profile translation feature
- Support for 10 languages (ru, en, es, fr, de, it, pt, zh, ja, ko)

�� Database & Models:
- Added language field to users table with migration
- Updated User model to support language preferences
- Added language constraints and indexing

🎛️ User Interface:
- Added language settings menu in bot settings
- Implemented callback handlers for language selection
- Added translate profile button for VIP users
- Localized all interface strings

📚 Documentation:
- Created comprehensive LOCALIZATION.md guide
- Documented API usage and configuration
- Added examples for extending language support
2025-09-13 08:59:10 +09:00
975eb348dd feat: VIP search now shows only opposite gender - Modified VIP search filtering to always show opposite gender regardless of user's interested_in preference - Male users see only female profiles - Female users see only male profiles - Improved gender filtering logic in vipService.ts 2025-09-13 08:45:41 +09:00
83 changed files with 6281 additions and 340 deletions

181
README.md
View File

@@ -2,6 +2,71 @@
Полнофункциональный Telegram бот для знакомств в стиле Tinder с инлайн-кнопками и красивым интерфейсом. Пользователи могут создавать профили, просматривать анкеты других пользователей, ставить лайки, получать матчи и общаться друг с другом. Полнофункциональный 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 ```bash
# Клонировать репозиторий # Клонировать репозиторий
git clone <repository-url> git clone <repository-url>
cd telegram-tinder-bot 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 npm install
# Создать базу данных # Скомпилировать TypeScript
createdb telegram_tinder_bot
psql -d telegram_tinder_bot -f src/database/migrations/init.sql
# Запустить бота
npm run build npm run build
npm start
``` ```
### ☁️ Продакшен ### 3. Настройка базы данных
```bash ```bash
# Установить PM2 # Создать базу данных PostgreSQL
npm install -g pm2 createdb telegram_tinder_bot
# Запустить через PM2 # Инициализация базы данных
pm2 start ecosystem.config.js npm run init:db
```
# Мониторинг ### 4. Запуск бота
pm2 monit
pm2 logs telegram-tinder-bot ```bash
# Запуск на Windows
.\bin\start_bot.bat
# Запуск на Linux/macOS
npm run start
```
## <20> Развертывание на Ubuntu
Для развертывания на Ubuntu 24.04 используйте скрипт установки:
```bash
# Сделать скрипт исполняемым
chmod +x ./bin/install_ubuntu.sh
# Запустить установку
sudo ./bin/install_ubuntu.sh
```
Подробные инструкции по развертыванию на Ubuntu находятся в [docs/DEPLOY_UBUNTU.md](docs/DEPLOY_UBUNTU.md).
## 🔄 Обновление бота
### На Windows:
```bash
# Обновление с ветки main
npm run update:win
# Обновление с определенной ветки
.\bin\update.bat develop
```
### На Linux/macOS:
```bash
# Обновление с ветки main
npm run update
# Обновление с определенной ветки и перезапуском сервиса
./bin/update.sh develop --restart-service
``` ```
## 🔧 Настройка переменных окружения ## 🔧 Настройка переменных окружения

84
bin/README.md Normal file
View File

@@ -0,0 +1,84 @@
# Автоматическое обновление Telegram Tinder Bot
Этот документ описывает процесс автоматического обновления бота с помощью созданных скриптов.
## Скрипт обновления
Скрипт обновления выполняет следующие действия:
1. Получает последние изменения из Git-репозитория
2. Устанавливает зависимости
3. Применяет миграции базы данных
4. Собирает проект
5. Проверяет наличие файла .env
6. Проверяет наличие Docker-сервисов
7. При запуске на Ubuntu: проверяет и перезапускает PM2 сервис
## Подробные инструкции по развертыванию
Для подробных инструкций по развертыванию бота на сервере Ubuntu 24.04, пожалуйста, обратитесь к файлу `DEPLOY_UBUNTU.md` в корне проекта.
## Как использовать
### На Linux/macOS:
```bash
# Обновление с ветки main (по умолчанию)
npm run update
# Обновление с определенной ветки
bash ./bin/update.sh develop
# Обновление с определенной ветки и перезапуском сервиса PM2 (для Ubuntu)
bash ./bin/update.sh develop --restart-service
```
### На Windows:
```powershell
# Обновление с ветки main (по умолчанию)
npm run update:win
# Обновление с определенной ветки
.\bin\update.bat develop
```
## Добавление прав на выполнение (только для Linux/macOS)
Если у вас возникают проблемы с запуском скрипта, добавьте права на выполнение:
```bash
chmod +x ./bin/update.sh
```
## Автоматизация обновлений
Для автоматизации регулярных обновлений вы можете использовать cron (Linux/macOS) или Планировщик заданий (Windows).
### Пример cron-задания для Ubuntu (ежедневное обновление в 4:00 с перезапуском сервиса):
```
0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh --restart-service >> /var/log/tg_bot_update.log 2>&1
```
### Пример cron-задания (ежедневное обновление в 4:00 без перезапуска):
```
0 4 * * * cd /path/to/bot && ./bin/update.sh
```
### Для Windows:
Создайте задачу в Планировщике заданий, которая запускает:
```
cmd.exe /c "cd /d D:\Projects\tg_tinder_bot && .\bin\update.bat"
```
## Что делать после обновления
После обновления вы можете:
1. Запустить бота: `npm run start`
2. Запустить бота в режиме разработки: `npm run dev`
3. Перезапустить Docker-контейнеры, если используете Docker: `docker-compose down && docker-compose up -d`

190
bin/install_ubuntu.sh Normal file
View File

@@ -0,0 +1,190 @@
#!/bin/bash
# Script for installing Telegram Tinder Bot on Ubuntu
# This script automates the deployment process on a fresh Ubuntu server
# Usage: ./bin/install_ubuntu.sh [--with-nginx] [--with-ssl domain.com]
set -e # Exit immediately if a command exits with a non-zero status
# Define colors for pretty output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Default settings
INSTALL_NGINX=false
INSTALL_SSL=false
DOMAIN=""
# Parse command line arguments
for arg in "$@"; do
if [[ "$arg" == "--with-nginx" ]]; then
INSTALL_NGINX=true
elif [[ "$arg" == "--with-ssl" ]]; then
INSTALL_SSL=true
# Next argument should be domain
shift
DOMAIN="$1"
if [[ -z "$DOMAIN" || "$DOMAIN" == --* ]]; then
echo -e "${RED}Error: Domain name required after --with-ssl${NC}"
exit 1
fi
fi
shift
done
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} Telegram Tinder Bot Ubuntu Installer ${NC}"
echo -e "${BLUE}========================================${NC}"
# Check if running on Ubuntu
if [ -f /etc/os-release ]; then
source /etc/os-release
if [[ "$ID" != "ubuntu" ]]; then
echo -e "${RED}Error: This script is designed for Ubuntu. Current OS: $ID${NC}"
exit 1
else
echo -e "${GREEN}Detected Ubuntu ${VERSION_ID}${NC}"
fi
else
echo -e "${RED}Error: Could not detect operating system${NC}"
exit 1
fi
# Check for root privileges
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}Error: This script must be run as root${NC}"
echo -e "Please run: ${YELLOW}sudo $0 $*${NC}"
exit 1
fi
echo -e "\n${BLUE}Step 1: Updating system packages...${NC}"
apt update && apt upgrade -y
echo -e "${GREEN}✓ System packages updated${NC}"
echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}"
apt install -y curl wget git build-essential postgresql postgresql-contrib
echo -e "${GREEN}✓ Basic dependencies installed${NC}"
echo -e "\n${BLUE}Step 3: Installing Node.js...${NC}"
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
echo -e "${GREEN}✓ Node.js $(node --version) installed${NC}"
echo -e "${GREEN}✓ npm $(npm --version) installed${NC}"
echo -e "\n${BLUE}Step 4: Setting up PostgreSQL...${NC}"
systemctl start postgresql
systemctl enable postgresql
echo -e "\n${BLUE}Please enter a strong password for the database user:${NC}"
read -s DB_PASSWORD
echo
# Create database and user
su - postgres -c "psql -c \"CREATE DATABASE tg_tinder_bot;\""
su - postgres -c "psql -c \"CREATE USER tg_bot WITH PASSWORD '$DB_PASSWORD';\""
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot;\""
echo -e "${GREEN}✓ PostgreSQL configured${NC}"
echo -e "\n${BLUE}Step 5: Setting up application directory...${NC}"
mkdir -p /opt/tg_tinder_bot
chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot
echo -e "${GREEN}✓ Application directory created${NC}"
echo -e "\n${BLUE}Step 6: Installing PM2...${NC}"
npm install -g pm2
echo -e "${GREEN}✓ PM2 installed${NC}"
echo -e "\n${BLUE}Step 7: Please enter your Telegram Bot Token:${NC}"
read BOT_TOKEN
echo -e "\n${BLUE}Step 8: Creating environment file...${NC}"
cat > /opt/tg_tinder_bot/.env << EOL
# Bot settings
BOT_TOKEN=${BOT_TOKEN}
LOG_LEVEL=info
# Database settings
DB_HOST=localhost
DB_PORT=5432
DB_USER=tg_bot
DB_PASSWORD=${DB_PASSWORD}
DB_NAME=tg_tinder_bot
EOL
chmod 600 /opt/tg_tinder_bot/.env
chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot/.env
echo -e "${GREEN}✓ Environment file created${NC}"
echo -e "\n${BLUE}Step 9: Setting up systemd service...${NC}"
cat > /etc/systemd/system/tg-tinder-bot.service << EOL
[Unit]
Description=Telegram Tinder Bot
After=network.target postgresql.service
[Service]
Type=simple
User=${SUDO_USER}
WorkingDirectory=/opt/tg_tinder_bot
ExecStart=/usr/bin/node dist/bot.js
Restart=on-failure
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=tg-tinder-bot
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOL
echo -e "${GREEN}✓ Systemd service created${NC}"
if [ "$INSTALL_NGINX" = true ]; then
echo -e "\n${BLUE}Step 10: Installing and configuring Nginx...${NC}"
apt install -y nginx
# Create Nginx configuration
cat > /etc/nginx/sites-available/tg_tinder_bot << EOL
server {
listen 80;
server_name ${DOMAIN:-_};
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_cache_bypass \$http_upgrade;
}
}
EOL
# Enable site
ln -sf /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/
nginx -t && systemctl restart nginx
echo -e "${GREEN}✓ Nginx configured${NC}"
if [ "$INSTALL_SSL" = true ] && [ ! -z "$DOMAIN" ]; then
echo -e "\n${BLUE}Step 11: Setting up SSL with Certbot...${NC}"
apt install -y certbot python3-certbot-nginx
certbot --nginx --non-interactive --agree-tos --email admin@${DOMAIN} -d ${DOMAIN}
echo -e "${GREEN}✓ SSL certificate installed${NC}"
fi
fi
echo -e "\n${BLUE}Step 12: Clone your repository${NC}"
echo -e "Now you should clone your repository to /opt/tg_tinder_bot"
echo -e "Example: ${YELLOW}git clone https://your-git-repo-url.git /opt/tg_tinder_bot${NC}"
echo -e "Then run the update script: ${YELLOW}cd /opt/tg_tinder_bot && ./bin/update.sh${NC}"
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN} Installation completed! ${NC}"
echo -e "${GREEN}========================================${NC}"
echo -e "Next steps:"
echo -e "1. Clone your repository to ${YELLOW}/opt/tg_tinder_bot${NC}"
echo -e "2. Run the update script to set up the application"
echo -e "3. Start the service with: ${YELLOW}sudo systemctl start tg-tinder-bot${NC}"
echo -e "4. Enable auto-start with: ${YELLOW}sudo systemctl enable tg-tinder-bot${NC}"
echo -e "5. Check status with: ${YELLOW}sudo systemctl status tg-tinder-bot${NC}"

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

27
bin/start_bot.bat Normal file
View File

@@ -0,0 +1,27 @@
@echo off
REM Скрипт для запуска Telegram Tinder Bot в производственном режиме на Windows
REM Запускает собранный JavaScript из dist/bot.js
echo 🚀 Запуск Telegram Tinder Bot в производственном режиме...
REM Добавляем Node.js в PATH, если нужно
set PATH=%PATH%;C:\Program Files\nodejs
REM Переходим в корневую директорию проекта
cd /d %~dp0..
REM Проверяем, существует ли собранный файл
if not exist ".\dist\bot.js" (
echo ❌ Ошибка: Собранный файл не найден. Сначала выполните 'npm run build'.
exit /b 1
)
REM Устанавливаем переменную окружения для производственного режима
set NODE_ENV=production
REM Запускаем бот
echo 🤖 Запуск Telegram Tinder Bot...
node .\dist\bot.js
REM Если скрипт дойдет до этой точки, значит бот завершил работу
echo 👋 Бот был остановлен.

24
bin/start_bot.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Скрипт для запуска бота в производственном режиме
# Запускает собранный JavaScript из dist/bot.js
# Переходим в корневую директорию проекта (предполагается, что скрипт находится в bin/)
PROJECT_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
cd "$PROJECT_DIR" || { echo "❌ Error: Could not change directory to $PROJECT_DIR"; exit 1; }
# Проверяем, существует ли собранный файл
if [ ! -f "./dist/bot.js" ]; then
echo "❌ Error: Built file not found. Please run 'npm run build' first."
exit 1
fi
# Устанавливаем переменную окружения для производственного режима
export NODE_ENV=production
# Запускаем бот
echo "🚀 Starting Telegram Tinder Bot in production mode..."
node ./dist/bot.js
# Если скрипт дойдет до этой точки, значит бот завершил работу
echo "👋 Bot has been stopped."

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

@@ -0,0 +1,18 @@
[Unit]
Description=Telegram Tinder Bot
After=network.target postgresql.service
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/tg_tinder_bot
ExecStart=/usr/bin/node dist/bot.js
Restart=on-failure
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=tg-tinder-bot
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

113
bin/update.bat Normal file
View File

@@ -0,0 +1,113 @@
@echo off
REM Script for updating the Telegram Tinder Bot on Windows
REM This script updates the code from Git, applies migrations, and prepares the bot for running
REM Usage: .\bin\update.bat [branch]
REM If branch is not specified, 'main' is used
setlocal enableextensions enabledelayedexpansion
echo ========================================
echo Telegram Tinder Bot Updater
echo ========================================
REM Get the branch name from the command line arguments
set BRANCH=%1
if "%BRANCH%"=="" set BRANCH=main
echo Updating from branch: %BRANCH%
REM Store the current directory
set CURRENT_DIR=%CD%
set SCRIPT_DIR=%~dp0
set PROJECT_DIR=%SCRIPT_DIR%..
REM Navigate to the project directory
cd /d %PROJECT_DIR%
echo Working directory: %PROJECT_DIR%
REM Check if we're in a git repository
if not exist .git (
echo Error: Not a git repository
exit /b 1
)
echo.
echo Step 1: Pulling latest changes from Git repository...
REM Save any local changes
git stash save "Auto-stash before update: %DATE% %TIME%"
REM Fetch all branches
git fetch --all
REM Check if the branch exists
git rev-parse --verify %BRANCH% >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
git rev-parse --verify origin/%BRANCH% >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo Error: Branch '%BRANCH%' does not exist locally or remotely
exit /b 1
)
)
REM Checkout the specified branch
git checkout %BRANCH%
REM Pull the latest changes
git pull origin %BRANCH%
echo ✓ Successfully pulled latest changes
echo.
echo Step 2: Installing dependencies...
call npm ci
echo ✓ Dependencies installed
echo.
echo Step 3: Running database migrations...
REM Check if migrations directory exists
if exist migrations (
echo Applying database migrations...
call npm run migrate:up
echo ✓ Migrations applied successfully
) else (
echo ⚠ No migrations directory found, running database initialization script...
call npm run init:db
echo ✓ Database initialized
)
echo.
echo Step 4: Building the project...
call npm run build
echo ✓ Project built successfully
echo.
echo Step 5: Checking for .env file...
if exist .env (
echo ✓ .env file exists
) else (
echo ⚠ .env file not found
if exist .env.example (
echo Creating .env from .env.example
copy .env.example .env
echo ⚠ Please update the .env file with your configuration!
) else (
echo Error: .env.example file not found
exit /b 1
)
)
echo.
echo Step 6: Checking for services...
REM Check if Docker is being used
if exist docker-compose.yml (
echo Docker Compose configuration found
echo You might want to restart containers with: docker-compose down ^&^& docker-compose up -d
)
echo.
echo ========================================
echo Update completed successfully!
echo ========================================
echo To start the bot, run: npm run start
echo For development mode: npm run dev
REM Return to the original directory
cd /d %CURRENT_DIR%

155
bin/update.sh Normal file
View File

@@ -0,0 +1,155 @@
#!/bin/bash
# Script for updating the Telegram Tinder Bot
# This script updates the code from Git, applies migrations, and prepares the bot for running
# Usage: ./bin/update.sh [branch] [--restart-service]
# If branch is not specified, 'main' is used
# Use --restart-service flag to restart PM2 service after update (for production deployments)
set -e # Exit immediately if a command exits with a non-zero status
# Define colors for pretty output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} Telegram Tinder Bot Updater ${NC}"
echo -e "${BLUE}========================================${NC}"
# Parse command line arguments
BRANCH="main"
RESTART_SERVICE=false
for arg in "$@"; do
if [[ "$arg" == "--restart-service" ]]; then
RESTART_SERVICE=true
elif [[ "$arg" != --* ]]; then
BRANCH="$arg"
fi
done
echo -e "${YELLOW}Updating from branch: ${BRANCH}${NC}"
if [ "$RESTART_SERVICE" = true ]; then
echo -e "${YELLOW}Will restart service after update${NC}"
fi
# Store the current directory
CURRENT_DIR=$(pwd)
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
PROJECT_DIR=$(dirname "$SCRIPT_DIR")
# Check if running on Ubuntu
IS_UBUNTU=false
if [ -f /etc/os-release ]; then
source /etc/os-release
if [[ "$ID" == "ubuntu" ]]; then
IS_UBUNTU=true
echo -e "${BLUE}Detected Ubuntu: ${VERSION_ID}${NC}"
fi
fi
# Navigate to the project directory
cd "$PROJECT_DIR"
echo -e "${BLUE}Working directory: ${PROJECT_DIR}${NC}"
# Check if we're in a git repository
if [ ! -d ".git" ]; then
echo -e "${RED}Error: Not a git repository${NC}"
exit 1
fi
echo -e "\n${BLUE}Step 1: Pulling latest changes from Git repository...${NC}"
# Save any local changes
git stash save "Auto-stash before update: $(date)"
# Fetch all branches
git fetch --all
# Check if the branch exists
if ! git rev-parse --verify "$BRANCH" &>/dev/null && ! git rev-parse --verify "origin/$BRANCH" &>/dev/null; then
echo -e "${RED}Error: Branch '$BRANCH' does not exist locally or remotely${NC}"
exit 1
fi
# Checkout the specified branch
git checkout "$BRANCH"
# Pull the latest changes
git pull origin "$BRANCH"
echo -e "${GREEN}✓ Successfully pulled latest changes${NC}"
echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}"
npm ci
echo -e "${GREEN}✓ Dependencies installed${NC}"
echo -e "\n${BLUE}Step 3: Running database migrations...${NC}"
# Check if migrations directory exists
if [ -d "./migrations" ]; then
echo "Applying database migrations..."
npm run migrate:up
echo -e "${GREEN}✓ Migrations applied successfully${NC}"
else
echo -e "${YELLOW}⚠ No migrations directory found, running database initialization script...${NC}"
npm run init:db
echo -e "${GREEN}✓ Database initialized${NC}"
fi
echo -e "\n${BLUE}Step 4: Building the project...${NC}"
npm run build
echo -e "${GREEN}✓ Project built successfully${NC}"
echo -e "\n${BLUE}Step 5: Checking for .env file...${NC}"
if [ -f .env ]; then
echo -e "${GREEN}✓ .env file exists${NC}"
else
echo -e "${YELLOW}⚠ .env file not found${NC}"
if [ -f .env.example ]; then
echo "Creating .env from .env.example"
cp .env.example .env
echo -e "${YELLOW}⚠ Please update the .env file with your configuration!${NC}"
else
echo -e "${RED}Error: .env.example file not found${NC}"
exit 1
fi
fi
echo -e "\n${BLUE}Step 6: Checking for services...${NC}"
# Check if Docker is being used
if [ -f docker-compose.yml ]; then
echo "Docker Compose configuration found"
echo "You might want to restart containers with: docker-compose down && docker-compose up -d"
fi
# Check for PM2 process on Ubuntu
if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then
echo -e "\n${BLUE}Step 7: Checking PM2 service...${NC}"
if pm2 list | grep -q "tg_tinder_bot"; then
echo "PM2 service for tg_tinder_bot found"
if [ "$RESTART_SERVICE" = true ]; then
echo "Restarting PM2 service..."
pm2 restart tg_tinder_bot
echo -e "${GREEN}✓ PM2 service restarted${NC}"
else
echo "To restart the service, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}"
fi
fi
fi
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN} Update completed successfully! ${NC}"
echo -e "${GREEN}========================================${NC}"
if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then
echo -e "To start the bot with PM2, run: ${YELLOW}pm2 start dist/bot.js --name tg_tinder_bot${NC}"
echo -e "To restart the bot, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}"
echo -e "To view logs, run: ${YELLOW}pm2 logs tg_tinder_bot${NC}"
else
echo -e "To start the bot, run: ${YELLOW}npm run start${NC}"
echo -e "For development mode: ${YELLOW}npm run dev${NC}"
fi
# Return to the original directory
cd "$CURRENT_DIR"

14
check_schema.ts Normal file
View File

@@ -0,0 +1,14 @@
import { query } from './src/database/connection';
async function checkSchema() {
try {
const result = await query('SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1', ['messages']);
console.log(result.rows);
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}
}
checkSchema();

7
database.json Normal file
View File

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

221
docs/DEPLOY_UBUNTU.md Normal file
View File

@@ -0,0 +1,221 @@
# Деплой Telegram Tinder Bot на Ubuntu 24.04
Это руководство поможет вам настроить и развернуть Telegram Tinder Bot на сервере с Ubuntu 24.04.
## Предварительные требования
- Сервер с Ubuntu 24.04
- Права администратора (sudo)
- Доменное имя (опционально, для SSL)
## Шаг 1: Подготовка сервера
```bash
# Обновите систему
sudo apt update && sudo apt upgrade -y
# Установите необходимые пакеты
sudo apt install -y curl wget git build-essential postgresql postgresql-contrib nginx
# Установите Node.js и npm
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Проверьте установку
node --version
npm --version
```
## Шаг 2: Настройка PostgreSQL
```bash
# Запустите и включите PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Подключитесь к PostgreSQL
sudo -u postgres psql
# В консоли PostgreSQL создайте базу данных и пользователя
CREATE DATABASE tg_tinder_bot;
CREATE USER tg_bot WITH PASSWORD 'сложный_пароль';
GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot;
\q
# Проверьте подключение
psql -h localhost -U tg_bot -d tg_tinder_bot
# Введите пароль, когда будет запрошено
```
## Шаг 3: Клонирование репозитория и установка зависимостей
```bash
# Создайте директорию для бота
sudo mkdir -p /opt/tg_tinder_bot
sudo chown $USER:$USER /opt/tg_tinder_bot
# Клонируйте репозиторий
git clone https://your-git-repo-url.git /opt/tg_tinder_bot
cd /opt/tg_tinder_bot
# Установите зависимости
npm ci
# Сделайте скрипты исполняемыми
chmod +x bin/update.sh
```
## Шаг 4: Настройка окружения
```bash
# Создайте файл .env из примера
cp .env.example .env
# Отредактируйте файл .env
nano .env
# Укажите следующие параметры:
# BOT_TOKEN=your_telegram_bot_token
# DB_HOST=localhost
# DB_PORT=5432
# DB_USER=tg_bot
# DB_PASSWORD=сложный_пароль
# DB_NAME=tg_tinder_bot
# и другие необходимые параметры
```
## Шаг 5: Инициализация базы данных и сборка проекта
```bash
# Запустите миграции
npm run migrate:up
# Соберите проект
npm run build
```
## Шаг 6: Настройка PM2 для управления процессами
```bash
# Установите PM2 глобально
sudo npm install -g pm2
# Запустите бота через PM2
pm2 start dist/bot.js --name tg_tinder_bot
# Настройте автозапуск PM2
pm2 startup
# Выполните команду, которую выдаст предыдущая команда
# Сохраните конфигурацию PM2
pm2 save
```
## Шаг 7: Настройка Nginx (если нужен веб-интерфейс)
```bash
# Создайте конфигурационный файл Nginx
sudo nano /etc/nginx/sites-available/tg_tinder_bot
# Добавьте следующее содержимое
# server {
# listen 80;
# server_name ваш-домен.com;
#
# location / {
# proxy_pass http://localhost:3000; # Замените на порт вашего веб-интерфейса
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
# }
# }
# Создайте символьную ссылку
sudo ln -s /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/
# Проверьте конфигурацию Nginx
sudo nginx -t
# Перезапустите Nginx
sudo systemctl restart nginx
```
## Шаг 8: Настройка SSL с Certbot (опционально, но рекомендуется)
```bash
# Установите Certbot
sudo apt install -y certbot python3-certbot-nginx
# Получите SSL-сертификат
sudo certbot --nginx -d ваш-домен.com
# Certbot автоматически обновит конфигурацию Nginx
```
## Шаг 9: Настройка автоматического обновления
```bash
# Отредактируйте crontab
crontab -e
# Добавьте строку для ежедневного обновления в 4:00
0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh >> /var/log/tg_bot_update.log 2>&1
```
## Управление ботом
```bash
# Перезапустить бота
pm2 restart tg_tinder_bot
# Остановить бота
pm2 stop tg_tinder_bot
# Посмотреть логи
pm2 logs tg_tinder_bot
# Посмотреть статус
pm2 status
```
## Обновление вручную
```bash
cd /opt/tg_tinder_bot
./bin/update.sh
```
## Резервное копирование базы данных
```bash
# Создайте директорию для резервных копий
mkdir -p ~/backups
# Создайте резервную копию
pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql
# Автоматическое резервное копирование (добавьте в crontab)
# 0 3 * * * pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql && find ~/backups -name "tg_tinder_bot_*.sql" -mtime +7 -delete
```
## Решение проблем
### Проблемы с базой данных
Проверьте журналы PostgreSQL:
```bash
sudo tail -f /var/log/postgresql/postgresql-*.log
```
### Проблемы с ботом
Проверьте журналы PM2:
```bash
pm2 logs tg_tinder_bot
```
### Проблемы с Nginx
Проверьте журналы Nginx:
```bash
sudo tail -f /var/log/nginx/error.log
```

160
docs/LOCALIZATION.md Normal file
View File

@@ -0,0 +1,160 @@
# Система локализации Telegram Tinder Bot
## Обзор
Система локализации обеспечивает многоязычную поддержку бота с использованием i18next для интерфейса и DeepSeek AI для перевода анкет пользователей.
## Архитектура
### Компоненты системы
1. **LocalizationService** - основной сервис локализации интерфейса
2. **DeepSeekTranslationService** - сервис для перевода анкет с помощью AI
3. **TranslationController** - контроллер для управления переводами
4. **Файлы переводов** - JSON файлы с переводами для каждого языка
### Поддерживаемые языки
- 🇷🇺 Русский (ru) - по умолчанию
- 🇺🇸 Английский (en)
- 🇪🇸 Испанский (es)
- 🇫🇷 Французский (fr)
- 🇩🇪 Немецкий (de)
- 🇮🇹 Итальянский (it)
- 🇵🇹 Португальский (pt)
- 🇨🇳 Китайский (zh)
- 🇯🇵 Японский (ja)
- 🇰🇷 Корейский (ko)
## Использование
### Локализация интерфейса
```typescript
import { t } from '../services/localizationService';
// Простой перевод
const message = t('welcome.greeting');
// Перевод с параметрами
const message = t('profile.ageRange', { min: 18, max: 65 });
// Установка языка пользователя
localizationService.setLanguage('en');
```
### Структура файлов переводов
```json
{
"welcome": {
"greeting": "Добро пожаловать в Telegram Tinder Bot! 💕",
"description": "Найди свою вторую половинку прямо здесь!"
},
"profile": {
"name": "Имя",
"age": "Возраст",
"bio": "О себе"
}
}
```
### Перевод анкет (Premium функция)
```typescript
import DeepSeekTranslationService from '../services/deepSeekTranslationService';
const translationService = DeepSeekTranslationService.getInstance();
// Перевод текста анкеты
const result = await translationService.translateProfile({
text: "Привет! Я люблю путешествовать и читать книги.",
targetLanguage: 'en',
sourceLanguage: 'ru'
});
```
## Настройка
### Переменные окружения
```env
# DeepSeek API для перевода анкет
DEEPSEEK_API_KEY=your_deepseek_api_key_here
```
### База данных
Таблица `users` содержит поле `language` для хранения предпочитаемого языка пользователя:
```sql
ALTER TABLE users
ADD COLUMN language VARCHAR(5) DEFAULT 'ru';
```
## Функции
### Автоматическое определение языка
- При регистрации пользователя язык определяется по `language_code` из Telegram
- Пользователь может изменить язык в настройках
- Поддерживается определение языка текста для перевода
### Премиум функции перевода
- **Перевод анкет** - доступен только для премиум пользователей
- **AI-перевод** - используется DeepSeek API для качественного перевода
- **Контекстный перевод** - сохраняется тон и стиль исходного текста
### Клавиатуры и меню
Все кнопки и меню автоматически локализуются на основе языка пользователя:
```typescript
// Пример создания локализованной клавиатуры
public getLanguageSelectionKeyboard() {
return {
inline_keyboard: [
[
{ text: '🇷🇺 Русский', callback_data: 'set_language_ru' },
{ text: '🇺🇸 English', callback_data: 'set_language_en' }
]
]
};
}
```
## Расширение
### Добавление нового языка
1. Создать файл перевода `src/locales/{language_code}.json`
2. Добавить язык в массив поддерживаемых языков в `LocalizationService`
3. Обновить ограничение в базе данных
4. Добавить кнопку в меню выбора языка
### Добавление новых переводов
1. Добавить ключи в основной файл перевода (`ru.json`)
2. Перевести на все поддерживаемые языки
3. Использовать в коде через функцию `t()`
## Безопасность
- API ключ DeepSeek хранится в переменных окружения
- Проверка премиум статуса перед доступом к переводу
- Ограничение по количеству запросов к API
- Таймауты для предотвращения зависания
## Мониторинг
- Логирование ошибок перевода
- Отслеживание использования API
- Статистика по языкам пользователей
## Производительность
- Кэширование переводов интерфейса
- Ленивая загрузка файлов переводов
- Асинхронная обработка запросов к DeepSeek API
- Индексы в базе данных для быстрого поиска по языку

105
docs/VIP_FUNCTIONS.md Normal file
View File

@@ -0,0 +1,105 @@
# VIP Функции - Документация
## Обзор
Реализованы VIP функции с проверкой премиум статуса пользователя в базе данных.
## База данных
### Новые поля в таблице users:
- `premium` (BOOLEAN) - флаг премиум статуса
- `premium_expires_at` (TIMESTAMP) - дата окончания премиум
## Логика работы
### 1. Кнопка "VIP Поиск"
- **Если premium = false**: показывает информацию о премиум и предложение купить
- **Если premium = true**: открывает VIP поиск с фильтрами
### 2. VIP Поиск включает:
#### Быстрый VIP поиск
- Только пользователи с фото
- Только онлайн пользователи
#### Расширенный поиск
- Фильтр по возрасту
- Фильтр по городу
- Фильтр по целям знакомства
- Фильтр по хобби
- Фильтр по образу жизни
#### Поиск по целям знакомства
- Серьезные отношения
- Общение и дружба
- Развлечения
- Деловые знакомства
#### Поиск по хобби
- Фильтрация по массиву хобби в профиле
## Файлы
### Новые файлы:
- `src/services/vipService.ts` - сервис для работы с VIP функциями
- `src/controllers/vipController.ts` - контроллер VIP поиска
- `src/database/migrations/add_premium_field.sql` - миграция для premium полей
### Изменённые файлы:
- `src/handlers/callbackHandlers.ts` - добавлены VIP обработчики
## Методы VipService
### checkPremiumStatus(telegramId: string)
Проверяет премиум статус пользователя, автоматически убирает истёкший премиум.
### addPremium(telegramId: string, durationDays: number)
Добавляет премиум статус на указанное количество дней.
### vipSearch(telegramId: string, filters: VipSearchFilters)
Выполняет VIP поиск с фильтрами (только для премиум пользователей).
### getPremiumFeatures()
Возвращает описание премиум возможностей.
## Методы VipController
### showVipSearch(chatId, telegramId)
Основной метод - показывает VIP поиск или информацию о премиум.
### performQuickVipSearch(chatId, telegramId)
Быстрый VIP поиск (фото + онлайн).
### showDatingGoalSearch(chatId, telegramId)
Показывает поиск по целям знакомства.
## Тестирование
### Добавить премиум пользователю:
```sql
UPDATE users SET premium = true, premium_expires_at = NOW() + INTERVAL '30 days'
WHERE telegram_id = 'YOUR_TELEGRAM_ID';
```
### Убрать премиум:
```sql
UPDATE users SET premium = false, premium_expires_at = NULL
WHERE telegram_id = 'YOUR_TELEGRAM_ID';
```
## Callback данные
- `get_vip` / `vip_search` - показать VIP поиск
- `vip_quick_search` - быстрый VIP поиск
- `vip_advanced_search` - расширенный поиск
- `vip_dating_goal_search` - поиск по целям
- `vip_goal_{goal}` - поиск по конкретной цели
- `vip_like_{telegramId}` - VIP лайк
- `vip_superlike_{telegramId}` - VIP супер-лайк
- `vip_dislike_{telegramId}` - VIP дизлайк
## Безопасность
- Все VIP функции проверяют премиум статус
- Автоматическое удаление истёкшего премиум
- Валидация всех входных данных
- Проверка существования пользователей перед операциями

View File

@@ -0,0 +1,152 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// Создание расширения для генерации UUID
pgm.createExtension('pgcrypto', { ifNotExists: true });
// Таблица пользователей
pgm.createTable('users', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
telegram_id: { type: 'bigint', notNull: true, unique: true },
username: { type: 'varchar(255)' },
first_name: { type: 'varchar(255)' },
last_name: { type: 'varchar(255)' },
language_code: { type: 'varchar(10)', default: 'en' },
is_active: { type: 'boolean', default: true },
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
last_active_at: { type: 'timestamp', default: pgm.func('NOW()') },
updated_at: { type: 'timestamp', default: pgm.func('NOW()') },
premium: { type: 'boolean', default: false },
premium_expires_at: { type: 'timestamp' }
});
// Таблица профилей
pgm.createTable('profiles', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
name: { type: 'varchar(255)', notNull: true },
age: {
type: 'integer',
notNull: true,
check: 'age >= 18 AND age <= 100'
},
gender: {
type: 'varchar(10)',
notNull: true,
check: "gender IN ('male', 'female', 'other')"
},
interested_in: {
type: 'varchar(10)',
notNull: true,
check: "interested_in IN ('male', 'female', 'both')"
},
looking_for: {
type: 'varchar(20)',
default: 'both',
check: "looking_for IN ('male', 'female', 'both')"
},
bio: { type: 'text' },
photos: { type: 'jsonb', default: '[]' },
interests: { type: 'jsonb', default: '[]' },
city: { type: 'varchar(255)' },
education: { type: 'varchar(255)' },
job: { type: 'varchar(255)' },
height: { type: 'integer' },
religion: { type: 'varchar(255)' },
dating_goal: { type: 'varchar(255)' },
smoking: { type: 'boolean' },
drinking: { type: 'boolean' },
has_kids: { type: 'boolean' },
location_lat: { type: 'decimal(10,8)' },
location_lon: { type: 'decimal(11,8)' },
search_min_age: { type: 'integer', default: 18 },
search_max_age: { type: 'integer', default: 50 },
search_max_distance: { type: 'integer', default: 50 },
is_verified: { type: 'boolean', default: false },
is_visible: { type: 'boolean', default: true },
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
updated_at: { type: 'timestamp', default: pgm.func('NOW()') }
});
// Таблица свайпов
pgm.createTable('swipes', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
target_user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
type: {
type: 'varchar(20)',
notNull: true,
check: "type IN ('like', 'pass', 'superlike')"
},
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
is_match: { type: 'boolean', default: false }
});
pgm.addConstraint('swipes', 'unique_swipe', {
unique: ['user_id', 'target_user_id']
});
// Таблица матчей
pgm.createTable('matches', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
user_id_1: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
user_id_2: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
last_message_at: { type: 'timestamp' },
is_active: { type: 'boolean', default: true },
is_super_match: { type: 'boolean', default: false },
unread_count_1: { type: 'integer', default: 0 },
unread_count_2: { type: 'integer', default: 0 }
});
pgm.addConstraint('matches', 'unique_match', {
unique: ['user_id_1', 'user_id_2']
});
// Таблица сообщений
pgm.createTable('messages', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
match_id: { type: 'uuid', references: 'matches(id)', onDelete: 'CASCADE' },
sender_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
receiver_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
content: { type: 'text', notNull: true },
message_type: {
type: 'varchar(20)',
default: 'text',
check: "message_type IN ('text', 'photo', 'gif', 'sticker')"
},
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
is_read: { type: 'boolean', default: false }
});
// Создание индексов
pgm.createIndex('users', 'telegram_id');
pgm.createIndex('profiles', 'user_id');
pgm.createIndex('profiles', ['location_lat', 'location_lon'], {
where: 'location_lat IS NOT NULL AND location_lon IS NOT NULL'
});
pgm.createIndex('profiles', ['age', 'gender', 'interested_in']);
pgm.createIndex('swipes', ['user_id', 'target_user_id']);
pgm.createIndex('matches', ['user_id_1', 'user_id_2']);
pgm.createIndex('messages', ['match_id', 'created_at']);
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropTable('messages');
pgm.dropTable('matches');
pgm.dropTable('swipes');
pgm.dropTable('profiles');
pgm.dropTable('users');
pgm.dropExtension('pgcrypto');
};

View File

@@ -0,0 +1,25 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// Добавляем колонки, которые могли быть пропущены в схеме
pgm.addColumns('profiles', {
hobbies: { type: 'text' }
});
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropColumns('profiles', ['hobbies']);
};

View File

@@ -0,0 +1,18 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -0,0 +1,29 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// Добавляем отсутствующие колонки в таблицу profiles
pgm.addColumns('profiles', {
religion: { type: 'varchar(255)' },
dating_goal: { type: 'varchar(255)' },
smoking: { type: 'boolean' },
drinking: { type: 'boolean' },
has_kids: { type: 'boolean' }
}, { ifNotExists: true });
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropColumns('profiles', ['religion', 'dating_goal', 'smoking', 'drinking', 'has_kids'], { ifExists: true });
};

View File

@@ -0,0 +1,18 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -0,0 +1,18 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {};

View File

@@ -0,0 +1,42 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// Изменяем тип столбцов с boolean на varchar для хранения строковых значений
pgm.alterColumn('profiles', 'smoking', {
type: 'varchar(50)',
using: 'smoking::text'
});
pgm.alterColumn('profiles', 'drinking', {
type: 'varchar(50)',
using: 'drinking::text'
});
// has_kids оставляем boolean, так как у него всего два состояния
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
// Возвращаем столбцы к типу boolean
pgm.alterColumn('profiles', 'smoking', {
type: 'boolean',
using: "CASE WHEN smoking = 'regularly' OR smoking = 'sometimes' THEN true ELSE false END"
});
pgm.alterColumn('profiles', 'drinking', {
type: 'boolean',
using: "CASE WHEN drinking = 'regularly' OR drinking = 'sometimes' THEN true ELSE false END"
});
};

View File

@@ -0,0 +1,50 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// Создание представления для совместимости со старым кодом (swipes)
pgm.sql(`
CREATE OR REPLACE VIEW swipes_view AS
SELECT
id,
user_id AS swiper_id,
target_user_id AS swiped_id,
type AS direction,
created_at,
is_match
FROM swipes;
`);
// Создание представления для совместимости со старым кодом (matches)
pgm.sql(`
CREATE OR REPLACE VIEW matches_view AS
SELECT
id,
user_id_1 AS user1_id,
user_id_2 AS user2_id,
created_at AS matched_at,
is_active AS status,
last_message_at,
is_super_match,
unread_count_1,
unread_count_2
FROM matches;
`);
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.sql(`DROP VIEW IF EXISTS swipes_view;`);
pgm.sql(`DROP VIEW IF EXISTS matches_view;`);
};

394
package-lock.json generated
View File

@@ -10,10 +10,12 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node-telegram-bot-api": "^0.64.11", "@types/node-telegram-bot-api": "^0.64.11",
"axios": "^1.6.2", "axios": "^1.12.1",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"i18next": "^25.5.2",
"node-pg-migrate": "^8.0.3",
"node-telegram-bot-api": "^0.64.0", "node-telegram-bot-api": "^0.64.0",
"pg": "^8.11.3", "pg": "^8.16.3",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
@@ -438,6 +440,14 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -590,6 +600,123 @@
"uuid": "dist/bin/uuid" "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": { "node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1096,7 +1223,7 @@
"version": "8.15.5", "version": "8.15.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"pg-protocol": "*", "pg-protocol": "*",
@@ -1193,7 +1320,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1202,7 +1328,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@@ -1349,9 +1474,9 @@
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.12.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.4", "form-data": "^4.0.4",
@@ -1867,7 +1992,6 @@
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": { "dependencies": {
"string-width": "^4.2.0", "string-width": "^4.2.0",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
@@ -1989,7 +2113,6 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@@ -2219,6 +2342,12 @@
"node": ">= 0.4" "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": { "node_modules/ecc-jsbn": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -2249,8 +2378,7 @@
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"dev": true
}, },
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
@@ -2408,7 +2536,6 @@
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -2600,6 +2727,34 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/forever-agent": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -2697,7 +2852,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
@@ -2993,6 +3147,36 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/i18next": {
"version": "25.5.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz",
"integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -3218,7 +3402,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3447,8 +3630,7 @@
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"dev": true
}, },
"node_modules/isstream": { "node_modules/isstream": {
"version": "0.1.2", "version": "0.1.2",
@@ -3533,6 +3715,21 @@
"node": ">=8" "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": { "node_modules/jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -4379,6 +4576,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -4439,6 +4645,69 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true "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": { "node_modules/node-releases": {
"version": "2.0.20", "version": "2.0.20",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
@@ -4630,6 +4899,12 @@
"node": ">=6" "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": { "node_modules/parse-json": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -4670,7 +4945,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -4681,6 +4955,31 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "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": { "node_modules/performance-now": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -4690,6 +4989,7 @@
"version": "8.16.3", "version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",
@@ -5257,7 +5557,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5489,7 +5788,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
}, },
@@ -5501,7 +5799,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -5782,7 +6079,21 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "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": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
@@ -5849,7 +6160,19 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "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": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
}, },
@@ -6255,7 +6578,7 @@
"version": "5.9.2", "version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -6423,7 +6746,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },
@@ -6530,7 +6852,24 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "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": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
"string-width": "^4.1.0", "string-width": "^4.1.0",
@@ -6573,7 +6912,6 @@
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@@ -6588,7 +6926,6 @@
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": { "dependencies": {
"cliui": "^8.0.1", "cliui": "^8.0.1",
"escalade": "^3.1.1", "escalade": "^3.1.1",
@@ -6606,7 +6943,6 @@
"version": "21.1.1", "version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }

View File

@@ -5,17 +5,35 @@
"main": "dist/bot.js", "main": "dist/bot.js",
"scripts": { "scripts": {
"start": "node dist/bot.js", "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", "dev": "ts-node src/bot.ts",
"build": "tsc", "build": "tsc && xcopy /E /I src\\locales dist\\locales",
"build:linux": "tsc && cp -R src/locales dist/",
"test": "jest", "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": { "dependencies": {
"@types/node-telegram-bot-api": "^0.64.11", "@types/node-telegram-bot-api": "^0.64.11",
"axios": "^1.6.2", "axios": "^1.12.1",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"i18next": "^25.5.2",
"node-pg-migrate": "^8.0.3",
"node-telegram-bot-api": "^0.64.0", "node-telegram-bot-api": "^0.64.0",
"pg": "^8.11.3", "pg": "^8.16.3",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },

View File

@@ -0,0 +1,43 @@
// add-hobbies-column.js
// Скрипт для добавления колонки hobbies в таблицу profiles
const { Pool } = require('pg');
// Настройки подключения к базе данных
const pool = new Pool({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!'
});
async function addHobbiesColumn() {
try {
console.log('Подключение к базе данных...');
const client = await pool.connect();
console.log('Добавление колонки hobbies в таблицу profiles...');
// SQL запрос для добавления колонки
const sql = `
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS hobbies TEXT;
`;
await client.query(sql);
console.log('✅ Колонка hobbies успешно добавлена в таблицу profiles');
// Закрытие соединения
client.release();
await pool.end();
console.log('Подключение к базе данных закрыто');
} catch (error) {
console.error('❌ Ошибка при добавлении колонки:', error);
await pool.end();
process.exit(1);
}
}
// Запуск функции
addHobbiesColumn();

View File

@@ -0,0 +1,44 @@
// add-premium-columns.js
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
const { Pool } = require('pg');
// Настройки подключения к базе данных - используем те же настройки, что и раньше
const pool = new Pool({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!'
});
async function addPremiumColumns() {
try {
console.log('Подключение к базе данных...');
const client = await pool.connect();
console.log('Добавление колонок premium и premium_expires_at в таблицу users...');
// SQL запрос для добавления колонок
const sql = `
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
`;
await client.query(sql);
console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users');
// Закрытие соединения
client.release();
await pool.end();
console.log('Подключение к базе данных закрыто');
} catch (error) {
console.error('❌ Ошибка при добавлении колонок:', error);
await pool.end();
process.exit(1);
}
}
// Запуск функции
addPremiumColumns();

View File

@@ -0,0 +1,40 @@
// add-premium-columns.js
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
const { Client } = require('pg');
// Настройки подключения к базе данных
const client = new Client({
host: '192.168.0.102',
port: 5432,
user: 'trevor',
password: 'Cl0ud_1985!',
database: 'telegram_tinder_bot'
});
async function addPremiumColumns() {
try {
await client.connect();
console.log('Подключение к базе данных успешно установлено');
// SQL запрос для добавления колонок
const sql = `
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
`;
await client.query(sql);
console.log('Колонки premium и premium_expires_at успешно добавлены в таблицу users');
// Закрываем подключение
await client.end();
console.log('Подключение к базе данных закрыто');
} catch (error) {
console.error('Ошибка при добавлении колонок:', error);
await client.end();
}
}
// Запуск функции
addPremiumColumns();

View File

@@ -0,0 +1,28 @@
// add-premium-columns.ts
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
import { query } from '../src/database/connection';
async function addPremiumColumns() {
try {
console.log('Добавление колонок premium и premium_expires_at в таблицу users...');
// SQL запрос для добавления колонок
const sql = `
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
`;
await query(sql);
console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users');
process.exit(0);
} catch (error) {
console.error('❌ Ошибка при добавлении колонок:', error);
process.exit(1);
}
}
// Запуск функции
addPremiumColumns();

View File

@@ -0,0 +1,101 @@
// Исправленный код для создания профиля
const { Client } = require('pg');
const { v4: uuidv4 } = require('uuid');
// Получаем аргументы из командной строки
const args = process.argv.slice(2);
const telegramId = args[0];
const name = args[1];
const age = parseInt(args[2]);
const gender = args[3];
const city = args[4];
const bio = args[5];
const photoFileId = args[6];
// Проверяем, что все необходимые аргументы предоставлены
if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) {
console.error('Необходимо указать все параметры: telegramId, name, age, gender, city, bio, photoFileId');
process.exit(1);
}
// Устанавливаем соединение с базой данных
const client = new Client({
host: '192.168.0.102',
port: 5432,
user: 'trevor',
password: 'Cl0ud_1985!',
database: 'telegram_tinder_bot'
});
async function createProfile() {
try {
await client.connect();
// Шаг 1: Создаем или обновляем пользователя
const userResult = await client.query(`
INSERT INTO users (telegram_id, username, first_name, last_name)
VALUES ($1, $2, $3, $4)
ON CONFLICT (telegram_id) DO UPDATE SET
username = EXCLUDED.username,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name
RETURNING id
`, [parseInt(telegramId), null, name, null]);
const userId = userResult.rows[0].id;
// Шаг 2: Создаем профиль
const profileId = uuidv4();
const now = new Date();
const interestedIn = gender === 'male' ? 'female' : 'male';
const columns = [
'id', 'user_id', 'name', 'age', 'gender', 'interested_in',
'bio', 'city', 'photos', 'is_verified',
'is_visible', 'created_at', 'updated_at'
].join(', ');
const values = [
profileId, userId, name, age, gender, interestedIn,
bio, city, JSON.stringify([photoFileId]),
false, true, now, now
];
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
await client.query(`
INSERT INTO profiles (${columns})
VALUES (${placeholders})
`, values);
console.log('Профиль успешно создан!');
// Возвращаем информацию о созданном профиле
return {
userId,
profileId,
name,
age,
gender,
interestedIn,
bio,
city,
photos: [photoFileId]
};
} catch (error) {
console.error('Ошибка при создании профиля:', error);
throw error;
} finally {
await client.end();
}
}
createProfile()
.then(profile => {
console.log('Созданный профиль:', profile);
process.exit(0);
})
.catch(error => {
console.error('Создание профиля не удалось:', error);
process.exit(1);
});

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

@@ -0,0 +1,62 @@
// migrate-sync.js
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
// Используется для синхронизации существующей базы с миграциями
const { Client } = require('pg');
const fs = require('fs');
const path = require('path');
// Подключение к базе данных
const client = new Client({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!'
});
async function syncMigrations() {
try {
console.log('Подключение к базе данных...');
await client.connect();
// Создаем таблицу миграций, если её нет
await client.query(`
CREATE TABLE IF NOT EXISTS pgmigrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
`);
// Получаем список файлов миграций
const migrationsDir = path.join(__dirname, '../migrations');
const files = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.js'))
.sort();
// Проверяем, какие миграции уже записаны
const { rows: existingMigrations } = await client.query('SELECT name FROM pgmigrations');
const existingNames = existingMigrations.map(m => m.name);
// Записываем новые миграции
for (const file of files) {
const migrationName = file.replace('.js', '');
if (!existingNames.includes(migrationName)) {
console.log(`Добавление записи о миграции: ${migrationName}`);
await client.query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]);
} else {
console.log(`Миграция ${migrationName} уже записана`);
}
}
console.log('✅ Синхронизация миграций завершена успешно');
await client.end();
} catch (error) {
console.error('❌ Ошибка при синхронизации миграций:', error);
await client.end();
process.exit(1);
}
}
syncMigrations();

BIN
set-premium.js Normal file

Binary file not shown.

2
sql/add_looking_for.sql Normal file
View File

@@ -0,0 +1,2 @@
-- Добавление колонки looking_for в таблицу profiles
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both'));

View File

@@ -0,0 +1,7 @@
-- Добавление недостающих колонок в таблицу profiles
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS hobbies TEXT[];
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS religion VARCHAR(255);
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS dating_goal VARCHAR(50);
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS smoking VARCHAR(20) CHECK (smoking IS NULL OR smoking IN ('never', 'sometimes', 'regularly'));
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS drinking VARCHAR(20) CHECK (drinking IS NULL OR drinking IN ('never', 'sometimes', 'regularly'));
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS has_kids BOOLEAN DEFAULT FALSE;

View File

@@ -0,0 +1,4 @@
-- Добавление колонок premium и premium_expires_at в таблицу users
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;

21
sql/add_updated_at.sql Normal file
View File

@@ -0,0 +1,21 @@
-- Добавление колонки updated_at в таблицу users
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
-- Обновление триггера для автоматического обновления updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Проверка и создание триггера, если он не существует
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'users_updated_at') THEN
CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
END IF;
END$$;

67
sql/recreate_tables.sql Normal file
View File

@@ -0,0 +1,67 @@
-- Сначала создаем таблицы заново
DROP TABLE IF EXISTS profiles CASCADE;
DROP TABLE IF EXISTS users CASCADE;
-- Users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
telegram_id BIGINT UNIQUE NOT NULL,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
language_code VARCHAR(10) DEFAULT 'en',
is_premium BOOLEAN DEFAULT FALSE,
is_blocked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Profiles table
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
age INTEGER NOT NULL CHECK (age >= 18 AND age <= 100),
gender VARCHAR(20) NOT NULL CHECK (gender IN ('male', 'female', 'other')),
looking_for VARCHAR(20) NOT NULL CHECK (looking_for IN ('male', 'female', 'both')),
bio TEXT,
location VARCHAR(255),
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
photos TEXT[], -- Array of photo URLs/file IDs
interests TEXT[], -- Array of interests
hobbies TEXT[],
education VARCHAR(255),
occupation VARCHAR(255),
height INTEGER, -- in cm
religion VARCHAR(255),
dating_goal VARCHAR(50),
smoking VARCHAR(20) CHECK (smoking IN ('never', 'sometimes', 'regularly')),
drinking VARCHAR(20) CHECK (drinking IN ('never', 'sometimes', 'regularly')),
has_kids BOOLEAN DEFAULT FALSE,
relationship_type VARCHAR(30) CHECK (relationship_type IN ('casual', 'serious', 'friendship', 'anything')),
verification_status VARCHAR(20) DEFAULT 'unverified' CHECK (verification_status IN ('unverified', 'pending', 'verified')),
is_active BOOLEAN DEFAULT TRUE,
is_visible BOOLEAN DEFAULT TRUE,
last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id)
);
-- Создаем тригеры для автоматического обновления updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

View File

@@ -4,6 +4,7 @@ import { testConnection, query } from './database/connection';
import { ProfileService } from './services/profileService'; import { ProfileService } from './services/profileService';
import { MatchingService } from './services/matchingService'; import { MatchingService } from './services/matchingService';
import { NotificationService } from './services/notificationService'; import { NotificationService } from './services/notificationService';
import LocalizationService from './services/localizationService';
import { CommandHandlers } from './handlers/commandHandlers'; import { CommandHandlers } from './handlers/commandHandlers';
import { CallbackHandlers } from './handlers/callbackHandlers'; import { CallbackHandlers } from './handlers/callbackHandlers';
import { MessageHandlers } from './handlers/messageHandlers'; import { MessageHandlers } from './handlers/messageHandlers';
@@ -13,6 +14,7 @@ class TelegramTinderBot {
private profileService: ProfileService; private profileService: ProfileService;
private matchingService: MatchingService; private matchingService: MatchingService;
private notificationService: NotificationService; private notificationService: NotificationService;
private localizationService: LocalizationService;
private commandHandlers: CommandHandlers; private commandHandlers: CommandHandlers;
private callbackHandlers: CallbackHandlers; private callbackHandlers: CallbackHandlers;
private messageHandlers: MessageHandlers; private messageHandlers: MessageHandlers;
@@ -27,6 +29,7 @@ class TelegramTinderBot {
this.profileService = new ProfileService(); this.profileService = new ProfileService();
this.matchingService = new MatchingService(); this.matchingService = new MatchingService();
this.notificationService = new NotificationService(this.bot); this.notificationService = new NotificationService(this.bot);
this.localizationService = LocalizationService.getInstance();
this.commandHandlers = new CommandHandlers(this.bot); this.commandHandlers = new CommandHandlers(this.bot);
this.messageHandlers = new MessageHandlers(this.bot); this.messageHandlers = new MessageHandlers(this.bot);
@@ -41,6 +44,9 @@ class TelegramTinderBot {
try { try {
console.log('🚀 Initializing Telegram Tinder Bot...'); console.log('🚀 Initializing Telegram Tinder Bot...');
// Инициализация сервиса локализации
await this.localizationService.initialize();
// Проверка подключения к базе данных // Проверка подключения к базе данных
const dbConnected = await testConnection(); const dbConnected = await testConnection();
if (!dbConnected) { if (!dbConnected) {

View File

@@ -0,0 +1,212 @@
import LocalizationService, { t } from '../services/localizationService';
import DeepSeekTranslationService from '../services/deepSeekTranslationService';
import { VipService } from '../services/vipService';
export class TranslationController {
private localizationService: LocalizationService;
private translationService: DeepSeekTranslationService;
private vipService: VipService;
constructor() {
this.localizationService = LocalizationService.getInstance();
this.translationService = DeepSeekTranslationService.getInstance();
this.vipService = new VipService();
}
// Показать меню выбора языка
public getLanguageSelectionKeyboard() {
return {
inline_keyboard: [
[
{ text: '🇷🇺 Русский', callback_data: 'set_language_ru' },
{ text: '🇺🇸 English', callback_data: 'set_language_en' }
],
[
{ text: '🇪🇸 Español', callback_data: 'set_language_es' },
{ text: '🇫🇷 Français', callback_data: 'set_language_fr' }
],
[
{ text: '🇩🇪 Deutsch', callback_data: 'set_language_de' },
{ text: '🇮🇹 Italiano', callback_data: 'set_language_it' }
],
[
{ text: '🇵🇹 Português', callback_data: 'set_language_pt' },
{ text: '🇨🇳 中文', callback_data: 'set_language_zh' }
],
[
{ text: '🇯🇵 日本語', callback_data: 'set_language_ja' },
{ text: '🇰🇷 한국어', callback_data: 'set_language_ko' }
],
[{ text: t('buttons.back'), callback_data: 'back_to_settings' }]
]
};
}
// Обработать установку языка
public async handleLanguageSelection(telegramId: number, languageCode: string): Promise<string> {
try {
// Здесь должно быть обновление в базе данных
// await userService.updateUserLanguage(telegramId, languageCode);
this.localizationService.setLanguage(languageCode);
const languageNames: { [key: string]: string } = {
'ru': '🇷🇺 Русский',
'en': '🇺🇸 English',
'es': '🇪🇸 Español',
'fr': '🇫🇷 Français',
'de': '🇩🇪 Deutsch',
'it': '🇮🇹 Italiano',
'pt': '🇵🇹 Português',
'zh': '🇨🇳 中文',
'ja': '🇯🇵 日本語',
'ko': '🇰🇷 한국어'
};
return `✅ Язык интерфейса изменен на ${languageNames[languageCode] || languageCode}`;
} catch (error) {
console.error('Error setting language:', error);
return t('errors.serverError');
}
}
// Получить кнопку перевода анкеты
public getTranslateProfileButton(telegramId: number, profileUserId: number) {
return {
inline_keyboard: [
[{ text: t('vip.translateProfile'), callback_data: `translate_profile_${profileUserId}` }]
]
};
}
// Обработать запрос на перевод анкеты
public async handleProfileTranslation(
telegramId: number,
profileUserId: number,
targetLanguage: string
): Promise<{ success: boolean; message: string; translatedProfile?: any }> {
try {
// Проверяем премиум статус
const isPremium = await this.vipService.checkPremiumStatus(telegramId.toString());
if (!isPremium) {
return {
success: false,
message: t('translation.premiumOnly')
};
}
// Получаем профиль для перевода
const profile = await this.getProfileForTranslation(profileUserId);
if (!profile) {
return {
success: false,
message: t('errors.profileNotFound')
};
}
// Переводим профиль
const translatedProfile = await this.translateProfileData(profile, targetLanguage);
return {
success: true,
message: t('translation.translated'),
translatedProfile
};
} catch (error) {
console.error('Profile translation error:', error);
return {
success: false,
message: t('translation.error')
};
}
}
// Получить профиль для перевода (заглушка - нужна реализация)
private async getProfileForTranslation(userId: number): Promise<any> {
// TODO: Реализовать получение профиля из базы данных
// Это должно быть интегрировано с существующим ProfileService
return {
name: 'Sample Name',
bio: 'Sample bio text',
city: 'Sample City',
hobbies: 'Sample hobbies',
datingGoal: 'relationship'
};
}
// Перевести данные профиля
private async translateProfileData(profile: any, targetLanguage: string): Promise<any> {
const fieldsToTranslate = ['bio', 'hobbies'];
const translatedProfile = { ...profile };
for (const field of fieldsToTranslate) {
if (profile[field] && typeof profile[field] === 'string') {
try {
const sourceLanguage = this.translationService.detectLanguage(profile[field]);
// Пропускаем перевод, если исходный и целевой языки совпадают
if (sourceLanguage === targetLanguage) {
continue;
}
const translation = await this.translationService.translateProfile({
text: profile[field],
targetLanguage,
sourceLanguage
});
translatedProfile[field] = translation.translatedText;
} catch (error) {
console.error(`Error translating field ${field}:`, error);
// Оставляем оригинальный текст при ошибке
}
}
}
return translatedProfile;
}
// Форматировать переведенный профиль для отображения
public formatTranslatedProfile(profile: any, originalLanguage: string, targetLanguage: string): string {
const languageNames: { [key: string]: string } = {
'ru': '🇷🇺 Русский',
'en': '🇺🇸 English',
'es': '🇪🇸 Español',
'fr': '🇫🇷 Français',
'de': '🇩🇪 Deutsch',
'it': '🇮🇹 Italiano',
'pt': '🇵🇹 Português',
'zh': '🇨🇳 中文',
'ja': '🇯🇵 日本語',
'ko': '🇰🇷 한국어'
};
let text = `🌐 ${t('translation.translated')}\n`;
text += `📝 ${originalLanguage}${targetLanguage}\n\n`;
text += `👤 ${t('profile.name')}: ${profile.name}\n`;
text += `📍 ${t('profile.city')}: ${profile.city}\n\n`;
if (profile.bio) {
text += `💭 ${t('profile.bio')}:\n${profile.bio}\n\n`;
}
if (profile.hobbies) {
text += `🎯 ${t('profile.hobbies')}:\n${profile.hobbies}\n\n`;
}
if (profile.datingGoal) {
text += `💕 ${t('profile.datingGoal')}: ${t(`profile.${profile.datingGoal}`)}\n`;
}
return text;
}
// Проверить доступность сервиса перевода
public async checkTranslationServiceStatus(): Promise<boolean> {
return await this.translationService.checkServiceAvailability();
}
}
export default TranslationController;

View File

@@ -0,0 +1,291 @@
import TelegramBot, { InlineKeyboardMarkup } from 'node-telegram-bot-api';
import { VipService, VipSearchFilters } from '../services/vipService';
import { ProfileService } from '../services/profileService';
interface VipSearchState {
filters: VipSearchFilters;
currentStep: string;
}
export class VipController {
private bot: TelegramBot;
private vipService: VipService;
private profileService: ProfileService;
private vipSearchStates: Map<string, VipSearchState> = new Map();
constructor(bot: TelegramBot) {
this.bot = bot;
this.vipService = new VipService();
this.profileService = new ProfileService();
}
// Показать VIP поиск или информацию о премиум
async showVipSearch(chatId: number, telegramId: string): Promise<void> {
try {
const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
if (!premiumInfo.isPremium) {
// Показываем информацию о премиум
await this.showPremiumInfo(chatId);
} else {
// Показываем VIP поиск
await this.showVipSearchMenu(chatId, telegramId, premiumInfo);
}
} catch (error) {
console.error('Error showing VIP search:', error);
await this.bot.sendMessage(chatId, '❌ Ошибка при загрузке VIP поиска');
}
}
// Показать информацию о премиум подписке
private async showPremiumInfo(chatId: number): Promise<void> {
const premiumText = this.vipService.getPremiumFeatures();
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: '💎 Купить VIP', url: 'https://t.me/admin_bot' }],
[{ text: '🔙 Назад в меню', callback_data: 'main_menu' }]
]
};
await this.bot.sendMessage(chatId, premiumText, {
reply_markup: keyboard
});
}
// Показать меню VIP поиска
private async showVipSearchMenu(chatId: number, telegramId: string, premiumInfo: any): Promise<void> {
const daysText = premiumInfo.daysLeft ? ` (остался ${premiumInfo.daysLeft} дн.)` : '';
const text = `💎 VIP ПОИСК 💎\n\n` +
`✅ Премиум статус активен${daysText}\n\n` +
`🎯 Выберите тип поиска:`;
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: '🔍 Быстрый VIP поиск', callback_data: 'vip_quick_search' }],
[{ text: '⚙️ Расширенный поиск с фильтрами', callback_data: 'vip_advanced_search' }],
[{ text: '🎯 Поиск по целям знакомства', callback_data: 'vip_dating_goal_search' }],
[{ text: '🎨 Поиск по хобби', callback_data: 'vip_hobbies_search' }],
[{ text: '🔙 Назад в меню', callback_data: 'main_menu' }]
]
};
await this.bot.sendMessage(chatId, text, {
reply_markup: keyboard
});
}
// Быстрый VIP поиск
async performQuickVipSearch(chatId: number, telegramId: string): Promise<void> {
try {
const filters: VipSearchFilters = {
hasPhotos: true,
isOnline: true
};
const results = await this.vipService.vipSearch(telegramId, filters);
await this.showSearchResults(chatId, telegramId, results, 'Быстрый VIP поиск');
} catch (error) {
console.error('Error in quick VIP search:', error);
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении поиска');
}
}
// Начать настройку фильтров для расширенного поиска
async startAdvancedSearch(chatId: number, telegramId: string): Promise<void> {
const state: VipSearchState = {
filters: {},
currentStep: 'age_min'
};
this.vipSearchStates.set(telegramId, state);
await this.bot.sendMessage(
chatId,
'⚙️ Расширенный VIP поиск\n\n' +
'🔢 Укажите минимальный возраст (18-65) или отправьте "-" чтобы пропустить:',
{ }
);
}
// Поиск по целям знакомства
async showDatingGoalSearch(chatId: number, telegramId: string): Promise<void> {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: '💕 Серьёзные отношения', callback_data: 'vip_goal_serious' }],
[{ text: '🎉 Лёгкие отношения', callback_data: 'vip_goal_casual' }],
[{ text: '👥 Дружба', callback_data: 'vip_goal_friends' }],
[{ text: '🔥 Одна ночь', callback_data: 'vip_goal_one_night' }],
[{ text: '😏 FWB', callback_data: 'vip_goal_fwb' }],
[{ text: '💎 Спонсорство', callback_data: 'vip_goal_sugar' }],
[{ text: '💍 Брак с переездом', callback_data: 'vip_goal_marriage_abroad' }],
[{ text: '💫 Полиамория', callback_data: 'vip_goal_polyamory' }],
[{ text: '🤷‍♀️ Пока не определился', callback_data: 'vip_goal_unsure' }],
[{ text: '🔙 Назад', callback_data: 'vip_search' }]
]
};
await this.bot.sendMessage(
chatId,
'🎯 Поиск по целям знакомства\n\nВыберите цель:',
{
reply_markup: keyboard
}
);
}
// Выполнить поиск по цели знакомства
async performDatingGoalSearch(chatId: number, telegramId: string, goal: string): Promise<void> {
try {
// Используем значения из базы данных как есть
const filters: VipSearchFilters = {
datingGoal: goal,
hasPhotos: true
};
const results = await this.vipService.vipSearch(telegramId, filters);
const goalNames: { [key: string]: string } = {
'serious': 'Серьёзные отношения',
'casual': 'Лёгкие отношения',
'friends': 'Дружба',
'one_night': 'Одна ночь',
'fwb': 'FWB',
'sugar': 'Спонсорство',
'marriage_abroad': 'Брак с переездом',
'polyamory': 'Полиамория',
'unsure': 'Пока не определился'
};
const goalName = goalNames[goal] || goal;
await this.showSearchResults(chatId, telegramId, results, `Поиск: ${goalName}`);
} catch (error) {
console.error('Error in dating goal search:', error);
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении поиска');
}
}
// Показать результаты поиска
private async showSearchResults(chatId: number, telegramId: string, results: any[], searchType: string): Promise<void> {
if (results.length === 0) {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: '🔍 Новый поиск', callback_data: 'vip_search' }],
[{ text: '🔙 Главное меню', callback_data: 'main_menu' }]
]
};
await this.bot.sendMessage(
chatId,
`😔 ${searchType}\n\n` +
'К сожалению, никого не найдено по вашим критериям.\n\n' +
'💡 Попробуйте изменить фильтры или выполнить обычный поиск.',
{
reply_markup: keyboard,
}
);
return;
}
await this.bot.sendMessage(
chatId,
`🎉 ${searchType}\n\n` +
`Найдено: ${results.length} ${this.getCountText(results.length)}\n\n` +
'Начинаем просмотр профилей...',
{ }
);
// Показываем первый профиль из результатов
const firstProfile = results[0];
await this.showVipSearchProfile(chatId, telegramId, firstProfile, results, 0);
}
// Показать профиль из VIP поиска
private async showVipSearchProfile(chatId: number, telegramId: string, profile: any, allResults: any[], currentIndex: number): Promise<void> {
try {
let profileText = `💎 VIP Поиск (${currentIndex + 1}/${allResults.length})\n\n`;
profileText += `👤 ${profile.name}, ${profile.age}\n`;
profileText += `📍 ${profile.city || 'Не указан'}\n`;
if (profile.dating_goal) {
const goalText = this.getDatingGoalText(profile.dating_goal);
profileText += `🎯 ${goalText}\n`;
}
if (profile.bio) {
profileText += `\n📝 ${profile.bio}\n`;
}
if (profile.is_online) {
profileText += `\n🟢 Онлайн\n`;
}
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '❤️', callback_data: `vip_like_${profile.telegram_id}` },
{ text: '⭐', callback_data: `vip_superlike_${profile.telegram_id}` },
{ text: '👎', callback_data: `vip_dislike_${profile.telegram_id}` }
],
[{ text: '👤 Полный профиль', callback_data: `view_profile_${profile.user_id}` }],
[
{ text: '⬅️ Предыдущий', callback_data: `vip_prev_${currentIndex}` },
{ text: '➡️ Следующий', callback_data: `vip_next_${currentIndex}` }
],
[{ text: '🔍 Новый поиск', callback_data: 'vip_search' }],
[{ text: '🔙 Главное меню', callback_data: 'main_menu' }]
]
};
// Проверяем есть ли фотографии
if (profile.photos && Array.isArray(profile.photos) && profile.photos.length > 0) {
await this.bot.sendPhoto(chatId, profile.photos[0], {
caption: profileText,
reply_markup: keyboard,
});
} else {
await this.bot.sendMessage(chatId, profileText, {
reply_markup: keyboard,
});
}
// Сохраняем результаты поиска для навигации
// Можно сохранить в Redis или временной переменной
} catch (error) {
console.error('Error showing VIP search profile:', error);
await this.bot.sendMessage(chatId, '❌ Ошибка при показе профиля');
}
}
private getCountText(count: number): string {
const lastDigit = count % 10;
const lastTwoDigits = count % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
return 'пользователей';
}
switch (lastDigit) {
case 1: return 'пользователь';
case 2:
case 3:
case 4: return 'пользователя';
default: return 'пользователей';
}
}
private getDatingGoalText(goal: string): string {
const goals: { [key: string]: string } = {
'serious_relationship': 'Серьезные отношения',
'friendship': 'Общение и дружба',
'fun': 'Развлечения',
'networking': 'Деловые знакомства'
};
return goals[goal] || 'Не указано';
}
}

View File

@@ -2,10 +2,10 @@ import { Pool, PoolConfig } from 'pg';
// Конфигурация пула соединений PostgreSQL // Конфигурация пула соединений PostgreSQL
const poolConfig: PoolConfig = { const poolConfig: PoolConfig = {
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'), port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'telegram_tinder_bot', 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 }), ...(process.env.DB_PASSWORD && { password: process.env.DB_PASSWORD }),
max: 20, // максимальное количество соединений в пуле max: 20, // максимальное количество соединений в пуле
idleTimeoutMillis: 30000, // закрыть соединения, простаивающие 30 секунд idleTimeoutMillis: 30000, // закрыть соединения, простаивающие 30 секунд
@@ -154,10 +154,10 @@ export async function initializeDatabase(): Promise<void> {
await query(` await query(`
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); 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_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_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, looking_for); CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, interested_in);
CREATE INDEX IF NOT EXISTS idx_swipes_swiper_swiped ON swipes(swiper_id, swiped_id); 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(user1_id, user2_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); CREATE INDEX IF NOT EXISTS idx_messages_match ON messages(match_id, created_at);
`); `);

View File

@@ -0,0 +1,14 @@
-- Добавляем поле языка пользователя в таблицу users
ALTER TABLE users
ADD COLUMN language VARCHAR(5) DEFAULT 'ru';
-- Создаем индекс для оптимизации запросов по языку
CREATE INDEX idx_users_language ON users(language);
-- Добавляем ограничение на поддерживаемые языки
ALTER TABLE users
ADD CONSTRAINT check_users_language
CHECK (language IN ('ru', 'en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko'));
-- Обновляем существующих пользователей
UPDATE users SET language = 'ru' WHERE language IS NULL;

View File

@@ -0,0 +1,10 @@
-- Добавление поля premium для VIP функций
ALTER TABLE users ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL;
-- Индекс для быстрого поиска premium пользователей
CREATE INDEX IF NOT EXISTS idx_users_premium ON users(premium, premium_expires_at);
-- Комментарии
COMMENT ON COLUMN users.premium IS 'VIP статус пользователя';
COMMENT ON COLUMN users.premium_expires_at IS 'Дата окончания VIP статуса';

View File

@@ -6,6 +6,10 @@ import { Profile } from '../models/Profile';
import { MessageHandlers } from './messageHandlers'; import { MessageHandlers } from './messageHandlers';
import { ProfileEditController } from '../controllers/profileEditController'; import { ProfileEditController } from '../controllers/profileEditController';
import { EnhancedChatHandlers } from './enhancedChatHandlers'; import { EnhancedChatHandlers } from './enhancedChatHandlers';
import { VipController } from '../controllers/vipController';
import { VipService } from '../services/vipService';
import { TranslationController } from '../controllers/translationController';
import { t } from '../services/localizationService';
export class CallbackHandlers { export class CallbackHandlers {
private bot: TelegramBot; private bot: TelegramBot;
@@ -15,6 +19,9 @@ export class CallbackHandlers {
private messageHandlers: MessageHandlers; private messageHandlers: MessageHandlers;
private profileEditController: ProfileEditController; private profileEditController: ProfileEditController;
private enhancedChatHandlers: EnhancedChatHandlers; private enhancedChatHandlers: EnhancedChatHandlers;
private vipController: VipController;
private vipService: VipService;
private translationController: TranslationController;
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
this.bot = bot; this.bot = bot;
@@ -24,6 +31,9 @@ export class CallbackHandlers {
this.messageHandlers = messageHandlers; this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService); this.profileEditController = new ProfileEditController(this.profileService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot); this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
this.vipController = new VipController(bot);
this.vipService = new VipService();
this.translationController = new TranslationController();
} }
register(): void { register(): void {
@@ -129,7 +139,10 @@ export class CallbackHandlers {
// Просмотр анкет и свайпы // Просмотр анкет и свайпы
else if (data === 'start_browsing') { 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') { } else if (data === 'vip_search') {
await this.handleVipSearch(chatId, telegramId); await this.handleVipSearch(chatId, telegramId);
} else if (data.startsWith('search_by_goal_')) { } else if (data.startsWith('search_by_goal_')) {
@@ -211,7 +224,43 @@ export class CallbackHandlers {
} else if (data === 'back_to_browsing') { } else if (data === 'back_to_browsing') {
await this.handleStartBrowsing(chatId, telegramId); await this.handleStartBrowsing(chatId, telegramId);
} else if (data === 'get_vip') { } else if (data === 'get_vip') {
await this.handleGetVip(chatId, telegramId); await this.vipController.showVipSearch(chatId, telegramId);
}
// VIP функции
else if (data === 'vip_search') {
await this.vipController.showVipSearch(chatId, telegramId);
} else if (data === 'vip_quick_search') {
await this.vipController.performQuickVipSearch(chatId, telegramId);
} else if (data === 'vip_advanced_search') {
await this.vipController.startAdvancedSearch(chatId, telegramId);
} else if (data === 'vip_dating_goal_search') {
await this.vipController.showDatingGoalSearch(chatId, telegramId);
} else if (data.startsWith('vip_goal_')) {
const goal = data.replace('vip_goal_', '');
await this.vipController.performDatingGoalSearch(chatId, telegramId, goal);
} else if (data.startsWith('vip_like_')) {
const targetTelegramId = data.replace('vip_like_', '');
await this.handleVipLike(chatId, telegramId, targetTelegramId);
} else if (data.startsWith('vip_superlike_')) {
const targetTelegramId = data.replace('vip_superlike_', '');
await this.handleVipSuperlike(chatId, telegramId, targetTelegramId);
} else if (data.startsWith('vip_dislike_')) {
const targetTelegramId = data.replace('vip_dislike_', '');
await this.handleVipDislike(chatId, telegramId, targetTelegramId);
}
// Настройки языка и переводы
else if (data === 'language_settings') {
await this.handleLanguageSettings(chatId, telegramId);
} else if (data.startsWith('set_language_')) {
const languageCode = data.replace('set_language_', '');
await this.handleSetLanguage(chatId, telegramId, languageCode);
} else if (data.startsWith('translate_profile_')) {
const profileUserId = parseInt(data.replace('translate_profile_', ''));
await this.handleTranslateProfile(chatId, telegramId, profileUserId);
} else if (data === 'back_to_settings') {
await this.handleSettings(chatId, telegramId);
} }
else { else {
@@ -284,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); const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile) { if (!profile) {
@@ -292,7 +341,7 @@ export class CallbackHandlers {
return; return;
} }
await this.showNextCandidate(chatId, telegramId); await this.showNextCandidate(chatId, telegramId, isNewUser);
} }
// Следующий кандидат // Следующий кандидат
@@ -692,8 +741,8 @@ export class CallbackHandlers {
{ text: '🔔 Уведомления', callback_data: 'notification_settings' } { text: '🔔 Уведомления', callback_data: 'notification_settings' }
], ],
[ [
{ text: '<EFBFBD> Статистика', callback_data: 'view_stats' }, { text: '🌐 Язык интерфейса', callback_data: 'language_settings' },
{ text: '👀 Кто смотрел', callback_data: 'view_profile_viewers' } { text: '📊 Статистика', callback_data: 'view_stats' }
], ],
[ [
{ text: '<27>🚫 Скрыть профиль', callback_data: 'hide_profile' }, { text: '<27>🚫 Скрыть профиль', callback_data: 'hide_profile' },
@@ -845,8 +894,8 @@ export class CallbackHandlers {
} }
} }
async showNextCandidate(chatId: number, telegramId: string): Promise<void> { async showNextCandidate(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
const candidate = await this.matchingService.getNextCandidate(telegramId); const candidate = await this.matchingService.getNextCandidate(telegramId, isNewUser);
if (!candidate) { if (!candidate) {
const keyboard: InlineKeyboardMarkup = { const keyboard: InlineKeyboardMarkup = {
@@ -1324,12 +1373,28 @@ export class CallbackHandlers {
return; return;
} }
const lifestyle = profile.lifestyle || {}; // Обновляем отдельные колонки напрямую, а не через объект lifestyle
lifestyle[type as keyof typeof lifestyle] = value as any; 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, { await this.profileService.updateProfile(profile.userId, updates);
lifestyle: lifestyle
});
const typeTexts: { [key: string]: string } = { const typeTexts: { [key: string]: string } = {
'smoking': 'курение', 'smoking': 'курение',
@@ -1724,20 +1789,30 @@ export class CallbackHandlers {
const profile = await this.profileService.getProfileByTelegramId(telegramId); const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (profile) { if (profile) {
const keyboard: InlineKeyboardMarkup = { // Проверяем премиум статус
inline_keyboard: [ const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
[
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' }, let keyboardRows = [
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' } [
], { text: '👤 Мой профиль', callback_data: 'view_my_profile' },
[ { text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
{ text: '💕 Мои матчи', callback_data: 'view_matches' }, ],
{ text: '⭐ VIP поиск', callback_data: 'vip_search' } [
], { text: '💕 Мои матчи', callback_data: 'view_matches' }
[
{ text: '⚙️ Настройки', callback_data: 'settings' }
]
] ]
];
// Добавляем кнопку VIP поиска если есть премиум, или кнопку "Получить VIP" если нет
if (premiumInfo && premiumInfo.isPremium) {
keyboardRows[1].push({ text: '⭐ VIP поиск', callback_data: 'vip_search' });
} else {
keyboardRows[1].push({ text: '💎 Получить VIP', callback_data: 'get_vip' });
}
keyboardRows.push([{ text: '⚙️ Настройки', callback_data: 'settings' }]);
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: keyboardRows
}; };
await this.bot.sendMessage( await this.bot.sendMessage(
@@ -1886,4 +1961,155 @@ export class CallbackHandlers {
} }
); );
} }
// VIP лайк
async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
try {
// Получаем user_id по telegram_id для совместимости с существующей логикой
const targetUserId = await this.profileService.getUserIdByTelegramId(targetTelegramId);
if (!targetUserId) {
throw new Error('Target user not found');
}
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like');
if (result.isMatch) {
// Это матч!
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '💬 Написать сообщение', callback_data: 'chat_' + targetUserId },
{ text: '📱 Нативный чат', callback_data: 'open_native_chat_' + result.match?.id }
],
[{ text: '🔍 Продолжить VIP поиск', callback_data: 'vip_search' }]
]
};
await this.bot.sendMessage(
chatId,
'🎉 ЭТО МАТЧ! 💕\n\n' +
'Вы понравились друг другу с ' + (targetProfile?.name || 'этим пользователем') + '!\n\n' +
'Теперь вы можете начать общение!',
{ reply_markup: keyboard }
);
} else {
await this.bot.sendMessage(chatId, '👍 Лайк отправлен! Продолжайте VIP поиск.');
}
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
console.error('VIP Like error:', error);
}
}
// VIP супер-лайк
async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
try {
const targetUserId = await this.profileService.getUserIdByTelegramId(targetTelegramId);
if (!targetUserId) {
throw new Error('Target user not found');
}
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'superlike');
if (result.isMatch) {
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '💬 Написать сообщение', callback_data: 'chat_' + targetUserId },
{ text: '📱 Нативный чат', callback_data: 'open_native_chat_' + result.match?.id }
],
[{ text: '🔍 Продолжить VIP поиск', callback_data: 'vip_search' }]
]
};
await this.bot.sendMessage(
chatId,
'⭐ СУПЕР МАТЧ! ⭐\n\n' +
'Ваш супер-лайк привел к матчу с ' + (targetProfile?.name || 'этим пользователем') + '!\n\n' +
'Начните общение прямо сейчас!',
{ reply_markup: keyboard }
);
} else {
await this.bot.sendMessage(chatId, '⭐ Супер-лайк отправлен! Это повышает ваши шансы.');
}
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер-лайка');
console.error('VIP Superlike error:', error);
}
}
// VIP дизлайк
async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
try {
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
await this.bot.sendMessage(chatId, '👎 Профиль пропущен. Продолжайте VIP поиск.');
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении действия');
console.error('VIP Dislike error:', error);
}
}
// Обработчики языковых настроек
async handleLanguageSettings(chatId: number, telegramId: string): Promise<void> {
try {
const keyboard = this.translationController.getLanguageSelectionKeyboard();
await this.bot.sendMessage(
chatId,
`🌐 ${t('commands.settings')} - Выбор языка\n\nВыберите язык интерфейса:`,
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Language settings error:', error);
await this.bot.sendMessage(chatId, t('errors.serverError'));
}
}
async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise<void> {
try {
const result = await this.translationController.handleLanguageSelection(parseInt(telegramId), languageCode);
await this.bot.sendMessage(chatId, result);
// Показать обновленное меню настроек
setTimeout(() => {
this.handleSettings(chatId, telegramId);
}, 1000);
} catch (error) {
console.error('Set language error:', error);
await this.bot.sendMessage(chatId, t('errors.serverError'));
}
}
async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise<void> {
try {
// Показать индикатор загрузки
await this.bot.sendMessage(chatId, t('translation.translating'));
// Получить текущий язык пользователя
const userLanguage = 'ru'; // TODO: получить из базы данных
const result = await this.translationController.handleProfileTranslation(
parseInt(telegramId),
profileUserId,
userLanguage
);
if (result.success && result.translatedProfile) {
const formattedProfile = this.translationController.formatTranslatedProfile(
result.translatedProfile,
'auto',
userLanguage
);
await this.bot.sendMessage(chatId, formattedProfile);
} else {
await this.bot.sendMessage(chatId, result.message);
}
} catch (error) {
console.error('Translate profile error:', error);
await this.bot.sendMessage(chatId, t('translation.error'));
}
}
} }

View File

@@ -2,6 +2,7 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-ap
import { ProfileService } from '../services/profileService'; import { ProfileService } from '../services/profileService';
import { MatchingService } from '../services/matchingService'; import { MatchingService } from '../services/matchingService';
import { Profile } from '../models/Profile'; import { Profile } from '../models/Profile';
import { getUserTranslation } from '../services/localizationService';
export class CommandHandlers { export class CommandHandlers {
private bot: TelegramBot; private bot: TelegramBot;
@@ -104,15 +105,18 @@ export class CommandHandlers {
const profile = await this.profileService.getProfileByTelegramId(userId); const profile = await this.profileService.getProfileByTelegramId(userId);
if (!profile) { if (!profile) {
const createProfileText = await getUserTranslation(userId, 'profile.create');
const noProfileText = await getUserTranslation(userId, 'profile.noProfile');
const keyboard: InlineKeyboardMarkup = { const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [ inline_keyboard: [
[{ text: '🚀 Создать профиль', callback_data: 'create_profile' }] [{ text: createProfileText, callback_data: 'create_profile' }]
] ]
}; };
await this.bot.sendMessage( await this.bot.sendMessage(
msg.chat.id, msg.chat.id,
'❌ У вас пока нет профиля.\nСоздайте его для начала использования бота!', noProfileText,
{ reply_markup: keyboard } { reply_markup: keyboard }
); );
return; return;
@@ -129,9 +133,11 @@ export class CommandHandlers {
const profile = await this.profileService.getProfileByTelegramId(userId); const profile = await this.profileService.getProfileByTelegramId(userId);
if (!profile) { if (!profile) {
const createFirstText = await getUserTranslation(userId, 'profile.createFirst');
await this.bot.sendMessage( await this.bot.sendMessage(
msg.chat.id, msg.chat.id,
'❌ Сначала создайте профиль!\nИспользуйте команду /start' createFirstText
); );
return; return;
} }

View File

@@ -218,9 +218,10 @@ export class EnhancedChatHandlers {
const messageId = await this.chatService.sendMessage( const messageId = await this.chatService.sendMessage(
matchId, matchId,
telegramId, telegramId,
msg.text || '[Медиа]', msg.photo ?
msg.photo ? 'photo' : 'text', (msg.caption || '[Фото]') + ' [file_id: ' + msg.photo[msg.photo.length - 1].file_id + ']' :
msg.photo ? msg.photo[msg.photo.length - 1].file_id : undefined (msg.text || '[Медиа]'),
msg.photo ? 'photo' : 'text'
); );
if (messageId) { if (messageId) {

View File

@@ -217,11 +217,12 @@ export class MessageHandlers {
} }
}); });
// Добавляем специальный callback для новых пользователей
const keyboard: InlineKeyboardMarkup = { const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [ inline_keyboard: [
[ [
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' }, { text: '👤 Мой профиль', callback_data: 'view_my_profile' },
{ text: '🔍 Начать поиск', callback_data: 'start_browsing' } { text: '🔍 Начать поиск', callback_data: 'start_browsing_first' }
], ],
[{ text: '⚙️ Настройки', callback_data: 'settings' }] [{ text: '⚙️ Настройки', callback_data: 'settings' }]
] ]
@@ -493,7 +494,7 @@ export class MessageHandlers {
updates.hobbies = value; updates.hobbies = value;
break; break;
case 'city': case 'city':
// В БД поле называется 'location', но мы используем city в модели // В БД поле называется 'city' (не 'location')
updates.city = value; updates.city = value;
break; break;
case 'job': case 'job':

101
src/locales/de.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "Willkommen beim Telegram Tinder Bot! 💕",
"description": "Finde deine Seelenverwandte direkt hier!",
"getStarted": "Loslegen"
},
"profile": {
"create": "Profil Erstellen",
"edit": "Profil Bearbeiten",
"view": "Profil Ansehen",
"name": "Name",
"age": "Alter",
"city": "Stadt",
"bio": "Über mich",
"photos": "Fotos",
"gender": "Geschlecht",
"lookingFor": "Suche nach",
"datingGoal": "Dating-Ziel",
"hobbies": "Hobbys",
"lifestyle": "Lebensstil",
"male": "Männlich",
"female": "Weiblich",
"both": "Beide",
"relationship": "Beziehung",
"friendship": "Freundschaft",
"dating": "Dating",
"hookup": "Abenteuer",
"marriage": "Ehe",
"networking": "Networking",
"travel": "Reisen",
"business": "Geschäft",
"other": "Andere"
},
"search": {
"title": "Profile Durchsuchen",
"noProfiles": "Keine weiteren Profile! Versuche es später erneut.",
"like": "❤️ Gefällt mir",
"dislike": "👎 Überspringen",
"superLike": "⭐ Super Like",
"match": "Es ist ein Match! 🎉"
},
"vip": {
"title": "VIP-Suche",
"premiumRequired": "Diese Funktion ist nur für Premium-Nutzer verfügbar",
"filters": "Filter",
"ageRange": "Altersbereich",
"cityFilter": "Stadt",
"datingGoalFilter": "Dating-Ziel",
"hobbiesFilter": "Hobbys",
"lifestyleFilter": "Lebensstil",
"applyFilters": "Filter Anwenden",
"clearFilters": "Filter Löschen",
"noResults": "Keine Profile mit deinen Filtern gefunden",
"translateProfile": "🌐 Profil Übersetzen"
},
"premium": {
"title": "Premium-Abonnement",
"features": "Premium-Features:",
"vipSearch": "• VIP-Suche mit Filtern",
"profileTranslation": "• Profilübersetzung in deine Sprache",
"unlimitedLikes": "• Unbegrenzte Likes",
"superLikes": "• Zusätzliche Super-Likes",
"price": "Preis: 4,99€/Monat",
"activate": "Premium Aktivieren"
},
"translation": {
"translating": "Profil wird übersetzt...",
"translated": "Profil übersetzt:",
"error": "Übersetzungsfehler. Bitte versuche es später erneut.",
"premiumOnly": "Übersetzung ist nur für Premium-Nutzer verfügbar"
},
"commands": {
"start": "Hauptmenü",
"profile": "Mein Profil",
"search": "Durchsuchen",
"vip": "VIP-Suche",
"matches": "Matches",
"premium": "Premium",
"settings": "Einstellungen",
"help": "Hilfe"
},
"buttons": {
"back": "« Zurück",
"next": "Weiter »",
"save": "Speichern",
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"edit": "Bearbeiten",
"delete": "Löschen",
"yes": "Ja",
"no": "Nein"
},
"errors": {
"profileNotFound": "Profil nicht gefunden",
"profileIncomplete": "Bitte vervollständige dein Profil",
"ageInvalid": "Bitte gib ein gültiges Alter ein (18-100)",
"photoRequired": "Bitte füge mindestens ein Foto hinzu",
"networkError": "Netzwerkfehler. Bitte versuche es später erneut.",
"serverError": "Serverfehler. Bitte versuche es später erneut."
}
}

113
src/locales/en.json Normal file
View File

@@ -0,0 +1,113 @@
{
"welcome": {
"greeting": "Welcome to Telegram Tinder Bot! 💕",
"description": "Find your soulmate right here!",
"getStarted": "Get Started"
},
"profile": {
"create": "Create Profile",
"edit": "✏️ Edit",
"view": "View Profile",
"name": "Name",
"age": "Age",
"city": "City",
"bio": "About",
"photos": "📸 Photos",
"gender": "Gender",
"lookingFor": "Looking for",
"datingGoal": "Dating Goal",
"hobbies": "Hobbies",
"lifestyle": "Lifestyle",
"male": "Male",
"female": "Female",
"both": "Both",
"relationship": "Relationship",
"friendship": "Friendship",
"dating": "Dating",
"hookup": "Hookup",
"marriage": "Marriage",
"networking": "Networking",
"travel": "Travel",
"business": "Business",
"other": "Other",
"cityNotSpecified": "Not specified",
"bioNotSpecified": "No description provided",
"interests": "Interests",
"startSearch": "🔍 Start Search",
"noProfile": "❌ You don't have a profile yet.\nCreate one to start using the bot!",
"createFirst": "❌ Create a profile first!\nUse /start command"
},
"search": {
"title": "Browse Profiles",
"noProfiles": "No more profiles! Try again later.",
"like": "❤️ Like",
"dislike": "👎 Pass",
"superLike": "⭐ Super Like",
"match": "It's a match! 🎉"
},
"vip": {
"title": "VIP Search",
"premiumRequired": "This feature is available for premium users only",
"filters": "Filters",
"ageRange": "Age Range",
"cityFilter": "City",
"datingGoalFilter": "Dating Goal",
"hobbiesFilter": "Hobbies",
"lifestyleFilter": "Lifestyle",
"applyFilters": "Apply Filters",
"clearFilters": "Clear Filters",
"noResults": "No profiles found with your filters",
"translateProfile": "🌐 Translate Profile"
},
"premium": {
"title": "Premium Subscription",
"features": "Premium features:",
"vipSearch": "• VIP search with filters",
"profileTranslation": "• Profile translation to your language",
"unlimitedLikes": "• Unlimited likes",
"superLikes": "• Extra super likes",
"price": "Price: $4.99/month",
"activate": "Activate Premium"
},
"translation": {
"translating": "Translating profile...",
"translated": "Profile translated:",
"error": "Translation error. Please try again later.",
"premiumOnly": "Translation is available for premium users only"
},
"commands": {
"start": "Main Menu",
"profile": "My Profile",
"search": "Browse",
"vip": "VIP Search",
"matches": "Matches",
"premium": "Premium",
"settings": "Settings",
"help": "Help"
},
"buttons": {
"back": "« Back",
"next": "Next »",
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"edit": "Edit",
"delete": "Delete",
"yes": "Yes",
"no": "No"
},
"errors": {
"profileNotFound": "Profile not found",
"profileIncomplete": "Please complete your profile",
"ageInvalid": "Please enter a valid age (18-100)",
"photoRequired": "Please add at least one photo",
"networkError": "Network error. Please try again later.",
"serverError": "Server error. Please try again later."
},
"common": {
"back": "👈 Back"
},
"matches": {
"noMatches": "✨ You don't have any matches yet.\n\n🔍 Try browsing more profiles!\nUse /browse to search."
}
}

94
src/locales/en_fixed.json Normal file
View File

@@ -0,0 +1,94 @@
{
"commands": {
"start": "🏠 Main menu",
"help": " Help",
"profile": "👤 My profile",
"search": "🔍 Browse profiles",
"matches": "💕 Matches",
"premium": "⭐ Premium",
"settings": "⚙️ Settings"
},
"menu": {
"main": "🏠 Main menu",
"back": "👈 Back",
"profile": "👤 Profile",
"search": "🔍 Browse",
"matches": "💕 Matches",
"premium": "⭐ Premium",
"settings": "⚙️ Settings"
},
"welcome": {
"newUser": "Welcome to Telegram Tinder Bot! 💕\\n\\nHere you can find interesting people for communication and dating.\\n\\nTo get started, create your profile!",
"existingUser": "Welcome back! 👋\\n\\nChoose an action:",
"createProfile": "🚀 Create profile"
},
"help": {
"title": "📋 How to use the bot:",
"step1": "1⃣ Create profile",
"step1Desc": " • Enter name, age, city\\n • Add description\\n • Upload photo",
"step2": "2⃣ Browse profiles",
"step2Desc": " • Swipe through other users' profiles\\n • Like (❤️) or dislike (👎)",
"step3": "3⃣ Get match",
"step3Desc": " • When two people like each other\\n • Chat becomes available",
"step4": "4⃣ Communication",
"step4Desc": " • Find common interests\\n • Arrange meetings",
"tipsTitle": "💡 Tips:",
"tips": "• Use quality photos\\n• Write interesting description\\n• Be polite in communication",
"createProfile": "🚀 Create profile"
},
"settings": {
"title": "⚙️ Settings",
"language": "🌐 Interface language",
"ageRange": "📅 Age range",
"showAge": "🎂 Show age",
"showCity": "📍 Show city",
"notifications": "🔔 Notifications",
"privacy": "🔒 Privacy",
"back": "👈 Back"
},
"languages": {
"ru": "🇷🇺 Русский",
"en": "🇺🇸 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어",
"uz": "🇺🇿 O'zbekcha",
"kk": "🇰🇿 Қазақша"
},
"howItWorks": {
"title": "🤔 How does it work?",
"step1": "1⃣ Create profile",
"step1Desc": " • Enter name, age, city\\n • Add description\\n • Upload photo",
"step2": "2⃣ Browse profiles",
"step2Desc": " • Swipe through other users' profiles\\n • Like (❤️) or dislike (👎)",
"step3": "3⃣ Get match",
"step3Desc": " • When two people like each other\\n • Chat becomes available",
"step4": "4⃣ Communication",
"step4Desc": " • Find common interests\\n • Arrange meetings",
"tipsTitle": "💡 Tips:",
"tips": "• Use quality photos\\n• Write interesting description\\n• Be polite in communication",
"createProfile": "🚀 Create profile"
},
"noProfile": {
"message": "❌ You don't have a profile yet.\\nCreate one to start using the bot!",
"createButton": "🚀 Create profile"
},
"profileCreated": {
"success": "🎉 Profile created successfully!\\n\\nWelcome, {{name}}! 💖\\n\\nNow you can start searching for your soulmate!",
"myProfile": "👤 My profile",
"startSearch": "🔍 Start search"
},
"errors": {
"profileNotFound": "Profile not found",
"profileIncomplete": "Fill out the profile completely",
"ageInvalid": "Enter correct age (18-100)",
"photoRequired": "Add at least one photo",
"networkError": "Network error. Try later.",
"serverError": "Server error. Try later."
}
}

101
src/locales/es.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "¡Bienvenido al Bot de Tinder en Telegram! 💕",
"description": "¡Encuentra a tu alma gemela aquí mismo!",
"getStarted": "Comenzar"
},
"profile": {
"create": "Crear Perfil",
"edit": "Editar Perfil",
"view": "Ver Perfil",
"name": "Nombre",
"age": "Edad",
"city": "Ciudad",
"bio": "Acerca de",
"photos": "Fotos",
"gender": "Género",
"lookingFor": "Buscando",
"datingGoal": "Objetivo de Cita",
"hobbies": "Aficiones",
"lifestyle": "Estilo de Vida",
"male": "Masculino",
"female": "Femenino",
"both": "Ambos",
"relationship": "Relación",
"friendship": "Amistad",
"dating": "Citas",
"hookup": "Aventura",
"marriage": "Matrimonio",
"networking": "Networking",
"travel": "Viajes",
"business": "Negocios",
"other": "Otro"
},
"search": {
"title": "Explorar Perfiles",
"noProfiles": "¡No hay más perfiles! Inténtalo más tarde.",
"like": "❤️ Me Gusta",
"dislike": "👎 Pasar",
"superLike": "⭐ Super Like",
"match": "¡Es un match! 🎉"
},
"vip": {
"title": "Búsqueda VIP",
"premiumRequired": "Esta función está disponible solo para usuarios premium",
"filters": "Filtros",
"ageRange": "Rango de Edad",
"cityFilter": "Ciudad",
"datingGoalFilter": "Objetivo de Cita",
"hobbiesFilter": "Aficiones",
"lifestyleFilter": "Estilo de Vida",
"applyFilters": "Aplicar Filtros",
"clearFilters": "Limpiar Filtros",
"noResults": "No se encontraron perfiles con tus filtros",
"translateProfile": "🌐 Traducir Perfil"
},
"premium": {
"title": "Suscripción Premium",
"features": "Características premium:",
"vipSearch": "• Búsqueda VIP con filtros",
"profileTranslation": "• Traducción de perfiles a tu idioma",
"unlimitedLikes": "• Me gusta ilimitados",
"superLikes": "• Super likes adicionales",
"price": "Precio: $4.99/mes",
"activate": "Activar Premium"
},
"translation": {
"translating": "Traduciendo perfil...",
"translated": "Perfil traducido:",
"error": "Error de traducción. Por favor, inténtalo más tarde.",
"premiumOnly": "La traducción está disponible solo para usuarios premium"
},
"commands": {
"start": "Menú Principal",
"profile": "Mi Perfil",
"search": "Explorar",
"vip": "Búsqueda VIP",
"matches": "Matches",
"premium": "Premium",
"settings": "Configuración",
"help": "Ayuda"
},
"buttons": {
"back": "« Atrás",
"next": "Siguiente »",
"save": "Guardar",
"cancel": "Cancelar",
"confirm": "Confirmar",
"edit": "Editar",
"delete": "Eliminar",
"yes": "Sí",
"no": "No"
},
"errors": {
"profileNotFound": "Perfil no encontrado",
"profileIncomplete": "Por favor completa tu perfil",
"ageInvalid": "Por favor ingresa una edad válida (18-100)",
"photoRequired": "Por favor agrega al menos una foto",
"networkError": "Error de red. Por favor inténtalo más tarde.",
"serverError": "Error del servidor. Por favor inténtalo más tarde."
}
}

94
src/locales/es_fixed.json Normal file
View File

@@ -0,0 +1,94 @@
{
"commands": {
"start": "🏠 Menú principal",
"help": " Ayuda",
"profile": "👤 Mi perfil",
"search": "🔍 Buscar perfiles",
"matches": "💕 Matches",
"premium": "⭐ Premium",
"settings": "⚙️ Configuración"
},
"menu": {
"main": "🏠 Menú principal",
"back": "👈 Atrás",
"profile": "👤 Perfil",
"search": "🔍 Buscar",
"matches": "💕 Matches",
"premium": "⭐ Premium",
"settings": "⚙️ Configuración"
},
"welcome": {
"newUser": "¡Bienvenido a Telegram Tinder Bot! 💕\\n\\nAquí puedes encontrar personas interesantes para comunicarte y conocer.\\n\\n¡Para comenzar, crea tu perfil!",
"existingUser": "¡Bienvenido de vuelta! 👋\\n\\nElige una acción:",
"createProfile": "🚀 Crear perfil"
},
"help": {
"title": "📋 Cómo usar el bot:",
"step1": "1⃣ Crear perfil",
"step1Desc": " • Indica nombre, edad, ciudad\\n • Agrega descripción\\n • Sube una foto",
"step2": "2⃣ Navegar perfiles",
"step2Desc": " • Desliza por los perfiles de otros usuarios\\n • Dale me gusta (❤️) o no me gusta (👎)",
"step3": "3⃣ Obtener match",
"step3Desc": " • Cuando dos personas se gustan mutuamente\\n • Se habilita el chat",
"step4": "4⃣ Comunicación",
"step4Desc": " • Encuentra intereses comunes\\n • Organiza encuentros",
"tipsTitle": "💡 Consejos:",
"tips": "• Usa fotos de calidad\\n• Escribe una descripción interesante\\n• Sé educado en la comunicación",
"createProfile": "🚀 Crear perfil"
},
"settings": {
"title": "⚙️ Configuración",
"language": "🌐 Idioma de la interfaz",
"ageRange": "📅 Rango de edad",
"showAge": "🎂 Mostrar edad",
"showCity": "📍 Mostrar ciudad",
"notifications": "🔔 Notificaciones",
"privacy": "🔒 Privacidad",
"back": "👈 Atrás"
},
"languages": {
"ru": "🇷🇺 Русский",
"en": "🇺🇸 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어",
"uz": "🇺🇿 O'zbekcha",
"kk": "🇰🇿 Қазақша"
},
"howItWorks": {
"title": "🤔 ¿Cómo funciona?",
"step1": "1⃣ Crear perfil",
"step1Desc": " • Indica nombre, edad, ciudad\\n • Agrega descripción\\n • Sube una foto",
"step2": "2⃣ Navegar perfiles",
"step2Desc": " • Desliza por los perfiles de otros usuarios\\n • Dale me gusta (❤️) o no me gusta (👎)",
"step3": "3⃣ Obtener match",
"step3Desc": " • Cuando dos personas se gustan mutuamente\\n • Se habilita el chat",
"step4": "4⃣ Comunicación",
"step4Desc": " • Encuentra intereses comunes\\n • Organiza encuentros",
"tipsTitle": "💡 Consejos:",
"tips": "• Usa fotos de calidad\\n• Escribe una descripción interesante\\n• Sé educado en la comunicación",
"createProfile": "🚀 Crear perfil"
},
"noProfile": {
"message": "❌ Aún no tienes un perfil.\\n¡Crea uno para empezar a usar el bot!",
"createButton": "🚀 Crear perfil"
},
"profileCreated": {
"success": "🎉 ¡Perfil creado exitosamente!\\n\\n¡Bienvenido, {{name}}! 💖\\n\\n¡Ahora puedes empezar a buscar tu media naranja!",
"myProfile": "👤 Mi perfil",
"startSearch": "🔍 Comenzar búsqueda"
},
"errors": {
"profileNotFound": "Perfil no encontrado",
"profileIncomplete": "Completa el perfil por completo",
"ageInvalid": "Ingresa edad correcta (18-100)",
"photoRequired": "Agrega al menos una foto",
"networkError": "Error de red. Intenta más tarde.",
"serverError": "Error del servidor. Intenta más tarde."
}
}

101
src/locales/fr.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "Bienvenue sur le Bot Tinder Telegram ! 💕",
"description": "Trouvez votre âme sœur ici même !",
"getStarted": "Commencer"
},
"profile": {
"create": "Créer un Profil",
"edit": "Modifier le Profil",
"view": "Voir le Profil",
"name": "Nom",
"age": "Âge",
"city": "Ville",
"bio": "À propos",
"photos": "Photos",
"gender": "Genre",
"lookingFor": "Recherche",
"datingGoal": "Objectif de Rencontre",
"hobbies": "Loisirs",
"lifestyle": "Style de Vie",
"male": "Masculin",
"female": "Féminin",
"both": "Les Deux",
"relationship": "Relation",
"friendship": "Amitié",
"dating": "Rendez-vous",
"hookup": "Aventure",
"marriage": "Mariage",
"networking": "Réseautage",
"travel": "Voyage",
"business": "Affaires",
"other": "Autre"
},
"search": {
"title": "Parcourir les Profils",
"noProfiles": "Plus de profils ! Réessayez plus tard.",
"like": "❤️ J'aime",
"dislike": "👎 Passer",
"superLike": "⭐ Super Like",
"match": "C'est un match ! 🎉"
},
"vip": {
"title": "Recherche VIP",
"premiumRequired": "Cette fonction est disponible uniquement pour les utilisateurs premium",
"filters": "Filtres",
"ageRange": "Tranche d'Âge",
"cityFilter": "Ville",
"datingGoalFilter": "Objectif de Rencontre",
"hobbiesFilter": "Loisirs",
"lifestyleFilter": "Style de Vie",
"applyFilters": "Appliquer les Filtres",
"clearFilters": "Effacer les Filtres",
"noResults": "Aucun profil trouvé avec vos filtres",
"translateProfile": "🌐 Traduire le Profil"
},
"premium": {
"title": "Abonnement Premium",
"features": "Fonctionnalités premium :",
"vipSearch": "• Recherche VIP avec filtres",
"profileTranslation": "• Traduction de profils dans votre langue",
"unlimitedLikes": "• J'aime illimités",
"superLikes": "• Super likes supplémentaires",
"price": "Prix : 4,99€/mois",
"activate": "Activer Premium"
},
"translation": {
"translating": "Traduction du profil...",
"translated": "Profil traduit :",
"error": "Erreur de traduction. Veuillez réessayer plus tard.",
"premiumOnly": "La traduction est disponible uniquement pour les utilisateurs premium"
},
"commands": {
"start": "Menu Principal",
"profile": "Mon Profil",
"search": "Parcourir",
"vip": "Recherche VIP",
"matches": "Matches",
"premium": "Premium",
"settings": "Paramètres",
"help": "Aide"
},
"buttons": {
"back": "« Retour",
"next": "Suivant »",
"save": "Sauvegarder",
"cancel": "Annuler",
"confirm": "Confirmer",
"edit": "Modifier",
"delete": "Supprimer",
"yes": "Oui",
"no": "Non"
},
"errors": {
"profileNotFound": "Profil non trouvé",
"profileIncomplete": "Veuillez compléter votre profil",
"ageInvalid": "Veuillez entrer un âge valide (18-100)",
"photoRequired": "Veuillez ajouter au moins une photo",
"networkError": "Erreur réseau. Veuillez réessayer plus tard.",
"serverError": "Erreur serveur. Veuillez réessayer plus tard."
}
}

101
src/locales/it.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "Benvenuto su Telegram Tinder Bot! 💕",
"description": "Trova la tua anima gemella proprio qui!",
"getStarted": "Inizia"
},
"profile": {
"create": "Crea Profilo",
"edit": "Modifica Profilo",
"view": "Visualizza Profilo",
"name": "Nome",
"age": "Età",
"city": "Città",
"bio": "Info",
"photos": "Foto",
"gender": "Genere",
"lookingFor": "Cerco",
"datingGoal": "Obiettivo Appuntamenti",
"hobbies": "Hobby",
"lifestyle": "Stile di Vita",
"male": "Maschio",
"female": "Femmina",
"both": "Entrambi",
"relationship": "Relazione",
"friendship": "Amicizia",
"dating": "Appuntamenti",
"hookup": "Avventura",
"marriage": "Matrimonio",
"networking": "Networking",
"travel": "Viaggi",
"business": "Affari",
"other": "Altro"
},
"search": {
"title": "Sfoglia Profili",
"noProfiles": "Nessun altro profilo! Riprova più tardi.",
"like": "❤️ Mi Piace",
"dislike": "👎 Salta",
"superLike": "⭐ Super Like",
"match": "È un match! 🎉"
},
"vip": {
"title": "Ricerca VIP",
"premiumRequired": "Questa funzione è disponibile solo per utenti premium",
"filters": "Filtri",
"ageRange": "Fascia di Età",
"cityFilter": "Città",
"datingGoalFilter": "Obiettivo Appuntamenti",
"hobbiesFilter": "Hobby",
"lifestyleFilter": "Stile di Vita",
"applyFilters": "Applica Filtri",
"clearFilters": "Cancella Filtri",
"noResults": "Nessun profilo trovato con i tuoi filtri",
"translateProfile": "🌐 Traduci Profilo"
},
"premium": {
"title": "Abbonamento Premium",
"features": "Funzionalità premium:",
"vipSearch": "• Ricerca VIP con filtri",
"profileTranslation": "• Traduzione profili nella tua lingua",
"unlimitedLikes": "• Mi piace illimitati",
"superLikes": "• Super like extra",
"price": "Prezzo: €4,99/mese",
"activate": "Attiva Premium"
},
"translation": {
"translating": "Traduzione profilo...",
"translated": "Profilo tradotto:",
"error": "Errore di traduzione. Riprova più tardi.",
"premiumOnly": "La traduzione è disponibile solo per utenti premium"
},
"commands": {
"start": "Menu Principale",
"profile": "Il Mio Profilo",
"search": "Sfoglia",
"vip": "Ricerca VIP",
"matches": "Match",
"premium": "Premium",
"settings": "Impostazioni",
"help": "Aiuto"
},
"buttons": {
"back": "« Indietro",
"next": "Avanti »",
"save": "Salva",
"cancel": "Annulla",
"confirm": "Conferma",
"edit": "Modifica",
"delete": "Elimina",
"yes": "Sì",
"no": "No"
},
"errors": {
"profileNotFound": "Profilo non trovato",
"profileIncomplete": "Per favore completa il tuo profilo",
"ageInvalid": "Per favore inserisci un'età valida (18-100)",
"photoRequired": "Per favore aggiungi almeno una foto",
"networkError": "Errore di rete. Riprova più tardi.",
"serverError": "Errore del server. Riprova più tardi."
}
}

101
src/locales/ja.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "Telegram Tinder Botへようこそ💕",
"description": "ここであなたの運命の人を見つけましょう!",
"getStarted": "始める"
},
"profile": {
"create": "プロフィール作成",
"edit": "プロフィール編集",
"view": "プロフィール表示",
"name": "名前",
"age": "年齢",
"city": "都市",
"bio": "自己紹介",
"photos": "写真",
"gender": "性別",
"lookingFor": "探している相手",
"datingGoal": "出会いの目的",
"hobbies": "趣味",
"lifestyle": "ライフスタイル",
"male": "男性",
"female": "女性",
"both": "どちらでも",
"relationship": "恋愛関係",
"friendship": "友達",
"dating": "デート",
"hookup": "カジュアル",
"marriage": "結婚",
"networking": "ネットワーキング",
"travel": "旅行",
"business": "ビジネス",
"other": "その他"
},
"search": {
"title": "プロフィール閲覧",
"noProfiles": "これ以上プロフィールがありません!後でもう一度お試しください。",
"like": "❤️ いいね",
"dislike": "👎 スキップ",
"superLike": "⭐ スーパーライク",
"match": "マッチしました!🎉"
},
"vip": {
"title": "VIP検索",
"premiumRequired": "この機能はプレミアムユーザーのみご利用いただけます",
"filters": "フィルター",
"ageRange": "年齢範囲",
"cityFilter": "都市",
"datingGoalFilter": "出会いの目的",
"hobbiesFilter": "趣味",
"lifestyleFilter": "ライフスタイル",
"applyFilters": "フィルター適用",
"clearFilters": "フィルタークリア",
"noResults": "フィルター条件に一致するプロフィールが見つかりません",
"translateProfile": "🌐 プロフィール翻訳"
},
"premium": {
"title": "プレミアム購読",
"features": "プレミアム機能:",
"vipSearch": "• フィルター付きVIP検索",
"profileTranslation": "• プロフィールをあなたの言語に翻訳",
"unlimitedLikes": "• 無制限いいね",
"superLikes": "• 追加スーパーライク",
"price": "価格¥650/月",
"activate": "プレミアム有効化"
},
"translation": {
"translating": "プロフィールを翻訳中...",
"translated": "翻訳されたプロフィール:",
"error": "翻訳エラー。後でもう一度お試しください。",
"premiumOnly": "翻訳機能はプレミアムユーザーのみご利用いただけます"
},
"commands": {
"start": "メインメニュー",
"profile": "マイプロフィール",
"search": "閲覧",
"vip": "VIP検索",
"matches": "マッチ",
"premium": "プレミアム",
"settings": "設定",
"help": "ヘルプ"
},
"buttons": {
"back": "« 戻る",
"next": "次へ »",
"save": "保存",
"cancel": "キャンセル",
"confirm": "確認",
"edit": "編集",
"delete": "削除",
"yes": "はい",
"no": "いいえ"
},
"errors": {
"profileNotFound": "プロフィールが見つかりません",
"profileIncomplete": "プロフィールを完成させてください",
"ageInvalid": "有効な年齢を入力してください18-100",
"photoRequired": "最低1枚の写真を追加してください",
"networkError": "ネットワークエラー。後でもう一度お試しください。",
"serverError": "サーバーエラー。後でもう一度お試しください。"
}
}

152
src/locales/kk.json Normal file
View File

@@ -0,0 +1,152 @@
{
"welcome": {
"greeting": "🎉 Telegram Tinder Botқа қош келдіңіз!\n\n💕 Мұнда сіз өзіңіздің жарыңызды таба аласыз!\n\nБастау үшін профиліңізді жасаңыз:",
"description": "Өзіңіздің жарыңызды осы жерден табыңыз!",
"getStarted": "Танысуды бастау",
"haveProfile": "🎉 Қош келдіңіз, {{name}}!\n\n💖 Telegram Tinder Bot жұмысқа дайын!\n\nНе істегіңіз келеді?"
},
"profile": {
"create": "Профиль жасау",
"edit": "Профильді өңдеу",
"view": "Профильді көру",
"name": "Аты",
"age": "Жасы",
"city": "Қала",
"bio": "Өзім туралы",
"photos": "Суреттер",
"gender": "Жынысы",
"lookingFor": "Іздеймін",
"datingGoal": "Танысу мақсаты",
"hobbies": "Хоббилер",
"lifestyle": "Өмір салты",
"male": "Ер",
"female": "Әйел",
"both": "Маңызды емес",
"relationship": "Серьезды қатынас",
"friendship": "Достық",
"dating": "Кездесулер",
"hookup": "Қысқа қатынас",
"marriage": "Неке",
"networking": "Қарым-қатынас",
"travel": "Саяхат",
"business": "Бизнес",
"other": "Басқа"
},
"search": {
"title": "Профильдерді іздеу",
"noProfiles": "Профильдер таусылды! Кейінірек көріңіз.",
"like": "👍 Ұнайды",
"dislike": "👎 Ұнамайды",
"superlike": "💖 Супер ұнайды",
"match": "Бұл өзара ұнау! 🎉",
"tryAgain": "🔄 Қайта көру",
"myMatches": "💕 Менің матчтарым",
"allViewed": "🎉 Сіз барлық қолжетімді кандидаттарды қарап шықтыңыз!\n\n⏰ Кейінірек көріңіз - жаңа профильдер пайда болуы мүмкін!",
"viewProfile": "👤 Профиль",
"morePhotos": "📸 Тағы суреттер",
"next": "⏭ Келесі",
"sendMessage": "💬 Хабар жазу",
"continueBrowsing": "🔍 Іздеуді жалғастыру",
"matchFound": "🎉 БҰЛ МАТЧ! 💕\n\n{{name}} пен өзара ұнадыңыз!\n\nЕнді сөйлесуді бастай аласыз!",
"noMoreProfiles": "😔 Қазір жаңа профильдер жоқ.\n\n⏰ Кейінірек қайта келуіңізге болады!"
},
"vip": {
"title": "⭐ VIP Іздеу",
"description": "Премиум мүмкіндіктермен іздеңіз!",
"features": "• Шексіз лайктар\n• Супер лайктар\n• Кімдер ұнатқанын көру\n• Жарнамасыз тәжірибе",
"getVip": "VIP алу",
"alreadyVip": "Сіз қазірдің өзінде VIP пайдаланушысыз!"
},
"translation": {
"inProgress": "🔄 Аударылуда...",
"completed": "✅ Аударма дайын!",
"failed": "❌ Аударма қатесі",
"error": "Аударма қатесі. Кейінірек көріңіз.",
"premiumOnly": "Аударма тек премиум пайдаланушылар үшін"
},
"commands": {
"start": "Басты мәзір",
"profile": "Менің профилім",
"search": "Іздеу",
"vip": "VIP іздеу",
"matches": "Өзара ұнатулар",
"premium": "Премиум",
"settings": "Баптаулар",
"help": "Көмек"
},
"buttons": {
"back": "« Артқа",
"next": "Келесі »",
"save": "Сақтау",
"cancel": "Бас тарту",
"confirm": "Растау",
"edit": "Өңдеу",
"delete": "Жою",
"yes": "Иә",
"no": "Жоқ"
},
"help": {
"title": "🤖 Telegram Tinder Bot - Көмек",
"commands": "📋 Қолжетімді командалар:",
"commandStart": "/start - Басты мәзір",
"commandProfile": "/profile - Профиль басқаруы",
"commandBrowse": "/browse - Профильдерді көру",
"commandMatches": "/matches - Сіздің матчтарыңыз",
"commandSettings": "/settings - Баптаулар",
"commandHelp": "/help - Осы көмек",
"howToUse": "📱 Қалай пайдалану:",
"step1": "1. Сурет пен сипаттамамен профиль жасаңыз",
"step2": "2. Басқа пайдаланушылардың профильдерін қараңыз",
"step3": "3. Ұнағандарға лайк басыңыз",
"step4": "4. Өзара ұнатқандармен сөйлесіңіз!",
"goodLuck": "❤️ Махаббат табуда сәттілік тілейміз!"
},
"settings": {
"title": "⚙️ Профиль баптаулары\n\nӨзгерткіңіз келетін нәрсені таңдаңыз:",
"searchSettings": "🔍 Іздеу баптаулары",
"notifications": "🔔 Хабарландырулар",
"language": "🌐 Интерфейс тілі",
"stats": "📊 Статистика",
"hideProfile": "🚫 Профильді жасыру",
"deleteProfile": "🗑 Профильді жою",
"searchComingSoon": "🔍 Іздеу баптаулары келесі жаңартуда болады!",
"notificationsComingSoon": "🔔 Хабарландыру баптаулары келесі жаңартуда болады!"
},
"howItWorks": {
"title": "🎯 Telegram Tinder Bot қалай жұмыс істейді?",
"step1Title": "1⃣ Профиль жасаңыз",
"step1Desc": " • Сурет пен сипаттама қосыңыз\n • Өзіңіздің қалауларыңызды белгілеңіз",
"step2Title": "2⃣ Профильдерді қараңыз",
"step2Desc": " • Ұнағандарға лайк басыңыз\n • Ерекше жағдайлар үшін супер лайк пайдаланыңыз",
"step3Title": "3⃣ Матчтар алыңыз",
"step3Desc": " • Лайкіңіз өзара болса - бұл матч!\n • Сөйлесуді бастаңыз",
"step4Title": "4⃣ Сөйлесіңіз және танысыңыз",
"step4Desc": " • Ортақ қызығушылықтарды табыңыз\n • Кездесуді жоспарлаңыз",
"tipsTitle": "💡 Кеңестер:",
"tips": "• Сапалы суреттер пайдаланыңыз\n• Қызықты сипаттама жазыңыз\n• Сөйлесуде сыпайы болыңыз",
"createProfile": "🚀 Профиль жасау"
},
"noProfile": {
"message": "❌ Сізде әлі профиль жоқ.\\nБотты пайдалану үшін профиль жасаңыз!",
"createButton": "🚀 Профиль жасау"
},
"noMatches": {
"message": "💔 Сізде әлі матчтар жоқ.\\n\\n🔍 Көбірек профильдерді қарап шығыңыз!\\nІздеу үшін /browse пайдаланыңыз."
},
"browsing": {
"needProfile": "❌ Алдымен профиль жасаңыз!\\n/start командасын пайдаланыңыз"
},
"profileCreated": {
"success": "🎉 Профиль сәтті жасалды!\n\nҚош келдіңіз, {{name}}! 💖\n\nЕнді сіз өзіңіздің жарыңызды іздеуді бастай аласыз!",
"myProfile": "👤 Менің профилім",
"startSearch": "🔍 Іздеуді бастау"
},
"errors": {
"profileNotFound": "Профиль табылмады",
"profileIncomplete": "Профильді толық толтырыңыз",
"ageInvalid": "Дұрыс жасты енгізіңіз (18-100)",
"photoRequired": "Кемінде бір сурет қосыңыз",
"networkError": "Желі қатесі. Кейінірек көріңіз.",
"serverError": "Сервер қатесі. Кейінірек көріңіз."
}
}

101
src/locales/ko.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "텔레그램 틴더 봇에 오신 것을 환영합니다! 💕",
"description": "바로 여기서 당신의 소울메이트를 찾아보세요!",
"getStarted": "시작하기"
},
"profile": {
"create": "프로필 생성",
"edit": "프로필 수정",
"view": "프로필 보기",
"name": "이름",
"age": "나이",
"city": "도시",
"bio": "자기소개",
"photos": "사진",
"gender": "성별",
"lookingFor": "찾는 상대",
"datingGoal": "만남 목적",
"hobbies": "취미",
"lifestyle": "라이프스타일",
"male": "남성",
"female": "여성",
"both": "상관없음",
"relationship": "진지한 관계",
"friendship": "친구",
"dating": "데이트",
"hookup": "가벼운 만남",
"marriage": "결혼",
"networking": "네트워킹",
"travel": "여행",
"business": "비즈니스",
"other": "기타"
},
"search": {
"title": "프로필 둘러보기",
"noProfiles": "더 이상 프로필이 없습니다! 나중에 다시 시도해보세요.",
"like": "❤️ 좋아요",
"dislike": "👎 패스",
"superLike": "⭐ 슈퍼 라이크",
"match": "매치 성공! 🎉"
},
"vip": {
"title": "VIP 검색",
"premiumRequired": "이 기능은 프리미엄 사용자만 이용할 수 있습니다",
"filters": "필터",
"ageRange": "연령대",
"cityFilter": "도시",
"datingGoalFilter": "만남 목적",
"hobbiesFilter": "취미",
"lifestyleFilter": "라이프스타일",
"applyFilters": "필터 적용",
"clearFilters": "필터 초기화",
"noResults": "필터 조건에 맞는 프로필을 찾을 수 없습니다",
"translateProfile": "🌐 프로필 번역"
},
"premium": {
"title": "프리미엄 구독",
"features": "프리미엄 기능:",
"vipSearch": "• 필터가 있는 VIP 검색",
"profileTranslation": "• 프로필을 내 언어로 번역",
"unlimitedLikes": "• 무제한 좋아요",
"superLikes": "• 추가 슈퍼 라이크",
"price": "가격: ₩5,900/월",
"activate": "프리미엄 활성화"
},
"translation": {
"translating": "프로필을 번역하는 중...",
"translated": "번역된 프로필:",
"error": "번역 오류. 나중에 다시 시도해주세요.",
"premiumOnly": "번역은 프리미엄 사용자만 이용할 수 있습니다"
},
"commands": {
"start": "메인 메뉴",
"profile": "내 프로필",
"search": "둘러보기",
"vip": "VIP 검색",
"matches": "매치",
"premium": "프리미엄",
"settings": "설정",
"help": "도움말"
},
"buttons": {
"back": "« 뒤로",
"next": "다음 »",
"save": "저장",
"cancel": "취소",
"confirm": "확인",
"edit": "수정",
"delete": "삭제",
"yes": "예",
"no": "아니오"
},
"errors": {
"profileNotFound": "프로필을 찾을 수 없습니다",
"profileIncomplete": "프로필을 완성해주세요",
"ageInvalid": "올바른 나이를 입력해주세요 (18-100)",
"photoRequired": "최소 한 장의 사진을 추가해주세요",
"networkError": "네트워크 오류. 나중에 다시 시도해주세요.",
"serverError": "서버 오류. 나중에 다시 시도해주세요."
}
}

101
src/locales/pt.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "Bem-vindo ao Bot Tinder do Telegram! 💕",
"description": "Encontre sua alma gêmea bem aqui!",
"getStarted": "Começar"
},
"profile": {
"create": "Criar Perfil",
"edit": "Editar Perfil",
"view": "Ver Perfil",
"name": "Nome",
"age": "Idade",
"city": "Cidade",
"bio": "Sobre",
"photos": "Fotos",
"gender": "Gênero",
"lookingFor": "Procurando",
"datingGoal": "Objetivo do Encontro",
"hobbies": "Hobbies",
"lifestyle": "Estilo de Vida",
"male": "Masculino",
"female": "Feminino",
"both": "Ambos",
"relationship": "Relacionamento",
"friendship": "Amizade",
"dating": "Encontros",
"hookup": "Aventura",
"marriage": "Casamento",
"networking": "Networking",
"travel": "Viagem",
"business": "Negócios",
"other": "Outro"
},
"search": {
"title": "Explorar Perfis",
"noProfiles": "Não há mais perfis! Tente novamente mais tarde.",
"like": "❤️ Curtir",
"dislike": "👎 Pular",
"superLike": "⭐ Super Like",
"match": "É um match! 🎉"
},
"vip": {
"title": "Busca VIP",
"premiumRequired": "Este recurso está disponível apenas para usuários premium",
"filters": "Filtros",
"ageRange": "Faixa Etária",
"cityFilter": "Cidade",
"datingGoalFilter": "Objetivo do Encontro",
"hobbiesFilter": "Hobbies",
"lifestyleFilter": "Estilo de Vida",
"applyFilters": "Aplicar Filtros",
"clearFilters": "Limpar Filtros",
"noResults": "Nenhum perfil encontrado com seus filtros",
"translateProfile": "🌐 Traduzir Perfil"
},
"premium": {
"title": "Assinatura Premium",
"features": "Recursos premium:",
"vipSearch": "• Busca VIP com filtros",
"profileTranslation": "• Tradução de perfis para seu idioma",
"unlimitedLikes": "• Curtidas ilimitadas",
"superLikes": "• Super likes extras",
"price": "Preço: R$ 24,90/mês",
"activate": "Ativar Premium"
},
"translation": {
"translating": "Traduzindo perfil...",
"translated": "Perfil traduzido:",
"error": "Erro de tradução. Tente novamente mais tarde.",
"premiumOnly": "A tradução está disponível apenas para usuários premium"
},
"commands": {
"start": "Menu Principal",
"profile": "Meu Perfil",
"search": "Explorar",
"vip": "Busca VIP",
"matches": "Matches",
"premium": "Premium",
"settings": "Configurações",
"help": "Ajuda"
},
"buttons": {
"back": "« Voltar",
"next": "Próximo »",
"save": "Salvar",
"cancel": "Cancelar",
"confirm": "Confirmar",
"edit": "Editar",
"delete": "Excluir",
"yes": "Sim",
"no": "Não"
},
"errors": {
"profileNotFound": "Perfil não encontrado",
"profileIncomplete": "Por favor, complete seu perfil",
"ageInvalid": "Por favor, insira uma idade válida (18-100)",
"photoRequired": "Por favor, adicione pelo menos uma foto",
"networkError": "Erro de rede. Tente novamente mais tarde.",
"serverError": "Erro do servidor. Tente novamente mais tarde."
}
}

129
src/locales/ru.json Normal file
View File

@@ -0,0 +1,129 @@
{
"welcome": {
"greeting": "Добро пожаловать в Telegram Tinder Bot! 💕",
"description": "Найди свою вторую половинку прямо здесь!",
"getStarted": "Начать знакомство"
},
"profile": {
"create": "Создать анкету",
"edit": "✏️ Редактировать",
"view": "Посмотреть анкету",
"name": "Имя",
"age": "Возраст",
"city": "Город",
"bio": "О себе",
"photos": "📸 Фото",
"gender": "Пол",
"lookingFor": "Ищу",
"datingGoal": "Цель знакомства",
"hobbies": "Хобби",
"lifestyle": "Образ жизни",
"male": "Мужской",
"female": "Женский",
"both": "Не важно",
"relationship": "Серьезные отношения",
"friendship": "Дружба",
"dating": "Свидания",
"hookup": "Интрижка",
"marriage": "Брак",
"networking": "Общение",
"travel": "Путешествия",
"business": "Бизнес",
"other": "Другое",
"cityNotSpecified": "Не указан",
"bioNotSpecified": "Описание не указано",
"interests": "Интересы",
"startSearch": "🔍 Начать поиск",
"noProfile": "❌ У вас пока нет профиля.\nСоздайте его для начала использования бота!",
"createFirst": "❌ Сначала создайте профиль!\nИспользуйте команду /start"
},
"search": {
"title": "Поиск анкет",
"noProfiles": "Анкеты закончились! Попробуйте позже.",
"like": "❤️ Нравится",
"dislike": "👎 Не нравится",
"superLike": "⭐ Супер лайк",
"match": "Это взаимность! 🎉"
},
"vip": {
"title": "VIP Поиск",
"premiumRequired": "Функция доступна только для премиум пользователей",
"filters": "Фильтры",
"ageRange": "Возрастной диапазон",
"cityFilter": "Город",
"datingGoalFilter": "Цель знакомства",
"hobbiesFilter": "Хобби",
"lifestyleFilter": "Образ жизни",
"applyFilters": "Применить фильтры",
"clearFilters": "Очистить фильтры",
"noResults": "По вашим фильтрам никого не найдено",
"translateProfile": "🌐 Перевести анкету"
},
"premium": {
"title": "Премиум подписка",
"features": "Возможности премиум:",
"vipSearch": "• VIP поиск с фильтрами",
"profileTranslation": "• Перевод анкет на ваш язык",
"unlimitedLikes": "• Безлимитные лайки",
"superLikes": "• Дополнительные супер-лайки",
"price": "Стоимость: 299₽/месяц",
"activate": "Активировать премиум"
},
"translation": {
"translating": "Переводим анкету...",
"translated": "Анкета переведена:",
"error": "Ошибка перевода. Попробуйте позже.",
"premiumOnly": "Перевод доступен только для премиум пользователей"
},
"commands": {
"start": "Главное меню",
"profile": "Моя анкета",
"search": "Поиск",
"vip": "VIP поиск",
"matches": "Взаимности",
"premium": "Премиум",
"settings": "Настройки",
"help": "Помощь"
},
"buttons": {
"back": "« Назад",
"next": "Далее »",
"save": "Сохранить",
"cancel": "Отмена",
"confirm": "Подтвердить",
"edit": "Редактировать",
"delete": "Удалить",
"yes": "Да",
"no": "Нет"
},
"errors": {
"profileNotFound": "Анкета не найдена",
"profileIncomplete": "Заполните анкету полностью",
"ageInvalid": "Введите корректный возраст (18-100)",
"photoRequired": "Добавьте хотя бы одну фотографию",
"networkError": "Ошибка сети. Попробуйте позже.",
"serverError": "Ошибка сервера. Попробуйте позже."
},
"common": {
"back": "👈 Назад"
},
"matches": {
"noMatches": "✨ У вас пока нет матчей.\n\n🔍 Попробуйте просмотреть больше анкет!\nИспользуйте /browse для поиска."
},
"start": {
"welcomeBack": "🎉 С возвращением, {name}!\n\n💖 Telegram Tinder Bot готов к работе!\n\nЧто хотите сделать?",
"welcomeNew": "🎉 Добро пожаловать в Telegram Tinder Bot!\n\n💕 Здесь вы можете найти свою вторую половинку!\n\nДля начала создайте свой профиль:",
"myProfile": "👤 Мой профиль",
"browseProfiles": "🔍 Просмотр анкет",
"myMatches": "💕 Мои матчи",
"vipSearch": "⭐ VIP поиск",
"settings": "⚙️ Настройки",
"createProfile": "🚀 Создать профиль",
"howItWorks": " Как это работает?"
},
"help": {
"title": "🤖 Telegram Tinder Bot - Справка",
"description": "Бот для знакомств в Telegram\n\n📝 Создайте профиль\n🔍 Просматривайте анкеты\n❤ Ставьте лайки\n💕 Находите взаимности\n💬 Общайтесь в чатах",
"commands": "📋 Команды:\n/start - Главное меню\n/profile - Мой профиль\n/browse - Просмотр анкет\n/matches - Мои матчи\n/settings - Настройки\n/help - Эта справка"
}
}

94
src/locales/ru_fixed.json Normal file
View File

@@ -0,0 +1,94 @@
{
"commands": {
"start": "🏠 Главное меню",
"help": " Помощь",
"profile": "👤 Мой профиль",
"search": "🔍 Поиск анкет",
"matches": "💕 Матчи",
"premium": "⭐ Premium",
"settings": "⚙️ Настройки"
},
"menu": {
"main": "🏠 Главное меню",
"back": "👈 Назад",
"profile": "👤 Профиль",
"search": "🔍 Поиск",
"matches": "💕 Матчи",
"premium": "⭐ Premium",
"settings": "⚙️ Настройки"
},
"welcome": {
"newUser": "Добро пожаловать в Telegram Tinder Bot! 💕\n\nЗдесь вы сможете найти интересных людей для общения и знакомств.\n\nДля начала работы создайте свой профиль!",
"existingUser": "С возвращением! 👋\n\nВыберите действие:",
"createProfile": "🚀 Создать профиль"
},
"help": {
"title": "📋 Как пользоваться ботом:",
"step1": "1⃣ Создать профиль",
"step1Desc": " • Укажите имя, возраст, город\n • Добавьте описание\n • Загрузите фото",
"step2": "2⃣ Просматривать анкеты",
"step2Desc": " • Листайте профили других пользователей\n • Ставьте лайки (❤️) или дизлайки (👎)",
"step3": "3⃣ Получить матч",
"step3Desc": " • Когда два человека ставят лайки друг другу\n • Появляется возможность общения",
"step4": "4⃣ Общение",
"step4Desc": " • Находите общие интересы\n • Договаривайтесь о встрече",
"tipsTitle": "💡 Советы:",
"tips": "• Используйте качественные фото\n• Напишите интересное описание\n• Будьте вежливы в общении",
"createProfile": "🚀 Создать профиль"
},
"settings": {
"title": "⚙️ Настройки",
"language": "🌐 Язык интерфейса",
"ageRange": "📅 Возрастной диапазон",
"showAge": "🎂 Показывать возраст",
"showCity": "📍 Показывать город",
"notifications": "🔔 Уведомления",
"privacy": "🔒 Приватность",
"back": "👈 Назад"
},
"languages": {
"ru": "🇷🇺 Русский",
"en": "🇺🇸 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어",
"uz": "🇺🇿 O'zbekcha",
"kk": "🇰🇿 Қазақша"
},
"howItWorks": {
"title": "🤔 Как это работает?",
"step1": "1⃣ Создать профиль",
"step1Desc": " • Укажите имя, возраст, город\n • Добавьте описание\n • Загрузите фото",
"step2": "2⃣ Просматривать анкеты",
"step2Desc": " • Листайте профили других пользователей\n • Ставьте лайки (❤️) или дизлайки (👎)",
"step3": "3⃣ Получить матч",
"step3Desc": " • Когда два человека ставят лайки друг другу\n • Появляется возможность общения",
"step4": "4⃣ Общение",
"step4Desc": " • Находите общие интересы\n • Договаривайтесь о встрече",
"tipsTitle": "💡 Советы:",
"tips": "• Используйте качественные фото\n• Напишите интересное описание\n• Будьте вежливы в общении",
"createProfile": "🚀 Создать профиль"
},
"noProfile": {
"message": "❌ У вас пока нет профиля.\\nСоздайте его для начала использования бота!",
"createButton": "🚀 Создать профиль"
},
"profileCreated": {
"success": "🎉 Профиль успешно создан!\\n\\nДобро пожаловать, {{name}}! 💖\\n\\nТеперь вы можете начать поиск своей второй половинки!",
"myProfile": "👤 Мой профиль",
"startSearch": "🔍 Начать поиск"
},
"errors": {
"profileNotFound": "Анкета не найдена",
"profileIncomplete": "Заполните анкету полностью",
"ageInvalid": "Введите корректный возраст (18-100)",
"photoRequired": "Добавьте хотя бы одну фотографию",
"networkError": "Ошибка сети. Попробуйте позже.",
"serverError": "Ошибка сервера. Попробуйте позже."
}
}

152
src/locales/uz.json Normal file
View File

@@ -0,0 +1,152 @@
{
"welcome": {
"greeting": "🎉 Telegram Tinder Botga xush kelibsiz!\n\n💕 Bu yerda siz o'zingizning hayot sherigigingizni topa olasiz!\n\nBoshlash uchun profilingizni yarating:",
"description": "O'zingizning hayot sherigigingizni shu yerda toping!",
"getStarted": "Tanishishni boshlash",
"haveProfile": "🎉 Xush kelibsiz, {{name}}!\n\n💖 Telegram Tinder Bot ishga tayyor!\n\nNima qilmoqchisiz?"
},
"profile": {
"create": "Profil yaratish",
"edit": "Profilni tahrirlash",
"view": "Profilni ko'rish",
"name": "Ism",
"age": "Yosh",
"city": "Shahar",
"bio": "O'zim haqimda",
"photos": "Rasmlar",
"gender": "Jins",
"lookingFor": "Qidiraman",
"datingGoal": "Tanishuv maqsadi",
"hobbies": "Sevimli mashg'ulotlar",
"lifestyle": "Turmush tarzi",
"male": "Erkak",
"female": "Ayol",
"both": "Muhim emas",
"relationship": "Jiddiy munosabatlar",
"friendship": "Do'stlik",
"dating": "Uchrashuvlar",
"hookup": "Qisqa munosabat",
"marriage": "Nikoh",
"networking": "Muloqot",
"travel": "Sayohat",
"business": "Biznes",
"other": "Boshqa"
},
"search": {
"title": "Profillarni qidirish",
"noProfiles": "Profillar tugadi! Keyinroq urinib ko'ring.",
"like": "👍 Yoqadi",
"dislike": "👎 Yoqmadi",
"superlike": "💖 Super yoqdi",
"match": "Bu o'zaro yoqish! 🎉",
"tryAgain": "🔄 Yana urinish",
"myMatches": "💕 Mening matchlarim",
"allViewed": "🎉 Siz barcha mavjud nomzodlarni ko'rib chiqdingiz!\n\n⏰ Keyinroq urinib ko'ring - yangi profillar paydo bo'lishi mumkin!",
"viewProfile": "👤 Profil",
"morePhotos": "📸 Yana rasmlar",
"next": "⏭ Keyingi",
"sendMessage": "💬 Xabar yozish",
"continueBrowsing": "🔍 Qidirishni davom ettirish",
"matchFound": "🎉 BU MATCH! 💕\n\n{{name}} bilan o'zaro yoqdingiz!\n\nEndi suhbatni boshlashingiz mumkin!",
"noMoreProfiles": "😔 Hozircha yangi profillar yo'q.\n\n⏰ Keyinroq qaytib kelishingiz mumkin!"
},
"vip": {
"title": "⭐ VIP Qidiruv",
"description": "Premium imkoniyatlar bilan qidiring!",
"features": "• Cheksiz yoqish\n• Super yoqishlar\n• Kimlar yoqganini ko'rish\n• Reklamasiz tajriba",
"getVip": "VIP olish",
"alreadyVip": "Siz allaqachon VIP foydalanuvchisiz!"
},
"translation": {
"inProgress": "🔄 Tarjima qilinmoqda...",
"completed": "✅ Tarjima tayyor!",
"failed": "❌ Tarjima xatosi",
"error": "Tarjima xatosi. Keyinroq urinib ko'ring.",
"premiumOnly": "Tarjima faqat premium foydalanuvchilar uchun"
},
"commands": {
"start": "Bosh menyu",
"profile": "Mening profilim",
"search": "Qidiruv",
"vip": "VIP qidiruv",
"matches": "O'zaro yoqishlar",
"premium": "Premium",
"settings": "Sozlamalar",
"help": "Yordam"
},
"buttons": {
"back": "« Orqaga",
"next": "Keyingi »",
"save": "Saqlash",
"cancel": "Bekor qilish",
"confirm": "Tasdiqlash",
"edit": "Tahrirlash",
"delete": "O'chirish",
"yes": "Ha",
"no": "Yo'q"
},
"help": {
"title": "🤖 Telegram Tinder Bot - Yordam",
"commands": "📋 Mavjud buyruqlar:",
"commandStart": "/start - Bosh menyu",
"commandProfile": "/profile - Profil boshqaruvi",
"commandBrowse": "/browse - Profillarni ko'rish",
"commandMatches": "/matches - Sizning matchlaringiz",
"commandSettings": "/settings - Sozlamalar",
"commandHelp": "/help - Ushbu yordam",
"howToUse": "📱 Qanday foydalanish:",
"step1": "1. Rasm va tavsif bilan profil yarating",
"step2": "2. Boshqa foydalanuvchilarning profillarini ko'ring",
"step3": "3. Yoqganlaringizga yoqish bosing",
"step4": "4. O'zaro yoqganlar bilan suhbatlashing!",
"goodLuck": "❤️ Sevgi topishda omad tilaymiz!"
},
"settings": {
"title": "⚙️ Profil sozlamalari\n\nO'zgartirmoqchi bo'lgan narsani tanlang:",
"searchSettings": "🔍 Qidiruv sozlamalari",
"notifications": "🔔 Bildirishnomalar",
"language": "🌐 Interfeys tili",
"stats": "📊 Statistika",
"hideProfile": "🚫 Profilni yashirish",
"deleteProfile": "🗑 Profilni o'chirish",
"searchComingSoon": "🔍 Qidiruv sozlamalari keyingi yangilanishda bo'ladi!",
"notificationsComingSoon": "🔔 Bildirishnoma sozlamalari keyingi yangilanishda bo'ladi!"
},
"howItWorks": {
"title": "🎯 Telegram Tinder Bot qanday ishlaydi?",
"step1Title": "1⃣ Profil yarating",
"step1Desc": " • Rasm va tavsif qo'shing\n • O'zingizning xohishlaringizni belgilang",
"step2Title": "2⃣ Profillarni ko'ring",
"step2Desc": " • Yoqganlaringizga yoqish bosing\n • Maxsus holatlar uchun super yoqish ishlating",
"step3Title": "3⃣ Matchlar oling",
"step3Desc": " • Yoqishingiz o'zaro bo'lsa - bu match!\n • Suhbatni boshlang",
"step4Title": "4⃣ Suhbatlashing va tanishing",
"step4Desc": " • Umumiy qiziqishlarni toping\n • Uchrashuvni rejalang",
"tipsTitle": "💡 Maslahatlar:",
"tips": "• Sifatli rasmlar ishlating\n• Qiziqarli tavsif yozing\n• Suhbatda xushmuomala bo'ling",
"createProfile": "🚀 Profil yaratish"
},
"noProfile": {
"message": "❌ Sizda hali profil yo'q.\\nBotdan foydalanish uchun profil yarating!",
"createButton": "🚀 Profil yaratish"
},
"noMatches": {
"message": "💔 Sizda hali matchlar yo'q.\\n\\n🔍 Ko'proq profillarni ko'rib chiqing!\\nQidiruv uchun /browse dan foydalaning."
},
"browsing": {
"needProfile": "❌ Avval profil yarating!\\n/start buyrug'idan foydalaning"
},
"profileCreated": {
"success": "🎉 Profil muvaffaqiyatli yaratildi!\n\nXush kelibsiz, {{name}}! 💖\n\nEndi siz o'zingizning hayot sherigigingizni qidirishni boshlashingiz mumkin!",
"myProfile": "👤 Mening profilim",
"startSearch": "🔍 Qidirishni boshlash"
},
"errors": {
"profileNotFound": "Profil topilmadi",
"profileIncomplete": "Profilni to'liq to'ldiring",
"ageInvalid": "To'g'ri yoshni kiriting (18-100)",
"photoRequired": "Kamida bitta rasm qo'shing",
"networkError": "Tarmoq xatosi. Keyinroq urinib ko'ring.",
"serverError": "Server xatosi. Keyinroq urinib ko'ring."
}
}

101
src/locales/zh.json Normal file
View File

@@ -0,0 +1,101 @@
{
"welcome": {
"greeting": "欢迎使用Telegram Tinder机器人💕",
"description": "在这里找到你的灵魂伴侣!",
"getStarted": "开始"
},
"profile": {
"create": "创建资料",
"edit": "编辑资料",
"view": "查看资料",
"name": "姓名",
"age": "年龄",
"city": "城市",
"bio": "关于我",
"photos": "照片",
"gender": "性别",
"lookingFor": "寻找",
"datingGoal": "约会目的",
"hobbies": "爱好",
"lifestyle": "生活方式",
"male": "男性",
"female": "女性",
"both": "都可以",
"relationship": "恋爱关系",
"friendship": "友谊",
"dating": "约会",
"hookup": "随意交往",
"marriage": "结婚",
"networking": "社交",
"travel": "旅行",
"business": "商务",
"other": "其他"
},
"search": {
"title": "浏览资料",
"noProfiles": "没有更多资料了!请稍后再试。",
"like": "❤️ 喜欢",
"dislike": "👎 跳过",
"superLike": "⭐ 超级喜欢",
"match": "配对成功!🎉"
},
"vip": {
"title": "VIP搜索",
"premiumRequired": "此功能仅对高级用户开放",
"filters": "筛选器",
"ageRange": "年龄范围",
"cityFilter": "城市",
"datingGoalFilter": "约会目的",
"hobbiesFilter": "爱好",
"lifestyleFilter": "生活方式",
"applyFilters": "应用筛选器",
"clearFilters": "清除筛选器",
"noResults": "没有找到符合您筛选条件的资料",
"translateProfile": "🌐 翻译资料"
},
"premium": {
"title": "高级订阅",
"features": "高级功能:",
"vipSearch": "• 带筛选器的VIP搜索",
"profileTranslation": "• 将资料翻译成您的语言",
"unlimitedLikes": "• 无限点赞",
"superLikes": "• 额外的超级喜欢",
"price": "价格¥35/月",
"activate": "激活高级版"
},
"translation": {
"translating": "正在翻译资料...",
"translated": "已翻译的资料:",
"error": "翻译错误。请稍后再试。",
"premiumOnly": "翻译功能仅对高级用户开放"
},
"commands": {
"start": "主菜单",
"profile": "我的资料",
"search": "浏览",
"vip": "VIP搜索",
"matches": "配对",
"premium": "高级版",
"settings": "设置",
"help": "帮助"
},
"buttons": {
"back": "« 返回",
"next": "下一步 »",
"save": "保存",
"cancel": "取消",
"confirm": "确认",
"edit": "编辑",
"delete": "删除",
"yes": "是",
"no": "否"
},
"errors": {
"profileNotFound": "未找到资料",
"profileIncomplete": "请完善您的资料",
"ageInvalid": "请输入有效年龄18-100",
"photoRequired": "请至少添加一张照片",
"networkError": "网络错误。请稍后再试。",
"serverError": "服务器错误。请稍后再试。"
}
}

View File

@@ -5,6 +5,7 @@ export interface UserData {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
languageCode?: string; languageCode?: string;
language?: string; // Предпочитаемый язык интерфейса
isActive: boolean; isActive: boolean;
createdAt: Date; createdAt: Date;
lastActiveAt: Date; lastActiveAt: Date;
@@ -17,6 +18,7 @@ export class User {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
languageCode?: string; languageCode?: string;
language: string; // Предпочитаемый язык интерфейса
isActive: boolean; isActive: boolean;
createdAt: Date; createdAt: Date;
lastActiveAt: Date; lastActiveAt: Date;
@@ -28,6 +30,7 @@ export class User {
this.firstName = data.firstName; this.firstName = data.firstName;
this.lastName = data.lastName; this.lastName = data.lastName;
this.languageCode = data.languageCode || 'en'; this.languageCode = data.languageCode || 'en';
this.language = data.language || 'ru'; // Язык интерфейса по умолчанию
this.isActive = data.isActive; this.isActive = data.isActive;
this.createdAt = data.createdAt; this.createdAt = data.createdAt;
this.lastActiveAt = data.lastActiveAt; this.lastActiveAt = data.lastActiveAt;
@@ -67,4 +70,14 @@ export class User {
this.isActive = true; this.isActive = true;
this.updateLastActive(); this.updateLastActive();
} }
// Установить язык интерфейса
setLanguage(language: string): void {
this.language = language;
}
// Получить язык интерфейса
getLanguage(): string {
return this.language;
}
} }

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

@@ -0,0 +1,115 @@
#!/usr/bin/env ts-node
import 'dotenv/config';
import { testConnection, closePool, query } from '../database/connection';
/**
* Очистка базы данных и пересоздание схемы с нуля
*/
async function main() {
console.log('🚀 Очистка базы данных...');
try {
// Проверяем подключение
const connected = await testConnection();
if (!connected) {
console.error('❌ Не удалось подключиться к базе данных');
process.exit(1);
}
// Сначала проверяем наличие таблиц
const tablesExist = await checkTablesExist();
if (tablesExist) {
console.log('🔍 Таблицы существуют. Выполняем удаление...');
// Удаляем существующие таблицы в правильном порядке
await dropAllTables();
console.log('✅ Все таблицы успешно удалены');
} else {
console.log('⚠️ Таблицы не обнаружены');
}
console.log('🛠️ База данных очищена успешно');
console.log(' Теперь вы можете выполнить npm run init:db для создания новой схемы');
} catch (error) {
console.error('❌ Ошибка при очистке базы данных:', error);
process.exit(1);
} finally {
await closePool();
console.log('👋 Соединение с базой данных закрыто');
}
}
/**
* Проверка существования таблиц в базе данных
*/
async function checkTablesExist(): Promise<boolean> {
try {
const result = await query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('users', 'profiles', 'matches')
);
`);
return result.rows[0].exists;
} catch (error) {
console.error('❌ Ошибка при проверке наличия таблиц:', error);
return false;
}
}
/**
* Удаление всех таблиц из базы данных
*/
async function dropAllTables(): Promise<void> {
try {
// Отключаем ограничения внешних ключей для удаления таблиц
await query('SET CONSTRAINTS ALL DEFERRED;');
// Удаляем таблицы в порядке, учитывающем зависимости
console.log('Удаление таблицы notifications...');
await query('DROP TABLE IF EXISTS notifications CASCADE;');
console.log('Удаление таблицы scheduled_notifications...');
await query('DROP TABLE IF EXISTS scheduled_notifications CASCADE;');
console.log('Удаление таблицы reports...');
await query('DROP TABLE IF EXISTS reports CASCADE;');
console.log('Удаление таблицы blocks...');
await query('DROP TABLE IF EXISTS blocks CASCADE;');
console.log('Удаление таблицы messages...');
await query('DROP TABLE IF EXISTS messages CASCADE;');
console.log('Удаление таблицы matches...');
await query('DROP TABLE IF EXISTS matches CASCADE;');
console.log('Удаление таблицы swipes...');
await query('DROP TABLE IF EXISTS swipes CASCADE;');
console.log('Удаление таблицы profiles...');
await query('DROP TABLE IF EXISTS profiles CASCADE;');
console.log('Удаление таблицы users...');
await query('DROP TABLE IF EXISTS users CASCADE;');
console.log('Удаление таблицы pgmigrations...');
await query('DROP TABLE IF EXISTS pgmigrations CASCADE;');
// Восстанавливаем ограничения внешних ключей
await query('SET CONSTRAINTS ALL IMMEDIATE;');
} catch (error) {
console.error('❌ Ошибка при удалении таблиц:', error);
throw error;
}
}
// Запуск скрипта
if (require.main === module) {
main();
}
export { main as cleanDB };

View File

@@ -0,0 +1,166 @@
import { Pool } from 'pg';
import { v4 as uuidv4 } from 'uuid';
import 'dotenv/config';
async function createTestSwipes() {
const pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
});
try {
// Сначала получаем ID всех пользователей
const usersResult = await pool.query(`
SELECT users.id, telegram_id, first_name, gender
FROM users
JOIN profiles ON users.id = profiles.user_id
`);
const users = usersResult.rows;
console.log('Пользователи в системе:');
console.table(users);
if (users.length < 2) {
console.log('Недостаточно пользователей для создания свайпов');
return;
}
// Создаем свайпы
console.log('Создаем тестовые свайпы...');
// Сначала проверим ограничения базы данных
const constraintsResult = await pool.query(`
SELECT
constraint_name,
table_name,
constraint_type
FROM
information_schema.table_constraints
WHERE
table_name = 'swipes'
`);
console.log('Ограничения таблицы swipes:');
console.table(constraintsResult.rows);
// Создаем пары пользователей без дублирования
const userPairs = [];
const swipes = [];
// Мужчины и женщины для создания пар
const maleUsers = users.filter(user => user.gender === 'male');
const femaleUsers = users.filter(user => user.gender === 'female');
console.log(`Мужчин: ${maleUsers.length}, Женщин: ${femaleUsers.length}`);
for (const male of maleUsers) {
for (const female of femaleUsers) {
// Мужчина -> Женщина (70% лайк, 20% пропуск, 10% суперлайк)
const randomNum1 = Math.random();
let maleToFemaleType;
if (randomNum1 < 0.7) {
maleToFemaleType = 'like';
} else if (randomNum1 < 0.9) {
maleToFemaleType = 'pass';
} else {
maleToFemaleType = 'superlike';
}
const maleToFemale = {
id: uuidv4(),
user_id: male.id,
target_user_id: female.id,
type: maleToFemaleType,
is_match: false,
created_at: new Date()
};
// Женщина -> Мужчина (80% шанс на лайк, если мужчина лайкнул)
if ((maleToFemaleType === 'like' || maleToFemaleType === 'superlike') && Math.random() < 0.8) {
const femaleToMale = {
id: uuidv4(),
user_id: female.id,
target_user_id: male.id,
type: Math.random() < 0.9 ? 'like' : 'superlike',
is_match: true,
created_at: new Date(new Date().getTime() + 1000) // На секунду позже
};
swipes.push(femaleToMale);
maleToFemale.is_match = true;
}
swipes.push(maleToFemale);
}
}
console.log(`Подготовлено ${swipes.length} свайпов для добавления в базу`);
// Сначала удаляем все существующие свайпы
await pool.query('DELETE FROM swipes');
console.log('Существующие свайпы удалены');
// Добавляем новые свайпы
for (const swipe of swipes) {
await pool.query(`
INSERT INTO swipes (id, user_id, target_user_id, type, is_match, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, [swipe.id, swipe.user_id, swipe.target_user_id, swipe.type, swipe.is_match, swipe.created_at]);
}
console.log(`Успешно добавлено ${swipes.length} свайпов`);
// Создаем матчи для взаимных лайков
console.log('Создаем матчи для взаимных лайков...');
// Сначала удаляем все существующие матчи
await pool.query('DELETE FROM matches');
console.log('Существующие матчи удалены');
// Находим пары взаимных лайков для создания матчей
const mutualLikesResult = await pool.query(`
SELECT
s1.user_id as user_id_1,
s1.target_user_id as user_id_2,
s1.created_at
FROM swipes s1
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
WHERE (s1.type = 'like' OR s1.type = 'superlike')
AND (s2.type = 'like' OR s2.type = 'superlike')
AND s1.user_id < s2.user_id -- Избегаем дублирования пар
`);
const matches = [];
for (const mutualLike of mutualLikesResult.rows) {
const match = {
id: uuidv4(),
user_id_1: mutualLike.user_id_1,
user_id_2: mutualLike.user_id_2,
created_at: new Date(),
is_active: true,
is_super_match: Math.random() < 0.2 // 20% шанс быть суперматчем
};
matches.push(match);
await pool.query(`
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
VALUES ($1, $2, $3, $4, $5, $6)
`, [match.id, match.user_id_1, match.user_id_2, match.created_at, match.is_active, match.is_super_match]);
}
console.log(`Успешно создано ${matches.length} матчей`);
} catch (error) {
console.error('Ошибка при создании тестовых данных:', error);
} finally {
await pool.end();
}
}
createTestSwipes();

View File

@@ -0,0 +1,120 @@
import { Pool } from 'pg';
import 'dotenv/config';
async function getDatabaseInfo() {
const pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
});
try {
console.log('Подключение к базе данных...');
// Получаем информацию о пользователях
console.log('\n=== ПОЛЬЗОВАТЕЛИ ===');
const usersResult = await pool.query(`
SELECT id, telegram_id, username, first_name, last_name, premium, created_at
FROM users
ORDER BY created_at DESC
`);
console.log(`Всего пользователей: ${usersResult.rows.length}`);
console.table(usersResult.rows);
// Получаем информацию о профилях
console.log('\n=== ПРОФИЛИ ===');
const profilesResult = await pool.query(`
SELECT
p.user_id,
u.telegram_id,
u.first_name,
p.age,
p.gender,
p.interested_in as "интересуется",
p.bio,
p.dating_goal as "цель_знакомства",
p.is_visible,
p.created_at
FROM profiles p
JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC
`);
console.log(`Всего профилей: ${profilesResult.rows.length}`);
console.table(profilesResult.rows);
// Получаем информацию о свайпах
console.log('\n=== СВАЙПЫ ===');
// Сначала проверим, какие столбцы есть в таблице swipes
console.log('Получение структуры таблицы swipes...');
const swipesColumns = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'swipes'
`);
console.log('Структура таблицы swipes:');
console.table(swipesColumns.rows);
// Теперь запросим данные, используя правильные имена столбцов
const swipesResult = await pool.query(`
SELECT
s.id,
s.user_id,
u1.first_name as "от_кого",
s.target_user_id,
u2.first_name as "кому",
s.type,
s.created_at
FROM swipes s
JOIN users u1 ON s.user_id = u1.id
JOIN users u2 ON s.target_user_id = u2.id
ORDER BY s.created_at DESC
`);
console.log(`Всего свайпов: ${swipesResult.rows.length}`);
console.table(swipesResult.rows);
// Получаем информацию о матчах
console.log('\n=== МАТЧИ ===');
// Сначала проверим, какие столбцы есть в таблице matches
console.log('Получение структуры таблицы matches...');
const matchesColumns = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'matches'
`);
console.log('Структура таблицы matches:');
console.table(matchesColumns.rows);
// Теперь запросим данные, используя правильные имена столбцов
const matchesResult = await pool.query(`
SELECT
m.id,
m.user_id_1,
u1.first_name as "пользователь_1",
m.user_id_2,
u2.first_name as "пользователь_2",
m.is_active,
m.created_at
FROM matches m
JOIN users u1 ON m.user_id_1 = u1.id
JOIN users u2 ON m.user_id_2 = u2.id
ORDER BY m.created_at DESC
`);
console.log(`Всего матчей: ${matchesResult.rows.length}`);
console.table(matchesResult.rows);
} catch (error) {
console.error('Ошибка при получении данных:', error);
} finally {
await pool.end();
}
}
getDatabaseInfo();

View File

@@ -1,7 +1,13 @@
#!/usr/bin/env ts-node #!/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() { async function main() {
console.log('🚀 Initializing database...'); console.log('🚀 Initializing database...');
@@ -13,90 +19,245 @@ async function main() {
process.exit(1); process.exit(1);
} }
// Инициализируем схему // Проверяем наличие таблицы миграций
await initializeDatabase(); const migrationTableExists = await checkMigrationsTable();
console.log('✅ Database initialized successfully');
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(); await ensureRequiredColumns();
console.log('✅ Additional tables created'); console.log('✅ Все необходимые колонки присутствуют');
} catch (error) { } catch (error) {
console.error('❌ Database initialization failed:', error); console.error('❌ Ошибка инициализации базы данных:', error);
process.exit(1); process.exit(1);
} finally { } finally {
await closePool(); 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(), async function getPendingMigrations(): Promise<string[]> {
user_id UUID REFERENCES users(id) ON DELETE CASCADE, try {
type VARCHAR(50) NOT NULL, // Получаем выполненные миграции
data JSONB DEFAULT '{}', const { rows } = await query('SELECT name FROM pgmigrations');
is_read BOOLEAN DEFAULT false, const appliedMigrations = rows.map((row: { name: string }) => row.name);
created_at TIMESTAMP DEFAULT NOW()
); // Получаем файлы миграций
`); 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(), async function setupMigrations(): Promise<void> {
user_id UUID REFERENCES users(id) ON DELETE CASCADE, try {
type VARCHAR(50) NOT NULL, // Создаем таблицу миграций
data JSONB DEFAULT '{}', await query(`
scheduled_at TIMESTAMP NOT NULL, CREATE TABLE IF NOT EXISTS pgmigrations (
sent BOOLEAN DEFAULT false, id SERIAL PRIMARY KEY,
sent_at TIMESTAMP, name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW() 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(), async function createAdditionalTables(): Promise<void> {
reporter_id UUID REFERENCES users(id) ON DELETE CASCADE, try {
reported_id UUID REFERENCES users(id) ON DELETE CASCADE, // Таблица для уведомлений
reason VARCHAR(100) NOT NULL, await query(`
description TEXT, CREATE TABLE IF NOT EXISTS notifications (
status VARCHAR(20) DEFAULT 'pending', id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMP DEFAULT NOW(), user_id UUID REFERENCES users(id) ON DELETE CASCADE,
resolved_at TIMESTAMP type VARCHAR(50) NOT NULL,
); data JSONB DEFAULT '{}',
`); is_read BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
`);
// Таблица для блокировок // Таблица для запланированных уведомлений
await query(` await query(`
CREATE TABLE IF NOT EXISTS blocks ( CREATE TABLE IF NOT EXISTS scheduled_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blocker_id UUID REFERENCES users(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE,
blocked_id UUID REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(), data JSONB DEFAULT '{}',
UNIQUE(blocker_id, blocked_id) scheduled_at TIMESTAMP NOT NULL,
); sent BOOLEAN DEFAULT false,
`); sent_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
`);
// Добавляем недостающие поля в users // Таблица для отчетов и блокировок
await query(` await query(`
ALTER TABLE users CREATE TABLE IF NOT EXISTS reports (
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}'; 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(` await query(`
CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type); CREATE TABLE IF NOT EXISTS blocks (
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent); id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status); blocker_id UUID REFERENCES users(id) ON DELETE CASCADE,
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id); blocked_id UUID REFERENCES users(id) ON DELETE CASCADE,
`); created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(blocker_id, blocked_id)
);
`);
// Добавляем настройки уведомлений в users
await query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}';
`);
// Индексы для производительности
await query(`
CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent);
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status);
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
`);
} catch (error) {
console.error('❌ Ошибка при создании дополнительных таблиц:', error);
throw error;
}
}
/**
* Проверка наличия всех необходимых колонок
*/
async function ensureRequiredColumns(): Promise<void> {
try {
// Проверка и добавление колонки updated_at в users
await query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
`);
// Проверка и добавление колонок premium и premium_expires_at в users
await query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE;
`);
await query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
`);
// Проверка и добавление колонки looking_for в profiles
await query(`
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both'));
`);
// Проверка и добавление колонки hobbies в profiles
await query(`
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS hobbies TEXT;
`);
} catch (error) {
console.error('❌ Ошибка при проверке необходимых колонок:', error);
throw error;
}
} }
// Запуск скрипта // Запуск скрипта

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

@@ -0,0 +1,42 @@
import { Pool } from 'pg';
async function setPremium(): Promise<void> {
// Создаем соединение с базой данных используя прямые параметры
const pool = new Pool({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!',
});
try {
// Установка премиума для всех пользователей
const result = await pool.query(`
UPDATE users
SET premium = true,
premium_expires_at = NOW() + INTERVAL '1 year'
`);
console.log('Premium set for all users successfully!');
console.log(`Updated ${result.rowCount} users`);
// Закрываем соединение с базой данных
await pool.end();
process.exit(0);
} catch (error) {
console.error('Error setting premium:', error);
// Закрываем соединение с базой данных в случае ошибки
try {
await pool.end();
} catch (e) {
console.error('Error closing pool:', e);
}
process.exit(1);
}
}
setPremium();

View File

@@ -0,0 +1,43 @@
import { Pool } from 'pg';
import 'dotenv/config';
async function setAllUsersToPremium() {
const pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
});
try {
console.log('Setting premium status for all users...');
console.log(`Connecting to database at ${process.env.DB_HOST}:${process.env.DB_PORT}...`);
const result = await pool.query(`
UPDATE users
SET premium = true
WHERE true
RETURNING id, telegram_id, username, first_name, premium
`);
console.log(`Successfully set premium status for ${result.rows.length} users:`);
console.table(result.rows);
console.log('All users are now premium!');
} catch (error) {
console.error('Error setting premium status:', error);
console.error('Please check your database connection settings in .env file.');
console.log('Current settings:');
console.log(`- DB_HOST: ${process.env.DB_HOST}`);
console.log(`- DB_PORT: ${process.env.DB_PORT}`);
console.log(`- DB_NAME: ${process.env.DB_NAME}`);
console.log(`- DB_USERNAME: ${process.env.DB_USERNAME}`);
console.log(`- DB_PASSWORD: ${process.env.DB_PASSWORD ? '********' : 'not set'}`);
} finally {
await pool.end();
process.exit(0);
}
}
setAllUsersToPremium();

View File

@@ -0,0 +1,25 @@
import { query } from '../database/connection';
async function setAllUsersToPremium() {
try {
console.log('Setting premium status for all users...');
const result = await query(`
UPDATE users
SET premium = true
WHERE true
RETURNING id, telegram_id, username, first_name, premium
`);
console.log(`Successfully set premium status for ${result.rows.length} users:`);
console.table(result.rows);
console.log('All users are now premium!');
} catch (error) {
console.error('Error setting premium status:', error);
} finally {
process.exit(0);
}
}
setAllUsersToPremium();

View File

@@ -24,8 +24,8 @@ export class ChatService {
SELECT SELECT
m.*, m.*,
CASE CASE
WHEN m.user1_id = $1 THEN m.user2_id WHEN m.user_id_1 = $1 THEN m.user_id_2
ELSE m.user1_id ELSE m.user_id_1
END as other_user_id, END as other_user_id,
p.name as other_user_name, p.name as other_user_name,
p.photos as other_user_photos, p.photos as other_user_photos,
@@ -42,8 +42,8 @@ export class ChatService {
FROM matches m FROM matches m
LEFT JOIN profiles p ON ( LEFT JOIN profiles p ON (
CASE CASE
WHEN m.user1_id = $1 THEN p.user_id = m.user2_id WHEN m.user_id_1 = $1 THEN p.user_id = m.user_id_2
ELSE p.user_id = m.user1_id ELSE p.user_id = m.user_id_1
END END
) )
LEFT JOIN messages msg ON msg.id = ( LEFT JOIN messages msg ON msg.id = (
@@ -52,10 +52,10 @@ export class ChatService {
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 1 LIMIT 1
) )
WHERE (m.user1_id = $1 OR m.user2_id = $1) WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1)
AND m.status = 'active' AND m.is_active = true
ORDER BY 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]); `, [userId]);
return result.rows.map((row: any) => ({ return result.rows.map((row: any) => ({
@@ -91,7 +91,6 @@ export class ChatService {
senderId: row.sender_id, senderId: row.sender_id,
content: row.content, content: row.content,
messageType: row.message_type, messageType: row.message_type,
fileId: row.file_id,
isRead: row.is_read, isRead: row.is_read,
createdAt: new Date(row.created_at) createdAt: new Date(row.created_at)
})).reverse(); // Возвращаем в хронологическом порядке })).reverse(); // Возвращаем в хронологическом порядке
@@ -106,8 +105,7 @@ export class ChatService {
matchId: string, matchId: string,
senderTelegramId: string, senderTelegramId: string,
content: string, content: string,
messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text', messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text'
fileId?: string
): Promise<Message | null> { ): Promise<Message | null> {
try { try {
// Получаем senderId по telegramId // Получаем senderId по telegramId
@@ -119,7 +117,7 @@ export class ChatService {
// Проверяем, что матч активен и пользователь является участником // Проверяем, что матч активен и пользователь является участником
const matchResult = await query(` const matchResult = await query(`
SELECT * FROM matches 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]); `, [matchId, senderId]);
if (matchResult.rows.length === 0) { if (matchResult.rows.length === 0) {
@@ -130,9 +128,9 @@ export class ChatService {
// Создаем сообщение // Создаем сообщение
await query(` await query(`
INSERT INTO messages (id, match_id, sender_id, content, message_type, file_id, is_read, created_at) INSERT INTO messages (id, match_id, sender_id, content, message_type, is_read, created_at)
VALUES ($1, $2, $3, $4, $5, $6, false, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, false, CURRENT_TIMESTAMP)
`, [messageId, matchId, senderId, content, messageType, fileId]); `, [messageId, matchId, senderId, content, messageType]);
// Обновляем время последнего сообщения в матче // Обновляем время последнего сообщения в матче
await query(` await query(`
@@ -157,7 +155,6 @@ export class ChatService {
senderId: row.sender_id, senderId: row.sender_id,
content: row.content, content: row.content,
messageType: row.message_type, messageType: row.message_type,
fileId: row.file_id,
isRead: row.is_read, isRead: row.is_read,
createdAt: new Date(row.created_at) createdAt: new Date(row.created_at)
}); });
@@ -197,11 +194,11 @@ export class ChatService {
SELECT SELECT
m.*, m.*,
CASE CASE
WHEN m.user1_id = $2 THEN m.user2_id WHEN m.user_id_1 = $2 THEN m.user_id_2
ELSE m.user1_id ELSE m.user_id_1
END as other_user_id END as other_user_id
FROM matches m 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]); `, [matchId, userId]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
@@ -234,7 +231,7 @@ export class ChatService {
// Проверяем, что пользователь является участником матча // Проверяем, что пользователь является участником матча
const matchResult = await query(` const matchResult = await query(`
SELECT * FROM matches 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]); `, [matchId, userId]);
if (matchResult.rows.length === 0) { if (matchResult.rows.length === 0) {
@@ -244,9 +241,11 @@ export class ChatService {
// Помечаем матч как неактивный // Помечаем матч как неактивный
await query(` await query(`
UPDATE matches UPDATE matches
SET status = 'unmatched' SET is_active = false,
unmatched_at = NOW(),
unmatched_by = $2
WHERE id = $1 WHERE id = $1
`, [matchId]); `, [matchId, userId]);
return true; return true;
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,171 @@
import axios from 'axios';
export interface TranslationRequest {
text: string;
targetLanguage: string;
sourceLanguage?: string;
}
export interface TranslationResponse {
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
}
export class DeepSeekTranslationService {
private static instance: DeepSeekTranslationService;
private apiKey: string;
private apiUrl: string = 'https://api.deepseek.com/v1/chat/completions';
private constructor() {
this.apiKey = process.env.DEEPSEEK_API_KEY || '';
if (!this.apiKey) {
console.warn('⚠️ DEEPSEEK_API_KEY not found in environment variables');
}
}
public static getInstance(): DeepSeekTranslationService {
if (!DeepSeekTranslationService.instance) {
DeepSeekTranslationService.instance = new DeepSeekTranslationService();
}
return DeepSeekTranslationService.instance;
}
public async translateProfile(request: TranslationRequest): Promise<TranslationResponse> {
if (!this.apiKey) {
throw new Error('DeepSeek API key is not configured');
}
try {
const prompt = this.createTranslationPrompt(request);
const response = await axios.post(this.apiUrl, {
model: 'deepseek-chat',
messages: [
{
role: 'system',
content: 'You are a professional translator specializing in dating profiles. Translate the given text naturally, preserving the tone and personality. Respond only with the translated text, no additional comments.'
},
{
role: 'user',
content: prompt
}
],
max_tokens: 1000,
temperature: 0.3
}, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 30000 // 30 секунд таймаут
});
if (response.data?.choices?.[0]?.message?.content) {
const translatedText = response.data.choices[0].message.content.trim();
return {
translatedText,
sourceLanguage: request.sourceLanguage || 'auto',
targetLanguage: request.targetLanguage
};
} else {
throw new Error('Invalid response from DeepSeek API');
}
} catch (error) {
console.error('❌ DeepSeek translation error:', error);
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error('Invalid DeepSeek API key');
} else if (error.response?.status === 429) {
throw new Error('Translation rate limit exceeded. Please try again later.');
} else if (error.code === 'ECONNABORTED') {
throw new Error('Translation request timed out. Please try again.');
}
}
throw new Error('Translation service temporarily unavailable');
}
}
private createTranslationPrompt(request: TranslationRequest): string {
const languageMap: { [key: string]: string } = {
'en': 'English',
'ru': 'Russian',
'es': 'Spanish',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'pt': 'Portuguese',
'zh': 'Chinese',
'ja': 'Japanese',
'ko': 'Korean'
};
const targetLanguageName = languageMap[request.targetLanguage] || request.targetLanguage;
let prompt = `Translate the following dating profile text to ${targetLanguageName}. `;
if (request.sourceLanguage && request.sourceLanguage !== 'auto') {
const sourceLanguageName = languageMap[request.sourceLanguage] || request.sourceLanguage;
prompt += `The source language is ${sourceLanguageName}. `;
}
prompt += `Keep the tone natural and personal, as if the person is describing themselves:\n\n${request.text}`;
return prompt;
}
// Определить язык текста (базовая логика)
public detectLanguage(text: string): string {
// Простая эвристика для определения языка
const cyrillicPattern = /[а-яё]/i;
const latinPattern = /[a-z]/i;
const cyrillicCount = (text.match(cyrillicPattern) || []).length;
const latinCount = (text.match(latinPattern) || []).length;
if (cyrillicCount > latinCount) {
return 'ru';
} else if (latinCount > 0) {
return 'en';
}
return 'auto';
}
// Проверить доступность сервиса
public async checkServiceAvailability(): Promise<boolean> {
if (!this.apiKey) {
return false;
}
try {
const response = await axios.post(this.apiUrl, {
model: 'deepseek-chat',
messages: [
{
role: 'user',
content: 'Test'
}
],
max_tokens: 5
}, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 10000
});
return response.status === 200;
} catch (error) {
console.error('DeepSeek service availability check failed:', error);
return false;
}
}
}
export default DeepSeekTranslationService;

View File

@@ -0,0 +1,154 @@
import i18next from 'i18next';
import * as fs from 'fs';
import * as path from 'path';
import { pool } from '../database/connection';
export class LocalizationService {
private static instance: LocalizationService;
private initialized = false;
private constructor() {}
public static getInstance(): LocalizationService {
if (!LocalizationService.instance) {
LocalizationService.instance = new LocalizationService();
}
return LocalizationService.instance;
}
public async initialize(): Promise<void> {
if (this.initialized) return;
try {
// Загружаем файлы переводов
const localesPath = path.join(__dirname, '..', 'locales');
const ruTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'ru.json'), 'utf8'));
const enTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'en.json'), 'utf8'));
const esTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'es.json'), 'utf8'));
const frTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'fr.json'), 'utf8'));
const deTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'de.json'), 'utf8'));
const itTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'it.json'), 'utf8'));
const ptTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'pt.json'), 'utf8'));
const zhTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'zh.json'), 'utf8'));
const jaTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'ja.json'), 'utf8'));
const koTranslations = JSON.parse(fs.readFileSync(path.join(localesPath, 'ko.json'), 'utf8'));
await i18next.init({
lng: 'ru', // Язык по умолчанию
fallbackLng: 'ru',
debug: false,
resources: {
ru: {
translation: ruTranslations
},
en: {
translation: enTranslations
},
es: {
translation: esTranslations
},
fr: {
translation: frTranslations
},
de: {
translation: deTranslations
},
it: {
translation: itTranslations
},
pt: {
translation: ptTranslations
},
zh: {
translation: zhTranslations
},
ja: {
translation: jaTranslations
},
ko: {
translation: koTranslations
}
},
interpolation: {
escapeValue: false
}
});
this.initialized = true;
console.log('✅ Localization service initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize localization service:', error);
throw error;
}
}
public t(key: string, options?: any): string {
return i18next.t(key, options) as string;
}
public setLanguage(language: string): void {
i18next.changeLanguage(language);
}
public getCurrentLanguage(): string {
return i18next.language;
}
public getSupportedLanguages(): string[] {
return ['ru', 'en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko'];
}
// Получить перевод для определенного языка без изменения текущего
public getTranslation(key: string, language: string, options?: any): string {
const currentLang = i18next.language;
i18next.changeLanguage(language);
const translation = i18next.t(key, options) as string;
i18next.changeLanguage(currentLang);
return translation;
}
// Определить язык пользователя по его настройкам Telegram
public detectUserLanguage(telegramLanguageCode?: string): string {
if (!telegramLanguageCode) return 'ru';
// Поддерживаемые языки
const supportedLanguages = this.getSupportedLanguages();
// Проверяем точное совпадение
if (supportedLanguages.includes(telegramLanguageCode)) {
return telegramLanguageCode;
}
// Проверяем по первым двум символам (например, en-US -> en)
const languagePrefix = telegramLanguageCode.substring(0, 2);
if (supportedLanguages.includes(languagePrefix)) {
return languagePrefix;
}
// По умолчанию русский
return 'ru';
}
}
// Функция для получения персонализированного перевода пользователя
export const getUserTranslation = async (telegramId: string, key: string, options?: any): Promise<string> => {
try {
// Получаем язык пользователя из базы данных
const result = await pool.query('SELECT language FROM users WHERE telegram_id = $1', [telegramId]);
const userLanguage = result.rows[0]?.language || 'ru';
// Получаем перевод для языка пользователя
return LocalizationService.getInstance().getTranslation(key, userLanguage, options);
} catch (error) {
console.error('Error getting user translation:', error);
// Возвращаем перевод на русском языке по умолчанию
return LocalizationService.getInstance().getTranslation(key, 'ru', options);
}
};
// Функция-хелпер для быстрого доступа к переводам
export const t = (key: string, options?: any): string => {
return LocalizationService.getInstance().t(key, options);
};
export default LocalizationService;

View File

@@ -70,7 +70,7 @@ export class MatchingService {
await transaction(async (client) => { await transaction(async (client) => {
// Создаем свайп // Создаем свайп
await client.query(` 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) VALUES ($1, $2, $3, $4, $5)
`, [swipeId, userId, targetUserId, direction, new Date()]); `, [swipeId, userId, targetUserId, direction, new Date()]);
@@ -78,14 +78,14 @@ export class MatchingService {
if (swipeType === 'like' || swipeType === 'superlike') { if (swipeType === 'like' || swipeType === 'superlike') {
const reciprocalSwipe = await client.query(` const reciprocalSwipe = await client.query(`
SELECT * FROM swipes 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]); `, [targetUserId, userId]);
if (reciprocalSwipe.rows.length > 0) { if (reciprocalSwipe.rows.length > 0) {
// Проверяем, что матч еще не существует // Проверяем, что матч еще не существует
const existingMatch = await client.query(` const existingMatch = await client.query(`
SELECT * FROM matches 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]); `, [userId, targetUserId]);
if (existingMatch.rows.length === 0) { if (existingMatch.rows.length === 0) {
@@ -98,9 +98,9 @@ export class MatchingService {
// Создаем матч // Создаем матч
await client.query(` 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) VALUES ($1, $2, $3, $4, $5)
`, [matchId, user1Id, user2Id, new Date(), 'active']); `, [matchId, user1Id, user2Id, new Date(), true]);
match = new Match({ match = new Match({
id: matchId, id: matchId,
@@ -143,7 +143,7 @@ export class MatchingService {
async getSwipe(userId: string, targetUserId: string): Promise<Swipe | null> { async getSwipe(userId: string, targetUserId: string): Promise<Swipe | null> {
const result = await query(` const result = await query(`
SELECT * FROM swipes SELECT * FROM swipes
WHERE swiper_id = $1 AND swiped_id = $2 WHERE user_id = $1 AND target_user_id = $2
`, [userId, targetUserId]); `, [userId, targetUserId]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
@@ -163,8 +163,8 @@ export class MatchingService {
const result = await query(` const result = await query(`
SELECT * FROM matches SELECT * FROM matches
WHERE (user1_id = $1 OR user2_id = $1) AND status = 'active' WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true
ORDER BY matched_at DESC ORDER BY created_at DESC
LIMIT $2 LIMIT $2
`, [userId, limit]); `, [userId, limit]);
@@ -217,7 +217,7 @@ export class MatchingService {
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> { async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
const result = await query(` const result = await query(`
SELECT * FROM swipes 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 ORDER BY created_at DESC
LIMIT $2 LIMIT $2
`, [userId, limit]); `, [userId, limit]);
@@ -311,11 +311,11 @@ export class MatchingService {
private mapEntityToMatch(entity: any): Match { private mapEntityToMatch(entity: any): Match {
return new Match({ return new Match({
id: entity.id, id: entity.id,
userId1: entity.user1_id, userId1: entity.user_id_1,
userId2: entity.user2_id, userId2: entity.user_id_2,
createdAt: entity.matched_at || entity.created_at, createdAt: entity.created_at,
lastMessageAt: entity.last_message_at, lastMessageAt: entity.last_message_at,
isActive: entity.status === 'active', isActive: entity.is_active === true,
isSuperMatch: false, // Определяется из swipes если нужно isSuperMatch: false, // Определяется из swipes если нужно
unreadCount1: 0, unreadCount1: 0,
unreadCount2: 0 unreadCount2: 0
@@ -329,8 +329,8 @@ export class MatchingService {
FROM swipes s1 FROM swipes s1
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
WHERE s1.user_id = $1 WHERE s1.user_id = $1
AND s1.type IN ('like', 'superlike') AND s1.direction IN ('right', 'super')
AND s2.type IN ('like', 'superlike') AND s2.direction IN ('right', 'super')
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM matches m SELECT 1 FROM matches m
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id) 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 // Сначала получаем профиль пользователя по telegramId
const userProfile = await this.profileService.getProfileByTelegramId(telegramId); const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
if (!userProfile) { if (!userProfile) {
@@ -354,18 +354,26 @@ export class MatchingService {
// Получаем список уже просмотренных пользователей // Получаем список уже просмотренных пользователей
const viewedUsers = await query(` const viewedUsers = await query(`
SELECT DISTINCT swiped_id SELECT DISTINCT target_user_id
FROM swipes FROM swipes
WHERE swiper_id = $1 WHERE user_id = $1
`, [userId]); `, [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); // Исключаем самого себя 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 = ` const candidateQuery = `

View File

@@ -233,8 +233,8 @@ export class NotificationService {
SELECT m.created_at SELECT m.created_at
FROM messages m FROM messages m
JOIN matches mt ON m.match_id = mt.id JOIN matches mt ON m.match_id = mt.id
WHERE (mt.user1_id = $1 OR mt.user2_id = $1) WHERE (mt.user_id_1 = $1 OR mt.user_id_2 = $1)
AND (mt.user1_id = $2 OR mt.user2_id = $2) AND (mt.user_id_1 = $2 OR mt.user_id_2 = $2)
AND m.sender_id = $1 AND m.sender_id = $1
ORDER BY m.created_at DESC ORDER BY m.created_at DESC
LIMIT 1 LIMIT 1
@@ -347,10 +347,33 @@ export class NotificationService {
// Планировщик уведомлений (вызывается периодически) // Планировщик уведомлений (вызывается периодически)
async processScheduledNotifications(): Promise<void> { async processScheduledNotifications(): Promise<void> {
try { 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(` const result = await query(`
SELECT * FROM scheduled_notifications SELECT * FROM scheduled_notifications
WHERE scheduled_at <= $1 AND processed = false WHERE scheduled_at <= $1 AND is_processed = false
ORDER BY scheduled_at ASC ORDER BY scheduled_at ASC
LIMIT 100 LIMIT 100
`, [new Date()]); `, [new Date()]);
@@ -370,7 +393,7 @@ export class NotificationService {
// Отмечаем как обработанное // Отмечаем как обработанное
await query( await query(
'UPDATE scheduled_notifications SET processed = true WHERE id = $1', 'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1',
[notification.id] [notification.id]
); );
} catch (error) { } catch (error) {

View File

@@ -49,18 +49,15 @@ export class ProfileService {
// Сохранение в базу данных // Сохранение в базу данных
await query(` await query(`
INSERT INTO profiles ( INSERT INTO profiles (
id, user_id, name, age, gender, looking_for, bio, photos, interests, id, user_id, name, age, gender, interested_in, bio, photos,
hobbies, location, education, occupation, height, religion, dating_goal, city, education, job, height, religion, dating_goal,
latitude, longitude, verification_status, is_active, is_visible, is_verified, is_visible, created_at, updated_at
created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
`, [ `, [
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn, profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
profile.bio, profile.photos, profile.interests, profile.hobbies, profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
profile.city, profile.education, profile.job, profile.height, profile.height, profile.religion, profile.datingGoal,
profile.religion, profile.datingGoal, profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
profile.location?.latitude, profile.location?.longitude,
'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt
]); ]);
return profile; return profile;
@@ -137,8 +134,7 @@ export class ProfileService {
ON CONFLICT (telegram_id) DO UPDATE SET ON CONFLICT (telegram_id) DO UPDATE SET
username = EXCLUDED.username, username = EXCLUDED.username,
first_name = EXCLUDED.first_name, first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name, last_name = EXCLUDED.last_name
updated_at = CURRENT_TIMESTAMP
RETURNING id RETURNING id
`, [ `, [
parseInt(telegramId), parseInt(telegramId),
@@ -177,12 +173,8 @@ export class ProfileService {
updateValues.push(value); updateValues.push(value);
break; break;
case 'location': case 'location':
if (value && typeof value === 'object' && 'latitude' in value) { // Пропускаем обработку местоположения, так как колонки location нет
updateFields.push(`latitude = $${paramIndex++}`); console.log('Skipping location update - column does not exist');
updateValues.push(value.latitude);
updateFields.push(`longitude = $${paramIndex++}`);
updateValues.push(value.longitude);
}
break; break;
case 'searchPreferences': case 'searchPreferences':
// Поля search preferences больше не хранятся в БД, пропускаем // Поля search preferences больше не хранятся в БД, пропускаем
@@ -339,8 +331,8 @@ export class ProfileService {
const [likesResult, matchesResult, likesReceivedResult] = await Promise.all([ const [likesResult, matchesResult, likesReceivedResult] = await Promise.all([
query('SELECT COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND direction IN ($2, $3)', query('SELECT COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND direction IN ($2, $3)',
[userId, 'like', 'super']), [userId, 'like', 'super']),
query('SELECT COUNT(*) as count FROM matches WHERE (user1_id = $1 OR user2_id = $1) AND status = $2', query('SELECT COUNT(*) as count FROM matches WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true',
[userId, 'active']), [userId]),
query('SELECT COUNT(*) as count FROM swipes WHERE swiped_id = $1 AND direction IN ($2, $3)', query('SELECT COUNT(*) as count FROM swipes WHERE swiped_id = $1 AND direction IN ($2, $3)',
[userId, 'like', 'super']) [userId, 'like', 'super'])
]); ]);
@@ -424,6 +416,27 @@ export class ProfileService {
return []; 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({ return new Profile({
userId: entity.user_id, userId: entity.user_id,
name: entity.name, name: entity.name,
@@ -431,8 +444,8 @@ export class ProfileService {
gender: entity.gender, gender: entity.gender,
interestedIn: entity.looking_for, interestedIn: entity.looking_for,
bio: entity.bio, bio: entity.bio,
photos: parsePostgresArray(entity.photos), photos: parseJsonField(entity.photos),
interests: parsePostgresArray(entity.interests), interests: parseJsonField(entity.interests),
hobbies: entity.hobbies, hobbies: entity.hobbies,
city: entity.location || entity.city, city: entity.location || entity.city,
education: entity.education, education: entity.education,
@@ -441,14 +454,11 @@ export class ProfileService {
religion: entity.religion, religion: entity.religion,
datingGoal: entity.dating_goal, datingGoal: entity.dating_goal,
lifestyle: { lifestyle: {
smoking: entity.smoking, smoking: undefined,
drinking: entity.drinking, drinking: undefined,
kids: entity.has_kids kids: undefined
}, }, // Пропускаем lifestyle, так как этих колонок нет
location: entity.latitude && entity.longitude ? { location: undefined, // Пропускаем location, так как этих колонок нет
latitude: entity.latitude,
longitude: entity.longitude
} : undefined,
searchPreferences: { searchPreferences: {
minAge: 18, minAge: 18,
maxAge: 50, maxAge: 50,
@@ -466,9 +476,10 @@ export class ProfileService {
// Специальные случаи для некоторых полей // Специальные случаи для некоторых полей
const specialCases: { [key: string]: string } = { const specialCases: { [key: string]: string } = {
'interestedIn': 'looking_for', 'interestedIn': 'looking_for',
'job': 'occupation', // Удалили 'job': 'occupation', так как колонка occupation не существует
'city': 'location', // Вместо этого используем job
'datingGoal': 'dating_goal' 'datingGoal': 'dating_goal'
// Удалили 'city': 'location', так как колонка location не существует
}; };
if (specialCases[str]) { if (specialCases[str]) {
@@ -484,7 +495,7 @@ export class ProfileService {
await transaction(async (client) => { await transaction(async (client) => {
// Удаляем связанные данные // Удаляем связанные данные
await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]); 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 swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]);
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]); await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
}); });

236
src/services/vipService.ts Normal file
View File

@@ -0,0 +1,236 @@
import { query } from '../database/connection';
import { BotError } from '../types/index';
export interface VipSearchFilters {
ageMin?: number;
ageMax?: number;
city?: string;
datingGoal?: string;
hobbies?: string[];
lifestyle?: string[];
distance?: number;
hasPhotos?: boolean;
isOnline?: boolean;
}
export interface PremiumInfo {
isPremium: boolean;
expiresAt?: Date;
daysLeft?: number;
}
export class VipService {
// Проверить премиум статус пользователя
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
try {
// Проверяем существование пользователя
const result = await query(`
SELECT id
FROM users
WHERE telegram_id = $1
`, [telegramId]);
if (result.rows.length === 0) {
throw new BotError('User not found', 'USER_NOT_FOUND', 404);
}
// Временно возвращаем false для всех пользователей, так как колонки premium нет
// В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно
return {
isPremium: false,
expiresAt: undefined,
daysLeft: undefined
};
} catch (error) {
console.error('Error checking premium status:', error);
throw error;
}
}
// Добавить премиум статус
async addPremium(telegramId: string, durationDays: number = 30): Promise<void> {
try {
// Временно заглушка, так как колонок 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;
}
}
// Удалить премиум статус
async removePremium(telegramId: string): Promise<void> {
try {
// Временно заглушка, так как колонок premium и premium_expires_at нет
console.log(`[VIP] Попытка удалить премиум для ${telegramId}`);
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
} catch (error) {
console.error('Error removing premium:', error);
throw error;
}
}
// VIP поиск с фильтрами
async vipSearch(telegramId: string, filters: VipSearchFilters): Promise<any[]> {
try {
// Проверяем премиум статус
const premiumInfo = await this.checkPremiumStatus(telegramId);
if (!premiumInfo.isPremium) {
throw new BotError('Premium subscription required', 'PREMIUM_REQUIRED', 403);
}
// Получаем профиль пользователя
const userProfile = await query(`
SELECT p.*, u.telegram_id
FROM profiles p
JOIN users u ON p.user_id = u.id
WHERE u.telegram_id = $1
`, [telegramId]);
if (userProfile.rows.length === 0) {
throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 404);
}
const currentUser = userProfile.rows[0];
// Строим запрос с фильтрами
let query_text = `
SELECT p.*, u.telegram_id,
CASE WHEN u.updated_at > NOW() - INTERVAL '15 minutes' THEN true ELSE false END as is_online
FROM profiles p
JOIN users u ON p.user_id = u.id
LEFT JOIN swipes s ON (
s.swiper_id = $1 AND s.swiped_id = u.id
)
WHERE u.telegram_id != $2
AND s.id IS NULL
AND p.is_active = true
`;
let params = [currentUser.user_id, telegramId];
let paramIndex = 3;
// Фильтр по противоположному полу
if (currentUser.gender === 'male') {
query_text += ` AND p.gender = 'female'`;
} else if (currentUser.gender === 'female') {
query_text += ` AND p.gender = 'male'`;
} else {
// Если пол не определен или 'other', показываем всех кроме того же пола
query_text += ` AND p.gender != $${paramIndex}`;
params.push(currentUser.gender);
paramIndex++;
}
// Фильтр по возрасту
if (filters.ageMin) {
query_text += ` AND p.age >= $${paramIndex}`;
params.push(filters.ageMin);
paramIndex++;
}
if (filters.ageMax) {
query_text += ` AND p.age <= $${paramIndex}`;
params.push(filters.ageMax);
paramIndex++;
}
// Фильтр по городу
if (filters.city) {
query_text += ` AND LOWER(p.city) LIKE LOWER($${paramIndex})`;
params.push(`%${filters.city}%`);
paramIndex++;
}
// Фильтр по цели знакомства
if (filters.datingGoal) {
query_text += ` AND p.dating_goal = $${paramIndex}`;
params.push(filters.datingGoal);
paramIndex++;
}
// Фильтр по хобби
if (filters.hobbies && filters.hobbies.length > 0) {
const hobbyConditions = filters.hobbies.map((_, index) => {
return `LOWER(p.hobbies) LIKE LOWER($${paramIndex + index})`;
});
query_text += ` AND (${hobbyConditions.join(' OR ')})`;
filters.hobbies.forEach(hobby => {
params.push(`%${hobby}%`);
});
paramIndex += filters.hobbies.length;
}
// Фильтр по образу жизни
if (filters.lifestyle && filters.lifestyle.length > 0) {
const lifestyleConditions = filters.lifestyle.map((field) => {
const condition = `p.lifestyle ? $${paramIndex}`;
params.push(field);
paramIndex++;
return condition;
});
query_text += ` AND (${lifestyleConditions.join(' OR ')})`;
}
// Фильтр по наличию фото
if (filters.hasPhotos) {
query_text += ` AND p.photos IS NOT NULL AND array_length(p.photos, 1) > 0`;
}
// Фильтр по онлайн статусу
if (filters.isOnline) {
query_text += ` AND u.updated_at > NOW() - INTERVAL '15 minutes'`;
}
query_text += ` ORDER BY
CASE WHEN u.updated_at > NOW() - INTERVAL '15 minutes' THEN 0 ELSE 1 END,
u.updated_at DESC,
p.created_at DESC
LIMIT 50`;
const result = await query(query_text, params);
return result.rows;
} catch (error) {
console.error('Error in VIP search:', error);
throw error;
}
}
// Получить информацию о премиум возможностях
getPremiumFeatures(): string {
return `💎 ПРЕМИУМ ПОДПИСКА 💎
🔥 Что дает VIP статус:
🎯 VIP Поиск с фильтрами:
• Поиск по возрасту
• Поиск по городу
• Фильтр по целям знакомства
• Поиск по хобби и интересам
• Фильтр по образу жизни
• Только пользователи с фото
• Только онлайн пользователи
⚡ Дополнительные возможности:
• Неограниченные супер-лайки
• Просмотр кто лайкнул вас
• Возможность отменить свайп
• Приоритет в показе другим
• Расширенная статистика
• Скрытый режим просмотра
💰 Тарифы:
• 1 месяц - 299₽
• 3 месяца - 699₽ (экономия 25%)
• 6 месяцев - 1199₽ (экономия 33%)
• 1 год - 1999₽ (экономия 44%)
📞 Для покупки обратитесь к администратору:
@admin_bot
✨ Попробуйте VIP уже сегодня!`;
}
}

View File

@@ -1,59 +0,0 @@
Table "public.profiles"
Column | Type | Collation | Nullable | Default
---------------------+--------------------------+-----------+----------+---------------------------------
id | uuid | | not null | uuid_generate_v4()
user_id | uuid | | not null |
name | character varying(255) | | not null |
age | integer | | not null |
gender | character varying(20) | | not null |
looking_for | character varying(20) | | not null |
bio | text | | |
location | character varying(255) | | |
latitude | numeric(10,8) | | |
longitude | numeric(11,8) | | |
photos | text[] | | |
interests | text[] | | |
education | character varying(255) | | |
occupation | character varying(255) | | |
height | integer | | |
smoking | character varying(20) | | |
drinking | character varying(20) | | |
relationship_type | character varying(30) | | |
verification_status | character varying(20) | | | 'unverified'::character varying
is_active | boolean | | | true
is_visible | boolean | | | true
last_active | timestamp with time zone | | | CURRENT_TIMESTAMP
created_at | timestamp with time zone | | | CURRENT_TIMESTAMP
updated_at | timestamp with time zone | | | CURRENT_TIMESTAMP
hobbies | text | | |
religion | character varying(100) | | |
dating_goal | character varying(20) | | |
has_kids | character varying(20) | | |
Indexes:
"profiles_pkey" PRIMARY KEY, btree (id)
"idx_profiles_active" btree (is_active, is_visible)
"idx_profiles_age_gender" btree (age, gender, looking_for)
"idx_profiles_dating_goal" btree (dating_goal)
"idx_profiles_has_kids" btree (has_kids)
"idx_profiles_location" btree (latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL
"idx_profiles_religion" btree (religion)
"idx_profiles_user_id" btree (user_id)
"profiles_user_id_key" UNIQUE CONSTRAINT, btree (user_id)
Check constraints:
"profiles_age_check" CHECK (age >= 18 AND age <= 100)
"profiles_dating_goal_check" CHECK (dating_goal::text = ANY (ARRAY['serious'::character varying, 'casual'::character varying, 'friends'::character varying, 'unsure'::character varying]::text[]))
"profiles_drinking_check" CHECK (drinking::text = ANY (ARRAY['never'::character varying, 'sometimes'::character varying, 'regularly'::character varying]::text[]))
"profiles_gender_check" CHECK (gender::text = ANY (ARRAY['male'::character varying, 'female'::character varying, 'other'::character varying]::text[]))
"profiles_has_kids_check" CHECK (has_kids::text = ANY (ARRAY['have'::character varying, 'want'::character varying, 'dont_want'::character varying, 'unsure'::character varying]::text[]))
"profiles_looking_for_check" CHECK (looking_for::text = ANY (ARRAY['male'::character varying, 'female'::character varying, 'both'::character varying]::text[]))
"profiles_relationship_type_check" CHECK (relationship_type::text = ANY (ARRAY['casual'::character varying, 'serious'::character varying, 'friendship'::character varying, 'anything'::character varying]::text[]))
"profiles_smoking_check" CHECK (smoking::text = ANY (ARRAY['never'::character varying, 'sometimes'::character varying, 'regularly'::character varying]::text[]))
"profiles_verification_status_check" CHECK (verification_status::text = ANY (ARRAY['unverified'::character varying, 'pending'::character varying, 'verified'::character varying]::text[]))
Foreign-key constraints:
"profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
Referenced by:
TABLE "profile_views" CONSTRAINT "profile_views_viewed_profile_id_fkey" FOREIGN KEY (viewed_profile_id) REFERENCES profiles(id) ON DELETE CASCADE
Triggers:
profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at()
update_profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()