Compare commits
9 Commits
vip__func
...
bdd7d0424f
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd7d0424f | |||
| 856bf3ca2a | |||
| e3baa9be63 | |||
| a3fb88e91e | |||
| c5a0593222 | |||
| 1eb7d1c9bc | |||
| e81725e4d5 | |||
| edddd52589 | |||
| 975eb348dd |
181
README.md
181
README.md
@@ -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
84
bin/README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Автоматическое обновление Telegram Tinder Bot
|
||||||
|
|
||||||
|
Этот документ описывает процесс автоматического обновления бота с помощью созданных скриптов.
|
||||||
|
|
||||||
|
## Скрипт обновления
|
||||||
|
|
||||||
|
Скрипт обновления выполняет следующие действия:
|
||||||
|
|
||||||
|
1. Получает последние изменения из Git-репозитория
|
||||||
|
2. Устанавливает зависимости
|
||||||
|
3. Применяет миграции базы данных
|
||||||
|
4. Собирает проект
|
||||||
|
5. Проверяет наличие файла .env
|
||||||
|
6. Проверяет наличие Docker-сервисов
|
||||||
|
7. При запуске на Ubuntu: проверяет и перезапускает PM2 сервис
|
||||||
|
|
||||||
|
## Подробные инструкции по развертыванию
|
||||||
|
|
||||||
|
Для подробных инструкций по развертыванию бота на сервере Ubuntu 24.04, пожалуйста, обратитесь к файлу `DEPLOY_UBUNTU.md` в корне проекта.
|
||||||
|
|
||||||
|
## Как использовать
|
||||||
|
|
||||||
|
### На Linux/macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обновление с ветки main (по умолчанию)
|
||||||
|
npm run update
|
||||||
|
|
||||||
|
# Обновление с определенной ветки
|
||||||
|
bash ./bin/update.sh develop
|
||||||
|
|
||||||
|
# Обновление с определенной ветки и перезапуском сервиса PM2 (для Ubuntu)
|
||||||
|
bash ./bin/update.sh develop --restart-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### На Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Обновление с ветки main (по умолчанию)
|
||||||
|
npm run update:win
|
||||||
|
|
||||||
|
# Обновление с определенной ветки
|
||||||
|
.\bin\update.bat develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Добавление прав на выполнение (только для Linux/macOS)
|
||||||
|
|
||||||
|
Если у вас возникают проблемы с запуском скрипта, добавьте права на выполнение:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ./bin/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Автоматизация обновлений
|
||||||
|
|
||||||
|
Для автоматизации регулярных обновлений вы можете использовать cron (Linux/macOS) или Планировщик заданий (Windows).
|
||||||
|
|
||||||
|
### Пример cron-задания для Ubuntu (ежедневное обновление в 4:00 с перезапуском сервиса):
|
||||||
|
|
||||||
|
```
|
||||||
|
0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh --restart-service >> /var/log/tg_bot_update.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример cron-задания (ежедневное обновление в 4:00 без перезапуска):
|
||||||
|
|
||||||
|
```
|
||||||
|
0 4 * * * cd /path/to/bot && ./bin/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для Windows:
|
||||||
|
|
||||||
|
Создайте задачу в Планировщике заданий, которая запускает:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd.exe /c "cd /d D:\Projects\tg_tinder_bot && .\bin\update.bat"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что делать после обновления
|
||||||
|
|
||||||
|
После обновления вы можете:
|
||||||
|
|
||||||
|
1. Запустить бота: `npm run start`
|
||||||
|
2. Запустить бота в режиме разработки: `npm run dev`
|
||||||
|
3. Перезапустить Docker-контейнеры, если используете Docker: `docker-compose down && docker-compose up -d`
|
||||||
190
bin/install_ubuntu.sh
Normal file
190
bin/install_ubuntu.sh
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script for installing Telegram Tinder Bot on Ubuntu
|
||||||
|
# This script automates the deployment process on a fresh Ubuntu server
|
||||||
|
# Usage: ./bin/install_ubuntu.sh [--with-nginx] [--with-ssl domain.com]
|
||||||
|
|
||||||
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
|
|
||||||
|
# Define colors for pretty output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Default settings
|
||||||
|
INSTALL_NGINX=false
|
||||||
|
INSTALL_SSL=false
|
||||||
|
DOMAIN=""
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--with-nginx" ]]; then
|
||||||
|
INSTALL_NGINX=true
|
||||||
|
elif [[ "$arg" == "--with-ssl" ]]; then
|
||||||
|
INSTALL_SSL=true
|
||||||
|
# Next argument should be domain
|
||||||
|
shift
|
||||||
|
DOMAIN="$1"
|
||||||
|
if [[ -z "$DOMAIN" || "$DOMAIN" == --* ]]; then
|
||||||
|
echo -e "${RED}Error: Domain name required after --with-ssl${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE} Telegram Tinder Bot Ubuntu Installer ${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
|
||||||
|
# Check if running on Ubuntu
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "$ID" != "ubuntu" ]]; then
|
||||||
|
echo -e "${RED}Error: This script is designed for Ubuntu. Current OS: $ID${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}Detected Ubuntu ${VERSION_ID}${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Could not detect operating system${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for root privileges
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Error: This script must be run as root${NC}"
|
||||||
|
echo -e "Please run: ${YELLOW}sudo $0 $*${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 1: Updating system packages...${NC}"
|
||||||
|
apt update && apt upgrade -y
|
||||||
|
echo -e "${GREEN}✓ System packages updated${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}"
|
||||||
|
apt install -y curl wget git build-essential postgresql postgresql-contrib
|
||||||
|
echo -e "${GREEN}✓ Basic dependencies installed${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 3: Installing Node.js...${NC}"
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||||
|
apt install -y nodejs
|
||||||
|
echo -e "${GREEN}✓ Node.js $(node --version) installed${NC}"
|
||||||
|
echo -e "${GREEN}✓ npm $(npm --version) installed${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 4: Setting up PostgreSQL...${NC}"
|
||||||
|
systemctl start postgresql
|
||||||
|
systemctl enable postgresql
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Please enter a strong password for the database user:${NC}"
|
||||||
|
read -s DB_PASSWORD
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Create database and user
|
||||||
|
su - postgres -c "psql -c \"CREATE DATABASE tg_tinder_bot;\""
|
||||||
|
su - postgres -c "psql -c \"CREATE USER tg_bot WITH PASSWORD '$DB_PASSWORD';\""
|
||||||
|
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot;\""
|
||||||
|
echo -e "${GREEN}✓ PostgreSQL configured${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 5: Setting up application directory...${NC}"
|
||||||
|
mkdir -p /opt/tg_tinder_bot
|
||||||
|
chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot
|
||||||
|
echo -e "${GREEN}✓ Application directory created${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 6: Installing PM2...${NC}"
|
||||||
|
npm install -g pm2
|
||||||
|
echo -e "${GREEN}✓ PM2 installed${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 7: Please enter your Telegram Bot Token:${NC}"
|
||||||
|
read BOT_TOKEN
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 8: Creating environment file...${NC}"
|
||||||
|
cat > /opt/tg_tinder_bot/.env << EOL
|
||||||
|
# Bot settings
|
||||||
|
BOT_TOKEN=${BOT_TOKEN}
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=tg_bot
|
||||||
|
DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
DB_NAME=tg_tinder_bot
|
||||||
|
EOL
|
||||||
|
chmod 600 /opt/tg_tinder_bot/.env
|
||||||
|
chown $SUDO_USER:$SUDO_USER /opt/tg_tinder_bot/.env
|
||||||
|
echo -e "${GREEN}✓ Environment file created${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 9: Setting up systemd service...${NC}"
|
||||||
|
cat > /etc/systemd/system/tg-tinder-bot.service << EOL
|
||||||
|
[Unit]
|
||||||
|
Description=Telegram Tinder Bot
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${SUDO_USER}
|
||||||
|
WorkingDirectory=/opt/tg_tinder_bot
|
||||||
|
ExecStart=/usr/bin/node dist/bot.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=tg-tinder-bot
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
echo -e "${GREEN}✓ Systemd service created${NC}"
|
||||||
|
|
||||||
|
if [ "$INSTALL_NGINX" = true ]; then
|
||||||
|
echo -e "\n${BLUE}Step 10: Installing and configuring Nginx...${NC}"
|
||||||
|
apt install -y nginx
|
||||||
|
|
||||||
|
# Create Nginx configuration
|
||||||
|
cat > /etc/nginx/sites-available/tg_tinder_bot << EOL
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${DOMAIN:-_};
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Enable site
|
||||||
|
ln -sf /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl restart nginx
|
||||||
|
echo -e "${GREEN}✓ Nginx configured${NC}"
|
||||||
|
|
||||||
|
if [ "$INSTALL_SSL" = true ] && [ ! -z "$DOMAIN" ]; then
|
||||||
|
echo -e "\n${BLUE}Step 11: Setting up SSL with Certbot...${NC}"
|
||||||
|
apt install -y certbot python3-certbot-nginx
|
||||||
|
certbot --nginx --non-interactive --agree-tos --email admin@${DOMAIN} -d ${DOMAIN}
|
||||||
|
echo -e "${GREEN}✓ SSL certificate installed${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 12: Clone your repository${NC}"
|
||||||
|
echo -e "Now you should clone your repository to /opt/tg_tinder_bot"
|
||||||
|
echo -e "Example: ${YELLOW}git clone https://your-git-repo-url.git /opt/tg_tinder_bot${NC}"
|
||||||
|
echo -e "Then run the update script: ${YELLOW}cd /opt/tg_tinder_bot && ./bin/update.sh${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}========================================${NC}"
|
||||||
|
echo -e "${GREEN} Installation completed! ${NC}"
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo -e "Next steps:"
|
||||||
|
echo -e "1. Clone your repository to ${YELLOW}/opt/tg_tinder_bot${NC}"
|
||||||
|
echo -e "2. Run the update script to set up the application"
|
||||||
|
echo -e "3. Start the service with: ${YELLOW}sudo systemctl start tg-tinder-bot${NC}"
|
||||||
|
echo -e "4. Enable auto-start with: ${YELLOW}sudo systemctl enable tg-tinder-bot${NC}"
|
||||||
|
echo -e "5. Check status with: ${YELLOW}sudo systemctl status tg-tinder-bot${NC}"
|
||||||
0
setup.sh → bin/setup.sh
Executable file → Normal file
0
setup.sh → bin/setup.sh
Executable file → Normal file
27
bin/start_bot.bat
Normal file
27
bin/start_bot.bat
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@echo off
|
||||||
|
REM Скрипт для запуска Telegram Tinder Bot в производственном режиме на Windows
|
||||||
|
REM Запускает собранный JavaScript из dist/bot.js
|
||||||
|
|
||||||
|
echo 🚀 Запуск Telegram Tinder Bot в производственном режиме...
|
||||||
|
|
||||||
|
REM Добавляем Node.js в PATH, если нужно
|
||||||
|
set PATH=%PATH%;C:\Program Files\nodejs
|
||||||
|
|
||||||
|
REM Переходим в корневую директорию проекта
|
||||||
|
cd /d %~dp0..
|
||||||
|
|
||||||
|
REM Проверяем, существует ли собранный файл
|
||||||
|
if not exist ".\dist\bot.js" (
|
||||||
|
echo ❌ Ошибка: Собранный файл не найден. Сначала выполните 'npm run build'.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Устанавливаем переменную окружения для производственного режима
|
||||||
|
set NODE_ENV=production
|
||||||
|
|
||||||
|
REM Запускаем бот
|
||||||
|
echo 🤖 Запуск Telegram Tinder Bot...
|
||||||
|
node .\dist\bot.js
|
||||||
|
|
||||||
|
REM Если скрипт дойдет до этой точки, значит бот завершил работу
|
||||||
|
echo 👋 Бот был остановлен.
|
||||||
24
bin/start_bot.sh
Normal file
24
bin/start_bot.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Скрипт для запуска бота в производственном режиме
|
||||||
|
# Запускает собранный JavaScript из dist/bot.js
|
||||||
|
|
||||||
|
# Переходим в корневую директорию проекта (предполагается, что скрипт находится в bin/)
|
||||||
|
PROJECT_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
|
||||||
|
cd "$PROJECT_DIR" || { echo "❌ Error: Could not change directory to $PROJECT_DIR"; exit 1; }
|
||||||
|
|
||||||
|
# Проверяем, существует ли собранный файл
|
||||||
|
if [ ! -f "./dist/bot.js" ]; then
|
||||||
|
echo "❌ Error: Built file not found. Please run 'npm run build' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Устанавливаем переменную окружения для производственного режима
|
||||||
|
export NODE_ENV=production
|
||||||
|
|
||||||
|
# Запускаем бот
|
||||||
|
echo "🚀 Starting Telegram Tinder Bot in production mode..."
|
||||||
|
node ./dist/bot.js
|
||||||
|
|
||||||
|
# Если скрипт дойдет до этой точки, значит бот завершил работу
|
||||||
|
echo "👋 Bot has been stopped."
|
||||||
18
bin/tg-tinder-bot.service
Normal file
18
bin/tg-tinder-bot.service
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Telegram Tinder Bot
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/opt/tg_tinder_bot
|
||||||
|
ExecStart=/usr/bin/node dist/bot.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=tg-tinder-bot
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
113
bin/update.bat
Normal file
113
bin/update.bat
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
@echo off
|
||||||
|
REM Script for updating the Telegram Tinder Bot on Windows
|
||||||
|
REM This script updates the code from Git, applies migrations, and prepares the bot for running
|
||||||
|
REM Usage: .\bin\update.bat [branch]
|
||||||
|
REM If branch is not specified, 'main' is used
|
||||||
|
|
||||||
|
setlocal enableextensions enabledelayedexpansion
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Telegram Tinder Bot Updater
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
REM Get the branch name from the command line arguments
|
||||||
|
set BRANCH=%1
|
||||||
|
if "%BRANCH%"=="" set BRANCH=main
|
||||||
|
echo Updating from branch: %BRANCH%
|
||||||
|
|
||||||
|
REM Store the current directory
|
||||||
|
set CURRENT_DIR=%CD%
|
||||||
|
set SCRIPT_DIR=%~dp0
|
||||||
|
set PROJECT_DIR=%SCRIPT_DIR%..
|
||||||
|
|
||||||
|
REM Navigate to the project directory
|
||||||
|
cd /d %PROJECT_DIR%
|
||||||
|
echo Working directory: %PROJECT_DIR%
|
||||||
|
|
||||||
|
REM Check if we're in a git repository
|
||||||
|
if not exist .git (
|
||||||
|
echo Error: Not a git repository
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 1: Pulling latest changes from Git repository...
|
||||||
|
REM Save any local changes
|
||||||
|
git stash save "Auto-stash before update: %DATE% %TIME%"
|
||||||
|
|
||||||
|
REM Fetch all branches
|
||||||
|
git fetch --all
|
||||||
|
|
||||||
|
REM Check if the branch exists
|
||||||
|
git rev-parse --verify %BRANCH% >nul 2>&1
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
git rev-parse --verify origin/%BRANCH% >nul 2>&1
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Error: Branch '%BRANCH%' does not exist locally or remotely
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Checkout the specified branch
|
||||||
|
git checkout %BRANCH%
|
||||||
|
|
||||||
|
REM Pull the latest changes
|
||||||
|
git pull origin %BRANCH%
|
||||||
|
echo ✓ Successfully pulled latest changes
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 2: Installing dependencies...
|
||||||
|
call npm ci
|
||||||
|
echo ✓ Dependencies installed
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 3: Running database migrations...
|
||||||
|
REM Check if migrations directory exists
|
||||||
|
if exist migrations (
|
||||||
|
echo Applying database migrations...
|
||||||
|
call npm run migrate:up
|
||||||
|
echo ✓ Migrations applied successfully
|
||||||
|
) else (
|
||||||
|
echo ⚠ No migrations directory found, running database initialization script...
|
||||||
|
call npm run init:db
|
||||||
|
echo ✓ Database initialized
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 4: Building the project...
|
||||||
|
call npm run build
|
||||||
|
echo ✓ Project built successfully
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 5: Checking for .env file...
|
||||||
|
if exist .env (
|
||||||
|
echo ✓ .env file exists
|
||||||
|
) else (
|
||||||
|
echo ⚠ .env file not found
|
||||||
|
if exist .env.example (
|
||||||
|
echo Creating .env from .env.example
|
||||||
|
copy .env.example .env
|
||||||
|
echo ⚠ Please update the .env file with your configuration!
|
||||||
|
) else (
|
||||||
|
echo Error: .env.example file not found
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Step 6: Checking for services...
|
||||||
|
REM Check if Docker is being used
|
||||||
|
if exist docker-compose.yml (
|
||||||
|
echo Docker Compose configuration found
|
||||||
|
echo You might want to restart containers with: docker-compose down ^&^& docker-compose up -d
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Update completed successfully!
|
||||||
|
echo ========================================
|
||||||
|
echo To start the bot, run: npm run start
|
||||||
|
echo For development mode: npm run dev
|
||||||
|
|
||||||
|
REM Return to the original directory
|
||||||
|
cd /d %CURRENT_DIR%
|
||||||
155
bin/update.sh
Normal file
155
bin/update.sh
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script for updating the Telegram Tinder Bot
|
||||||
|
# This script updates the code from Git, applies migrations, and prepares the bot for running
|
||||||
|
# Usage: ./bin/update.sh [branch] [--restart-service]
|
||||||
|
# If branch is not specified, 'main' is used
|
||||||
|
# Use --restart-service flag to restart PM2 service after update (for production deployments)
|
||||||
|
|
||||||
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
|
|
||||||
|
# Define colors for pretty output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE} Telegram Tinder Bot Updater ${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
BRANCH="main"
|
||||||
|
RESTART_SERVICE=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--restart-service" ]]; then
|
||||||
|
RESTART_SERVICE=true
|
||||||
|
elif [[ "$arg" != --* ]]; then
|
||||||
|
BRANCH="$arg"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Updating from branch: ${BRANCH}${NC}"
|
||||||
|
if [ "$RESTART_SERVICE" = true ]; then
|
||||||
|
echo -e "${YELLOW}Will restart service after update${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store the current directory
|
||||||
|
CURRENT_DIR=$(pwd)
|
||||||
|
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
|
||||||
|
PROJECT_DIR=$(dirname "$SCRIPT_DIR")
|
||||||
|
|
||||||
|
# Check if running on Ubuntu
|
||||||
|
IS_UBUNTU=false
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "$ID" == "ubuntu" ]]; then
|
||||||
|
IS_UBUNTU=true
|
||||||
|
echo -e "${BLUE}Detected Ubuntu: ${VERSION_ID}${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Navigate to the project directory
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
echo -e "${BLUE}Working directory: ${PROJECT_DIR}${NC}"
|
||||||
|
|
||||||
|
# Check if we're in a git repository
|
||||||
|
if [ ! -d ".git" ]; then
|
||||||
|
echo -e "${RED}Error: Not a git repository${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 1: Pulling latest changes from Git repository...${NC}"
|
||||||
|
# Save any local changes
|
||||||
|
git stash save "Auto-stash before update: $(date)"
|
||||||
|
|
||||||
|
# Fetch all branches
|
||||||
|
git fetch --all
|
||||||
|
|
||||||
|
# Check if the branch exists
|
||||||
|
if ! git rev-parse --verify "$BRANCH" &>/dev/null && ! git rev-parse --verify "origin/$BRANCH" &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: Branch '$BRANCH' does not exist locally or remotely${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Checkout the specified branch
|
||||||
|
git checkout "$BRANCH"
|
||||||
|
|
||||||
|
# Pull the latest changes
|
||||||
|
git pull origin "$BRANCH"
|
||||||
|
echo -e "${GREEN}✓ Successfully pulled latest changes${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 2: Installing dependencies...${NC}"
|
||||||
|
npm ci
|
||||||
|
echo -e "${GREEN}✓ Dependencies installed${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 3: Running database migrations...${NC}"
|
||||||
|
# Check if migrations directory exists
|
||||||
|
if [ -d "./migrations" ]; then
|
||||||
|
echo "Applying database migrations..."
|
||||||
|
npm run migrate:up
|
||||||
|
echo -e "${GREEN}✓ Migrations applied successfully${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ No migrations directory found, running database initialization script...${NC}"
|
||||||
|
npm run init:db
|
||||||
|
echo -e "${GREEN}✓ Database initialized${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 4: Building the project...${NC}"
|
||||||
|
npm run build
|
||||||
|
echo -e "${GREEN}✓ Project built successfully${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 5: Checking for .env file...${NC}"
|
||||||
|
if [ -f .env ]; then
|
||||||
|
echo -e "${GREEN}✓ .env file exists${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ .env file not found${NC}"
|
||||||
|
if [ -f .env.example ]; then
|
||||||
|
echo "Creating .env from .env.example"
|
||||||
|
cp .env.example .env
|
||||||
|
echo -e "${YELLOW}⚠ Please update the .env file with your configuration!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: .env.example file not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}Step 6: Checking for services...${NC}"
|
||||||
|
# Check if Docker is being used
|
||||||
|
if [ -f docker-compose.yml ]; then
|
||||||
|
echo "Docker Compose configuration found"
|
||||||
|
echo "You might want to restart containers with: docker-compose down && docker-compose up -d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for PM2 process on Ubuntu
|
||||||
|
if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then
|
||||||
|
echo -e "\n${BLUE}Step 7: Checking PM2 service...${NC}"
|
||||||
|
if pm2 list | grep -q "tg_tinder_bot"; then
|
||||||
|
echo "PM2 service for tg_tinder_bot found"
|
||||||
|
if [ "$RESTART_SERVICE" = true ]; then
|
||||||
|
echo "Restarting PM2 service..."
|
||||||
|
pm2 restart tg_tinder_bot
|
||||||
|
echo -e "${GREEN}✓ PM2 service restarted${NC}"
|
||||||
|
else
|
||||||
|
echo "To restart the service, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}========================================${NC}"
|
||||||
|
echo -e "${GREEN} Update completed successfully! ${NC}"
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
|
||||||
|
if [ "$IS_UBUNTU" = true ] && command -v pm2 &>/dev/null; then
|
||||||
|
echo -e "To start the bot with PM2, run: ${YELLOW}pm2 start dist/bot.js --name tg_tinder_bot${NC}"
|
||||||
|
echo -e "To restart the bot, run: ${YELLOW}pm2 restart tg_tinder_bot${NC}"
|
||||||
|
echo -e "To view logs, run: ${YELLOW}pm2 logs tg_tinder_bot${NC}"
|
||||||
|
else
|
||||||
|
echo -e "To start the bot, run: ${YELLOW}npm run start${NC}"
|
||||||
|
echo -e "For development mode: ${YELLOW}npm run dev${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return to the original directory
|
||||||
|
cd "$CURRENT_DIR"
|
||||||
14
check_schema.ts
Normal file
14
check_schema.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { query } from './src/database/connection';
|
||||||
|
|
||||||
|
async function checkSchema() {
|
||||||
|
try {
|
||||||
|
const result = await query('SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1', ['messages']);
|
||||||
|
console.log(result.rows);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSchema();
|
||||||
7
database.json
Normal file
7
database.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"connectionString": {
|
||||||
|
"ENV": "DATABASE_URL"
|
||||||
|
},
|
||||||
|
"migrationsTable": "pgmigrations",
|
||||||
|
"migrationsDirectory": "./migrations"
|
||||||
|
}
|
||||||
221
docs/DEPLOY_UBUNTU.md
Normal file
221
docs/DEPLOY_UBUNTU.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Деплой Telegram Tinder Bot на Ubuntu 24.04
|
||||||
|
|
||||||
|
Это руководство поможет вам настроить и развернуть Telegram Tinder Bot на сервере с Ubuntu 24.04.
|
||||||
|
|
||||||
|
## Предварительные требования
|
||||||
|
|
||||||
|
- Сервер с Ubuntu 24.04
|
||||||
|
- Права администратора (sudo)
|
||||||
|
- Доменное имя (опционально, для SSL)
|
||||||
|
|
||||||
|
## Шаг 1: Подготовка сервера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обновите систему
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
# Установите необходимые пакеты
|
||||||
|
sudo apt install -y curl wget git build-essential postgresql postgresql-contrib nginx
|
||||||
|
|
||||||
|
# Установите Node.js и npm
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
sudo apt install -y nodejs
|
||||||
|
|
||||||
|
# Проверьте установку
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 2: Настройка PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустите и включите PostgreSQL
|
||||||
|
sudo systemctl start postgresql
|
||||||
|
sudo systemctl enable postgresql
|
||||||
|
|
||||||
|
# Подключитесь к PostgreSQL
|
||||||
|
sudo -u postgres psql
|
||||||
|
|
||||||
|
# В консоли PostgreSQL создайте базу данных и пользователя
|
||||||
|
CREATE DATABASE tg_tinder_bot;
|
||||||
|
CREATE USER tg_bot WITH PASSWORD 'сложный_пароль';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE tg_tinder_bot TO tg_bot;
|
||||||
|
\q
|
||||||
|
|
||||||
|
# Проверьте подключение
|
||||||
|
psql -h localhost -U tg_bot -d tg_tinder_bot
|
||||||
|
# Введите пароль, когда будет запрошено
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 3: Клонирование репозитория и установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте директорию для бота
|
||||||
|
sudo mkdir -p /opt/tg_tinder_bot
|
||||||
|
sudo chown $USER:$USER /opt/tg_tinder_bot
|
||||||
|
|
||||||
|
# Клонируйте репозиторий
|
||||||
|
git clone https://your-git-repo-url.git /opt/tg_tinder_bot
|
||||||
|
cd /opt/tg_tinder_bot
|
||||||
|
|
||||||
|
# Установите зависимости
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# Сделайте скрипты исполняемыми
|
||||||
|
chmod +x bin/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 4: Настройка окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте файл .env из примера
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Отредактируйте файл .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Укажите следующие параметры:
|
||||||
|
# BOT_TOKEN=your_telegram_bot_token
|
||||||
|
# DB_HOST=localhost
|
||||||
|
# DB_PORT=5432
|
||||||
|
# DB_USER=tg_bot
|
||||||
|
# DB_PASSWORD=сложный_пароль
|
||||||
|
# DB_NAME=tg_tinder_bot
|
||||||
|
# и другие необходимые параметры
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 5: Инициализация базы данных и сборка проекта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустите миграции
|
||||||
|
npm run migrate:up
|
||||||
|
|
||||||
|
# Соберите проект
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 6: Настройка PM2 для управления процессами
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите PM2 глобально
|
||||||
|
sudo npm install -g pm2
|
||||||
|
|
||||||
|
# Запустите бота через PM2
|
||||||
|
pm2 start dist/bot.js --name tg_tinder_bot
|
||||||
|
|
||||||
|
# Настройте автозапуск PM2
|
||||||
|
pm2 startup
|
||||||
|
# Выполните команду, которую выдаст предыдущая команда
|
||||||
|
|
||||||
|
# Сохраните конфигурацию PM2
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 7: Настройка Nginx (если нужен веб-интерфейс)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте конфигурационный файл Nginx
|
||||||
|
sudo nano /etc/nginx/sites-available/tg_tinder_bot
|
||||||
|
|
||||||
|
# Добавьте следующее содержимое
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name ваш-домен.com;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://localhost:3000; # Замените на порт вашего веб-интерфейса
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection 'upgrade';
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Создайте символьную ссылку
|
||||||
|
sudo ln -s /etc/nginx/sites-available/tg_tinder_bot /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# Проверьте конфигурацию Nginx
|
||||||
|
sudo nginx -t
|
||||||
|
|
||||||
|
# Перезапустите Nginx
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 8: Настройка SSL с Certbot (опционально, но рекомендуется)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите Certbot
|
||||||
|
sudo apt install -y certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# Получите SSL-сертификат
|
||||||
|
sudo certbot --nginx -d ваш-домен.com
|
||||||
|
|
||||||
|
# Certbot автоматически обновит конфигурацию Nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 9: Настройка автоматического обновления
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Отредактируйте crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Добавьте строку для ежедневного обновления в 4:00
|
||||||
|
0 4 * * * cd /opt/tg_tinder_bot && ./bin/update.sh >> /var/log/tg_bot_update.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Управление ботом
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Перезапустить бота
|
||||||
|
pm2 restart tg_tinder_bot
|
||||||
|
|
||||||
|
# Остановить бота
|
||||||
|
pm2 stop tg_tinder_bot
|
||||||
|
|
||||||
|
# Посмотреть логи
|
||||||
|
pm2 logs tg_tinder_bot
|
||||||
|
|
||||||
|
# Посмотреть статус
|
||||||
|
pm2 status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обновление вручную
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/tg_tinder_bot
|
||||||
|
./bin/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Резервное копирование базы данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте директорию для резервных копий
|
||||||
|
mkdir -p ~/backups
|
||||||
|
|
||||||
|
# Создайте резервную копию
|
||||||
|
pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Автоматическое резервное копирование (добавьте в crontab)
|
||||||
|
# 0 3 * * * pg_dump -U tg_bot tg_tinder_bot > ~/backups/tg_tinder_bot_$(date +%Y%m%d).sql && find ~/backups -name "tg_tinder_bot_*.sql" -mtime +7 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Решение проблем
|
||||||
|
|
||||||
|
### Проблемы с базой данных
|
||||||
|
Проверьте журналы PostgreSQL:
|
||||||
|
```bash
|
||||||
|
sudo tail -f /var/log/postgresql/postgresql-*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с ботом
|
||||||
|
Проверьте журналы PM2:
|
||||||
|
```bash
|
||||||
|
pm2 logs tg_tinder_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с Nginx
|
||||||
|
Проверьте журналы Nginx:
|
||||||
|
```bash
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
160
docs/LOCALIZATION.md
Normal file
160
docs/LOCALIZATION.md
Normal 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
105
docs/VIP_FUNCTIONS.md
Normal 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 функции проверяют премиум статус
|
||||||
|
- Автоматическое удаление истёкшего премиум
|
||||||
|
- Валидация всех входных данных
|
||||||
|
- Проверка существования пользователей перед операциями
|
||||||
152
migrations/1758144488937_initial-schema.js
Normal file
152
migrations/1758144488937_initial-schema.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
// Создание расширения для генерации UUID
|
||||||
|
pgm.createExtension('pgcrypto', { ifNotExists: true });
|
||||||
|
|
||||||
|
// Таблица пользователей
|
||||||
|
pgm.createTable('users', {
|
||||||
|
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||||
|
telegram_id: { type: 'bigint', notNull: true, unique: true },
|
||||||
|
username: { type: 'varchar(255)' },
|
||||||
|
first_name: { type: 'varchar(255)' },
|
||||||
|
last_name: { type: 'varchar(255)' },
|
||||||
|
language_code: { type: 'varchar(10)', default: 'en' },
|
||||||
|
is_active: { type: 'boolean', default: true },
|
||||||
|
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
last_active_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
updated_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
premium: { type: 'boolean', default: false },
|
||||||
|
premium_expires_at: { type: 'timestamp' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Таблица профилей
|
||||||
|
pgm.createTable('profiles', {
|
||||||
|
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||||
|
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
name: { type: 'varchar(255)', notNull: true },
|
||||||
|
age: {
|
||||||
|
type: 'integer',
|
||||||
|
notNull: true,
|
||||||
|
check: 'age >= 18 AND age <= 100'
|
||||||
|
},
|
||||||
|
gender: {
|
||||||
|
type: 'varchar(10)',
|
||||||
|
notNull: true,
|
||||||
|
check: "gender IN ('male', 'female', 'other')"
|
||||||
|
},
|
||||||
|
interested_in: {
|
||||||
|
type: 'varchar(10)',
|
||||||
|
notNull: true,
|
||||||
|
check: "interested_in IN ('male', 'female', 'both')"
|
||||||
|
},
|
||||||
|
looking_for: {
|
||||||
|
type: 'varchar(20)',
|
||||||
|
default: 'both',
|
||||||
|
check: "looking_for IN ('male', 'female', 'both')"
|
||||||
|
},
|
||||||
|
bio: { type: 'text' },
|
||||||
|
photos: { type: 'jsonb', default: '[]' },
|
||||||
|
interests: { type: 'jsonb', default: '[]' },
|
||||||
|
city: { type: 'varchar(255)' },
|
||||||
|
education: { type: 'varchar(255)' },
|
||||||
|
job: { type: 'varchar(255)' },
|
||||||
|
height: { type: 'integer' },
|
||||||
|
religion: { type: 'varchar(255)' },
|
||||||
|
dating_goal: { type: 'varchar(255)' },
|
||||||
|
smoking: { type: 'boolean' },
|
||||||
|
drinking: { type: 'boolean' },
|
||||||
|
has_kids: { type: 'boolean' },
|
||||||
|
location_lat: { type: 'decimal(10,8)' },
|
||||||
|
location_lon: { type: 'decimal(11,8)' },
|
||||||
|
search_min_age: { type: 'integer', default: 18 },
|
||||||
|
search_max_age: { type: 'integer', default: 50 },
|
||||||
|
search_max_distance: { type: 'integer', default: 50 },
|
||||||
|
is_verified: { type: 'boolean', default: false },
|
||||||
|
is_visible: { type: 'boolean', default: true },
|
||||||
|
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
updated_at: { type: 'timestamp', default: pgm.func('NOW()') }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Таблица свайпов
|
||||||
|
pgm.createTable('swipes', {
|
||||||
|
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||||
|
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
target_user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
type: {
|
||||||
|
type: 'varchar(20)',
|
||||||
|
notNull: true,
|
||||||
|
check: "type IN ('like', 'pass', 'superlike')"
|
||||||
|
},
|
||||||
|
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
is_match: { type: 'boolean', default: false }
|
||||||
|
});
|
||||||
|
pgm.addConstraint('swipes', 'unique_swipe', {
|
||||||
|
unique: ['user_id', 'target_user_id']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Таблица матчей
|
||||||
|
pgm.createTable('matches', {
|
||||||
|
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||||
|
user_id_1: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
user_id_2: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
last_message_at: { type: 'timestamp' },
|
||||||
|
is_active: { type: 'boolean', default: true },
|
||||||
|
is_super_match: { type: 'boolean', default: false },
|
||||||
|
unread_count_1: { type: 'integer', default: 0 },
|
||||||
|
unread_count_2: { type: 'integer', default: 0 }
|
||||||
|
});
|
||||||
|
pgm.addConstraint('matches', 'unique_match', {
|
||||||
|
unique: ['user_id_1', 'user_id_2']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Таблица сообщений
|
||||||
|
pgm.createTable('messages', {
|
||||||
|
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||||
|
match_id: { type: 'uuid', references: 'matches(id)', onDelete: 'CASCADE' },
|
||||||
|
sender_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
receiver_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||||
|
content: { type: 'text', notNull: true },
|
||||||
|
message_type: {
|
||||||
|
type: 'varchar(20)',
|
||||||
|
default: 'text',
|
||||||
|
check: "message_type IN ('text', 'photo', 'gif', 'sticker')"
|
||||||
|
},
|
||||||
|
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||||
|
is_read: { type: 'boolean', default: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание индексов
|
||||||
|
pgm.createIndex('users', 'telegram_id');
|
||||||
|
pgm.createIndex('profiles', 'user_id');
|
||||||
|
pgm.createIndex('profiles', ['location_lat', 'location_lon'], {
|
||||||
|
where: 'location_lat IS NOT NULL AND location_lon IS NOT NULL'
|
||||||
|
});
|
||||||
|
pgm.createIndex('profiles', ['age', 'gender', 'interested_in']);
|
||||||
|
pgm.createIndex('swipes', ['user_id', 'target_user_id']);
|
||||||
|
pgm.createIndex('matches', ['user_id_1', 'user_id_2']);
|
||||||
|
pgm.createIndex('messages', ['match_id', 'created_at']);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.dropTable('messages');
|
||||||
|
pgm.dropTable('matches');
|
||||||
|
pgm.dropTable('swipes');
|
||||||
|
pgm.dropTable('profiles');
|
||||||
|
pgm.dropTable('users');
|
||||||
|
pgm.dropExtension('pgcrypto');
|
||||||
|
};
|
||||||
25
migrations/1758144618548_add-missing-profile-columns.js
Normal file
25
migrations/1758144618548_add-missing-profile-columns.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
// Добавляем колонки, которые могли быть пропущены в схеме
|
||||||
|
pgm.addColumns('profiles', {
|
||||||
|
hobbies: { type: 'text' }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.dropColumns('profiles', ['hobbies']);
|
||||||
|
};
|
||||||
18
migrations/1758147898012_add-missing-religion-columns.js
Normal file
18
migrations/1758147898012_add-missing-religion-columns.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {};
|
||||||
29
migrations/1758147903378_add-missing-religion-columns.js
Normal file
29
migrations/1758147903378_add-missing-religion-columns.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
// Добавляем отсутствующие колонки в таблицу profiles
|
||||||
|
pgm.addColumns('profiles', {
|
||||||
|
religion: { type: 'varchar(255)' },
|
||||||
|
dating_goal: { type: 'varchar(255)' },
|
||||||
|
smoking: { type: 'boolean' },
|
||||||
|
drinking: { type: 'boolean' },
|
||||||
|
has_kids: { type: 'boolean' }
|
||||||
|
}, { ifNotExists: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.dropColumns('profiles', ['religion', 'dating_goal', 'smoking', 'drinking', 'has_kids'], { ifExists: true });
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
// Изменяем тип столбцов с boolean на varchar для хранения строковых значений
|
||||||
|
pgm.alterColumn('profiles', 'smoking', {
|
||||||
|
type: 'varchar(50)',
|
||||||
|
using: 'smoking::text'
|
||||||
|
});
|
||||||
|
|
||||||
|
pgm.alterColumn('profiles', 'drinking', {
|
||||||
|
type: 'varchar(50)',
|
||||||
|
using: 'drinking::text'
|
||||||
|
});
|
||||||
|
|
||||||
|
// has_kids оставляем boolean, так как у него всего два состояния
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {
|
||||||
|
// Возвращаем столбцы к типу boolean
|
||||||
|
pgm.alterColumn('profiles', 'smoking', {
|
||||||
|
type: 'boolean',
|
||||||
|
using: "CASE WHEN smoking = 'regularly' OR smoking = 'sometimes' THEN true ELSE false END"
|
||||||
|
});
|
||||||
|
|
||||||
|
pgm.alterColumn('profiles', 'drinking', {
|
||||||
|
type: 'boolean',
|
||||||
|
using: "CASE WHEN drinking = 'regularly' OR drinking = 'sometimes' THEN true ELSE false END"
|
||||||
|
});
|
||||||
|
};
|
||||||
50
migrations/1758149087361_add-column-synonyms.js
Normal file
50
migrations/1758149087361_add-column-synonyms.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||||
|
*/
|
||||||
|
export const shorthands = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const up = (pgm) => {
|
||||||
|
// Создание представления для совместимости со старым кодом (swipes)
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE OR REPLACE VIEW swipes_view AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id AS swiper_id,
|
||||||
|
target_user_id AS swiped_id,
|
||||||
|
type AS direction,
|
||||||
|
created_at,
|
||||||
|
is_match
|
||||||
|
FROM swipes;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Создание представления для совместимости со старым кодом (matches)
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE OR REPLACE VIEW matches_view AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id_1 AS user1_id,
|
||||||
|
user_id_2 AS user2_id,
|
||||||
|
created_at AS matched_at,
|
||||||
|
is_active AS status,
|
||||||
|
last_message_at,
|
||||||
|
is_super_match,
|
||||||
|
unread_count_1,
|
||||||
|
unread_count_2
|
||||||
|
FROM matches;
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||||
|
* @param run {() => void | undefined}
|
||||||
|
* @returns {Promise<void> | void}
|
||||||
|
*/
|
||||||
|
export const down = (pgm) => {
|
||||||
|
pgm.sql(`DROP VIEW IF EXISTS swipes_view;`);
|
||||||
|
pgm.sql(`DROP VIEW IF EXISTS matches_view;`);
|
||||||
|
};
|
||||||
394
package-lock.json
generated
394
package-lock.json
generated
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
43
scripts/add-hobbies-column.js
Normal file
43
scripts/add-hobbies-column.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// add-hobbies-column.js
|
||||||
|
// Скрипт для добавления колонки hobbies в таблицу profiles
|
||||||
|
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Настройки подключения к базе данных
|
||||||
|
const pool = new Pool({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
database: 'telegram_tinder_bot',
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addHobbiesColumn() {
|
||||||
|
try {
|
||||||
|
console.log('Подключение к базе данных...');
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
console.log('Добавление колонки hobbies в таблицу profiles...');
|
||||||
|
|
||||||
|
// SQL запрос для добавления колонки
|
||||||
|
const sql = `
|
||||||
|
ALTER TABLE profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS hobbies TEXT;
|
||||||
|
`;
|
||||||
|
|
||||||
|
await client.query(sql);
|
||||||
|
console.log('✅ Колонка hobbies успешно добавлена в таблицу profiles');
|
||||||
|
|
||||||
|
// Закрытие соединения
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
console.log('Подключение к базе данных закрыто');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при добавлении колонки:', error);
|
||||||
|
await pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск функции
|
||||||
|
addHobbiesColumn();
|
||||||
44
scripts/add-premium-columns-direct.js
Normal file
44
scripts/add-premium-columns-direct.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// add-premium-columns.js
|
||||||
|
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
|
||||||
|
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Настройки подключения к базе данных - используем те же настройки, что и раньше
|
||||||
|
const pool = new Pool({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
database: 'telegram_tinder_bot',
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addPremiumColumns() {
|
||||||
|
try {
|
||||||
|
console.log('Подключение к базе данных...');
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
console.log('Добавление колонок premium и premium_expires_at в таблицу users...');
|
||||||
|
|
||||||
|
// SQL запрос для добавления колонок
|
||||||
|
const sql = `
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||||
|
`;
|
||||||
|
|
||||||
|
await client.query(sql);
|
||||||
|
console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users');
|
||||||
|
|
||||||
|
// Закрытие соединения
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
console.log('Подключение к базе данных закрыто');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при добавлении колонок:', error);
|
||||||
|
await pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск функции
|
||||||
|
addPremiumColumns();
|
||||||
40
scripts/add-premium-columns.js
Normal file
40
scripts/add-premium-columns.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// add-premium-columns.js
|
||||||
|
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
|
||||||
|
|
||||||
|
const { Client } = require('pg');
|
||||||
|
|
||||||
|
// Настройки подключения к базе данных
|
||||||
|
const client = new Client({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!',
|
||||||
|
database: 'telegram_tinder_bot'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addPremiumColumns() {
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log('Подключение к базе данных успешно установлено');
|
||||||
|
|
||||||
|
// SQL запрос для добавления колонок
|
||||||
|
const sql = `
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||||
|
`;
|
||||||
|
|
||||||
|
await client.query(sql);
|
||||||
|
console.log('Колонки premium и premium_expires_at успешно добавлены в таблицу users');
|
||||||
|
|
||||||
|
// Закрываем подключение
|
||||||
|
await client.end();
|
||||||
|
console.log('Подключение к базе данных закрыто');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при добавлении колонок:', error);
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск функции
|
||||||
|
addPremiumColumns();
|
||||||
28
scripts/add-premium-columns.ts
Normal file
28
scripts/add-premium-columns.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// add-premium-columns.ts
|
||||||
|
// Скрипт для добавления колонок premium и premium_expires_at в таблицу users
|
||||||
|
|
||||||
|
import { query } from '../src/database/connection';
|
||||||
|
|
||||||
|
async function addPremiumColumns() {
|
||||||
|
try {
|
||||||
|
console.log('Добавление колонок premium и premium_expires_at в таблицу users...');
|
||||||
|
|
||||||
|
// SQL запрос для добавления колонок
|
||||||
|
const sql = `
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||||
|
`;
|
||||||
|
|
||||||
|
await query(sql);
|
||||||
|
console.log('✅ Колонки premium и premium_expires_at успешно добавлены в таблицу users');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при добавлении колонок:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск функции
|
||||||
|
addPremiumColumns();
|
||||||
101
scripts/create_profile_fix.js
Normal file
101
scripts/create_profile_fix.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Исправленный код для создания профиля
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
// Получаем аргументы из командной строки
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const telegramId = args[0];
|
||||||
|
const name = args[1];
|
||||||
|
const age = parseInt(args[2]);
|
||||||
|
const gender = args[3];
|
||||||
|
const city = args[4];
|
||||||
|
const bio = args[5];
|
||||||
|
const photoFileId = args[6];
|
||||||
|
|
||||||
|
// Проверяем, что все необходимые аргументы предоставлены
|
||||||
|
if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) {
|
||||||
|
console.error('Необходимо указать все параметры: telegramId, name, age, gender, city, bio, photoFileId');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем соединение с базой данных
|
||||||
|
const client = new Client({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!',
|
||||||
|
database: 'telegram_tinder_bot'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createProfile() {
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Шаг 1: Создаем или обновляем пользователя
|
||||||
|
const userResult = await client.query(`
|
||||||
|
INSERT INTO users (telegram_id, username, first_name, last_name)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (telegram_id) DO UPDATE SET
|
||||||
|
username = EXCLUDED.username,
|
||||||
|
first_name = EXCLUDED.first_name,
|
||||||
|
last_name = EXCLUDED.last_name
|
||||||
|
RETURNING id
|
||||||
|
`, [parseInt(telegramId), null, name, null]);
|
||||||
|
|
||||||
|
const userId = userResult.rows[0].id;
|
||||||
|
|
||||||
|
// Шаг 2: Создаем профиль
|
||||||
|
const profileId = uuidv4();
|
||||||
|
const now = new Date();
|
||||||
|
const interestedIn = gender === 'male' ? 'female' : 'male';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
'id', 'user_id', 'name', 'age', 'gender', 'interested_in',
|
||||||
|
'bio', 'city', 'photos', 'is_verified',
|
||||||
|
'is_visible', 'created_at', 'updated_at'
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
profileId, userId, name, age, gender, interestedIn,
|
||||||
|
bio, city, JSON.stringify([photoFileId]),
|
||||||
|
false, true, now, now
|
||||||
|
];
|
||||||
|
|
||||||
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO profiles (${columns})
|
||||||
|
VALUES (${placeholders})
|
||||||
|
`, values);
|
||||||
|
|
||||||
|
console.log('Профиль успешно создан!');
|
||||||
|
|
||||||
|
// Возвращаем информацию о созданном профиле
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
profileId,
|
||||||
|
name,
|
||||||
|
age,
|
||||||
|
gender,
|
||||||
|
interestedIn,
|
||||||
|
bio,
|
||||||
|
city,
|
||||||
|
photos: [photoFileId]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при создании профиля:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createProfile()
|
||||||
|
.then(profile => {
|
||||||
|
console.log('Созданный профиль:', profile);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Создание профиля не удалось:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
62
scripts/migrate-sync.js
Normal file
62
scripts/migrate-sync.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// migrate-sync.js
|
||||||
|
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
|
||||||
|
// Используется для синхронизации существующей базы с миграциями
|
||||||
|
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Подключение к базе данных
|
||||||
|
const client = new Client({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
database: 'telegram_tinder_bot',
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function syncMigrations() {
|
||||||
|
try {
|
||||||
|
console.log('Подключение к базе данных...');
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Создаем таблицу миграций, если её нет
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pgmigrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Получаем список файлов миграций
|
||||||
|
const migrationsDir = path.join(__dirname, '../migrations');
|
||||||
|
const files = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(file => file.endsWith('.js'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Проверяем, какие миграции уже записаны
|
||||||
|
const { rows: existingMigrations } = await client.query('SELECT name FROM pgmigrations');
|
||||||
|
const existingNames = existingMigrations.map(m => m.name);
|
||||||
|
|
||||||
|
// Записываем новые миграции
|
||||||
|
for (const file of files) {
|
||||||
|
const migrationName = file.replace('.js', '');
|
||||||
|
if (!existingNames.includes(migrationName)) {
|
||||||
|
console.log(`Добавление записи о миграции: ${migrationName}`);
|
||||||
|
await client.query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]);
|
||||||
|
} else {
|
||||||
|
console.log(`Миграция ${migrationName} уже записана`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Синхронизация миграций завершена успешно');
|
||||||
|
await client.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при синхронизации миграций:', error);
|
||||||
|
await client.end();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMigrations();
|
||||||
BIN
set-premium.js
Normal file
BIN
set-premium.js
Normal file
Binary file not shown.
2
sql/add_looking_for.sql
Normal file
2
sql/add_looking_for.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Добавление колонки looking_for в таблицу profiles
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both'));
|
||||||
7
sql/add_missing_columns.sql
Normal file
7
sql/add_missing_columns.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Добавление недостающих колонок в таблицу profiles
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS hobbies TEXT[];
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS religion VARCHAR(255);
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS dating_goal VARCHAR(50);
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS smoking VARCHAR(20) CHECK (smoking IS NULL OR smoking IN ('never', 'sometimes', 'regularly'));
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS drinking VARCHAR(20) CHECK (drinking IS NULL OR drinking IN ('never', 'sometimes', 'regularly'));
|
||||||
|
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS has_kids BOOLEAN DEFAULT FALSE;
|
||||||
4
sql/add_premium_columns.sql
Normal file
4
sql/add_premium_columns.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Добавление колонок premium и premium_expires_at в таблицу users
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||||
21
sql/add_updated_at.sql
Normal file
21
sql/add_updated_at.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-- Добавление колонки updated_at в таблицу users
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- Обновление триггера для автоматического обновления updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Проверка и создание триггера, если он не существует
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'users_updated_at') THEN
|
||||||
|
CREATE TRIGGER users_updated_at
|
||||||
|
BEFORE UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
67
sql/recreate_tables.sql
Normal file
67
sql/recreate_tables.sql
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
-- Сначала создаем таблицы заново
|
||||||
|
DROP TABLE IF EXISTS profiles CASCADE;
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
telegram_id BIGINT UNIQUE NOT NULL,
|
||||||
|
username VARCHAR(255),
|
||||||
|
first_name VARCHAR(255),
|
||||||
|
last_name VARCHAR(255),
|
||||||
|
language_code VARCHAR(10) DEFAULT 'en',
|
||||||
|
is_premium BOOLEAN DEFAULT FALSE,
|
||||||
|
is_blocked BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Profiles table
|
||||||
|
CREATE TABLE IF NOT EXISTS profiles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
age INTEGER NOT NULL CHECK (age >= 18 AND age <= 100),
|
||||||
|
gender VARCHAR(20) NOT NULL CHECK (gender IN ('male', 'female', 'other')),
|
||||||
|
looking_for VARCHAR(20) NOT NULL CHECK (looking_for IN ('male', 'female', 'both')),
|
||||||
|
bio TEXT,
|
||||||
|
location VARCHAR(255),
|
||||||
|
latitude DECIMAL(10, 8),
|
||||||
|
longitude DECIMAL(11, 8),
|
||||||
|
photos TEXT[], -- Array of photo URLs/file IDs
|
||||||
|
interests TEXT[], -- Array of interests
|
||||||
|
hobbies TEXT[],
|
||||||
|
education VARCHAR(255),
|
||||||
|
occupation VARCHAR(255),
|
||||||
|
height INTEGER, -- in cm
|
||||||
|
religion VARCHAR(255),
|
||||||
|
dating_goal VARCHAR(50),
|
||||||
|
smoking VARCHAR(20) CHECK (smoking IN ('never', 'sometimes', 'regularly')),
|
||||||
|
drinking VARCHAR(20) CHECK (drinking IN ('never', 'sometimes', 'regularly')),
|
||||||
|
has_kids BOOLEAN DEFAULT FALSE,
|
||||||
|
relationship_type VARCHAR(30) CHECK (relationship_type IN ('casual', 'serious', 'friendship', 'anything')),
|
||||||
|
verification_status VARCHAR(20) DEFAULT 'unverified' CHECK (verification_status IN ('unverified', 'pending', 'verified')),
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
is_visible BOOLEAN DEFAULT TRUE,
|
||||||
|
last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создаем тригеры для автоматического обновления updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER users_updated_at
|
||||||
|
BEFORE UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER profiles_updated_at
|
||||||
|
BEFORE UPDATE ON profiles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||||
@@ -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) {
|
||||||
|
|||||||
212
src/controllers/translationController.ts
Normal file
212
src/controllers/translationController.ts
Normal 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;
|
||||||
291
src/controllers/vipController.ts
Normal file
291
src/controllers/vipController.ts
Normal 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] || 'Не указано';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
14
src/database/migrations/add_language_support.sql
Normal file
14
src/database/migrations/add_language_support.sql
Normal 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;
|
||||||
10
src/database/migrations/add_premium_field.sql
Normal file
10
src/database/migrations/add_premium_field.sql
Normal 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 статуса';
|
||||||
@@ -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 = {};
|
||||||
|
|
||||||
await this.profileService.updateProfile(profile.userId, {
|
switch (type) {
|
||||||
lifestyle: lifestyle
|
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, updates);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
let keyboardRows = [
|
||||||
[
|
[
|
||||||
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
||||||
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
|
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ text: '💕 Мои матчи', callback_data: 'view_matches' },
|
{ text: '💕 Мои матчи', callback_data: 'view_matches' }
|
||||||
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
101
src/locales/de.json
Normal 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
113
src/locales/en.json
Normal 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
94
src/locales/en_fixed.json
Normal 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
101
src/locales/es.json
Normal 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
94
src/locales/es_fixed.json
Normal 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
101
src/locales/fr.json
Normal 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
101
src/locales/it.json
Normal 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
101
src/locales/ja.json
Normal 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
152
src/locales/kk.json
Normal 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
101
src/locales/ko.json
Normal 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
101
src/locales/pt.json
Normal 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
129
src/locales/ru.json
Normal 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
94
src/locales/ru_fixed.json
Normal 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
152
src/locales/uz.json
Normal 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
101
src/locales/zh.json
Normal 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": "服务器错误。请稍后再试。"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
115
src/scripts/cleanDb.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env ts-node
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { testConnection, closePool, query } from '../database/connection';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистка базы данных и пересоздание схемы с нуля
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('🚀 Очистка базы данных...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем подключение
|
||||||
|
const connected = await testConnection();
|
||||||
|
if (!connected) {
|
||||||
|
console.error('❌ Не удалось подключиться к базе данных');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сначала проверяем наличие таблиц
|
||||||
|
const tablesExist = await checkTablesExist();
|
||||||
|
|
||||||
|
if (tablesExist) {
|
||||||
|
console.log('🔍 Таблицы существуют. Выполняем удаление...');
|
||||||
|
|
||||||
|
// Удаляем существующие таблицы в правильном порядке
|
||||||
|
await dropAllTables();
|
||||||
|
console.log('✅ Все таблицы успешно удалены');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Таблицы не обнаружены');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🛠️ База данных очищена успешно');
|
||||||
|
console.log('ℹ️ Теперь вы можете выполнить npm run init:db для создания новой схемы');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при очистке базы данных:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await closePool();
|
||||||
|
console.log('👋 Соединение с базой данных закрыто');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка существования таблиц в базе данных
|
||||||
|
*/
|
||||||
|
async function checkTablesExist(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await query(`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name IN ('users', 'profiles', 'matches')
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
return result.rows[0].exists;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при проверке наличия таблиц:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаление всех таблиц из базы данных
|
||||||
|
*/
|
||||||
|
async function dropAllTables(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Отключаем ограничения внешних ключей для удаления таблиц
|
||||||
|
await query('SET CONSTRAINTS ALL DEFERRED;');
|
||||||
|
|
||||||
|
// Удаляем таблицы в порядке, учитывающем зависимости
|
||||||
|
console.log('Удаление таблицы notifications...');
|
||||||
|
await query('DROP TABLE IF EXISTS notifications CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы scheduled_notifications...');
|
||||||
|
await query('DROP TABLE IF EXISTS scheduled_notifications CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы reports...');
|
||||||
|
await query('DROP TABLE IF EXISTS reports CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы blocks...');
|
||||||
|
await query('DROP TABLE IF EXISTS blocks CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы messages...');
|
||||||
|
await query('DROP TABLE IF EXISTS messages CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы matches...');
|
||||||
|
await query('DROP TABLE IF EXISTS matches CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы swipes...');
|
||||||
|
await query('DROP TABLE IF EXISTS swipes CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы profiles...');
|
||||||
|
await query('DROP TABLE IF EXISTS profiles CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы users...');
|
||||||
|
await query('DROP TABLE IF EXISTS users CASCADE;');
|
||||||
|
|
||||||
|
console.log('Удаление таблицы pgmigrations...');
|
||||||
|
await query('DROP TABLE IF EXISTS pgmigrations CASCADE;');
|
||||||
|
|
||||||
|
// Восстанавливаем ограничения внешних ключей
|
||||||
|
await query('SET CONSTRAINTS ALL IMMEDIATE;');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при удалении таблиц:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск скрипта
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { main as cleanDB };
|
||||||
166
src/scripts/createTestData.ts
Normal file
166
src/scripts/createTestData.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { Pool } from 'pg';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
async function createTestSwipes() {
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
user: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сначала получаем ID всех пользователей
|
||||||
|
const usersResult = await pool.query(`
|
||||||
|
SELECT users.id, telegram_id, first_name, gender
|
||||||
|
FROM users
|
||||||
|
JOIN profiles ON users.id = profiles.user_id
|
||||||
|
`);
|
||||||
|
|
||||||
|
const users = usersResult.rows;
|
||||||
|
console.log('Пользователи в системе:');
|
||||||
|
console.table(users);
|
||||||
|
|
||||||
|
if (users.length < 2) {
|
||||||
|
console.log('Недостаточно пользователей для создания свайпов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем свайпы
|
||||||
|
console.log('Создаем тестовые свайпы...');
|
||||||
|
|
||||||
|
// Сначала проверим ограничения базы данных
|
||||||
|
const constraintsResult = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
constraint_name,
|
||||||
|
table_name,
|
||||||
|
constraint_type
|
||||||
|
FROM
|
||||||
|
information_schema.table_constraints
|
||||||
|
WHERE
|
||||||
|
table_name = 'swipes'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('Ограничения таблицы swipes:');
|
||||||
|
console.table(constraintsResult.rows);
|
||||||
|
|
||||||
|
// Создаем пары пользователей без дублирования
|
||||||
|
const userPairs = [];
|
||||||
|
const swipes = [];
|
||||||
|
|
||||||
|
// Мужчины и женщины для создания пар
|
||||||
|
const maleUsers = users.filter(user => user.gender === 'male');
|
||||||
|
const femaleUsers = users.filter(user => user.gender === 'female');
|
||||||
|
|
||||||
|
console.log(`Мужчин: ${maleUsers.length}, Женщин: ${femaleUsers.length}`);
|
||||||
|
|
||||||
|
for (const male of maleUsers) {
|
||||||
|
for (const female of femaleUsers) {
|
||||||
|
// Мужчина -> Женщина (70% лайк, 20% пропуск, 10% суперлайк)
|
||||||
|
const randomNum1 = Math.random();
|
||||||
|
let maleToFemaleType;
|
||||||
|
|
||||||
|
if (randomNum1 < 0.7) {
|
||||||
|
maleToFemaleType = 'like';
|
||||||
|
} else if (randomNum1 < 0.9) {
|
||||||
|
maleToFemaleType = 'pass';
|
||||||
|
} else {
|
||||||
|
maleToFemaleType = 'superlike';
|
||||||
|
}
|
||||||
|
|
||||||
|
const maleToFemale = {
|
||||||
|
id: uuidv4(),
|
||||||
|
user_id: male.id,
|
||||||
|
target_user_id: female.id,
|
||||||
|
type: maleToFemaleType,
|
||||||
|
is_match: false,
|
||||||
|
created_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Женщина -> Мужчина (80% шанс на лайк, если мужчина лайкнул)
|
||||||
|
if ((maleToFemaleType === 'like' || maleToFemaleType === 'superlike') && Math.random() < 0.8) {
|
||||||
|
const femaleToMale = {
|
||||||
|
id: uuidv4(),
|
||||||
|
user_id: female.id,
|
||||||
|
target_user_id: male.id,
|
||||||
|
type: Math.random() < 0.9 ? 'like' : 'superlike',
|
||||||
|
is_match: true,
|
||||||
|
created_at: new Date(new Date().getTime() + 1000) // На секунду позже
|
||||||
|
};
|
||||||
|
|
||||||
|
swipes.push(femaleToMale);
|
||||||
|
maleToFemale.is_match = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
swipes.push(maleToFemale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Подготовлено ${swipes.length} свайпов для добавления в базу`);
|
||||||
|
|
||||||
|
// Сначала удаляем все существующие свайпы
|
||||||
|
await pool.query('DELETE FROM swipes');
|
||||||
|
console.log('Существующие свайпы удалены');
|
||||||
|
|
||||||
|
// Добавляем новые свайпы
|
||||||
|
for (const swipe of swipes) {
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO swipes (id, user_id, target_user_id, type, is_match, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`, [swipe.id, swipe.user_id, swipe.target_user_id, swipe.type, swipe.is_match, swipe.created_at]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Успешно добавлено ${swipes.length} свайпов`);
|
||||||
|
|
||||||
|
// Создаем матчи для взаимных лайков
|
||||||
|
console.log('Создаем матчи для взаимных лайков...');
|
||||||
|
|
||||||
|
// Сначала удаляем все существующие матчи
|
||||||
|
await pool.query('DELETE FROM matches');
|
||||||
|
console.log('Существующие матчи удалены');
|
||||||
|
|
||||||
|
// Находим пары взаимных лайков для создания матчей
|
||||||
|
const mutualLikesResult = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
s1.user_id as user_id_1,
|
||||||
|
s1.target_user_id as user_id_2,
|
||||||
|
s1.created_at
|
||||||
|
FROM swipes s1
|
||||||
|
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||||||
|
WHERE (s1.type = 'like' OR s1.type = 'superlike')
|
||||||
|
AND (s2.type = 'like' OR s2.type = 'superlike')
|
||||||
|
AND s1.user_id < s2.user_id -- Избегаем дублирования пар
|
||||||
|
`);
|
||||||
|
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
for (const mutualLike of mutualLikesResult.rows) {
|
||||||
|
const match = {
|
||||||
|
id: uuidv4(),
|
||||||
|
user_id_1: mutualLike.user_id_1,
|
||||||
|
user_id_2: mutualLike.user_id_2,
|
||||||
|
created_at: new Date(),
|
||||||
|
is_active: true,
|
||||||
|
is_super_match: Math.random() < 0.2 // 20% шанс быть суперматчем
|
||||||
|
};
|
||||||
|
|
||||||
|
matches.push(match);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`, [match.id, match.user_id_1, match.user_id_2, match.created_at, match.is_active, match.is_super_match]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Успешно создано ${matches.length} матчей`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при создании тестовых данных:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestSwipes();
|
||||||
120
src/scripts/getDatabaseInfo.ts
Normal file
120
src/scripts/getDatabaseInfo.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Pool } from 'pg';
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
async function getDatabaseInfo() {
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
user: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Подключение к базе данных...');
|
||||||
|
|
||||||
|
// Получаем информацию о пользователях
|
||||||
|
console.log('\n=== ПОЛЬЗОВАТЕЛИ ===');
|
||||||
|
const usersResult = await pool.query(`
|
||||||
|
SELECT id, telegram_id, username, first_name, last_name, premium, created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Всего пользователей: ${usersResult.rows.length}`);
|
||||||
|
console.table(usersResult.rows);
|
||||||
|
|
||||||
|
// Получаем информацию о профилях
|
||||||
|
console.log('\n=== ПРОФИЛИ ===');
|
||||||
|
const profilesResult = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
p.user_id,
|
||||||
|
u.telegram_id,
|
||||||
|
u.first_name,
|
||||||
|
p.age,
|
||||||
|
p.gender,
|
||||||
|
p.interested_in as "интересуется",
|
||||||
|
p.bio,
|
||||||
|
p.dating_goal as "цель_знакомства",
|
||||||
|
p.is_visible,
|
||||||
|
p.created_at
|
||||||
|
FROM profiles p
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Всего профилей: ${profilesResult.rows.length}`);
|
||||||
|
console.table(profilesResult.rows);
|
||||||
|
|
||||||
|
// Получаем информацию о свайпах
|
||||||
|
console.log('\n=== СВАЙПЫ ===');
|
||||||
|
|
||||||
|
// Сначала проверим, какие столбцы есть в таблице swipes
|
||||||
|
console.log('Получение структуры таблицы swipes...');
|
||||||
|
const swipesColumns = await pool.query(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'swipes'
|
||||||
|
`);
|
||||||
|
console.log('Структура таблицы swipes:');
|
||||||
|
console.table(swipesColumns.rows);
|
||||||
|
|
||||||
|
// Теперь запросим данные, используя правильные имена столбцов
|
||||||
|
const swipesResult = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.user_id,
|
||||||
|
u1.first_name as "от_кого",
|
||||||
|
s.target_user_id,
|
||||||
|
u2.first_name as "кому",
|
||||||
|
s.type,
|
||||||
|
s.created_at
|
||||||
|
FROM swipes s
|
||||||
|
JOIN users u1 ON s.user_id = u1.id
|
||||||
|
JOIN users u2 ON s.target_user_id = u2.id
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Всего свайпов: ${swipesResult.rows.length}`);
|
||||||
|
console.table(swipesResult.rows);
|
||||||
|
|
||||||
|
// Получаем информацию о матчах
|
||||||
|
console.log('\n=== МАТЧИ ===');
|
||||||
|
|
||||||
|
// Сначала проверим, какие столбцы есть в таблице matches
|
||||||
|
console.log('Получение структуры таблицы matches...');
|
||||||
|
const matchesColumns = await pool.query(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'matches'
|
||||||
|
`);
|
||||||
|
console.log('Структура таблицы matches:');
|
||||||
|
console.table(matchesColumns.rows);
|
||||||
|
|
||||||
|
// Теперь запросим данные, используя правильные имена столбцов
|
||||||
|
const matchesResult = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.user_id_1,
|
||||||
|
u1.first_name as "пользователь_1",
|
||||||
|
m.user_id_2,
|
||||||
|
u2.first_name as "пользователь_2",
|
||||||
|
m.is_active,
|
||||||
|
m.created_at
|
||||||
|
FROM matches m
|
||||||
|
JOIN users u1 ON m.user_id_1 = u1.id
|
||||||
|
JOIN users u2 ON m.user_id_2 = u2.id
|
||||||
|
ORDER BY m.created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Всего матчей: ${matchesResult.rows.length}`);
|
||||||
|
console.table(matchesResult.rows);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении данных:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDatabaseInfo();
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
#!/usr/bin/env ts-node
|
#!/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,26 +19,138 @@ 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();
|
await createAdditionalTables();
|
||||||
console.log('✅ Additional tables created');
|
console.log('✅ Дополнительные таблицы созданы');
|
||||||
|
|
||||||
|
// Создаем таблицу миграций и отмечаем существующие миграции как выполненные
|
||||||
|
await setupMigrations();
|
||||||
|
console.log('✅ Настройка миграций завершена');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие необходимых колонок
|
||||||
|
await ensureRequiredColumns();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка ожидающих миграций
|
||||||
|
*/
|
||||||
|
async function getPendingMigrations(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
// Получаем выполненные миграции
|
||||||
|
const { rows } = await query('SELECT name FROM pgmigrations');
|
||||||
|
const appliedMigrations = rows.map((row: { name: string }) => row.name);
|
||||||
|
|
||||||
|
// Получаем файлы миграций
|
||||||
|
const migrationsDir = path.join(__dirname, '../../migrations');
|
||||||
|
if (!fs.existsSync(migrationsDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(file => file.endsWith('.js'))
|
||||||
|
.map(file => file.replace('.js', ''))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Находим невыполненные миграции
|
||||||
|
return migrationFiles.filter(file => !appliedMigrations.includes(file));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при проверке ожидающих миграций:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настройка системы миграций и отметка существующих миграций как выполненных
|
||||||
|
*/
|
||||||
|
async function setupMigrations(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Создаем таблицу миграций
|
||||||
|
await query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pgmigrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Получаем файлы миграций
|
||||||
|
const migrationsDir = path.join(__dirname, '../../migrations');
|
||||||
|
if (!fs.existsSync(migrationsDir)) {
|
||||||
|
console.log('⚠️ Директория миграций не найдена');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(file => file.endsWith('.js'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Отмечаем существующие миграции как выполненные
|
||||||
|
for (const file of files) {
|
||||||
|
const migrationName = file.replace('.js', '');
|
||||||
|
console.log(`✅ Отмечаем миграцию как выполненную: ${migrationName}`);
|
||||||
|
await query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при настройке миграций:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание дополнительных таблиц для приложения
|
||||||
|
*/
|
||||||
|
async function createAdditionalTables(): Promise<void> {
|
||||||
|
try {
|
||||||
// Таблица для уведомлений
|
// Таблица для уведомлений
|
||||||
await query(`
|
await query(`
|
||||||
CREATE TABLE IF NOT EXISTS notifications (
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
@@ -84,7 +202,7 @@ async function createAdditionalTables() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Добавляем недостающие поля в users
|
// Добавляем настройки уведомлений в users
|
||||||
await query(`
|
await query(`
|
||||||
ALTER TABLE users
|
ALTER TABLE users
|
||||||
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}';
|
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}';
|
||||||
@@ -97,6 +215,49 @@ async function createAdditionalTables() {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status);
|
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
|
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
|
||||||
`);
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при создании дополнительных таблиц:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка наличия всех необходимых колонок
|
||||||
|
*/
|
||||||
|
async function ensureRequiredColumns(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Проверка и добавление колонки updated_at в users
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Проверка и добавление колонок premium и premium_expires_at в users
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Проверка и добавление колонки looking_for в profiles
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both'));
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Проверка и добавление колонки hobbies в profiles
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS hobbies TEXT;
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при проверке необходимых колонок:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запуск скрипта
|
// Запуск скрипта
|
||||||
|
|||||||
42
src/scripts/setPremium.ts
Normal file
42
src/scripts/setPremium.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
|
async function setPremium(): Promise<void> {
|
||||||
|
// Создаем соединение с базой данных используя прямые параметры
|
||||||
|
const pool = new Pool({
|
||||||
|
host: '192.168.0.102',
|
||||||
|
port: 5432,
|
||||||
|
database: 'telegram_tinder_bot',
|
||||||
|
user: 'trevor',
|
||||||
|
password: 'Cl0ud_1985!',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Установка премиума для всех пользователей
|
||||||
|
const result = await pool.query(`
|
||||||
|
UPDATE users
|
||||||
|
SET premium = true,
|
||||||
|
premium_expires_at = NOW() + INTERVAL '1 year'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('Premium set for all users successfully!');
|
||||||
|
console.log(`Updated ${result.rowCount} users`);
|
||||||
|
|
||||||
|
// Закрываем соединение с базой данных
|
||||||
|
await pool.end();
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting premium:', error);
|
||||||
|
|
||||||
|
// Закрываем соединение с базой данных в случае ошибки
|
||||||
|
try {
|
||||||
|
await pool.end();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error closing pool:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPremium();
|
||||||
43
src/scripts/setPremiumDirectConnect.ts
Normal file
43
src/scripts/setPremiumDirectConnect.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Pool } from 'pg';
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
async function setAllUsersToPremium() {
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
user: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Setting premium status for all users...');
|
||||||
|
console.log(`Connecting to database at ${process.env.DB_HOST}:${process.env.DB_PORT}...`);
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
UPDATE users
|
||||||
|
SET premium = true
|
||||||
|
WHERE true
|
||||||
|
RETURNING id, telegram_id, username, first_name, premium
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Successfully set premium status for ${result.rows.length} users:`);
|
||||||
|
console.table(result.rows);
|
||||||
|
|
||||||
|
console.log('All users are now premium!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting premium status:', error);
|
||||||
|
console.error('Please check your database connection settings in .env file.');
|
||||||
|
console.log('Current settings:');
|
||||||
|
console.log(`- DB_HOST: ${process.env.DB_HOST}`);
|
||||||
|
console.log(`- DB_PORT: ${process.env.DB_PORT}`);
|
||||||
|
console.log(`- DB_NAME: ${process.env.DB_NAME}`);
|
||||||
|
console.log(`- DB_USERNAME: ${process.env.DB_USERNAME}`);
|
||||||
|
console.log(`- DB_PASSWORD: ${process.env.DB_PASSWORD ? '********' : 'not set'}`);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAllUsersToPremium();
|
||||||
25
src/scripts/setPremiumForAll.ts
Normal file
25
src/scripts/setPremiumForAll.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { query } from '../database/connection';
|
||||||
|
|
||||||
|
async function setAllUsersToPremium() {
|
||||||
|
try {
|
||||||
|
console.log('Setting premium status for all users...');
|
||||||
|
|
||||||
|
const result = await query(`
|
||||||
|
UPDATE users
|
||||||
|
SET premium = true
|
||||||
|
WHERE true
|
||||||
|
RETURNING id, telegram_id, username, first_name, premium
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Successfully set premium status for ${result.rows.length} users:`);
|
||||||
|
console.table(result.rows);
|
||||||
|
|
||||||
|
console.log('All users are now premium!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting premium status:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAllUsersToPremium();
|
||||||
@@ -24,8 +24,8 @@ export class ChatService {
|
|||||||
SELECT
|
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) {
|
||||||
|
|||||||
171
src/services/deepSeekTranslationService.ts
Normal file
171
src/services/deepSeekTranslationService.ts
Normal 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;
|
||||||
154
src/services/localizationService.ts
Normal file
154
src/services/localizationService.ts
Normal 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;
|
||||||
@@ -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
|
// показываем всех пользователей по очереди (исключая только себя)
|
||||||
|
let excludeCondition = '';
|
||||||
|
|
||||||
|
if (!isNewUser) {
|
||||||
|
excludeCondition = viewedUserIds.length > 0
|
||||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
? `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 = `
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
236
src/services/vipService.ts
Normal 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 уже сегодня!`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user