Compare commits

14 Commits

Author SHA1 Message Date
155e4d3b7b Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2025-09-18 05:21:26 +00:00
713eadc643 pre-deploy commit 2025-09-18 14:19:49 +09:00
5ea3e8c1f3 alpha-test 2025-09-18 13:46:35 +09:00
85027a7747 mainly functional matching 2025-09-18 11:42:18 +09:00
e275a9856b Fix JSON format issues with photos and add multi-photo gallery support 2025-09-18 10:38:29 +09:00
bdd7d0424f mass refactor 2025-09-18 08:31:14 +09:00
856bf3ca2a Merge branch 'main' of ssh://git.smartsoltech.kr:2222/trevor/tg_tinder_bot 2025-09-13 15:26:54 +09:00
e3baa9be63 Удалён файл с неправильным именем, содержащим команду PostgreSQL 2025-09-13 15:26:45 +09:00
a3fb88e91e Удалить sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \d profiles 2025-09-13 06:25:52 +00:00
c5a0593222 Merge pull request 'localization' (#2) from localization into main
Reviewed-on: #2
2025-09-13 06:17:17 +00:00
1eb7d1c9bc localization 2025-09-13 15:16:05 +09:00
e81725e4d5 feat: Complete multilingual interface with 10 languages including Korean
🌍 Added complete translation files:
- 🇪🇸 Spanish (es.json) - Español
- 🇫🇷 French (fr.json) - Français
- 🇩🇪 German (de.json) - Deutsch
- 🇮🇹 Italian (it.json) - Italiano
- 🇵🇹 Portuguese (pt.json) - Português
- 🇨🇳 Chinese (zh.json) - 中文
- 🇯🇵 Japanese (ja.json) - 日本語

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

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

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

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

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

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

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

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

View File

@@ -1,30 +1,58 @@
# Telegram Bot Configuration
# Telegram Tinder Bot Configuration
# Rename this file to .env before starting the application
# === REQUIRED SETTINGS ===
# Telegram Bot Token (Get from @BotFather)
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
# Database Configuration
# For local development (when running the bot directly)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=telegram_tinder_bot
DB_USERNAME=postgres
DB_PASSWORD=your_password_here
# Application Settings
# === APPLICATION SETTINGS ===
# Environment (development, production)
NODE_ENV=development
# Port for health checks
PORT=3000
# Optional: Redis for caching (if using)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# === FILE UPLOAD SETTINGS ===
# Optional: File upload settings
# Path for storing uploaded files
UPLOAD_PATH=./uploads
# Maximum file size for uploads (in bytes, default: 5MB)
MAX_FILE_SIZE=5242880
# Optional: External services
GOOGLE_MAPS_API_KEY=your_google_maps_key
CLOUDINARY_URL=your_cloudinary_url
# === LOGGING ===
# Security
# Log level (error, warn, info, debug)
LOG_LEVEL=info
# Path for storing log files
LOG_PATH=./logs
# === SECURITY ===
# Secret key for JWT tokens
JWT_SECRET=your_jwt_secret_here
# Encryption key for sensitive data
ENCRYPTION_KEY=your_encryption_key_here
# === ADVANCED SETTINGS ===
# Notification check interval in milliseconds (default: 60000 - 1 minute)
NOTIFICATION_CHECK_INTERVAL=60000
# Number of matches to show per page
MATCHES_PER_PAGE=10
# Number of profiles to load at once
PROFILES_BATCH_SIZE=5

68
.env.production Normal file
View File

@@ -0,0 +1,68 @@
# Конфигурация Telegram Tinder Bot для Production
# === НЕОБХОДИМЫЕ НАСТРОЙКИ ===
# Токен Telegram бота (получить у @BotFather)
TELEGRAM_BOT_TOKEN=your_bot_token_here
# Настройки базы данных PostgreSQL
DB_HOST=db
DB_PORT=5432
DB_NAME=telegram_tinder_bot
DB_USERNAME=postgres
DB_PASSWORD=your_secure_password_here
# === НАСТРОЙКИ ПРИЛОЖЕНИЯ ===
# Окружение
NODE_ENV=production
# Порт для проверок работоспособности
PORT=3000
# === НАСТРОЙКИ ЗАГРУЗКИ ФАЙЛОВ ===
# Путь для хранения загруженных файлов
UPLOAD_PATH=./uploads
# Максимальный размер загружаемого файла (в байтах, по умолчанию: 5MB)
MAX_FILE_SIZE=5242880
# Разрешенные типы файлов
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif
# === ЛОГИРОВАНИЕ ===
# Уровень логирования (error, warn, info, debug)
LOG_LEVEL=info
# Путь для хранения лог-файлов
LOG_PATH=./logs
# === БЕЗОПАСНОСТЬ ===
# Секретный ключ для JWT токенов
JWT_SECRET=your_jwt_secret_here
# Ключ шифрования для чувствительных данных
ENCRYPTION_KEY=your_encryption_key_here
# === РАСШИРЕННЫЕ НАСТРОЙКИ ===
# Интервал проверки уведомлений в миллисекундах (по умолчанию: 60000 - 1 минута)
NOTIFICATION_CHECK_INTERVAL=60000
# Количество матчей для отображения на странице
MATCHES_PER_PAGE=10
# Количество профилей для загрузки за один раз
PROFILES_BATCH_SIZE=5
# === НАСТРОЙКИ DOCKER ===
# Имя хоста для доступа извне
EXTERNAL_HOSTNAME=your_domain.com
# Настройки кеширования (Redis, если используется)
CACHE_HOST=redis
CACHE_PORT=6379

View File

@@ -8,11 +8,12 @@ WORKDIR /app
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Install all dependencies (including devDependencies for build)
RUN npm ci && npm cache clean --force
# Copy source code
COPY src/ ./src/
COPY .env.example ./
# Build the application
RUN npm run build
@@ -31,11 +32,19 @@ RUN npm ci --only=production && npm cache clean --force
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/.env.example ./.env.example
# Copy configuration files
COPY config/ ./config/
# Copy database migrations
COPY src/database/migrations/ ./dist/database/migrations/
# Create uploads directory
# Copy locales
COPY src/locales/ ./dist/locales/
# Copy scripts
COPY scripts/startup.sh ./startup.sh
RUN chmod +x ./startup.sh
# Create directories
RUN mkdir -p uploads logs
# Create non-root user for security
@@ -53,7 +62,7 @@ EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" || exit 1
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Start the application
CMD ["node", "dist/bot.js"]
# Start the application with migration script
CMD ["./startup.sh"]

224
README.md
View File

@@ -2,6 +2,71 @@
Полнофункциональный Telegram бот для знакомств в стиле Tinder с инлайн-кнопками и красивым интерфейсом. Пользователи могут создавать профили, просматривать анкеты других пользователей, ставить лайки, получать матчи и общаться друг с другом.
## 🗂️ Структура проекта
```
telegram-tinder-bot/
├── bin/ # Исполняемые скрипты и утилиты
│ ├── start_bot.bat # Скрипт запуска для Windows
│ ├── install_ubuntu.sh # Скрипт установки для Ubuntu
│ ├── update.sh # Скрипт обновления для Linux/macOS
│ ├── update.bat # Скрипт обновления для Windows
│ └── setup.sh # Скрипт настройки окружения
├── docs/ # Документация проекта
│ ├── ARCHITECTURE.md # Архитектура приложения
│ ├── DEPLOYMENT.md # Инструкции по развертыванию
│ ├── DEPLOY_UBUNTU.md # Инструкции по развертыванию на Ubuntu
│ ├── LOCALIZATION.md # Информация о локализации
│ ├── NATIVE_CHAT_SYSTEM.md # Документация по системе чата
│ ├── PROJECT_SUMMARY.md # Общее описание проекта
│ └── VIP_FUNCTIONS.md # Описание премиум функций
├── migrations/ # Миграции базы данных
│ ├── 1758144488937_initial-schema.js # Начальная схема БД
│ └── 1758144618548_add-missing-profile-columns.js # Дополнительные колонки
├── scripts/ # Вспомогательные скрипты
│ ├── add-hobbies-column.js # Скрипт добавления колонки hobbies
│ ├── add-premium-columns.js # Скрипт добавления премиум колонок
│ ├── add-premium-columns.ts # TypeScript версия скрипта
│ ├── create_profile_fix.js # Исправление профилей
│ └── migrate-sync.js # Синхронизация миграций
├── sql/ # SQL скрипты
│ ├── add_looking_for.sql # Добавление колонки looking_for
│ ├── add_missing_columns.sql # Добавление недостающих колонок
│ ├── add_premium_columns.sql # Добавление премиум колонок
│ ├── add_updated_at.sql # Добавление колонки updated_at
│ ├── clear_database.sql # Очистка базы данных
│ └── recreate_tables.sql # Пересоздание таблиц
├── src/ # Исходный код приложения
│ ├── bot.ts # Основной файл бота
│ ├── controllers/ # Контроллеры
│ ├── database/ # Функции для работы с БД
│ ├── handlers/ # Обработчики сообщений и команд
│ ├── locales/ # Локализация
│ ├── models/ # Модели данных
│ ├── scripts/ # Скрипты для запуска
│ │ └── initDb.ts # Инициализация базы данных
│ ├── services/ # Сервисы и бизнес-логика
│ ├── types/ # TypeScript типы
│ └── utils/ # Утилиты и вспомогательные функции
├── tests/ # Тесты
│ └── test-bot.ts # Тестовая версия бота
├── .dockerignore # Игнорируемые Docker файлы
├── .env # Переменные окружения (локальные)
├── .env.example # Пример файла переменных окружения
├── database.json # Конфигурация базы данных
├── docker-compose.yml # Настройка Docker Compose
├── Dockerfile # Docker-образ приложения
├── package.json # Зависимости и скрипты NPM
└── tsconfig.json # Настройки TypeScript
```
## ✨ Функционал
### 🎯 Основные возможности
@@ -74,82 +139,84 @@
[💬 Написать] [👤 Профиль] [🔍 Продолжить поиск]
```
## 🗂️ Структура проекта
```
telegram-tinder-bot/
├── src/
│ ├── bot.ts # Основной файл бота
│ ├── handlers/ # Обработчики событий
│ │ ├── commandHandlers.ts # Команды (/start, /profile, etc.)
│ │ ├── callbackHandlers.ts # Инлайн-кнопки (лайки, просмотр)
│ │ └── messageHandlers.ts # Текстовые сообщения
│ ├── services/ # Бизнес-логика
│ │ ├── profileService.ts # Управление профилями
│ │ ├── matchingService.ts # Алгоритм совпадений
│ │ └── notificationService.ts # Уведомления
│ ├── models/ # Модели данных
│ │ ├── User.ts # Пользователь Telegram
│ │ ├── Profile.ts # Профиль знакомств
│ │ ├── Swipe.ts # Лайки/дислайки
│ │ └── Match.ts # Совпадения
│ └── database/ # База данных
│ ├── connection.ts # Подключение к PostgreSQL
│ └── migrations/init.sql # Создание таблиц
├── config/ # Конфигурация
│ └── default.json # Настройки по умолчанию
├── docker-compose.yml # Docker Compose
├── Dockerfile # Docker контейнер
└── package.json # Зависимости npm
```
## 🚀 Развертывание
## 🚀 Быстрый старт
### 📦 Docker (Рекомендуется)
### 1. Предварительные требования
- Node.js 16+
- PostgreSQL 12+
- Telegram Bot Token (получить у [@BotFather](https://t.me/BotFather))
### 2. Установка
```bash
# Клонировать репозиторий
git clone <repository-url>
cd telegram-tinder-bot
# Настроить переменные окружения
cp .env.example .env
# Отредактируйте .env файл
# Запустить с Docker Compose
docker-compose up -d
# Применить миграции БД
docker-compose exec app npm run db:migrate
```
### 🖥️ Обычная установка
```bash
# Установить зависимости
npm install
# Создать базу данных
createdb telegram_tinder_bot
psql -d telegram_tinder_bot -f src/database/migrations/init.sql
# Запустить бота
# Скомпилировать TypeScript
npm run build
npm start
```
### ☁️ Продакшен
### 3. Настройка базы данных
```bash
# Установить PM2
npm install -g pm2
# Создать базу данных PostgreSQL
createdb telegram_tinder_bot
# Запустить через PM2
pm2 start ecosystem.config.js
# Инициализация базы данных
npm run init:db
```
# Мониторинг
pm2 monit
pm2 logs telegram-tinder-bot
### 4. Запуск бота
```bash
# Запуск на Windows
.\bin\start_bot.bat
# Запуск на Linux/macOS
npm run start
```
## <20> Развертывание на Ubuntu
Для развертывания на Ubuntu 24.04 используйте скрипт установки:
```bash
# Сделать скрипт исполняемым
chmod +x ./bin/install_ubuntu.sh
# Запустить установку
sudo ./bin/install_ubuntu.sh
```
Подробные инструкции по развертыванию на Ubuntu находятся в [docs/DEPLOY_UBUNTU.md](docs/DEPLOY_UBUNTU.md).
## 🔄 Обновление бота
### На Windows:
```bash
# Обновление с ветки main
npm run update:win
# Обновление с определенной ветки
.\bin\update.bat develop
```
### На Linux/macOS:
```bash
# Обновление с ветки main
npm run update
# Обновление с определенной ветки и перезапуском сервиса
./bin/update.sh develop --restart-service
```
## 🔧 Настройка переменных окружения
@@ -201,8 +268,32 @@ npm run dev
- Node.js 16+
- PostgreSQL 12+
- Telegram Bot Token (получить у [@BotFather](https://t.me/BotFather))
- Docker и Docker Compose (опционально)
### 2. Установка
### 2. Установка и запуск
#### С использованием стартовых скриптов (рекомендуется)
```bash
# Клонировать репозиторий
git clone <repository-url>
cd telegram-tinder-bot
# На Windows:
.\start.bat
# На Linux/macOS:
chmod +x start.sh
./start.sh
```
Скрипт автоматически:
- Проверит наличие файла .env и создаст его из шаблона при необходимости
- Предложит выбор между запуском с локальной БД или подключением к внешней
- Настроит все необходимые параметры окружения
- Запустит контейнеры Docker
#### Без Docker
```bash
# Клонировать репозиторий
@@ -212,24 +303,17 @@ cd telegram-tinder-bot
# Установить зависимости
npm install
# Скомпилировать TypeScript
npm run build
```
# Скопировать файл конфигурации
cp .env.example .env
# Отредактируйте файл .env и укажите свой TELEGRAM_BOT_TOKEN
### 3. Настройка базы данных
```bash
# Создать базу данных PostgreSQL
createdb telegram_tinder_bot
# Запустить миграции
psql -d telegram_tinder_bot -f src/database/migrations/init.sql
```
npm run migrate:up
### 4. Запуск бота
```bash
# Компиляция TypeScript
# Скомпилировать TypeScript
npm run build
# Запуск бота

84
bin/README.md Normal file
View File

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

54
bin/backup_db.sh Normal file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# backup_db.sh - Script for backing up the PostgreSQL database
echo "📦 Backing up PostgreSQL database..."
# Default backup directory
BACKUP_DIR="${BACKUP_DIR:-/var/backups/tg_tinder_bot}"
BACKUP_FILENAME="tg_tinder_bot_$(date +%Y%m%d_%H%M%S).sql"
BACKUP_PATH="$BACKUP_DIR/$BACKUP_FILENAME"
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Check if running in docker-compose environment
if [ -f /.dockerenv ] || [ -f /proc/self/cgroup ] && grep -q docker /proc/self/cgroup; then
echo "🐳 Running in Docker environment, using docker-compose exec..."
docker-compose exec -T db pg_dump -U postgres telegram_tinder_bot > "$BACKUP_PATH"
else
# Check if PGPASSWORD is set in environment
if [ -z "$PGPASSWORD" ]; then
# If .env file exists, try to get password from there
if [ -f .env ]; then
DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d '=' -f2)
export PGPASSWORD="$DB_PASSWORD"
else
echo "⚠️ No DB_PASSWORD found in environment or .env file."
echo "Please enter PostgreSQL password:"
read -s PGPASSWORD
export PGPASSWORD
fi
fi
echo "💾 Backing up database to $BACKUP_PATH..."
pg_dump -h localhost -U postgres -d telegram_tinder_bot > "$BACKUP_PATH"
fi
# Check if backup was successful
if [ $? -eq 0 ]; then
echo "✅ Backup completed successfully: $BACKUP_PATH"
echo "📊 Backup size: $(du -h $BACKUP_PATH | cut -f1)"
# Compress the backup
gzip -f "$BACKUP_PATH"
echo "🗜️ Compressed backup: $BACKUP_PATH.gz"
# Keep only the last 7 backups
echo "🧹 Cleaning up old backups..."
find "$BACKUP_DIR" -name "tg_tinder_bot_*.sql.gz" -type f -mtime +7 -delete
echo "🎉 Backup process completed!"
else
echo "❌ Backup failed!"
exit 1
fi

72
bin/create_release.sh Normal file
View File

@@ -0,0 +1,72 @@
#!/bin/bash
# Скрипт для создания релиза Telegram Tinder Bot
# Получение версии из package.json
VERSION=$(grep -m1 "version" package.json | cut -d'"' -f4)
RELEASE_NAME="tg-tinder-bot-v$VERSION"
RELEASE_DIR="bin/releases/$RELEASE_NAME"
echo "🚀 Создание релиза $RELEASE_NAME"
# Создание директории релиза
mkdir -p "$RELEASE_DIR"
# Очистка временных файлов
echo "🧹 Очистка временных файлов..."
rm -rf dist node_modules
# Установка зависимостей
echo "📦 Установка зависимостей production..."
npm ci --only=production
# Сборка проекта
echo "🔧 Сборка проекта..."
npm run build
# Копирование файлов релиза
echo "📋 Копирование файлов..."
cp -r dist "$RELEASE_DIR/"
cp -r src/locales "$RELEASE_DIR/dist/"
cp package.json package-lock.json .env.example "$RELEASE_DIR/"
cp -r bin/start_bot.* bin/install_ubuntu.sh "$RELEASE_DIR/"
cp README.md LICENSE "$RELEASE_DIR/" 2>/dev/null || echo "Файлы документации не найдены"
cp sql/consolidated.sql "$RELEASE_DIR/"
cp docker-compose.yml Dockerfile "$RELEASE_DIR/"
cp deploy.sh "$RELEASE_DIR/" && chmod +x "$RELEASE_DIR/deploy.sh"
# Создание README для релиза
cat > "$RELEASE_DIR/RELEASE.md" << EOL
# Telegram Tinder Bot v$VERSION
Эта папка содержит релиз Telegram Tinder Bot версии $VERSION.
## Содержимое
- \`dist/\` - Скомпилированный код
- \`package.json\` - Зависимости и скрипты
- \`.env.example\` - Пример конфигурации
- \`docker-compose.yml\` и \`Dockerfile\` - Для запуска через Docker
- \`consolidated.sql\` - SQL-скрипт для инициализации базы данных
- \`deploy.sh\` - Скрипт для простого деплоя
## Быстрый старт
1. Создайте файл \`.env\` на основе \`.env.example\`
2. Запустите бота одним из способов:
- Через Docker: \`./deploy.sh\`
- Через Node.js: \`node dist/bot.js\`
## Дата релиза
$(date "+%d.%m.%Y %H:%M")
EOL
# Архивирование релиза
echo "📦 Создание архива..."
cd bin/releases
zip -r "$RELEASE_NAME.zip" "$RELEASE_NAME"
cd ../..
echo "✅ Релиз создан успешно!"
echo "📂 Релиз доступен в: bin/releases/$RELEASE_NAME"
echo "📦 Архив релиза: bin/releases/$RELEASE_NAME.zip"

58
bin/install_docker.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# install_docker.sh - Script for installing Docker and Docker Compose
echo "🚀 Installing Docker and Docker Compose..."
# Check if script is run as root
if [ "$(id -u)" -ne 0 ]; then
echo "❌ This script must be run as root. Please run with sudo."
exit 1
fi
# Update package lists
echo "📦 Updating package lists..."
apt update
# Install required packages
echo "📦 Installing required packages..."
apt install -y apt-transport-https ca-certificates curl software-properties-common
# Add Docker GPG key
echo "🔑 Adding Docker GPG key..."
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
# Add Docker repository
echo "📁 Adding Docker repository..."
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# Update package lists again
apt update
# Install Docker
echo "🐳 Installing Docker..."
apt install -y docker-ce docker-ce-cli containerd.io
# Enable and start Docker service
systemctl enable docker
systemctl start docker
# Install Docker Compose
echo "🐳 Installing Docker Compose..."
curl -L "https://github.com/docker/compose/releases/download/v2.24.6/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# Check versions
echo "✅ Installation complete!"
echo "Docker version:"
docker --version
echo "Docker Compose version:"
docker-compose --version
# Add current user to docker group if not root
if [ -n "$SUDO_USER" ]; then
echo "👤 Adding user $SUDO_USER to docker group..."
usermod -aG docker $SUDO_USER
echo "⚠️ Please log out and log back in for group changes to take effect."
fi
echo "🎉 Docker installation completed successfully!"

190
bin/install_ubuntu.sh Normal file
View File

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

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

27
bin/start_bot.bat Normal file
View File

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

24
bin/start_bot.sh Normal file
View File

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

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

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

113
bin/update.bat Normal file
View File

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

155
bin/update.sh Normal file
View File

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

7
database.json Normal file
View File

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

55
deploy.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# deploy.sh - Скрипт для деплоя Telegram Tinder Bot
echo "🚀 Деплой Telegram Tinder Bot..."
# Проверяем наличие Docker
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
echo "❌ Docker и Docker Compose должны быть установлены!"
echo "Для установки на Ubuntu выполните:"
echo "sudo apt update && sudo apt install -y docker.io docker-compose"
exit 1
fi
# Определяем рабочую директорию
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
# Получаем последние изменения
echo "📥 Получение последних изменений..."
git pull origin main
# Проверяем наличие .env файла
if [ ! -f .env ]; then
echo "📝 Создание .env файла из .env.production..."
cp .env.production .env
echo "⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!"
exit 1
fi
# Запускаем Docker Compose
echo "🐳 Сборка и запуск контейнеров Docker..."
docker-compose down
docker-compose build
docker-compose up -d
# Проверяем статус контейнеров
echo "🔍 Проверка статуса контейнеров..."
docker-compose ps
echo "✅ Деплой успешно завершен! Бот должен быть доступен через Telegram."
echo ""
echo "📊 Полезные команды:"
echo "- Просмотр логов: docker-compose logs -f"
echo "- Перезапуск сервисов: docker-compose restart"
echo "- Остановка всех сервисов: docker-compose down"
echo "- Доступ к базе данных: docker-compose exec db psql -U postgres -d telegram_tinder_bot"
echo "- Проверка состояния бота: curl http://localhost:3000/health"
echo ""
echo "🌟 Для администрирования базы данных:"
echo "Adminer доступен по адресу: http://ваш_сервер:8080"
echo " - Система: PostgreSQL"
echo " - Сервер: db"
echo " - Пользователь: postgres"
echo " - Пароль: (из переменной DB_PASSWORD в .env)"
echo " - База данных: telegram_tinder_bot"

View File

@@ -0,0 +1,69 @@
# Используем версию Docker Compose для локальной разработки
version: '3.8'
services:
bot:
build:
context: .
dockerfile: Dockerfile
args:
- NODE_ENV=development
environment:
- NODE_ENV=development
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=telegram_tinder_bot
- DB_USERNAME=postgres
- DB_PASSWORD=dev_password
volumes:
# Монтируем исходный код для горячей перезагрузки
- ./src:/app/src
- ./dist:/app/dist
- ./.env:/app/.env
ports:
# Открываем порт для отладки
- "9229:9229"
command: npm run dev
networks:
- bot-network
depends_on:
- db
db:
# Используем последнюю версию PostgreSQL для разработки
image: postgres:16-alpine
environment:
- POSTGRES_DB=telegram_tinder_bot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=dev_password
volumes:
# Хранение данных локально для быстрого сброса
- postgres_data_dev:/var/lib/postgresql/data
# Монтируем скрипты инициализации
- ./sql:/docker-entrypoint-initdb.d
ports:
# Открываем порт для доступа к БД напрямую
- "5433:5432"
networks:
- bot-network
adminer:
image: adminer:latest
ports:
- "8080:8080"
networks:
- bot-network
depends_on:
- db
environment:
- ADMINER_DEFAULT_SERVER=db
- ADMINER_DEFAULT_USER=postgres
- ADMINER_DEFAULT_PASSWORD=dev_password
volumes:
postgres_data_dev:
networks:
bot-network:
driver: bridge

View File

@@ -6,19 +6,26 @@ services:
container_name: telegram-tinder-bot
restart: unless-stopped
depends_on:
- db
db:
condition: service_healthy
env_file: .env
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=telegram_tinder_bot
- DB_USERNAME=postgres
- DB_PASSWORD=${DB_PASSWORD}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
volumes:
- ./uploads:/app/uploads
- ./logs:/app/logs
networks:
- bot-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:15-alpine
@@ -27,14 +34,18 @@ services:
environment:
- POSTGRES_DB=telegram_tinder_bot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password123
- POSTGRES_PASSWORD=${DB_PASSWORD:-password123}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./src/database/migrations/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5433:5432"
networks:
- bot-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:latest

221
docs/DEPLOY_UBUNTU.md Normal file
View File

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

160
docs/LOCALIZATION.md Normal file
View File

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

View File

@@ -0,0 +1,264 @@
# Инструкция по развертыванию Telegram Tinder Bot в Production
Это подробное руководство по развертыванию Telegram Tinder Bot в production-окружении с использованием Docker и Docker Compose.
## 📋 Требования
- **Операционная система**: Ubuntu 20.04 или выше (рекомендуется) / Windows Server с Docker
- **Программное обеспечение**:
- Docker (последняя версия)
- Docker Compose (последняя версия)
- Git
## 🚀 Быстрое развертывание
### 1. Клонирование репозитория
```bash
git clone https://github.com/your-username/telegram-tinder-bot.git
cd telegram-tinder-bot
```
### 2. Настройка конфигурации
```bash
# Создание файла конфигурации из шаблона
cp .env.production .env
# Редактирование конфигурационного файла
nano .env
```
Важно указать следующие параметры:
- `TELEGRAM_BOT_TOKEN`: токен от @BotFather
- `DB_PASSWORD`: надежный пароль для базы данных
- `JWT_SECRET`: случайная строка для JWT
- `ENCRYPTION_KEY`: случайная строка для шифрования
### 3. Запуск деплоя
```bash
# Сделайте скрипт исполняемым
chmod +x deploy.sh
# Запустите деплой
./deploy.sh
```
## 🔧 Подробное руководство по установке
### Подготовка сервера Ubuntu
```bash
# Обновление системы
sudo apt update && sudo apt upgrade -y
# Установка необходимых пакетов
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common git
# Установка Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Добавление текущего пользователя в группу docker
sudo usermod -aG docker ${USER}
# Установка Docker Compose
sudo apt install -y docker-compose
```
### Клонирование и настройка проекта
```bash
# Создание директории для проекта
mkdir -p /opt/telegram-tinder
cd /opt/telegram-tinder
# Клонирование репозитория
git clone https://github.com/your-username/telegram-tinder-bot.git .
# Настройка .env файла
cp .env.production .env
nano .env
# Создание директорий для данных и логов
mkdir -p uploads logs
chmod 777 uploads logs
```
### Запуск проекта
```bash
# Запуск в фоновом режиме
docker-compose up -d
# Проверка статуса контейнеров
docker-compose ps
# Просмотр логов
docker-compose logs -f
```
## 🔄 Обновление бота
Для обновления бота выполните:
```bash
cd /путь/к/telegram-tinder-bot
./deploy.sh
```
Скрипт автоматически выполнит:
1. Получение последних изменений из репозитория
2. Перезапуск контейнеров с новой версией кода
3. Применение миграций базы данных
## 🛡️ Обеспечение безопасности
### Настройка файрвола
```bash
# Разрешение только необходимых портов
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
### Настройка HTTPS с Let's Encrypt (опционально)
Для использования HTTPS с Let's Encrypt и Nginx:
```bash
# Установка Certbot
sudo apt install -y certbot python3-certbot-nginx
# Получение SSL-сертификата
sudo certbot --nginx -d your-domain.com
```
## 📊 Мониторинг и управление
### Просмотр логов
```bash
# Логи всех контейнеров
docker-compose logs -f
# Логи конкретного контейнера (например, бота)
docker-compose logs -f bot
# Последние 100 строк логов
docker-compose logs --tail=100 bot
```
### Управление сервисами
```bash
# Остановка всех контейнеров
docker-compose down
# Перезапуск всех контейнеров
docker-compose restart
# Перезапуск только бота
docker-compose restart bot
```
### Доступ к базе данных
```bash
# Вход в консоль PostgreSQL
docker-compose exec db psql -U postgres -d telegram_tinder_bot
# Резервное копирование базы данных
docker-compose exec db pg_dump -U postgres telegram_tinder_bot > backup_$(date +%Y%m%d).sql
# Восстановление базы из резервной копии
cat backup.sql | docker-compose exec -T db psql -U postgres -d telegram_tinder_bot
```
## 🔍 Устранение неполадок
### Проверка работоспособности
```bash
# Проверка API бота
curl http://localhost:3000/health
# Проверка подключения к базе данных
docker-compose exec bot node -e "const { Client } = require('pg'); const client = new Client({ host: 'db', port: 5432, database: 'telegram_tinder_bot', user: 'postgres', password: process.env.DB_PASSWORD }); client.connect().then(() => { console.log('Connected to DB!'); client.end(); }).catch(e => console.error(e));"
```
### Общие проблемы и решения
**Проблема**: Бот не отвечает в Telegram
**Решение**:
- Проверьте валидность токена бота
- Проверьте логи на наличие ошибок: `docker-compose logs -f bot`
**Проблема**: Ошибки подключения к базе данных
**Решение**:
- Проверьте настройки подключения в `.env`
- Убедитесь, что контейнер с базой данных запущен: `docker-compose ps`
- Проверьте логи базы данных: `docker-compose logs db`
**Проблема**: Недостаточно свободного места на диске
**Решение**:
- Очистите неиспользуемые Docker образы: `docker image prune -a`
- Очистите неиспользуемые Docker тома: `docker volume prune`
## 🔁 Настройка автоматического обновления
### Настройка автообновления через Cron
```bash
# Редактирование crontab
crontab -e
# Добавление задачи (обновление каждую ночь в 3:00)
0 3 * * * cd /путь/к/telegram-tinder-bot && ./deploy.sh > /tmp/tg-tinder-update.log 2>&1
```
## 📝 Рекомендации по обслуживанию
1. **Регулярное резервное копирование**:
```bash
# Ежедневное резервное копирование через cron
0 2 * * * docker-compose exec -T db pg_dump -U postgres telegram_tinder_bot > /path/to/backups/tg_$(date +\%Y\%m\%d).sql
```
2. **Мониторинг использования ресурсов**:
```bash
# Просмотр использования ресурсов контейнерами
docker stats
```
3. **Обновление Docker образов**:
```bash
# Обновление образов
docker-compose pull
docker-compose up -d
```
4. **Проверка журналов на наличие ошибок**:
```bash
# Поиск ошибок в логах
docker-compose logs | grep -i error
docker-compose logs | grep -i exception
```
---
## 📋 Контрольный список деплоя
- [ ] Установлены Docker и Docker Compose
- [ ] Клонирован репозиторий
- [ ] Настроен файл .env с реальными данными
- [ ] Запущены контейнеры через docker-compose
- [ ] Проверено подключение бота к Telegram API
- [ ] Настроено резервное копирование
- [ ] Настроен файрвол и безопасность сервера
- [ ] Проверены и настроены логи
- [ ] (Опционально) Настроен SSL для веб-интерфейса
- [ ] (Опционально) Настроено автоматическое обновление

105
docs/VIP_FUNCTIONS.md Normal file
View File

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

View File

@@ -0,0 +1,44 @@
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
export const shorthands: ColumnDefinitions | undefined = undefined;
export async function up(pgm: MigrationBuilder): Promise<void> {
// Создание таблицы profile_views для хранения информации о просмотренных профилях
pgm.createTable('profile_views', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('uuid_generate_v4()') },
viewer_id: {
type: 'uuid',
notNull: true,
references: 'users',
onDelete: 'CASCADE'
},
viewed_profile_id: {
type: 'uuid',
notNull: true,
references: 'profiles(user_id)',
onDelete: 'CASCADE'
},
view_date: { type: 'timestamp', notNull: true, default: pgm.func('now()') },
view_type: { type: 'varchar(20)', notNull: true, default: 'browse' }, // browse, match, like, etc.
});
// Создание индекса для быстрого поиска по паре (просмотревший - просмотренный)
pgm.createIndex('profile_views', ['viewer_id', 'viewed_profile_id'], {
unique: true,
name: 'profile_views_viewer_viewed_idx'
});
// Индекс для быстрого поиска по viewer_id
pgm.createIndex('profile_views', ['viewer_id'], {
name: 'profile_views_viewer_idx'
});
// Индекс для быстрого поиска по viewed_profile_id
pgm.createIndex('profile_views', ['viewed_profile_id'], {
name: 'profile_views_viewed_idx'
});
}
export async function down(pgm: MigrationBuilder): Promise<void> {
pgm.dropTable('profile_views', { cascade: true });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
/* eslint-disable camelcase */
exports.shorthands = undefined;
exports.up = pgm => {
// Проверяем существование таблицы scheduled_notifications
pgm.sql(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'scheduled_notifications'
) THEN
-- Проверяем, нет ли уже столбца processed
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
) THEN
-- Добавляем столбец processed
ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE;
END IF;
ELSE
-- Создаем таблицу, если она не существует
CREATE TABLE scheduled_notifications (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
type VARCHAR(50) NOT NULL,
data JSONB,
scheduled_at TIMESTAMP NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
END IF;
END
$$;
`);
};
exports.down = pgm => {
pgm.sql(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
) THEN
ALTER TABLE scheduled_notifications DROP COLUMN processed;
END IF;
END
$$;
`);
};

View File

@@ -0,0 +1,14 @@
-- Добавление столбцов state и state_data в таблицу users для обработки состояний пользователя
-- Добавляем столбец state для хранения текущего состояния пользователя
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL;
-- Добавляем столбец state_data для хранения дополнительных данных о состоянии
ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb;
-- Добавляем индекс для быстрого поиска по state
CREATE INDEX IF NOT EXISTS idx_users_state ON users(state);
-- Комментарий к столбцам
COMMENT ON COLUMN users.state IS 'Текущее состояние пользователя (например, ожидание ввода)';
COMMENT ON COLUMN users.state_data IS 'Дополнительные данные о состоянии пользователя в формате JSON';

394
package-lock.json generated
View File

@@ -10,10 +10,12 @@
"license": "MIT",
"dependencies": {
"@types/node-telegram-bot-api": "^0.64.11",
"axios": "^1.6.2",
"axios": "^1.12.1",
"dotenv": "^16.6.1",
"i18next": "^25.5.2",
"node-pg-migrate": "^8.0.3",
"node-telegram-bot-api": "^0.64.0",
"pg": "^8.11.3",
"pg": "^8.16.3",
"sharp": "^0.32.6",
"uuid": "^9.0.1"
},
@@ -438,6 +440,14 @@
"@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": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -590,6 +600,123 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1096,7 +1223,7 @@
"version": "8.15.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -1193,7 +1320,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -1202,7 +1328,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -1349,9 +1474,9 @@
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="
},
"node_modules/axios": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz",
"integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -1867,7 +1992,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@@ -1989,7 +2113,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -2219,6 +2342,12 @@
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -2249,8 +2378,7 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/end-of-stream": {
"version": "1.4.5",
@@ -2408,7 +2536,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -2600,6 +2727,34 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -2697,7 +2852,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -2993,6 +3147,36 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -3218,7 +3402,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3447,8 +3630,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/isstream": {
"version": "0.1.2",
@@ -3533,6 +3715,21 @@
"node": ">=8"
}
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -4379,6 +4576,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -4439,6 +4645,69 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
"node_modules/node-pg-migrate": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.3.tgz",
"integrity": "sha512-oKzZyzTULTryO1jehX19VnyPCGf3G/3oWZg3gODphvID56T0WjPOShTVPVnxGdlcueaIW3uAVrr7M8xLZq5TcA==",
"license": "MIT",
"dependencies": {
"glob": "~11.0.0",
"yargs": "~17.7.0"
},
"bin": {
"node-pg-migrate": "bin/node-pg-migrate.js"
},
"engines": {
"node": ">=20.11.0"
},
"peerDependencies": {
"@types/pg": ">=6.0.0 <9.0.0",
"pg": ">=4.3.0 <9.0.0"
},
"peerDependenciesMeta": {
"@types/pg": {
"optional": true
}
}
},
"node_modules/node-pg-migrate/node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/node-pg-migrate/node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/node-releases": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
@@ -4630,6 +4899,12 @@
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -4670,7 +4945,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -4681,6 +4955,31 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -4690,6 +4989,7 @@
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -5257,7 +5557,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5489,7 +5788,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -5501,7 +5799,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -5782,7 +6079,21 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -5849,7 +6160,19 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -6255,7 +6578,7 @@
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6423,7 +6746,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -6530,7 +6852,24 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -6573,7 +6912,6 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -6588,7 +6926,6 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@@ -6606,7 +6943,6 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}

View File

@@ -5,17 +5,36 @@
"main": "dist/bot.js",
"scripts": {
"start": "node dist/bot.js",
"start:prod": "NODE_ENV=production node dist/bot.js",
"start:win:prod": "set NODE_ENV=production&& node dist/bot.js",
"dev": "ts-node src/bot.ts",
"build": "tsc",
"build": "tsc && xcopy /E /I src\\locales dist\\locales",
"build:linux": "tsc && cp -R src/locales dist/",
"test": "jest",
"db:init": "ts-node src/scripts/initDb.ts"
"test:bot": "ts-node tests/test-bot.ts",
"db:init": "ts-node src/scripts/initDb.ts",
"init:db": "ts-node src/scripts/initDb.ts",
"migrate": "node-pg-migrate",
"migrate:up": "node-pg-migrate up",
"migrate:down": "node-pg-migrate down",
"migrate:create": "node-pg-migrate create",
"premium:set-all": "ts-node src/scripts/setPremiumForAll.ts",
"premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts",
"db:info": "ts-node src/scripts/getDatabaseInfo.ts",
"db:test-data": "ts-node src/scripts/createTestData.ts",
"enhance-notifications": "ts-node src/scripts/enhanceNotifications.ts",
"update": "bash ./bin/update.sh",
"update:win": ".\\bin\\update.bat",
"start:sh": "bash ./bin/start_bot.sh"
},
"dependencies": {
"@types/node-telegram-bot-api": "^0.64.11",
"axios": "^1.6.2",
"axios": "^1.12.1",
"dotenv": "^16.6.1",
"i18next": "^25.5.2",
"node-pg-migrate": "^8.0.3",
"node-telegram-bot-api": "^0.64.0",
"pg": "^8.11.3",
"pg": "^8.16.3",
"sharp": "^0.32.6",
"uuid": "^9.0.1"
},

49
scripts/README.md Normal file
View File

@@ -0,0 +1,49 @@
# Структура скриптов в директории `/scripts`
Эта директория содержит вспомогательные скрипты для работы с Telegram Tinder Bot.
## Основные скрипты
- `startup.sh` - Скрипт запуска бота в Docker-контейнере
- `migrate-sync.js` - Синхронизация миграций базы данных
- `createNotificationTables.js` - Создание таблиц для системы уведомлений
- `add-hobbies-column.js` - Добавление колонки интересов в профиль
- `create_profile_fix.js` - Исправление профилей пользователей
- `createProfileViewsTable.js` - Создание таблицы для учета просмотров профилей
- `update_bot_with_notifications.js` - Обновление бота с поддержкой уведомлений
## Директории
- `/legacy` - Устаревшие и тестовые скрипты, сохраненные для истории
## Использование скриптов
Скрипты JavaScript можно запускать с помощью Node.js:
```bash
node scripts/script-name.js
```
Bash скрипты должны быть сделаны исполняемыми:
```bash
chmod +x scripts/script-name.sh
./scripts/script-name.sh
```
## Добавление новых скриптов
При добавлении новых скриптов соблюдайте следующие правила:
1. Используйте понятное имя файла, отражающее его назначение
2. Добавьте комментарии в начало файла с описанием его функциональности
3. Добавьте запись об этом скрипте в текущий файл README.md
## Скрипты миграций
Миграции базы данных следует создавать с помощью команды:
```bash
npm run migrate:create your_migration_name
```
Это создаст файл миграции в директории `/migrations`.

View File

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

View File

@@ -0,0 +1,259 @@
const { Pool } = require('pg');
const dotenv = require('dotenv');
const uuid = require('uuid');
dotenv.config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function createNotificationTables() {
const client = await pool.connect();
try {
await client.query('BEGIN');
console.log('Creating UUID extension if not exists...');
await client.query(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
`);
// Проверяем существование таблицы notifications
const notificationsExists = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'notifications'
) as exists
`);
if (!notificationsExists.rows[0].exists) {
console.log('Creating notifications table...');
await client.query(`
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
console.log('Creating index on notifications...');
await client.query(`
CREATE INDEX idx_notifications_user_id ON notifications (user_id);
CREATE INDEX idx_notifications_type ON notifications (type);
CREATE INDEX idx_notifications_created_at ON notifications (created_at);
`);
} else {
console.log('Notifications table already exists.');
}
// Проверяем существование таблицы scheduled_notifications
const scheduledExists = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'scheduled_notifications'
) as exists
`);
if (!scheduledExists.rows[0].exists) {
console.log('Creating scheduled_notifications table...');
await client.query(`
CREATE TABLE scheduled_notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
scheduled_at TIMESTAMP NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
console.log('Creating index on scheduled_notifications...');
await client.query(`
CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications (user_id);
CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications (scheduled_at);
CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications (processed);
`);
} else {
console.log('Scheduled_notifications table already exists.');
}
// Проверяем существование таблицы notification_templates
const templatesExists = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'notification_templates'
) as exists
`);
if (!templatesExists.rows[0].exists) {
console.log('Creating notification_templates table...');
await client.query(`
CREATE TABLE notification_templates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type VARCHAR(50) NOT NULL UNIQUE,
title TEXT NOT NULL,
message_template TEXT NOT NULL,
button_template JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
`);
} else {
console.log('Notification_templates table already exists.');
}
// Проверяем наличие колонки notification_settings в таблице users
const settingsColumnExists = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'notification_settings'
) as exists
`);
if (!settingsColumnExists.rows[0].exists) {
console.log('Adding notification_settings column to users table...');
await client.query(`
ALTER TABLE users
ADD COLUMN notification_settings JSONB DEFAULT '{
"newMatches": true,
"newMessages": true,
"newLikes": true,
"reminders": true,
"dailySummary": true,
"timePreference": "evening",
"doNotDisturb": false
}'::jsonb
`);
} else {
console.log('Notification_settings column already exists in users table.');
}
// Заполнение таблицы шаблонов уведомлений базовыми шаблонами
if (!templatesExists.rows[0].exists) {
console.log('Populating notification templates...');
const templates = [
{
type: 'new_like',
title: 'Новый лайк!',
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
[
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
],
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
]
}
},
{
type: 'super_like',
title: 'Супер-лайк!',
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
[
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
],
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
]
}
},
{
type: 'new_match',
title: 'Новый матч!',
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
button_template: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }],
[
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
{ text: '📋 Все матчи', callback_data: 'view_matches' }
]
]
}
},
{
type: 'new_message',
title: 'Новое сообщение!',
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
button_template: {
inline_keyboard: [
[{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }],
[
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
{ text: '📋 Все чаты', callback_data: 'view_matches' }
]
]
}
},
{
type: 'match_reminder',
title: 'Напоминание о матче',
message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!',
button_template: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }],
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }]
]
}
},
{
type: 'inactive_matches',
title: 'Неактивные матчи',
message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!',
button_template: {
inline_keyboard: [
[{ text: '📋 Открыть матчи', callback_data: 'view_matches' }],
[{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }]
]
}
},
{
type: 'like_summary',
title: 'Сводка лайков',
message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }],
[{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }]
]
}
}
];
for (const template of templates) {
await client.query(`
INSERT INTO notification_templates (id, type, title, message_template, button_template)
VALUES ($1, $2, $3, $4, $5)
`, [
uuid.v4(),
template.type,
template.title,
template.message_template,
JSON.stringify(template.button_template)
]);
}
}
await client.query('COMMIT');
console.log('Successfully created notification tables');
} catch (err) {
await client.query('ROLLBACK');
console.error('Error creating notification tables:', err);
} finally {
client.release();
pool.end();
}
}
createNotificationTables().catch(err => console.error('Failed to create notification tables:', err));

View File

@@ -0,0 +1,86 @@
const { Pool } = require('pg');
require('dotenv').config();
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
});
async function createProfileViewsTable() {
const client = await pool.connect();
try {
console.log('Creating profile_views table...');
await client.query('BEGIN');
// Включаем расширение uuid-ossp, если оно еще не включено
await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
// Создаем таблицу profile_views, если она не существует
await client.query(`
CREATE TABLE IF NOT EXISTS profile_views (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
view_date TIMESTAMP NOT NULL DEFAULT NOW(),
view_type VARCHAR(20) NOT NULL DEFAULT 'browse'
)
`);
// Создаем уникальный индекс для пары (просмотревший - просмотренный)
await client.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'profile_views_viewer_viewed_idx'
) THEN
CREATE UNIQUE INDEX profile_views_viewer_viewed_idx
ON profile_views (viewer_id, viewed_profile_id);
END IF;
END $$;
`);
// Создаем индекс для быстрого поиска по viewer_id
await client.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'profile_views_viewer_idx'
) THEN
CREATE INDEX profile_views_viewer_idx
ON profile_views (viewer_id);
END IF;
END $$;
`);
// Создаем индекс для быстрого поиска по viewed_profile_id
await client.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'profile_views_viewed_idx'
) THEN
CREATE INDEX profile_views_viewed_idx
ON profile_views (viewed_profile_id);
END IF;
END $$;
`);
await client.query('COMMIT');
console.log('Table profile_views created successfully');
} catch (e) {
await client.query('ROLLBACK');
console.error('Error creating table:', e);
} finally {
client.release();
await pool.end();
}
}
// Запускаем функцию создания таблицы
createProfileViewsTable();

View File

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

View File

@@ -0,0 +1,88 @@
// Скрипт для анализа и отладки проблем с обработчиками коллбэков
require('dotenv').config();
const fs = require('fs');
const path = require('path');
function analyzeCallbackHandlers() {
const filePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts');
const content = fs.readFileSync(filePath, 'utf-8');
// Проверяем наличие реализаций методов
const methodsToCheck = [
'handleCreateProfile',
'handleGenderSelection',
'handleViewMyProfile',
'handleEditProfile',
'handleManagePhotos',
'handleStartBrowsing',
'handleSettings'
];
const issues = [];
let debugInfo = [];
methodsToCheck.forEach(method => {
debugInfo.push(`Проверяем метод: ${method}`);
// Проверяем наличие полной реализации метода (не только сигнатуры)
const methodSignatureRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*{`, 'g');
const hasSignature = methodSignatureRegex.test(content);
const methodBodyRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*{[\\s\\S]+?}`, 'g');
const methodMatch = content.match(methodBodyRegex);
debugInfo.push(` Сигнатура найдена: ${hasSignature}`);
debugInfo.push(` Реализация найдена: ${methodMatch !== null}`);
if (methodMatch) {
const methodContent = methodMatch[0];
debugInfo.push(` Длина метода: ${methodContent.length} символов`);
// Проверяем, содержит ли метод только заглушку
const isStub = methodContent.includes('// Заглушка метода') ||
(!methodContent.includes('await') && methodContent.split('\n').length <= 3);
if (isStub) {
issues.push(`❌ Метод ${method} содержит только заглушку, нет реальной реализации`);
} else {
debugInfo.push(` Метод ${method} имеет полную реализацию`);
}
} else if (hasSignature) {
issues.push(`❌ Метод ${method} имеет только сигнатуру, но нет реализации`);
} else {
issues.push(`❌ Метод ${method} не найден в файле`);
}
});
// Проверяем регистрацию обработчиков для NotificationHandlers
const notificationHandlersRegex = /this\.notificationHandlers\s*=\s*new\s+NotificationHandlers\(bot\);/g;
const hasNotificationHandlers = notificationHandlersRegex.test(content);
debugInfo.push(`NotificationHandlers инициализирован: ${hasNotificationHandlers}`);
// Проверяем обработку коллбэка notifications
const notificationsCallbackRegex = /if\s*\(data\s*===\s*['"]notifications['"].*?\)/g;
const hasNotificationsCallback = notificationsCallbackRegex.test(content);
debugInfo.push(`Обработчик для callback 'notifications' найден: ${hasNotificationsCallback}`);
// Выводим результаты
console.log('\n=== Анализ CallbackHandlers.ts ===\n');
if (issues.length > 0) {
console.log('НАЙДЕНЫ ПРОБЛЕМЫ:');
issues.forEach(issue => console.log(issue));
console.log('\nРЕКОМЕНДАЦИИ:');
console.log('1. Восстановите оригинальные реализации методов вместо заглушек');
console.log('2. Убедитесь, что методы содержат необходимую бизнес-логику');
console.log('3. Проверьте, что все коллбэки правильно обрабатываются');
} else {
console.log('✅ Проблем не обнаружено');
}
console.log('\n=== Отладочная информация ===\n');
debugInfo.forEach(info => console.log(info));
// Проверяем количество методов в файле
const asyncMethodsCount = (content.match(/async\s+handle[A-Za-z]+\s*\(/g) || []).length;
console.log(`\nВсего async методов в файле: ${asyncMethodsCount}`);
}
analyzeCallbackHandlers();

View File

@@ -0,0 +1,66 @@
const { Pool } = require('pg');
require('dotenv').config();
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
});
async function checkDatabase() {
const client = await pool.connect();
try {
console.log('\n===== ПРОВЕРКА СОСТОЯНИЯ БАЗЫ ДАННЫХ =====');
// Проверка таблицы users
const usersResult = await client.query('SELECT COUNT(*) as count FROM users');
console.log(`Пользователей в БД: ${usersResult.rows[0].count}`);
if (parseInt(usersResult.rows[0].count) > 0) {
const users = await client.query('SELECT id, telegram_id, username, first_name FROM users LIMIT 10');
console.log('Последние пользователи:');
users.rows.forEach(user => {
console.log(` - ID: ${user.id.substring(0, 8)}... | Telegram: ${user.telegram_id} | Имя: ${user.first_name || user.username}`);
});
}
// Проверка таблицы profiles
const profilesResult = await client.query('SELECT COUNT(*) as count FROM profiles');
console.log(`\nПрофилей в БД: ${profilesResult.rows[0].count}`);
if (parseInt(profilesResult.rows[0].count) > 0) {
const profiles = await client.query(`
SELECT p.id, p.user_id, p.name, p.age, p.gender, p.interested_in, p.is_visible
FROM profiles p
ORDER BY p.created_at DESC
LIMIT 10
`);
console.log('Последние профили:');
profiles.rows.forEach(profile => {
console.log(` - ID: ${profile.id.substring(0, 8)}... | UserID: ${profile.user_id.substring(0, 8)}... | Имя: ${profile.name} | Возраст: ${profile.age} | Пол: ${profile.gender} | Интересы: ${profile.interested_in} | Виден: ${profile.is_visible}`);
});
}
// Проверка таблицы swipes
const swipesResult = await client.query('SELECT COUNT(*) as count FROM swipes');
console.log(`\nСвайпов в БД: ${swipesResult.rows[0].count}`);
// Проверка таблицы profile_views
const viewsResult = await client.query('SELECT COUNT(*) as count FROM profile_views');
console.log(`Просмотров профилей в БД: ${viewsResult.rows[0].count}`);
// Проверка таблицы matches
const matchesResult = await client.query('SELECT COUNT(*) as count FROM matches');
console.log(`Матчей в БД: ${matchesResult.rows[0].count}`);
console.log('\n===== ПРОВЕРКА ЗАВЕРШЕНА =====\n');
} catch (e) {
console.error('Ошибка при проверке базы данных:', e);
} finally {
client.release();
await pool.end();
}
}
// Запускаем проверку
checkDatabase();

View File

@@ -0,0 +1,64 @@
// Скрипт для проверки таблицы profile_views
const { Pool } = require('pg');
require('dotenv').config();
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
});
async function checkProfileViewsTable() {
const client = await pool.connect();
try {
console.log('Проверка таблицы profile_views...');
// Проверяем наличие таблицы
const tableCheck = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'profile_views'
);
`);
const tableExists = tableCheck.rows[0].exists;
console.log(`Таблица profile_views ${tableExists ? 'существует' : 'не существует'}`);
if (tableExists) {
// Проверяем количество записей в таблице
const countResult = await client.query('SELECT COUNT(*) FROM profile_views');
console.log(`Количество записей в таблице: ${countResult.rows[0].count}`);
// Получаем данные из таблицы
const dataResult = await client.query(`
SELECT pv.*,
v.telegram_id as viewer_telegram_id,
vp.telegram_id as viewed_telegram_id
FROM profile_views pv
LEFT JOIN users v ON pv.viewer_id = v.id
LEFT JOIN users vp ON pv.viewed_profile_id = vp.id
LIMIT 10
`);
if (dataResult.rows.length > 0) {
console.log('Данные из таблицы profile_views:');
dataResult.rows.forEach((row, index) => {
console.log(`${index + 1}. Просмотр: ${row.viewer_telegram_id || 'Неизвестно'}${row.viewed_telegram_id || 'Неизвестно'}, дата: ${row.view_date}`);
});
} else {
console.log('Таблица profile_views пуста');
}
}
} catch (error) {
console.error('Ошибка при проверке таблицы profile_views:', error);
} finally {
client.release();
await pool.end();
}
}
// Запускаем проверку
checkProfileViewsTable();

View File

@@ -0,0 +1,74 @@
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
user: process.env.DB_USER || 'postgres',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'telegram_tinder_db',
password: process.env.DB_PASSWORD || 'postgres',
port: parseInt(process.env.DB_PORT || '5432')
});
async function checkUserTableStructure() {
try {
// Получаем информацию о структуре таблицы users
const result = await pool.query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;
`);
console.log('=== Структура таблицы users ===');
console.table(result.rows);
// Проверяем наличие столбцов state и state_data
const stateColumn = result.rows.find(row => row.column_name === 'state');
const stateDataColumn = result.rows.find(row => row.column_name === 'state_data');
if (!stateColumn) {
console.log('❌ Столбец state отсутствует в таблице users');
} else {
console.log('✅ Столбец state присутствует в таблице users');
}
if (!stateDataColumn) {
console.log('❌ Столбец state_data отсутствует в таблице users');
} else {
console.log('✅ Столбец state_data присутствует в таблице users');
}
// Добавляем эти столбцы, если их нет
if (!stateColumn || !stateDataColumn) {
console.log('🔄 Добавление отсутствующих столбцов...');
await pool.query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL,
ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb;
`);
console.log('✅ Столбцы успешно добавлены');
}
// Проверяем наличие других таблиц, связанных с уведомлениями
const tablesResult = await pool.query(`
SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
AND tablename IN ('notifications', 'notification_settings', 'scheduled_notifications');
`);
console.log('\n=== Таблицы для уведомлений ===');
console.table(tablesResult.rows);
// Закрываем соединение
await pool.end();
} catch (error) {
console.error('Ошибка при проверке структуры таблицы:', error);
await pool.end();
}
}
checkUserTableStructure();

View File

@@ -0,0 +1,55 @@
const { Pool } = require('pg');
require('dotenv').config();
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
});
async function cleanDatabase() {
const client = await pool.connect();
try {
console.log('Очистка базы данных...');
await client.query('BEGIN');
// Отключаем временно foreign key constraints
await client.query('SET CONSTRAINTS ALL DEFERRED');
// Очищаем таблицы в правильном порядке
console.log('Очистка таблицы messages...');
await client.query('DELETE FROM messages');
console.log('Очистка таблицы profile_views...');
await client.query('DELETE FROM profile_views');
console.log('Очистка таблицы matches...');
await client.query('DELETE FROM matches');
console.log('Очистка таблицы swipes...');
await client.query('DELETE FROM swipes');
console.log('Очистка таблицы profiles...');
await client.query('DELETE FROM profiles');
console.log('Очистка таблицы users...');
await client.query('DELETE FROM users');
// Возвращаем foreign key constraints
await client.query('SET CONSTRAINTS ALL IMMEDIATE');
await client.query('COMMIT');
console.log('✅ База данных успешно очищена');
} catch (e) {
await client.query('ROLLBACK');
console.error('❌ Ошибка при очистке базы данных:', e);
} finally {
client.release();
await pool.end();
}
}
// Запускаем функцию очистки
cleanDatabase();

View File

@@ -0,0 +1,66 @@
// Скрипт для очистки всех таблиц в базе данных
import { Pool } from 'pg';
import dotenv from 'dotenv';
// Загружаем переменные окружения из .env файла
dotenv.config();
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
});
async function clearDatabase() {
const client = await pool.connect();
try {
console.log('Начинаем очистку базы данных...');
// Начинаем транзакцию
await client.query('BEGIN');
// Отключаем внешние ключи на время выполнения (если они используются)
// await client.query('SET session_replication_role = \'replica\'');
// Очистка таблиц в порядке, учитывающем зависимости
console.log('Очистка таблицы сообщений...');
await client.query('TRUNCATE TABLE messages CASCADE');
console.log('Очистка таблицы просмотров профилей...');
await client.query('TRUNCATE TABLE profile_views CASCADE');
console.log('Очистка таблицы свайпов...');
await client.query('TRUNCATE TABLE swipes CASCADE');
console.log('Очистка таблицы матчей...');
await client.query('TRUNCATE TABLE matches CASCADE');
console.log('Очистка таблицы профилей...');
await client.query('TRUNCATE TABLE profiles CASCADE');
console.log('Очистка таблицы пользователей...');
await client.query('TRUNCATE TABLE users CASCADE');
// Возвращаем внешние ключи (если они использовались)
// await client.query('SET session_replication_role = \'origin\'');
// Фиксируем транзакцию
await client.query('COMMIT');
console.log('Все таблицы успешно очищены!');
} catch (error) {
// В случае ошибки откатываем транзакцию
await client.query('ROLLBACK');
console.error('Произошла ошибка при очистке базы данных:', error);
} finally {
// Освобождаем клиента
client.release();
// Закрываем пул соединений
await pool.end();
}
}
// Запускаем функцию очистки
clearDatabase();

View File

@@ -0,0 +1,81 @@
// Скрипт для очистки всех таблиц в базе данных
import { Pool } from 'pg';
import dotenv from 'dotenv';
// Загружаем переменные окружения из .env файла
dotenv.config();
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
});
async function clearDatabase() {
const client = await pool.connect();
try {
console.log('Начинаем очистку базы данных...');
// Начинаем транзакцию
await client.query('BEGIN');
// Получаем список существующих таблиц
const tablesResult = await client.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
`);
const tables = tablesResult.rows.map(row => row.table_name);
console.log('Найдены таблицы:', tables.join(', '));
// Очистка таблиц в порядке, учитывающем зависимости
if (tables.includes('messages')) {
console.log('Очистка таблицы messages...');
await client.query('TRUNCATE TABLE messages CASCADE');
}
if (tables.includes('swipes')) {
console.log('Очистка таблицы swipes...');
await client.query('TRUNCATE TABLE swipes CASCADE');
}
if (tables.includes('matches')) {
console.log('Очистка таблицы matches...');
await client.query('TRUNCATE TABLE matches CASCADE');
}
if (tables.includes('profiles')) {
console.log('Очистка таблицы profiles...');
await client.query('TRUNCATE TABLE profiles CASCADE');
}
if (tables.includes('users')) {
console.log('Очистка таблицы users...');
await client.query('TRUNCATE TABLE users CASCADE');
}
// Возвращаем внешние ключи (если они использовались)
// await client.query('SET session_replication_role = \'origin\'');
// Фиксируем транзакцию
await client.query('COMMIT');
console.log('Все таблицы успешно очищены!');
} catch (error) {
// В случае ошибки откатываем транзакцию
await client.query('ROLLBACK');
console.error('Произошла ошибка при очистке базы данных:', error);
} finally {
// Освобождаем клиента
client.release();
// Закрываем пул соединений
await pool.end();
}
}
// Запускаем функцию очистки
clearDatabase();

View File

@@ -0,0 +1,26 @@
-- Скрипт для очистки всех таблиц в базе данных
-- Важно: таблицы очищаются в порядке, учитывающем зависимости между ними
-- Отключаем внешние ключи на время выполнения (если они используются)
-- SET session_replication_role = 'replica';
-- Очистка таблицы сообщений
TRUNCATE TABLE messages CASCADE;
-- Очистка таблицы просмотров профилей
TRUNCATE TABLE profile_views CASCADE;
-- Очистка таблицы свайпов
TRUNCATE TABLE swipes CASCADE;
-- Очистка таблицы матчей
TRUNCATE TABLE matches CASCADE;
-- Очистка таблицы профилей
TRUNCATE TABLE profiles CASCADE;
-- Очистка таблицы пользователей
TRUNCATE TABLE users CASCADE;
-- Возвращаем внешние ключи (если они использовались)
-- SET session_replication_role = 'origin';

View File

@@ -0,0 +1,70 @@
// Скрипт для создания таблицы profile_views
// Функция для ручного запуска создания таблицы profile_views
async function createProfileViewsTable() {
const client = await require('../database/connection').pool.connect();
try {
console.log('Создание таблицы profile_views...');
// Проверяем, существует ли уже таблица profile_views
const tableCheck = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'profile_views'
);
`);
if (tableCheck.rows[0].exists) {
console.log('Таблица profile_views уже существует, пропускаем создание');
return;
}
// Начинаем транзакцию
await client.query('BEGIN');
// Создаем таблицу profile_views
await client.query(`
CREATE TABLE profile_views (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
view_date TIMESTAMP NOT NULL DEFAULT NOW(),
view_type VARCHAR(20) NOT NULL DEFAULT 'browse'
);
`);
// Создаем индекс для быстрого поиска по паре (просмотревший - просмотренный)
await client.query(`
CREATE UNIQUE INDEX profile_views_viewer_viewed_idx ON profile_views (viewer_id, viewed_profile_id);
`);
// Индекс для быстрого поиска по viewer_id
await client.query(`
CREATE INDEX profile_views_viewer_idx ON profile_views (viewer_id);
`);
// Индекс для быстрого поиска по viewed_profile_id
await client.query(`
CREATE INDEX profile_views_viewed_idx ON profile_views (viewed_profile_id);
`);
// Фиксируем транзакцию
await client.query('COMMIT');
console.log('Таблица profile_views успешно создана!');
} catch (error) {
// В случае ошибки откатываем транзакцию
await client.query('ROLLBACK');
console.error('Произошла ошибка при создании таблицы profile_views:', error);
} finally {
// Освобождаем клиента
client.release();
}
}
// Запускаем функцию создания таблицы
createProfileViewsTable()
.then(() => console.log('Скрипт выполнен'))
.catch(err => console.error('Ошибка выполнения скрипта:', err))
.finally(() => process.exit());

View File

@@ -0,0 +1,142 @@
// Скрипт для восстановления оригинальной функциональности callbackHandlers.ts
const fs = require('fs');
const path = require('path');
// Находим самую последнюю версию файла callbackHandlers.ts в репозитории
const { execSync } = require('child_process');
try {
console.log('Поиск оригинальной версии CallbackHandlers.ts с полной функциональностью...');
// Находим коммиты, содержащие значительные изменения в файле (более 1000 символов)
const commits = execSync('git log --format="%H" -- src/handlers/callbackHandlers.ts')
.toString()
.trim()
.split('\n');
console.log(`Найдено ${commits.length} коммитов с изменениями файла`);
// Пробуем разные коммиты, начиная с последнего, чтобы найти полную реализацию
let foundFullImplementation = false;
let fullImplementationContent = '';
for (const commit of commits) {
console.log(`Проверяем коммит ${commit.substring(0, 8)}...`);
try {
const fileContent = execSync(`git show ${commit}:src/handlers/callbackHandlers.ts`).toString();
// Проверяем, содержит ли файл полные реализации методов
const hasFullImplementations = !fileContent.includes('// Заглушка метода') &&
fileContent.includes('await this.bot.sendMessage');
if (hasFullImplementations) {
console.log(`✅ Найдена полная реализация в коммите ${commit.substring(0, 8)}`);
fullImplementationContent = fileContent;
foundFullImplementation = true;
break;
} else {
console.log(`❌ Коммит ${commit.substring(0, 8)} не содержит полной реализации`);
}
} catch (error) {
console.error(`Ошибка при проверке коммита ${commit}:`, error.message);
}
}
if (!foundFullImplementation) {
console.error('❌ Не удалось найти полную реализацию в истории коммитов');
process.exit(1);
}
// Теперь получаем текущую версию файла с поддержкой уведомлений
console.log('Получаем текущую версию с поддержкой уведомлений...');
const currentFilePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts');
const currentContent = fs.readFileSync(currentFilePath, 'utf-8');
// Сначала создаем бэкап текущего файла
const backupPath = currentFilePath + '.backup-' + Date.now();
fs.writeFileSync(backupPath, currentContent);
console.log(`✅ Создан бэкап текущей версии: ${path.basename(backupPath)}`);
// Извлекаем код для поддержки уведомлений из текущей версии
console.log('Извлекаем код для поддержки уведомлений...');
// Находим импорт NotificationHandlers
const notificationImportRegex = /import\s+{\s*NotificationHandlers\s*}\s*from\s*['"]\.\/notificationHandlers['"]\s*;/;
const notificationImport = currentContent.match(notificationImportRegex)?.[0] || '';
// Находим объявление поля notificationHandlers
const notificationFieldRegex = /private\s+notificationHandlers\?\s*:\s*NotificationHandlers\s*;/;
const notificationField = currentContent.match(notificationFieldRegex)?.[0] || '';
// Находим инициализацию notificationHandlers в конструкторе
const notificationInitRegex = /\/\/\s*Создаем экземпляр NotificationHandlers[\s\S]*?try\s*{[\s\S]*?this\.notificationHandlers\s*=\s*new\s*NotificationHandlers[\s\S]*?}\s*catch[\s\S]*?}/;
const notificationInit = currentContent.match(notificationInitRegex)?.[0] || '';
// Находим метод handleNotificationSettings
const notificationSettingsMethodRegex = /async\s+handleNotificationSettings[\s\S]*?}\s*}/;
const notificationSettingsMethod = currentContent.match(notificationSettingsMethodRegex)?.[0] || '';
// Находим обработку callback для notifications в handleCallback
const notificationCallbackRegex = /\/\/\s*Настройки уведомлений[\s\S]*?else\s+if\s*\(data\s*===\s*['"]notifications['"][\s\S]*?}\s*}/;
const notificationCallback = currentContent.match(notificationCallbackRegex)?.[0] || '';
// Получаем часть обработки коллбэков для уведомлений
const notificationToggleRegex = /\/\/\s*Обработка переключения настроек уведомлений[\s\S]*?else\s+if[\s\S]*?notif_[\s\S]*?}\s*}/;
const notificationToggle = currentContent.match(notificationToggleRegex)?.[0] || '';
console.log(`✅ Извлечены блоки кода для уведомлений`);
// Интегрируем код уведомлений в оригинальную версию
console.log('Интегрируем код уведомлений в оригинальную версию...');
// 1. Добавляем импорт
let newContent = fullImplementationContent;
if (notificationImport) {
newContent = newContent.replace(/import\s*{[^}]*}\s*from\s*['"]\.\/messageHandlers['"]\s*;/,
match => match + '\n' + notificationImport);
}
// 2. Добавляем объявление поля
if (notificationField) {
newContent = newContent.replace(/private\s+translationController\s*:\s*TranslationController\s*;/,
match => match + '\n ' + notificationField);
}
// 3. Добавляем инициализацию в конструкторе
if (notificationInit) {
newContent = newContent.replace(/this\.translationController\s*=\s*new\s*TranslationController\(\);/,
match => match + '\n ' + notificationInit);
}
// 4. Добавляем обработку коллбэков для уведомлений
if (notificationCallback) {
newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/,
match => notificationCallback + '\n ' + match);
}
// 5. Добавляем обработку переключения настроек уведомлений
if (notificationToggle) {
newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/,
match => notificationToggle + '\n ' + match);
}
// 6. Добавляем метод handleNotificationSettings в конец класса
if (notificationSettingsMethod) {
newContent = newContent.replace(/}(\s*)$/, notificationSettingsMethod + '\n}$1');
}
// Сохраняем обновленный файл
const outputPath = currentFilePath + '.fixed';
fs.writeFileSync(outputPath, newContent);
console.log(`✅ Создана исправленная версия файла: ${path.basename(outputPath)}`);
console.log('\nИнструкция по восстановлению:');
console.log(`1. Проверьте файл ${path.basename(outputPath)}`);
console.log('2. Если все выглядит правильно, выполните команду:');
console.log(` Move-Item -Force "${path.basename(outputPath)}" "${path.basename(currentFilePath)}"`);
console.log('3. Перезапустите бота');
} catch (error) {
console.error('Произошла ошибка:', error);
}

View File

@@ -0,0 +1,170 @@
// Скрипт для исправления проблемы с ботом
require('dotenv').config();
const { Pool } = require('pg');
// Получаем данные подключения из .env
console.log('Параметры подключения к БД:');
console.log('DB_USERNAME:', process.env.DB_USERNAME);
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_NAME:', process.env.DB_NAME);
console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '[указан]' : '[не указан]');
console.log('DB_PORT:', process.env.DB_PORT);
// Создаем пул соединений
const pool = new Pool({
user: process.env.DB_USERNAME,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432')
});
async function fixDatabase() {
try {
console.log('Начинаем исправление базы данных...');
// Проверяем существование таблицы users
const tableResult = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
);
`);
if (!tableResult.rows[0].exists) {
console.error('Таблица users не найдена!');
return;
}
console.log('✅ Таблица users существует');
// Проверяем и добавляем столбцы state и state_data, если они отсутствуют
console.log('Проверяем наличие столбцов state и state_data...');
const stateColumnResult = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'users'
AND column_name = 'state'
);
`);
const stateDataColumnResult = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'users'
AND column_name = 'state_data'
);
`);
if (!stateColumnResult.rows[0].exists) {
console.log('🔄 Добавляем столбец state...');
await pool.query(`ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL;`);
console.log('✅ Столбец state успешно добавлен');
} else {
console.log('✅ Столбец state уже существует');
}
if (!stateDataColumnResult.rows[0].exists) {
console.log('🔄 Добавляем столбец state_data...');
await pool.query(`ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb;`);
console.log('✅ Столбец state_data успешно добавлен');
} else {
console.log('✅ Столбец state_data уже существует');
}
// Проверка наличия таблиц для уведомлений
console.log('Проверяем наличие таблиц для уведомлений...');
const tablesCheck = await Promise.all([
checkTableExists('notifications'),
checkTableExists('notification_settings'),
checkTableExists('scheduled_notifications')
]);
// Создаем отсутствующие таблицы
if (!tablesCheck[0]) {
console.log('🔄 Создаем таблицу notifications...');
await pool.query(`
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
is_read BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
CREATE INDEX idx_notifications_type ON notifications(type);
CREATE INDEX idx_notifications_created_at ON notifications(created_at);
`);
console.log('✅ Таблица notifications успешно создана');
}
if (!tablesCheck[1]) {
console.log('🔄 Создаем таблицу notification_settings...');
await pool.query(`
CREATE TABLE notification_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
new_matches BOOLEAN DEFAULT true,
new_messages BOOLEAN DEFAULT true,
new_likes BOOLEAN DEFAULT true,
reminders BOOLEAN DEFAULT true,
daily_summary BOOLEAN DEFAULT false,
time_preference VARCHAR(20) DEFAULT 'evening',
do_not_disturb BOOLEAN DEFAULT false,
do_not_disturb_start TIME,
do_not_disturb_end TIME,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
console.log('✅ Таблица notification_settings успешно создана');
}
if (!tablesCheck[2]) {
console.log('🔄 Создаем таблицу scheduled_notifications...');
await pool.query(`
CREATE TABLE scheduled_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
processed BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications(user_id);
CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at);
CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications(processed);
`);
console.log('✅ Таблица scheduled_notifications успешно создана');
}
console.log('✅ Исправление базы данных завершено успешно');
} catch (error) {
console.error('Ошибка при исправлении базы данных:', error);
} finally {
await pool.end();
}
}
async function checkTableExists(tableName) {
const result = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
);
`, [tableName]);
const exists = result.rows[0].exists;
console.log(`${exists ? '✅' : '❌'} Таблица ${tableName} ${exists ? 'существует' : 'отсутствует'}`);
return exists;
}
fixDatabase();

View File

@@ -0,0 +1,48 @@
/**
* Комплексный скрипт для исправления всех проблем с уведомлениями
* Запускает последовательно оба скрипта исправления
*/
const { exec } = require('child_process');
const path = require('path');
console.log('🔧 Запуск комплексного исправления проблем с уведомлениями...');
// Путь к скриптам
const fixNotificationCallbacksScript = path.join(__dirname, 'fix_notification_callbacks.js');
const updateBotWithNotificationsScript = path.join(__dirname, 'update_bot_with_notifications.js');
// Запуск первого скрипта для исправления таблиц и колонок
console.log('\n📊 Шаг 1/2: Проверка и исправление таблиц базы данных...');
exec(`node ${fixNotificationCallbacksScript}`, (error, stdout, stderr) => {
if (error) {
console.error(`❌ Ошибка при запуске скрипта исправления таблиц: ${error}`);
return;
}
console.log(stdout);
if (stderr) {
console.error(`❌ Ошибки при выполнении скрипта: ${stderr}`);
}
// Запуск второго скрипта для обновления bot.ts
console.log('\n📝 Шаг 2/2: Обновление файла bot.ts для регистрации обработчиков уведомлений...');
exec(`node ${updateBotWithNotificationsScript}`, (error2, stdout2, stderr2) => {
if (error2) {
console.error(`❌ Ошибка при запуске скрипта обновления bot.ts: ${error2}`);
return;
}
console.log(stdout2);
if (stderr2) {
console.error(`❌ Ошибки при выполнении скрипта: ${stderr2}`);
}
console.log('\n✅ Все исправления успешно выполнены!');
console.log('🔄 Пожалуйста, перезапустите бота для применения изменений:');
console.log(' npm run start');
console.log('\n💡 Уведомления должны теперь работать корректно!');
});
});

View File

@@ -0,0 +1,332 @@
/**
* Скрипт для проверки и исправления проблем с обработчиками уведомлений в боте
*/
const { Client } = require('pg');
const fs = require('fs');
// Конфигурация базы данных
const dbConfig = {
host: 'localhost',
port: 5432,
database: 'telegram_tinder',
user: 'postgres',
password: 'postgres'
};
// Подключение к базе данных
const client = new Client(dbConfig);
async function main() {
try {
console.log('Подключение к базе данных...');
await client.connect();
console.log('Успешно подключено к базе данных');
// Шаг 1: Проверка существования необходимых таблиц для уведомлений
console.log('\n=== Проверка таблиц для уведомлений ===');
// Проверяем таблицу notifications
let notificationsTableExists = await checkTableExists('notifications');
if (!notificationsTableExists) {
console.log('Таблица notifications не найдена. Создаем...');
await createNotificationsTable();
console.log('Таблица notifications успешно создана');
} else {
console.log('Таблица notifications уже существует');
}
// Проверяем таблицу scheduled_notifications
let scheduledNotificationsTableExists = await checkTableExists('scheduled_notifications');
if (!scheduledNotificationsTableExists) {
console.log('Таблица scheduled_notifications не найдена. Создаем...');
await createScheduledNotificationsTable();
console.log('Таблица scheduled_notifications успешно создана');
} else {
console.log('Таблица scheduled_notifications уже существует');
}
// Проверяем таблицу notification_templates
let notificationTemplatesTableExists = await checkTableExists('notification_templates');
if (!notificationTemplatesTableExists) {
console.log('Таблица notification_templates не найдена. Создаем...');
await createNotificationTemplatesTable();
console.log('Таблица notification_templates успешно создана');
console.log('Заполняем таблицу базовыми шаблонами...');
await populateDefaultTemplates();
console.log('Шаблоны успешно добавлены');
} else {
console.log('Таблица notification_templates уже существует');
}
// Шаг 2: Проверка существования столбца notification_settings в таблице users
console.log('\n=== Проверка столбца notification_settings в таблице users ===');
const notificationSettingsColumnExists = await checkColumnExists('users', 'notification_settings');
if (!notificationSettingsColumnExists) {
console.log('Столбец notification_settings не найден. Добавляем...');
await addNotificationSettingsColumn();
console.log('Столбец notification_settings успешно добавлен');
} else {
console.log('Столбец notification_settings уже существует');
}
// Шаг 3: Проверка существования столбцов state и state_data в таблице users
console.log('\n=== Проверка столбцов state и state_data в таблице users ===');
const stateColumnExists = await checkColumnExists('users', 'state');
if (!stateColumnExists) {
console.log('Столбец state не найден. Добавляем...');
await addStateColumn();
console.log('Столбец state успешно добавлен');
} else {
console.log('Столбец state уже существует');
}
const stateDataColumnExists = await checkColumnExists('users', 'state_data');
if (!stateDataColumnExists) {
console.log('Столбец state_data не найден. Добавляем...');
await addStateDataColumn();
console.log('Столбец state_data успешно добавлен');
} else {
console.log('Столбец state_data уже существует');
}
console.log('\nВсе таблицы и столбцы успешно проверены и созданы при необходимости.');
console.log('Механизм уведомлений должен работать корректно.');
console.log('\n=== Проверка регистрации обработчиков уведомлений ===');
console.log('Подсказка: убедитесь, что в файле bot.ts создается экземпляр NotificationHandlers и регистрируются его обработчики:');
console.log(`
// Настройка обработчиков уведомлений
const notificationHandlers = new NotificationHandlers(bot);
notificationHandlers.register();
// Запуск обработчика запланированных уведомлений
setInterval(() => {
const notificationService = new NotificationService(bot);
notificationService.processScheduledNotifications();
}, 60000); // Проверяем каждую минуту
`);
} catch (error) {
console.error('Ошибка выполнения скрипта:', error);
} finally {
await client.end();
console.log('\nСоединение с базой данных закрыто.');
}
}
async function checkTableExists(tableName) {
const query = `
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = $1
) as exists
`;
const result = await client.query(query, [tableName]);
return result.rows[0].exists;
}
async function checkColumnExists(tableName, columnName) {
const query = `
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = $1 AND column_name = $2
) as exists
`;
const result = await client.query(query, [tableName, columnName]);
return result.rows[0].exists;
}
async function createNotificationsTable() {
await client.query(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
}
async function createScheduledNotificationsTable() {
await client.query(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE scheduled_notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
scheduled_at TIMESTAMP NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
}
async function createNotificationTemplatesTable() {
await client.query(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE notification_templates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type VARCHAR(50) NOT NULL UNIQUE,
title TEXT NOT NULL,
message_template TEXT NOT NULL,
button_template JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
`);
}
async function addNotificationSettingsColumn() {
await client.query(`
ALTER TABLE users
ADD COLUMN notification_settings JSONB DEFAULT '{
"newMatches": true,
"newMessages": true,
"newLikes": true,
"reminders": true,
"dailySummary": true,
"timePreference": "evening",
"doNotDisturb": false
}'::jsonb
`);
}
async function addStateColumn() {
await client.query(`
ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL
`);
}
async function addStateDataColumn() {
await client.query(`
ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb
`);
}
async function populateDefaultTemplates() {
const templates = [
{
type: 'new_like',
title: 'Новый лайк!',
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
[
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
],
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
]
}
},
{
type: 'super_like',
title: 'Супер-лайк!',
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
[
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
],
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
]
}
},
{
type: 'new_match',
title: 'Новый матч!',
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
button_template: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }],
[
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
{ text: '📋 Все матчи', callback_data: 'view_matches' }
]
]
}
},
{
type: 'new_message',
title: 'Новое сообщение!',
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
button_template: {
inline_keyboard: [
[{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }],
[
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
{ text: '📋 Все чаты', callback_data: 'view_matches' }
]
]
}
},
{
type: 'match_reminder',
title: 'Напоминание о матче',
message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!',
button_template: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }],
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }]
]
}
},
{
type: 'inactive_matches',
title: 'Неактивные матчи',
message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!',
button_template: {
inline_keyboard: [
[{ text: '📋 Открыть матчи', callback_data: 'view_matches' }],
[{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }]
]
}
},
{
type: 'like_summary',
title: 'Сводка лайков',
message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!',
button_template: {
inline_keyboard: [
[{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }],
[{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }]
]
}
}
];
for (const template of templates) {
await client.query(`
INSERT INTO notification_templates (type, title, message_template, button_template)
VALUES ($1, $2, $3, $4)
ON CONFLICT (type) DO UPDATE
SET title = EXCLUDED.title,
message_template = EXCLUDED.message_template,
button_template = EXCLUDED.button_template,
updated_at = NOW()
`, [
template.type,
template.title,
template.message_template,
JSON.stringify(template.button_template)
]);
}
}
// Запуск скрипта
main();

View File

@@ -0,0 +1,85 @@
// Скрипт для проверки работы callback-хэндлеров и уведомлений
require('dotenv').config();
const TelegramBot = require('node-telegram-bot-api');
const { Pool } = require('pg');
// Создаем пул соединений
const pool = new Pool({
user: process.env.DB_USERNAME,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432')
});
// Функция для имитации callback-запроса к боту
async function testCallback() {
try {
console.log('Начинаем тестирование callback-хэндлеров и уведомлений...');
// Используем последнего пользователя из базы данных
const userResult = await pool.query(`
SELECT * FROM users ORDER BY last_active_at DESC NULLS LAST LIMIT 1
`);
if (userResult.rows.length === 0) {
console.error('❌ Пользователи не найдены в базе данных');
return;
}
const user = userResult.rows[0];
console.log(`Выбран тестовый пользователь: ${user.first_name || 'Без имени'} (ID: ${user.telegram_id})`);
// Получаем токен бота из переменных окружения
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
console.error('❌ Токен бота не найден в переменных окружения');
return;
}
// Создаем экземпляр бота
const bot = new TelegramBot(token);
// Отправляем тестовое уведомление пользователю
console.log(`Отправляем тестовое уведомление пользователю ID: ${user.telegram_id}...`);
try {
const result = await bot.sendMessage(
user.telegram_id,
`🔔 *Тестовое уведомление*\n\nЭто проверка работы уведомлений и callback-хэндлеров.\n\nВаш премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`,
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '🔔 Уведомления', callback_data: 'notification_settings' },
{ text: '❤️ Профиль', callback_data: 'view_profile' }
],
[
{ text: '⚙️ Настройки', callback_data: 'settings' }
]
]
}
}
);
console.log('✅ Тестовое сообщение успешно отправлено!');
console.log('Информация о сообщении:', JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ Ошибка при отправке тестового сообщения:', error.message);
if (error.response && error.response.body) {
console.error('Детали ошибки:', JSON.stringify(error.response.body, null, 2));
}
}
} catch (error) {
console.error('❌ Ошибка при тестировании:', error);
} finally {
await pool.end();
console.log('Соединение с базой данных закрыто');
console.log('Тестирование завершено!');
}
}
// Запускаем тестирование
testCallback();

View File

@@ -0,0 +1,102 @@
require('dotenv').config();
const { MatchingService } = require('../dist/services/matchingService');
const { ProfileService } = require('../dist/services/profileService');
// Функция для создания тестовых пользователей
async function createTestUsers() {
const profileService = new ProfileService();
console.log('Создание тестовых пользователей...');
// Создаем мужской профиль
const maleUserId = await profileService.ensureUser('123456', {
username: 'test_male',
first_name: 'Иван',
last_name: 'Тестов'
});
await profileService.createProfile(maleUserId, {
name: 'Иван',
age: 30,
gender: 'male',
interestedIn: 'female',
bio: 'Тестовый мужской профиль',
photos: ['photo1.jpg'],
city: 'Москва',
searchPreferences: {
minAge: 18,
maxAge: 45,
maxDistance: 50
}
});
console.log(`Создан мужской профиль: userId=${maleUserId}, telegramId=123456`);
// Создаем женский профиль
const femaleUserId = await profileService.ensureUser('654321', {
username: 'test_female',
first_name: 'Анна',
last_name: 'Тестова'
});
await profileService.createProfile(femaleUserId, {
name: 'Анна',
age: 28,
gender: 'female',
interestedIn: 'male',
bio: 'Тестовый женский профиль',
photos: ['photo2.jpg'],
city: 'Москва',
searchPreferences: {
minAge: 25,
maxAge: 40,
maxDistance: 30
}
});
console.log(`Создан женский профиль: userId=${femaleUserId}, telegramId=654321`);
console.log('Тестовые пользователи созданы успешно');
}
// Функция для тестирования подбора анкет
async function testMatching() {
console.log('\n===== ТЕСТИРОВАНИЕ ПОДБОРА АНКЕТ =====');
const matchingService = new MatchingService();
console.log('\nТест 1: Получение анкеты для мужского профиля (должна вернуться женская анкета)');
const femaleProfile = await matchingService.getNextCandidate('123456', true);
if (femaleProfile) {
console.log(`✓ Получена анкета: ${femaleProfile.name}, возраст: ${femaleProfile.age}, пол: ${femaleProfile.gender}`);
} else {
console.log('✗ Анкета не найдена');
}
console.log('\nТест 2: Получение анкеты для женского профиля (должна вернуться мужская анкета)');
const maleProfile = await matchingService.getNextCandidate('654321', true);
if (maleProfile) {
console.log(`✓ Получена анкета: ${maleProfile.name}, возраст: ${maleProfile.age}, пол: ${maleProfile.gender}`);
} else {
console.log('✗ Анкета не найдена');
}
console.log('\n===== ТЕСТИРОВАНИЕ ЗАВЕРШЕНО =====\n');
// Завершение работы скрипта
process.exit(0);
}
// Главная функция
async function main() {
try {
// Создаем тестовых пользователей
await createTestUsers();
// Тестируем подбор анкет
await testMatching();
} catch (error) {
console.error('Ошибка при выполнении тестов:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,98 @@
// Тестирование работы с таблицей profile_views
const { Pool } = require('pg');
require('dotenv').config();
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
});
// Функция для тестирования записи просмотра профиля
async function testRecordProfileView(viewerId, viewedProfileId) {
const client = await pool.connect();
try {
console.log(`Запись просмотра профиля: ${viewerId} просмотрел ${viewedProfileId}`);
// Получаем UUID пользователей
const viewerResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewerId]);
if (viewerResult.rows.length === 0) {
console.log(`Пользователь с telegram_id ${viewerId} не найден, создаём нового пользователя`);
const newUserResult = await client.query(`
INSERT INTO users (telegram_id, username, first_name, last_name)
VALUES ($1, $2, $3, $4) RETURNING id
`, [viewerId, `user_${viewerId}`, `Имя ${viewerId}`, `Фамилия ${viewerId}`]);
var viewerUuid = newUserResult.rows[0].id;
} else {
var viewerUuid = viewerResult.rows[0].id;
}
const viewedResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewedProfileId]);
if (viewedResult.rows.length === 0) {
console.log(`Пользователь с telegram_id ${viewedProfileId} не найден, создаём нового пользователя`);
const newUserResult = await client.query(`
INSERT INTO users (telegram_id, username, first_name, last_name)
VALUES ($1, $2, $3, $4) RETURNING id
`, [viewedProfileId, `user_${viewedProfileId}`, `Имя ${viewedProfileId}`, `Фамилия ${viewedProfileId}`]);
var viewedUuid = newUserResult.rows[0].id;
} else {
var viewedUuid = viewedResult.rows[0].id;
}
console.log(`UUID просматривающего: ${viewerUuid}`);
console.log(`UUID просматриваемого: ${viewedUuid}`);
// Записываем просмотр
await client.query(`
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type, view_date)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE
SET view_date = NOW(), view_type = $3
`, [viewerUuid, viewedUuid, 'browse']);
console.log('Просмотр профиля успешно записан');
// Получаем список просмотренных профилей
const viewedProfiles = await client.query(`
SELECT v.viewed_profile_id, v.view_date, u.telegram_id
FROM profile_views v
JOIN users u ON u.id = v.viewed_profile_id
WHERE v.viewer_id = $1
ORDER BY v.view_date DESC
`, [viewerUuid]);
console.log('Список просмотренных профилей:');
viewedProfiles.rows.forEach((row, index) => {
console.log(`${index + 1}. ID: ${row.telegram_id}, просмотрен: ${row.view_date}`);
});
return true;
} catch (error) {
console.error('Ошибка записи просмотра профиля:', error);
return false;
} finally {
client.release();
}
}
// Запускаем тест
async function runTest() {
try {
// Тестируем запись просмотра профиля
await testRecordProfileView(123456, 789012);
await testRecordProfileView(123456, 345678);
await testRecordProfileView(789012, 123456);
console.log('Тесты завершены успешно');
} catch (error) {
console.error('Ошибка при выполнении тестов:', error);
} finally {
await pool.end();
}
}
runTest();

View File

@@ -0,0 +1,81 @@
// Скрипт для тестирования метода checkPremiumStatus
require('dotenv').config();
const { Pool } = require('pg');
// Создаем пул соединений
const pool = new Pool({
user: process.env.DB_USERNAME,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432')
});
async function testCheckPremiumMethod() {
try {
console.log('Тестирование метода checkPremiumStatus...');
// Получаем пользователя для тестирования
const userResult = await pool.query(`
SELECT id, telegram_id, first_name, username, premium
FROM users
ORDER BY last_active_at DESC NULLS LAST
LIMIT 1
`);
if (userResult.rows.length === 0) {
console.error('❌ Пользователи не найдены в базе данных');
return;
}
const user = userResult.rows[0];
console.log(`Выбран тестовый пользователь: ${user.first_name || user.username || 'Без имени'} (Telegram ID: ${user.telegram_id})`);
console.log(`Текущий премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`);
// Проверка работы метода checkPremiumStatus
console.log('\nЭмулируем вызов метода checkPremiumStatus из vipService:');
const result = await pool.query(`
SELECT id, premium
FROM users
WHERE telegram_id = $1
`, [user.telegram_id]);
if (result.rows.length === 0) {
console.log('❌ Пользователь не найден');
} else {
const isPremium = result.rows[0].premium || false;
console.log(`Результат метода: isPremium = ${isPremium ? '✅ true' : '❌ false'}`);
if (!isPremium) {
console.log('\nПремиум-статус отсутствует. Устанавливаем премиум...');
await pool.query(`
UPDATE users
SET premium = true
WHERE telegram_id = $1
`, [user.telegram_id]);
// Проверяем обновление
const updatedResult = await pool.query(`
SELECT premium
FROM users
WHERE telegram_id = $1
`, [user.telegram_id]);
const updatedPremium = updatedResult.rows[0].premium;
console.log(`Обновленный статус: isPremium = ${updatedPremium ? '✅ true' : '❌ false'}`);
}
}
console.log('\n✅ Тестирование завершено');
console.log('🔧 Теперь проверьте функциональность VIP поиска в боте');
} catch (error) {
console.error('❌ Ошибка при тестировании:', error);
} finally {
await pool.end();
console.log('Соединение с базой данных закрыто');
}
}
// Запускаем тест
testCheckPremiumMethod();

View File

@@ -0,0 +1,75 @@
// Скрипт для тестирования VIP функционала
require('dotenv').config();
const { Pool } = require('pg');
// Создаем пул соединений
const pool = new Pool({
user: process.env.DB_USERNAME,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432')
});
async function testVipStatus() {
try {
console.log('Тестирование функционала VIP статуса...');
// Получаем список пользователей с информацией о premium статусе
const users = await pool.query(`
SELECT id, telegram_id, username, first_name, premium
FROM users
ORDER BY last_active_at DESC
LIMIT 5
`);
console.log('Список пользователей и их премиум статус:');
users.rows.forEach(user => {
console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`);
});
// Если premium у всех false, устанавливаем premium = true
const nonPremiumUsers = users.rows.filter(user => !user.premium);
if (nonPremiumUsers.length > 0) {
console.log('\nОбнаружены пользователи без премиум статуса. Устанавливаем премиум...');
for (const user of nonPremiumUsers) {
await pool.query(`
UPDATE users
SET premium = true
WHERE id = $1
RETURNING id, telegram_id, premium
`, [user.id]);
console.log(`✅ Установлен премиум для пользователя ${user.first_name || user.username || user.telegram_id}`);
}
} else {
console.log('\nВсе пользователи уже имеют премиум-статус!');
}
// Проверяем результат
const updatedUsers = await pool.query(`
SELECT id, telegram_id, username, first_name, premium
FROM users
ORDER BY last_active_at DESC
LIMIT 5
`);
console.log('\nОбновленный список пользователей и их премиум статус:');
updatedUsers.rows.forEach(user => {
console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`);
});
console.log('\n✅ Тестирование VIP функционала завершено');
console.log('🔧 Проверьте доступность VIP поиска в боте через меню или команды');
} catch (error) {
console.error('❌ Ошибка при тестировании VIP статуса:', error);
} finally {
await pool.end();
console.log('Соединение с базой данных закрыто');
}
}
// Запускаем тест
testVipStatus();

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

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

16
scripts/startup.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# startup.sh - Script to run migrations and start the bot
echo "🚀 Starting Telegram Tinder Bot..."
# Wait for database to be ready
echo "⏳ Waiting for database to be ready..."
sleep 5
# Run database migrations
echo "🔄 Running database migrations..."
node dist/database/migrateOnStartup.js
# Start the bot
echo "✅ Starting the bot..."
node dist/bot.js

View File

@@ -0,0 +1,104 @@
/**
* Скрипт для проверки и исправления регистрации NotificationHandlers в bot.ts
*/
const fs = require('fs');
const path = require('path');
const botFilePath = path.join(__dirname, '../src/bot.ts');
// Проверка существования файла bot.ts
if (!fs.existsSync(botFilePath)) {
console.error(`❌ Файл ${botFilePath} не найден`);
process.exit(1);
}
// Чтение содержимого файла bot.ts
let botContent = fs.readFileSync(botFilePath, 'utf8');
// Проверка импорта NotificationHandlers
if (!botContent.includes('import { NotificationHandlers }')) {
console.log('Добавляем импорт NotificationHandlers в bot.ts...');
// Находим последний импорт
const importRegex = /^import.*?;/gms;
const matches = [...botContent.matchAll(importRegex)];
if (matches.length > 0) {
const lastImport = matches[matches.length - 1][0];
const lastImportIndex = botContent.lastIndexOf(lastImport) + lastImport.length;
// Добавляем импорт NotificationHandlers
botContent =
botContent.slice(0, lastImportIndex) +
'\nimport { NotificationHandlers } from \'./handlers/notificationHandlers\';\n' +
botContent.slice(lastImportIndex);
console.log('✅ Импорт NotificationHandlers добавлен');
} else {
console.error('❌ Не удалось найти место для добавления импорта');
}
}
// Проверка объявления NotificationHandlers в классе
if (!botContent.includes('private notificationHandlers')) {
console.log('Добавляем объявление notificationHandlers в класс...');
const classPropertiesRegex = /class TelegramTinderBot {([^}]+?)constructor/s;
const classPropertiesMatch = botContent.match(classPropertiesRegex);
if (classPropertiesMatch) {
const classProperties = classPropertiesMatch[1];
const updatedProperties = classProperties + ' private notificationHandlers: NotificationHandlers;\n ';
botContent = botContent.replace(classPropertiesRegex, `class TelegramTinderBot {${updatedProperties}constructor`);
console.log('✅ Объявление notificationHandlers добавлено');
} else {
console.error('❌ Не удалось найти место для добавления объявления notificationHandlers');
}
}
// Проверка создания экземпляра NotificationHandlers в конструкторе
if (!botContent.includes('this.notificationHandlers = new NotificationHandlers')) {
console.log('Добавляем инициализацию notificationHandlers в конструктор...');
const initializationRegex = /(this\.callbackHandlers = new CallbackHandlers[^;]+;)/;
const initializationMatch = botContent.match(initializationRegex);
if (initializationMatch) {
const callbackHandlersInit = initializationMatch[1];
const updatedInit = callbackHandlersInit + '\n this.notificationHandlers = new NotificationHandlers(this.bot);';
botContent = botContent.replace(initializationRegex, updatedInit);
console.log('✅ Инициализация notificationHandlers добавлена');
} else {
console.error('❌ Не удалось найти место для добавления инициализации notificationHandlers');
}
}
// Проверка регистрации notificationHandlers в методе registerHandlers
if (!botContent.includes('this.notificationHandlers.register()')) {
console.log('Добавляем регистрацию notificationHandlers...');
const registerHandlersRegex = /(private registerHandlers\(\): void {[^}]+?)}/s;
const registerHandlersMatch = botContent.match(registerHandlersRegex);
if (registerHandlersMatch) {
const registerHandlersBody = registerHandlersMatch[1];
const updatedBody = registerHandlersBody + '\n // Обработчики уведомлений\n this.notificationHandlers.register();\n }';
botContent = botContent.replace(registerHandlersRegex, updatedBody);
console.log('✅ Регистрация notificationHandlers добавлена');
} else {
console.error('❌ Не удалось найти место для добавления регистрации notificationHandlers');
}
}
// Запись обновленного содержимого в файл
fs.writeFileSync(botFilePath, botContent, 'utf8');
console.log('✅ Файл bot.ts успешно обновлен');
console.log('🔔 Перезапустите бота для применения изменений');

2
sql/add_looking_for.sql Normal file
View File

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

View File

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

View File

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

21
sql/add_updated_at.sql Normal file
View File

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

404
sql/consolidated.sql Normal file
View File

@@ -0,0 +1,404 @@
# Consolidated SQL файл для миграции базы данных Telegram Tinder Bot
# Этот файл содержит все необходимые SQL-запросы для создания базы данных с нуля
-- Создание расширения для UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Создание перечислений
CREATE TYPE gender_type AS ENUM ('male', 'female', 'other');
CREATE TYPE swipe_action AS ENUM ('like', 'dislike', 'superlike');
-- Создание таблицы пользователей
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) NOT NULL,
last_name VARCHAR(255),
language_code VARCHAR(10),
is_premium BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Создание таблицы профилей
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),
gender gender_type NOT NULL,
bio TEXT,
photos TEXT[], -- JSON array of photo file_ids
location VARCHAR(255),
job VARCHAR(255),
interests TEXT[], -- JSON array of interests
last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_profile UNIQUE (user_id)
);
-- Создание индекса для поиска по возрасту и полу
CREATE INDEX idx_profiles_age_gender ON profiles(age, gender);
-- Создание таблицы предпочтений поиска
CREATE TABLE IF NOT EXISTS search_preferences (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
age_min INTEGER NOT NULL DEFAULT 18 CHECK (age_min >= 18),
age_max INTEGER NOT NULL DEFAULT 99 CHECK (age_max >= age_min),
looking_for gender_type NOT NULL,
distance_max INTEGER, -- max distance in km, NULL means no limit
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_preferences UNIQUE (user_id)
);
-- Создание таблицы действий (лайки/дизлайки)
CREATE TABLE IF NOT EXISTS swipes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
action swipe_action NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_swipe UNIQUE (user_id, target_id)
);
-- Создание индекса для быстрого поиска матчей
CREATE INDEX idx_swipes_user_target ON swipes(user_id, target_id);
-- Создание таблицы матчей
CREATE TABLE IF NOT EXISTS matches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id_1 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id_2 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
CONSTRAINT unique_match UNIQUE (user_id_1, user_id_2)
);
-- Создание индекса для быстрого поиска матчей по пользователю
CREATE INDEX idx_matches_user_id_1 ON matches(user_id_1);
CREATE INDEX idx_matches_user_id_2 ON matches(user_id_2);
-- Создание таблицы блокировок
CREATE TABLE IF NOT EXISTS blocks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_block UNIQUE (blocker_id, blocked_id)
);
-- Создание таблицы сообщений
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
match_id UUID NOT NULL REFERENCES matches(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
text TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Создание индекса для быстрого поиска сообщений
CREATE INDEX idx_messages_match_id ON messages(match_id);
-- Создание таблицы уведомлений
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL, -- new_match, new_message, etc.
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
reference_id UUID, -- Can reference a match_id, message_id, etc.
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Создание индекса для быстрого поиска уведомлений
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
-- Создание таблицы настроек
CREATE TABLE IF NOT EXISTS settings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
notifications_enabled BOOLEAN DEFAULT TRUE,
show_online_status BOOLEAN DEFAULT TRUE,
visibility BOOLEAN DEFAULT TRUE, -- whether profile is visible in search
theme VARCHAR(20) DEFAULT 'light',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_settings UNIQUE (user_id)
);
-- Создание таблицы просмотров профиля
CREATE TABLE IF NOT EXISTS profile_views (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
viewed_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
view_count INTEGER DEFAULT 1,
last_viewed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_view UNIQUE (viewer_id, viewed_id)
);
-- Создание таблицы для премиум-пользователей
CREATE TABLE IF NOT EXISTS premium_features (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
is_premium BOOLEAN DEFAULT FALSE,
superlike_quota INTEGER DEFAULT 1,
spotlight_quota INTEGER DEFAULT 0,
see_likes BOOLEAN DEFAULT FALSE, -- Can see who liked their profile
unlimited_likes BOOLEAN DEFAULT FALSE,
expires_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_premium 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;
-- Триггеры для обновления поля updated_at
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_search_preferences_updated_at
BEFORE UPDATE ON search_preferences
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_settings_updated_at
BEFORE UPDATE ON settings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_premium_features_updated_at
BEFORE UPDATE ON premium_features
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
-- Индекс для поиска пользователей по Telegram ID (часто используемый запрос)
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
-- Индекс для статуса профиля (активный/неактивный, завершенный/незавершенный)
CREATE INDEX IF NOT EXISTS idx_profiles_is_completed ON profiles(is_completed);
-- Представление для статистики
CREATE OR REPLACE VIEW user_statistics AS
SELECT
u.id,
u.telegram_id,
(SELECT COUNT(*) FROM swipes WHERE user_id = u.id AND action = 'like') AS likes_given,
(SELECT COUNT(*) FROM swipes WHERE user_id = u.id AND action = 'dislike') AS dislikes_given,
(SELECT COUNT(*) FROM swipes WHERE target_id = u.id AND action = 'like') AS likes_received,
(SELECT COUNT(*) FROM matches WHERE user_id_1 = u.id OR user_id_2 = u.id) AS matches_count,
(SELECT COUNT(*) FROM messages WHERE sender_id = u.id) AS messages_sent,
(SELECT COUNT(*) FROM profile_views WHERE viewed_id = u.id) AS profile_views
FROM users u;
-- Функция для создания матча при взаимных лайках
CREATE OR REPLACE FUNCTION create_match_on_mutual_like()
RETURNS TRIGGER AS $$
DECLARE
reverse_like_exists BOOLEAN;
BEGIN
-- Check if there is a reverse like
SELECT EXISTS (
SELECT 1
FROM swipes
WHERE user_id = NEW.target_id
AND target_id = NEW.user_id
AND action = 'like'
) INTO reverse_like_exists;
-- If there is a reverse like, create a match
IF reverse_like_exists AND NEW.action = 'like' THEN
INSERT INTO matches (user_id_1, user_id_2)
VALUES (
LEAST(NEW.user_id, NEW.target_id),
GREATEST(NEW.user_id, NEW.target_id)
)
ON CONFLICT (user_id_1, user_id_2) DO NOTHING;
-- Create notifications for both users
INSERT INTO notifications (user_id, type, content, reference_id)
VALUES (
NEW.user_id,
'new_match',
'У вас новый матч!',
(SELECT id FROM matches WHERE
(user_id_1 = LEAST(NEW.user_id, NEW.target_id) AND user_id_2 = GREATEST(NEW.user_id, NEW.target_id))
)
);
INSERT INTO notifications (user_id, type, content, reference_id)
VALUES (
NEW.target_id,
'new_match',
'У вас новый матч!',
(SELECT id FROM matches WHERE
(user_id_1 = LEAST(NEW.user_id, NEW.target_id) AND user_id_2 = GREATEST(NEW.user_id, NEW.target_id))
)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Триггер для создания матча при взаимных лайках
CREATE TRIGGER create_match_trigger
AFTER INSERT ON swipes
FOR EACH ROW
EXECUTE FUNCTION create_match_on_mutual_like();
-- Функция для создания уведомления о новом сообщении
CREATE OR REPLACE FUNCTION notify_new_message()
RETURNS TRIGGER AS $$
DECLARE
recipient_id UUID;
match_record RECORD;
BEGIN
-- Get the match record
SELECT * INTO match_record FROM matches WHERE id = NEW.match_id;
-- Determine the recipient
IF match_record.user_id_1 = NEW.sender_id THEN
recipient_id := match_record.user_id_2;
ELSE
recipient_id := match_record.user_id_1;
END IF;
-- Create notification
INSERT INTO notifications (user_id, type, content, reference_id)
VALUES (
recipient_id,
'new_message',
'У вас новое сообщение!',
NEW.id
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Триггер для создания уведомления о новом сообщении
CREATE TRIGGER notify_new_message_trigger
AFTER INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION notify_new_message();
-- Функция для обновления времени последней активности пользователя
CREATE OR REPLACE FUNCTION update_last_active()
RETURNS TRIGGER AS $$
BEGIN
UPDATE profiles
SET last_active = CURRENT_TIMESTAMP
WHERE user_id = NEW.user_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Триггер для обновления времени последней активности при свайпах
CREATE TRIGGER update_last_active_on_swipe
AFTER INSERT ON swipes
FOR EACH ROW
EXECUTE FUNCTION update_last_active();
-- Триггер для обновления времени последней активности при отправке сообщений
CREATE TRIGGER update_last_active_on_message
AFTER INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION update_last_active();
-- Создание функции для автоматического создания профиля при создании пользователя
CREATE OR REPLACE FUNCTION create_initial_profile()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO profiles (user_id, name, age, gender)
VALUES (NEW.id, COALESCE(NEW.first_name, 'User'), 18, 'other');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Триггер для автоматического создания профиля при создании пользователя
CREATE TRIGGER create_profile_trigger
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION create_initial_profile();
-- Индексы для оптимизации частых запросов
CREATE INDEX IF NOT EXISTS idx_profiles_last_active ON profiles(last_active);
CREATE INDEX IF NOT EXISTS idx_swipes_action ON swipes(action);
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read);
CREATE INDEX IF NOT EXISTS idx_messages_is_read ON messages(is_read);
-- Добавление ограничений для проверки возраста
ALTER TABLE profiles DROP CONSTRAINT IF EXISTS age_check;
ALTER TABLE profiles ADD CONSTRAINT age_check CHECK (age >= 18 AND age <= 99);
-- Добавление ограничений для предпочтений
ALTER TABLE search_preferences DROP CONSTRAINT IF EXISTS age_range_check;
ALTER TABLE search_preferences ADD CONSTRAINT age_range_check CHECK (age_min >= 18 AND age_max >= age_min AND age_max <= 99);
-- Комментарии к таблицам для документации
COMMENT ON TABLE users IS 'Таблица пользователей Telegram';
COMMENT ON TABLE profiles IS 'Профили пользователей для знакомств';
COMMENT ON TABLE search_preferences IS 'Предпочтения поиска пользователей';
COMMENT ON TABLE swipes IS 'История лайков/дислайков';
COMMENT ON TABLE matches IS 'Совпадения (матчи) между пользователями';
COMMENT ON TABLE messages IS 'Сообщения между пользователями';
COMMENT ON TABLE notifications IS 'Уведомления для пользователей';
COMMENT ON TABLE settings IS 'Настройки пользователей';
COMMENT ON TABLE profile_views IS 'История просмотров профилей';
COMMENT ON TABLE premium_features IS 'Премиум-функции для пользователей';
-- Представление для быстрого получения активных матчей с информацией о пользователе
CREATE OR REPLACE VIEW active_matches AS
SELECT
m.id AS match_id,
m.created_at AS match_date,
CASE
WHEN m.user_id_1 = u1.id THEN u2.id
ELSE u1.id
END AS partner_id,
CASE
WHEN m.user_id_1 = u1.id THEN u2.telegram_id
ELSE u1.telegram_id
END AS partner_telegram_id,
CASE
WHEN m.user_id_1 = u1.id THEN p2.name
ELSE p1.name
END AS partner_name,
CASE
WHEN m.user_id_1 = u1.id THEN p2.photos[1]
ELSE p1.photos[1]
END AS partner_photo,
(SELECT COUNT(*) FROM messages WHERE match_id = m.id) AS message_count,
(SELECT COUNT(*) FROM messages WHERE match_id = m.id AND is_read = false AND sender_id != u1.id) AS unread_count,
m.is_active
FROM matches m
JOIN users u1 ON m.user_id_1 = u1.id
JOIN users u2 ON m.user_id_2 = u2.id
JOIN profiles p1 ON u1.id = p1.user_id
JOIN profiles p2 ON u2.id = p2.user_id
WHERE m.is_active = true;

67
sql/recreate_tables.sql Normal file
View File

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

View File

@@ -4,19 +4,23 @@ import { testConnection, query } from './database/connection';
import { ProfileService } from './services/profileService';
import { MatchingService } from './services/matchingService';
import { NotificationService } from './services/notificationService';
import LocalizationService from './services/localizationService';
import { CommandHandlers } from './handlers/commandHandlers';
import { CallbackHandlers } from './handlers/callbackHandlers';
import { MessageHandlers } from './handlers/messageHandlers';
import { NotificationHandlers } from './handlers/notificationHandlers';
class TelegramTinderBot {
private bot: TelegramBot;
private profileService: ProfileService;
private matchingService: MatchingService;
private notificationService: NotificationService;
private localizationService: LocalizationService;
private commandHandlers: CommandHandlers;
private callbackHandlers: CallbackHandlers;
private messageHandlers: MessageHandlers;
private notificationHandlers: NotificationHandlers;
constructor() {
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
@@ -27,10 +31,12 @@ class TelegramTinderBot {
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.notificationService = new NotificationService(this.bot);
this.localizationService = LocalizationService.getInstance();
this.commandHandlers = new CommandHandlers(this.bot);
this.messageHandlers = new MessageHandlers(this.bot);
this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers);
this.notificationHandlers = new NotificationHandlers(this.bot);
this.setupErrorHandling();
this.setupPeriodicTasks();
@@ -41,6 +47,9 @@ class TelegramTinderBot {
try {
console.log('🚀 Initializing Telegram Tinder Bot...');
// Инициализация сервиса локализации
await this.localizationService.initialize();
// Проверка подключения к базе данных
const dbConnected = await testConnection();
if (!dbConnected) {
@@ -72,6 +81,7 @@ class TelegramTinderBot {
{ command: 'browse', description: '💕 Смотреть анкеты' },
{ command: 'matches', description: '💖 Мои матчи' },
{ command: 'settings', description: '⚙️ Настройки' },
{ command: 'notifications', description: '🔔 Настройки уведомлений' },
{ command: 'help', description: '❓ Помощь' }
];
@@ -88,6 +98,9 @@ class TelegramTinderBot {
// Сообщения
this.messageHandlers.register();
// Обработчики уведомлений
this.notificationHandlers.register();
}
// Обработка ошибок
@@ -131,14 +144,31 @@ class TelegramTinderBot {
}
}, 5 * 60 * 1000);
// Очистка старых данных каждый день
// Планирование периодических уведомлений раз в день в 00:05
setInterval(async () => {
try {
await this.cleanupOldData();
const now = new Date();
if (now.getHours() === 0 && now.getMinutes() >= 5 && now.getMinutes() < 10) {
console.log('🔔 Scheduling periodic notifications...');
await this.notificationService.schedulePeriodicNotifications();
}
} catch (error) {
console.error('Error scheduling periodic notifications:', error);
}
}, 5 * 60 * 1000);
// Очистка старых данных каждый день в 03:00
setInterval(async () => {
try {
const now = new Date();
if (now.getHours() === 3 && now.getMinutes() < 5) {
console.log('🧹 Running scheduled cleanup...');
await this.cleanupOldData();
}
} catch (error) {
console.error('Error cleaning up old data:', error);
}
}, 24 * 60 * 60 * 1000);
}, 5 * 60 * 1000);
}
// Очистка старых данных
@@ -146,11 +176,18 @@ class TelegramTinderBot {
console.log('🧹 Running cleanup tasks...');
try {
// Очистка старых уведомлений (старше 30 дней)
const notificationsResult = await query(`
// Очистка старых запланированных уведомлений (старше 30 дней или обработанных)
const scheduledNotificationsResult = await query(`
DELETE FROM scheduled_notifications
WHERE processed = true
AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days'
WHERE (processed = true AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days')
OR (scheduled_at < CURRENT_TIMESTAMP - INTERVAL '7 days')
`);
console.log(`🗑️ Cleaned up ${scheduledNotificationsResult.rowCount} old scheduled notifications`);
// Очистка старых уведомлений (старше 90 дней)
const notificationsResult = await query(`
DELETE FROM notifications
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '90 days'
`);
console.log(`🗑️ Cleaned up ${notificationsResult.rowCount} old notifications`);
@@ -180,7 +217,7 @@ class TelegramTinderBot {
console.log(`💬 Cleaned up ${messagesResult.rowCount} old messages`);
// Обновление статистики таблиц после очистки
await query('VACUUM ANALYZE scheduled_notifications, profile_views, swipes, messages');
await query('VACUUM ANALYZE notifications, scheduled_notifications, profile_views, swipes, messages');
console.log('✅ Cleanup completed successfully');
} catch (error) {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
// Script to run migrations on startup
import { Pool } from 'pg';
import * as fs from 'fs';
import * as path from 'path';
import 'dotenv/config';
async function runMigrations() {
console.log('Starting database migration...');
// Create a connection pool
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'telegram_tinder_bot',
user: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
try {
// Test connection
const testRes = await pool.query('SELECT NOW()');
console.log(`Database connection successful at ${testRes.rows[0].now}`);
// Create migrations table if not exists
await pool.query(`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Get list of executed migrations
const migrationRes = await pool.query('SELECT name FROM migrations');
const executedMigrations = migrationRes.rows.map(row => row.name);
console.log(`Found ${executedMigrations.length} executed migrations`);
// Get migration files
const migrationsPath = path.join(__dirname, 'migrations');
let migrationFiles = [];
try {
migrationFiles = fs.readdirSync(migrationsPath)
.filter(file => file.endsWith('.sql'))
.sort();
console.log(`Found ${migrationFiles.length} migration files`);
} catch (error: any) {
console.error(`Error reading migrations directory: ${error.message}`);
console.log('Continuing with built-in consolidated migration...');
// If no external files found, use consolidated.sql
const consolidatedSQL = fs.readFileSync(path.join(__dirname, 'migrations', 'consolidated.sql'), 'utf8');
console.log('Executing consolidated migration...');
await pool.query(consolidatedSQL);
if (!executedMigrations.includes('consolidated.sql')) {
await pool.query(
'INSERT INTO migrations (name) VALUES ($1)',
['consolidated.sql']
);
}
console.log('Consolidated migration completed successfully');
return;
}
// Run each migration that hasn't been executed yet
for (const file of migrationFiles) {
if (!executedMigrations.includes(file)) {
console.log(`Executing migration: ${file}`);
const sql = fs.readFileSync(path.join(migrationsPath, file), 'utf8');
try {
await pool.query('BEGIN');
await pool.query(sql);
await pool.query(
'INSERT INTO migrations (name) VALUES ($1)',
[file]
);
await pool.query('COMMIT');
console.log(`Migration ${file} completed successfully`);
} catch (error: any) {
await pool.query('ROLLBACK');
console.error(`Error executing migration ${file}: ${error.message}`);
throw error;
}
} else {
console.log(`Migration ${file} already executed, skipping`);
}
}
console.log('All migrations completed successfully!');
} catch (error: any) {
console.error(`Migration failed: ${error.message}`);
process.exit(1);
} finally {
await pool.end();
}
}
runMigrations().catch((error: any) => {
console.error('Unhandled error during migration:', error);
process.exit(1);
});

View File

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

View File

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

View File

@@ -0,0 +1,182 @@
-- Consolidated migrations for Telegram Tinder Bot
-- Create extension for UUID if not exists
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
----------------------------------------------
-- Core Tables
----------------------------------------------
-- Users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
telegram_id BIGINT UNIQUE NOT NULL,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
language_code VARCHAR(10) DEFAULT 'ru',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
last_active_at TIMESTAMP DEFAULT NOW(),
premium BOOLEAN DEFAULT FALSE,
state VARCHAR(255),
state_data JSONB DEFAULT '{}'::jsonb
);
-- Profiles table
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
age INTEGER NOT NULL CHECK (age >= 18 AND age <= 100),
gender VARCHAR(10) NOT NULL CHECK (gender IN ('male', 'female', 'other')),
interested_in VARCHAR(10) NOT NULL CHECK (interested_in IN ('male', 'female', 'both')),
bio TEXT,
photos JSONB DEFAULT '[]',
interests JSONB DEFAULT '[]',
city VARCHAR(255),
education VARCHAR(255),
job VARCHAR(255),
height INTEGER,
location_lat DECIMAL(10, 8),
location_lon DECIMAL(11, 8),
search_min_age INTEGER DEFAULT 18,
search_max_age INTEGER DEFAULT 50,
search_max_distance INTEGER DEFAULT 50,
is_verified BOOLEAN DEFAULT FALSE,
is_visible BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
religion VARCHAR(255),
dating_goal VARCHAR(50),
smoking VARCHAR(20),
drinking VARCHAR(20),
has_kids BOOLEAN DEFAULT FALSE
);
-- Swipes table
CREATE TABLE IF NOT EXISTS swipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
target_user_id UUID REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL CHECK (type IN ('like', 'pass', 'superlike')),
created_at TIMESTAMP DEFAULT NOW(),
is_match BOOLEAN DEFAULT FALSE,
UNIQUE(user_id, target_user_id)
);
-- Matches table
CREATE TABLE IF NOT EXISTS matches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id_1 UUID REFERENCES users(id) ON DELETE CASCADE,
user_id_2 UUID REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
last_message_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
is_super_match BOOLEAN DEFAULT FALSE,
unread_count_1 INTEGER DEFAULT 0,
unread_count_2 INTEGER DEFAULT 0,
UNIQUE(user_id_1, user_id_2)
);
-- Messages table
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
match_id UUID REFERENCES matches(id) ON DELETE CASCADE,
sender_id UUID REFERENCES users(id) ON DELETE CASCADE,
receiver_id UUID REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
message_type VARCHAR(20) DEFAULT 'text' CHECK (message_type IN ('text', 'photo', 'gif', 'sticker')),
created_at TIMESTAMP DEFAULT NOW(),
is_read BOOLEAN DEFAULT FALSE
);
----------------------------------------------
-- Profile Views Table
----------------------------------------------
-- Table for tracking profile views
CREATE TABLE IF NOT EXISTS profile_views (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
viewer_id UUID REFERENCES users(id) ON DELETE CASCADE,
viewed_id UUID REFERENCES users(id) ON DELETE CASCADE,
view_type VARCHAR(20) DEFAULT 'browse' CHECK (view_type IN ('browse', 'search', 'recommended')),
viewed_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_profile_view UNIQUE (viewer_id, viewed_id, view_type)
);
-- Index for profile views
CREATE INDEX IF NOT EXISTS idx_profile_views_viewer ON profile_views(viewer_id);
CREATE INDEX IF NOT EXISTS idx_profile_views_viewed ON profile_views(viewed_id);
----------------------------------------------
-- Notification Tables
----------------------------------------------
-- Notifications table
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
is_read BOOLEAN DEFAULT FALSE,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Notification settings table
CREATE TABLE IF NOT EXISTS notification_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
new_matches BOOLEAN DEFAULT TRUE,
new_messages BOOLEAN DEFAULT TRUE,
new_likes BOOLEAN DEFAULT TRUE,
reminders BOOLEAN DEFAULT TRUE,
daily_summary BOOLEAN DEFAULT FALSE,
time_preference VARCHAR(20) DEFAULT 'evening',
do_not_disturb BOOLEAN DEFAULT FALSE,
do_not_disturb_start TIME,
do_not_disturb_end TIME,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Scheduled notifications table
CREATE TABLE IF NOT EXISTS scheduled_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
----------------------------------------------
-- Indexes for better performance
----------------------------------------------
-- User Indexes
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
-- Profile Indexes
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_profiles_location ON profiles(location_lat, location_lon)
WHERE location_lat IS NOT NULL AND location_lon IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, interested_in);
-- Swipe Indexes
CREATE INDEX IF NOT EXISTS idx_swipes_user ON swipes(user_id, target_user_id);
-- Match Indexes
CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user_id_1, user_id_2);
-- Message Indexes
CREATE INDEX IF NOT EXISTS idx_messages_match ON messages(match_id, created_at);
-- Notification Indexes
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(type);
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_user_id ON scheduled_notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_processed ON scheduled_notifications(processed);

View File

@@ -4,8 +4,14 @@ import { MatchingService } from '../services/matchingService';
import { ChatService } from '../services/chatService';
import { Profile } from '../models/Profile';
import { MessageHandlers } from './messageHandlers';
import { NotificationHandlers } from './notificationHandlers';
import { ProfileEditController } from '../controllers/profileEditController';
import { EnhancedChatHandlers } from './enhancedChatHandlers';
import { VipController } from '../controllers/vipController';
import { VipService } from '../services/vipService';
import { TranslationController } from '../controllers/translationController';
import { t } from '../services/localizationService';
import { LikeBackHandler } from './likeBackHandler';
export class CallbackHandlers {
private bot: TelegramBot;
@@ -15,6 +21,11 @@ export class CallbackHandlers {
private messageHandlers: MessageHandlers;
private profileEditController: ProfileEditController;
private enhancedChatHandlers: EnhancedChatHandlers;
private vipController: VipController;
private vipService: VipService;
private translationController: TranslationController;
private notificationHandlers?: NotificationHandlers;
private likeBackHandler: LikeBackHandler;
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
this.bot = bot;
@@ -24,6 +35,16 @@ export class CallbackHandlers {
this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
this.vipController = new VipController(bot);
this.vipService = new VipService();
this.translationController = new TranslationController();
// Создаем экземпляр NotificationHandlers
try {
this.notificationHandlers = new NotificationHandlers(bot);
} catch (error) {
console.error('Failed to initialize NotificationHandlers:', error);
}
this.likeBackHandler = new LikeBackHandler(bot);
}
register(): void {
@@ -129,7 +150,10 @@ export class CallbackHandlers {
// Просмотр анкет и свайпы
else if (data === 'start_browsing') {
await this.handleStartBrowsing(chatId, telegramId);
await this.handleStartBrowsing(chatId, telegramId, false);
} else if (data === 'start_browsing_first') {
// Показываем всех пользователей для нового пользователя
await this.handleStartBrowsing(chatId, telegramId, true);
} else if (data === 'vip_search') {
await this.handleVipSearch(chatId, telegramId);
} else if (data.startsWith('search_by_goal_')) {
@@ -154,6 +178,12 @@ export class CallbackHandlers {
await this.handleMorePhotos(chatId, telegramId, targetUserId);
}
// Обработка лайков и ответных лайков из уведомлений
else if (data.startsWith('like_back:')) {
const targetUserId = data.replace('like_back:', '');
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
}
// Матчи и чаты
else if (data === 'view_matches') {
await this.handleViewMatches(chatId, telegramId);
@@ -211,9 +241,80 @@ export class CallbackHandlers {
} else if (data === 'back_to_browsing') {
await this.handleStartBrowsing(chatId, telegramId);
} 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 if (data === 'notifications') {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
await this.handleNotificationSettings(chatId, telegramId);
}
}
// Обработка переключения настроек уведомлений
else if (data.startsWith('notif_toggle:') ||
data === 'notif_time' ||
data.startsWith('notif_time_set:') ||
data === 'notif_dnd' ||
data.startsWith('notif_dnd_set:') ||
data === 'notif_dnd_time' ||
data.startsWith('notif_dnd_time_set:') ||
data === 'notif_dnd_time_custom') {
// Делегируем обработку в NotificationHandlers, если он доступен
if (this.notificationHandlers) {
// Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем
// NotificationHandlers уже зарегистрировал свои обработчики в register()
} else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция настройки уведомлений недоступна.',
show_alert: true
});
}
}
else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция в разработке!',
@@ -284,7 +385,7 @@ export class CallbackHandlers {
}
// Начать просмотр анкет
async handleStartBrowsing(chatId: number, telegramId: string): Promise<void> {
async handleStartBrowsing(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile) {
@@ -292,7 +393,7 @@ export class CallbackHandlers {
return;
}
await this.showNextCandidate(chatId, telegramId);
await this.showNextCandidate(chatId, telegramId, isNewUser);
}
// Следующий кандидат
@@ -336,9 +437,15 @@ export class CallbackHandlers {
await this.bot.sendMessage(chatId, '👍 Лайк отправлен!');
await this.showNextCandidate(chatId, telegramId);
}
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
console.error('Like error:', error);
} catch (error: any) {
// Проверяем, связана ли ошибка с уже существующим свайпом
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
await this.showNextCandidate(chatId, telegramId);
} else {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
console.error('Like error:', error);
}
}
}
@@ -353,9 +460,15 @@ export class CallbackHandlers {
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
await this.showNextCandidate(chatId, telegramId);
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
console.error('Dislike error:', error);
} catch (error: any) {
// Проверяем, связана ли ошибка с уже существующим свайпом
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
await this.showNextCandidate(chatId, telegramId);
} else {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
console.error('Dislike error:', error);
}
}
}
@@ -394,9 +507,73 @@ export class CallbackHandlers {
await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!');
await this.showNextCandidate(chatId, telegramId);
}
} catch (error: any) {
// Проверяем, связана ли ошибка с уже существующим свайпом
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
await this.showNextCandidate(chatId, telegramId);
} else {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
console.error('Superlike error:', error);
}
}
}
// Обработка обратного лайка из уведомления
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
try {
// Получаем информацию о пользователях
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
if (!targetProfile) {
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
return;
}
// Получаем telegram ID целевого пользователя для свайпа
const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId);
if (!targetTelegramId) {
await this.bot.sendMessage(chatId, '❌ Не удалось найти пользователя');
return;
}
// Выполняем свайп
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like');
if (result.isMatch) {
// Это матч!
await this.bot.sendMessage(
chatId,
'🎉 *Поздравляем! Это взаимно!*\n\n' +
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
'Теперь вы можете начать общение.',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
]
}
}
);
} else {
await this.bot.sendMessage(
chatId,
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
'Если вы также понравитесь этому пользователю, будет создан матч.',
{
reply_markup: {
inline_keyboard: [
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
]
}
}
);
}
} catch (error) {
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
console.error('Superlike error:', error);
console.error('Error in handleLikeBack:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
}
}
@@ -427,9 +604,28 @@ export class CallbackHandlers {
return;
}
for (let i = 1; i < targetProfile.photos.length; i++) {
const photoFileId = targetProfile.photos[i];
await this.bot.sendPhoto(chatId, photoFileId);
// Отправляем фотографии в виде медиа-группы (коллажа)
// Создаем массив объектов медиа для группового отправления
const mediaGroup = targetProfile.photos.slice(1).map((photoFileId, index) => ({
type: 'photo' as const,
media: photoFileId,
caption: index === 0 ? `📸 Дополнительные фото ${targetProfile.name}` : undefined
}));
try {
// Отправляем все фото одним сообщением (медиа-группой)
await this.bot.sendMediaGroup(chatId, mediaGroup);
} catch (error) {
console.error('Error sending media group:', error);
// Если не получилось отправить медиа-группой, отправляем по одной
for (let i = 1; i < targetProfile.photos.length; i++) {
try {
await this.bot.sendPhoto(chatId, targetProfile.photos[i]);
} catch (photoError) {
console.error(`Error sending photo ${i}:`, photoError);
}
}
}
const keyboard: InlineKeyboardMarkup = {
@@ -692,8 +888,8 @@ export class CallbackHandlers {
{ text: '🔔 Уведомления', callback_data: 'notification_settings' }
],
[
{ text: '<EFBFBD> Статистика', callback_data: 'view_stats' },
{ text: '👀 Кто смотрел', callback_data: 'view_profile_viewers' }
{ text: '🌐 Язык интерфейса', callback_data: 'language_settings' },
{ text: '📊 Статистика', callback_data: 'view_stats' }
],
[
{ text: '<27>🚫 Скрыть профиль', callback_data: 'hide_profile' },
@@ -717,13 +913,7 @@ export class CallbackHandlers {
);
}
// Настройки уведомлений
async handleNotificationSettings(chatId: number, telegramId: string): Promise<void> {
await this.bot.sendMessage(
chatId,
'🔔 Настройки уведомлений будут доступны в следующем обновлении!'
);
}
// Настройки уведомлений - реализация перенесена в расширенную версию
// Как это работает
async handleHowItWorks(chatId: number): Promise<void> {
@@ -758,6 +948,7 @@ export class CallbackHandlers {
// Вспомогательные методы
async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise<void> {
const hasMultiplePhotos = profile.photos.length > 1;
const mainPhotoFileId = profile.photos[0]; // Первое фото - главное
let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n';
@@ -827,26 +1018,49 @@ export class CallbackHandlers {
if (hasValidPhoto) {
try {
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
caption: profileText,
reply_markup: keyboard
});
if (hasMultiplePhotos) {
// Если есть несколько фото, отправляем их как медиа-группу (коллаж)
const mediaGroup = profile.photos.map((photoFileId, index) => ({
type: 'photo' as const,
media: photoFileId,
caption: index === 0 ? profileText : undefined,
parse_mode: index === 0 ? 'Markdown' as const : undefined
}));
// Сначала отправляем медиа-группу
await this.bot.sendMediaGroup(chatId, mediaGroup);
// Затем отправляем отдельное сообщение с кнопками
await this.bot.sendMessage(chatId, '📸 Выберите действие:', {
reply_markup: keyboard
});
} else {
// Если только одно фото, отправляем его с текстом
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
caption: profileText,
reply_markup: keyboard,
parse_mode: 'Markdown'
});
}
} catch (error) {
console.error('Error sending profile photos:', error);
// Если не удалось отправить фото, отправляем текст
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, {
reply_markup: keyboard
reply_markup: keyboard,
parse_mode: 'Markdown'
});
}
} else {
// Отправляем как текстовое сообщение
await this.bot.sendMessage(chatId, profileText, {
reply_markup: keyboard
reply_markup: keyboard,
parse_mode: 'Markdown'
});
}
}
async showNextCandidate(chatId: number, telegramId: string): Promise<void> {
const candidate = await this.matchingService.getNextCandidate(telegramId);
async showNextCandidate(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
const candidate = await this.matchingService.getNextCandidate(telegramId, isNewUser);
if (!candidate) {
const keyboard: InlineKeyboardMarkup = {
@@ -1324,12 +1538,28 @@ export class CallbackHandlers {
return;
}
const lifestyle = profile.lifestyle || {};
lifestyle[type as keyof typeof lifestyle] = value as any;
// Обновляем отдельные колонки напрямую, а не через объект lifestyle
const updates: any = {};
await this.profileService.updateProfile(profile.userId, {
lifestyle: lifestyle
});
switch (type) {
case 'smoking':
updates.smoking = value;
break;
case 'drinking':
updates.drinking = value;
break;
case 'kids':
// Для поля has_kids, которое имеет тип boolean, преобразуем строковые значения
if (value === 'have') {
updates.has_kids = true;
} else {
// Для 'want', 'dont_want', 'unsure' ставим false
updates.has_kids = false;
}
break;
}
await this.profileService.updateProfile(profile.userId, updates);
const typeTexts: { [key: string]: string } = {
'smoking': 'курение',
@@ -1385,7 +1615,7 @@ export class CallbackHandlers {
try {
// Проверяем VIP статус пользователя
const user = await this.profileService.getUserByTelegramId(telegramId);
if (!user || !user.isPremium) {
if (!user || !user.premium) { // Изменено с isPremium на premium, чтобы соответствовать названию колонки в базе данных
const keyboard = {
inline_keyboard: [
[
@@ -1724,20 +1954,30 @@ export class CallbackHandlers {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (profile) {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
],
[
{ text: '💕 Мои матчи', callback_data: 'view_matches' },
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
],
[
{ text: '⚙️ Настройки', callback_data: 'settings' }
]
// Проверяем премиум статус
const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
let keyboardRows = [
[
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
],
[
{ text: '💕 Мои матчи', callback_data: 'view_matches' }
]
];
// Добавляем кнопку 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(
@@ -1886,4 +2126,178 @@ 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'));
}
}
async handleNotificationSettings(chatId: number, telegramId: string): Promise<void> {
try {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
// Вызываем метод из notificationHandlers для получения настроек и отображения меню
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
// Если NotificationHandlers недоступен, показываем сообщение об ошибке
await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.');
await this.handleSettings(chatId, telegramId);
}
} catch (error) {
console.error('Notification settings error:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений. Попробуйте позже.');
}
}
}

View File

@@ -0,0 +1,606 @@
import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api';
import { ProfileService } from '../services/profileService';
import { MatchingService } from '../services/matchingService';
import { ChatService } from '../services/chatService';
import { Profile } from '../models/Profile';
import { MessageHandlers } from './messageHandlers';
import { ProfileEditController } from '../controllers/profileEditController';
import { EnhancedChatHandlers } from './enhancedChatHandlers';
import { VipController } from '../controllers/vipController';
import { VipService } from '../services/vipService';
import { TranslationController } from '../controllers/translationController';
import { t } from '../services/localizationService';
import { LikeBackHandler } from './likeBackHandler';
import { NotificationHandlers } from './notificationHandlers';
export class CallbackHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private matchingService: MatchingService;
private chatService: ChatService;
private messageHandlers: MessageHandlers;
private profileEditController: ProfileEditController;
private enhancedChatHandlers: EnhancedChatHandlers;
private vipController: VipController;
private vipService: VipService;
private translationController: TranslationController;
private likeBackHandler: LikeBackHandler;
private notificationHandlers?: NotificationHandlers;
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
this.bot = bot;
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.chatService = new ChatService();
this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
this.vipController = new VipController(bot);
this.vipService = new VipService();
this.translationController = new TranslationController();
this.likeBackHandler = new LikeBackHandler(bot);
// Создаем экземпляр NotificationHandlers
try {
this.notificationHandlers = new NotificationHandlers(bot);
} catch (error) {
console.error('Failed to initialize NotificationHandlers:', error);
}
}
register(): void {
this.bot.on('callback_query', (query) => this.handleCallback(query));
}
async handleCallback(query: CallbackQuery): Promise<void> {
if (!query.data || !query.from || !query.message) return;
const telegramId = query.from.id.toString();
const chatId = query.message.chat.id;
const data = query.data;
try {
// Основные действия профиля
if (data === 'create_profile') {
await this.handleCreateProfile(chatId, telegramId);
} else if (data.startsWith('gender_')) {
const gender = data.replace('gender_', '');
await this.handleGenderSelection(chatId, telegramId, gender);
} else if (data === 'view_my_profile') {
await this.handleViewMyProfile(chatId, telegramId);
} else if (data === 'edit_profile') {
await this.handleEditProfile(chatId, telegramId);
} else if (data === 'manage_photos') {
await this.handleManagePhotos(chatId, telegramId);
} else if (data === 'preview_profile') {
await this.handlePreviewProfile(chatId, telegramId);
}
// Редактирование полей профиля
else if (data === 'edit_name') {
await this.handleEditName(chatId, telegramId);
} else if (data === 'edit_age') {
await this.handleEditAge(chatId, telegramId);
} else if (data === 'edit_bio') {
await this.handleEditBio(chatId, telegramId);
} else if (data === 'edit_hobbies') {
await this.handleEditHobbies(chatId, telegramId);
} else if (data === 'edit_city') {
await this.handleEditCity(chatId, telegramId);
} else if (data === 'edit_job') {
await this.handleEditJob(chatId, telegramId);
} else if (data === 'edit_education') {
await this.handleEditEducation(chatId, telegramId);
} else if (data === 'edit_height') {
await this.handleEditHeight(chatId, telegramId);
} else if (data === 'edit_religion') {
await this.handleEditReligion(chatId, telegramId);
} else if (data === 'edit_dating_goal') {
await this.handleEditDatingGoal(chatId, telegramId);
} else if (data === 'edit_lifestyle') {
await this.handleEditLifestyle(chatId, telegramId);
} else if (data === 'edit_search_preferences') {
await this.handleEditSearchPreferences(chatId, telegramId);
}
// Управление фотографиями
else if (data === 'add_photo') {
await this.handleAddPhoto(chatId, telegramId);
} else if (data === 'delete_photo') {
await this.handleDeletePhoto(chatId, telegramId);
} else if (data === 'set_main_photo') {
await this.handleSetMainPhoto(chatId, telegramId);
} else if (data.startsWith('delete_photo_')) {
const photoIndex = parseInt(data.replace('delete_photo_', ''));
await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex);
} else if (data.startsWith('set_main_photo_')) {
const photoIndex = parseInt(data.replace('set_main_photo_', ''));
await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex);
}
// Цели знакомства
else if (data.startsWith('set_dating_goal_')) {
const goal = data.replace('set_dating_goal_', '');
await this.handleSetDatingGoal(chatId, telegramId, goal);
}
// Образ жизни
else if (data === 'edit_smoking') {
await this.handleEditSmoking(chatId, telegramId);
} else if (data === 'edit_drinking') {
await this.handleEditDrinking(chatId, telegramId);
} else if (data === 'edit_kids') {
await this.handleEditKids(chatId, telegramId);
} else if (data.startsWith('set_smoking_')) {
const value = data.replace('set_smoking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'smoking', value);
} else if (data.startsWith('set_drinking_')) {
const value = data.replace('set_drinking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'drinking', value);
} else if (data.startsWith('set_kids_')) {
const value = data.replace('set_kids_', '');
await this.handleSetLifestyle(chatId, telegramId, 'kids', value);
}
// Настройки поиска
else if (data === 'edit_age_range') {
await this.handleEditAgeRange(chatId, telegramId);
} else if (data === 'edit_distance') {
await this.handleEditDistance(chatId, telegramId);
}
// Просмотр анкет и свайпы
else if (data === 'start_browsing') {
await this.handleStartBrowsing(chatId, telegramId, false);
} else if (data === 'start_browsing_first') {
// Показываем всех пользователей для нового пользователя
await this.handleStartBrowsing(chatId, telegramId, true);
} else if (data === 'vip_search') {
await this.handleVipSearch(chatId, telegramId);
} else if (data.startsWith('search_by_goal_')) {
const goal = data.replace('search_by_goal_', '');
await this.handleSearchByGoal(chatId, telegramId, goal);
} else if (data === 'next_candidate') {
await this.handleNextCandidate(chatId, telegramId);
} else if (data.startsWith('like_')) {
const targetUserId = data.replace('like_', '');
await this.handleLike(chatId, telegramId, targetUserId);
} else if (data.startsWith('dislike_')) {
const targetUserId = data.replace('dislike_', '');
await this.handleDislike(chatId, telegramId, targetUserId);
} else if (data.startsWith('superlike_')) {
const targetUserId = data.replace('superlike_', '');
await this.handleSuperlike(chatId, telegramId, targetUserId);
} else if (data.startsWith('view_profile_')) {
const targetUserId = data.replace('view_profile_', '');
await this.handleViewProfile(chatId, telegramId, targetUserId);
} else if (data.startsWith('more_photos_')) {
const targetUserId = data.replace('more_photos_', '');
await this.handleMorePhotos(chatId, telegramId, targetUserId);
}
// Обработка лайков и ответных лайков из уведомлений
else if (data.startsWith('like_back:')) {
const targetUserId = data.replace('like_back:', '');
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
}
// Матчи и чаты
else if (data === 'view_matches') {
await this.handleViewMatches(chatId, telegramId);
} else if (data === 'open_chats') {
await this.handleOpenChats(chatId, telegramId);
} else if (data === 'native_chats') {
await this.enhancedChatHandlers.showChatsNative(chatId, telegramId);
} else if (data.startsWith('open_native_chat_')) {
const matchId = data.replace('open_native_chat_', '');
await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId);
} else if (data.startsWith('chat_history_')) {
const matchId = data.replace('chat_history_', '');
await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId);
} else if (data.startsWith('chat_')) {
const matchId = data.replace('chat_', '');
await this.handleOpenChat(chatId, telegramId, matchId);
} else if (data.startsWith('send_message_')) {
const matchId = data.replace('send_message_', '');
await this.handleSendMessage(chatId, telegramId, matchId);
} else if (data.startsWith('view_chat_profile_')) {
const matchId = data.replace('view_chat_profile_', '');
await this.handleViewChatProfile(chatId, telegramId, matchId);
} else if (data.startsWith('unmatch_')) {
const matchId = data.replace('unmatch_', '');
await this.handleUnmatch(chatId, telegramId, matchId);
} else if (data.startsWith('confirm_unmatch_')) {
const matchId = data.replace('confirm_unmatch_', '');
await this.handleConfirmUnmatch(chatId, telegramId, matchId);
}
// Настройки
else if (data === 'settings') {
await this.handleSettings(chatId, telegramId);
} else if (data === 'search_settings') {
await this.handleSearchSettings(chatId, telegramId);
} else if (data === 'notification_settings') {
await this.handleNotificationSettings(chatId, telegramId);
} else if (data === 'view_stats') {
await this.handleViewStats(chatId, telegramId);
} else if (data === 'view_profile_viewers') {
await this.handleViewProfileViewers(chatId, telegramId);
} else if (data === 'hide_profile') {
await this.handleHideProfile(chatId, telegramId);
} else if (data === 'delete_profile') {
await this.handleDeleteProfile(chatId, telegramId);
} else if (data === 'main_menu') {
await this.handleMainMenu(chatId, telegramId);
} else if (data === 'confirm_delete_profile') {
await this.handleConfirmDeleteProfile(chatId, telegramId);
}
// Информация
else if (data === 'how_it_works') {
await this.handleHowItWorks(chatId);
} else if (data === 'back_to_browsing') {
await this.handleStartBrowsing(chatId, telegramId);
} else if (data === 'get_vip') {
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 if (data === 'notifications') {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
await this.handleNotificationSettings(chatId, telegramId);
}
}
// Обработка переключения настроек уведомлений
else if (data.startsWith('notif_toggle:') ||
data === 'notif_time' ||
data.startsWith('notif_time_set:') ||
data === 'notif_dnd' ||
data.startsWith('notif_dnd_set:') ||
data === 'notif_dnd_time' ||
data.startsWith('notif_dnd_time_set:') ||
data === 'notif_dnd_time_custom') {
// Делегируем обработку в NotificationHandlers, если он доступен
if (this.notificationHandlers) {
// Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем
// NotificationHandlers уже зарегистрировал свои обработчики в register()
} else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция настройки уведомлений недоступна.',
show_alert: true
});
}
}
else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция в разработке!',
show_alert: false
});
return;
}
await this.bot.answerCallbackQuery(query.id);
} catch (error) {
console.error('Callback handler error:', error);
await this.bot.answerCallbackQuery(query.id, {
text: 'Произошла ошибка. Попробуйте еще раз.',
show_alert: true
});
}
}
// Добавим все необходимые методы для обработки коллбэков
async handleCreateProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise<void> {
// Заглушка метода
}
async handleViewMyProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleManagePhotos(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handlePreviewProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditName(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditAge(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditBio(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditHobbies(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditJob(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditEducation(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditHeight(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditReligion(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDatingGoal(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditLifestyle(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditSearchPreferences(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleAddPhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeletePhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetMainPhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
// Заглушка метода
}
async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
// Заглушка метода
}
async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
// Заглушка метода
}
async handleEditSmoking(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDrinking(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditKids(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise<void> {
// Заглушка метода
}
async handleEditAgeRange(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDistance(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise<void> {
// Заглушка метода
}
async handleVipSearch(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
// Заглушка метода
}
async handleNextCandidate(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleViewMatches(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleOpenChats(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSearchSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleViewStats(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleViewProfileViewers(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleHideProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeleteProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleMainMenu(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleHowItWorks(chatId: number): Promise<void> {
// Заглушка метода
}
async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleLanguageSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise<void> {
// Заглушка метода
}
async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise<void> {
// Заглушка метода
}
// Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers)
async handleNotificationSettings(chatId: number, telegramId: string): Promise<void> {
try {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
// Вызываем метод из notificationHandlers для получения настроек и отображения меню
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
// Если NotificationHandlers недоступен, показываем сообщение об ошибке
await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.');
await this.handleSettings(chatId, telegramId);
}
} catch (error) {
console.error('Error handling notification settings:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.');
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,606 @@
import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api';
import { ProfileService } from '../services/profileService';
import { MatchingService } from '../services/matchingService';
import { ChatService } from '../services/chatService';
import { Profile } from '../models/Profile';
import { MessageHandlers } from './messageHandlers';
import { ProfileEditController } from '../controllers/profileEditController';
import { EnhancedChatHandlers } from './enhancedChatHandlers';
import { VipController } from '../controllers/vipController';
import { VipService } from '../services/vipService';
import { TranslationController } from '../controllers/translationController';
import { t } from '../services/localizationService';
import { LikeBackHandler } from './likeBackHandler';
import { NotificationHandlers } from './notificationHandlers';
export class CallbackHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private matchingService: MatchingService;
private chatService: ChatService;
private messageHandlers: MessageHandlers;
private profileEditController: ProfileEditController;
private enhancedChatHandlers: EnhancedChatHandlers;
private vipController: VipController;
private vipService: VipService;
private translationController: TranslationController;
private likeBackHandler: LikeBackHandler;
private notificationHandlers?: NotificationHandlers;
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
this.bot = bot;
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.chatService = new ChatService();
this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
this.vipController = new VipController(bot);
this.vipService = new VipService();
this.translationController = new TranslationController();
this.likeBackHandler = new LikeBackHandler(bot);
// Создаем экземпляр NotificationHandlers
try {
this.notificationHandlers = new NotificationHandlers(bot);
} catch (error) {
console.error('Failed to initialize NotificationHandlers:', error);
}
}
register(): void {
this.bot.on('callback_query', (query) => this.handleCallback(query));
}
async handleCallback(query: CallbackQuery): Promise<void> {
if (!query.data || !query.from || !query.message) return;
const telegramId = query.from.id.toString();
const chatId = query.message.chat.id;
const data = query.data;
try {
// Основные действия профиля
if (data === 'create_profile') {
await this.handleCreateProfile(chatId, telegramId);
} else if (data.startsWith('gender_')) {
const gender = data.replace('gender_', '');
await this.handleGenderSelection(chatId, telegramId, gender);
} else if (data === 'view_my_profile') {
await this.handleViewMyProfile(chatId, telegramId);
} else if (data === 'edit_profile') {
await this.handleEditProfile(chatId, telegramId);
} else if (data === 'manage_photos') {
await this.handleManagePhotos(chatId, telegramId);
} else if (data === 'preview_profile') {
await this.handlePreviewProfile(chatId, telegramId);
}
// Редактирование полей профиля
else if (data === 'edit_name') {
await this.handleEditName(chatId, telegramId);
} else if (data === 'edit_age') {
await this.handleEditAge(chatId, telegramId);
} else if (data === 'edit_bio') {
await this.handleEditBio(chatId, telegramId);
} else if (data === 'edit_hobbies') {
await this.handleEditHobbies(chatId, telegramId);
} else if (data === 'edit_city') {
await this.handleEditCity(chatId, telegramId);
} else if (data === 'edit_job') {
await this.handleEditJob(chatId, telegramId);
} else if (data === 'edit_education') {
await this.handleEditEducation(chatId, telegramId);
} else if (data === 'edit_height') {
await this.handleEditHeight(chatId, telegramId);
} else if (data === 'edit_religion') {
await this.handleEditReligion(chatId, telegramId);
} else if (data === 'edit_dating_goal') {
await this.handleEditDatingGoal(chatId, telegramId);
} else if (data === 'edit_lifestyle') {
await this.handleEditLifestyle(chatId, telegramId);
} else if (data === 'edit_search_preferences') {
await this.handleEditSearchPreferences(chatId, telegramId);
}
// Управление фотографиями
else if (data === 'add_photo') {
await this.handleAddPhoto(chatId, telegramId);
} else if (data === 'delete_photo') {
await this.handleDeletePhoto(chatId, telegramId);
} else if (data === 'set_main_photo') {
await this.handleSetMainPhoto(chatId, telegramId);
} else if (data.startsWith('delete_photo_')) {
const photoIndex = parseInt(data.replace('delete_photo_', ''));
await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex);
} else if (data.startsWith('set_main_photo_')) {
const photoIndex = parseInt(data.replace('set_main_photo_', ''));
await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex);
}
// Цели знакомства
else if (data.startsWith('set_dating_goal_')) {
const goal = data.replace('set_dating_goal_', '');
await this.handleSetDatingGoal(chatId, telegramId, goal);
}
// Образ жизни
else if (data === 'edit_smoking') {
await this.handleEditSmoking(chatId, telegramId);
} else if (data === 'edit_drinking') {
await this.handleEditDrinking(chatId, telegramId);
} else if (data === 'edit_kids') {
await this.handleEditKids(chatId, telegramId);
} else if (data.startsWith('set_smoking_')) {
const value = data.replace('set_smoking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'smoking', value);
} else if (data.startsWith('set_drinking_')) {
const value = data.replace('set_drinking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'drinking', value);
} else if (data.startsWith('set_kids_')) {
const value = data.replace('set_kids_', '');
await this.handleSetLifestyle(chatId, telegramId, 'kids', value);
}
// Настройки поиска
else if (data === 'edit_age_range') {
await this.handleEditAgeRange(chatId, telegramId);
} else if (data === 'edit_distance') {
await this.handleEditDistance(chatId, telegramId);
}
// Просмотр анкет и свайпы
else if (data === 'start_browsing') {
await this.handleStartBrowsing(chatId, telegramId, false);
} else if (data === 'start_browsing_first') {
// Показываем всех пользователей для нового пользователя
await this.handleStartBrowsing(chatId, telegramId, true);
} else if (data === 'vip_search') {
await this.handleVipSearch(chatId, telegramId);
} else if (data.startsWith('search_by_goal_')) {
const goal = data.replace('search_by_goal_', '');
await this.handleSearchByGoal(chatId, telegramId, goal);
} else if (data === 'next_candidate') {
await this.handleNextCandidate(chatId, telegramId);
} else if (data.startsWith('like_')) {
const targetUserId = data.replace('like_', '');
await this.handleLike(chatId, telegramId, targetUserId);
} else if (data.startsWith('dislike_')) {
const targetUserId = data.replace('dislike_', '');
await this.handleDislike(chatId, telegramId, targetUserId);
} else if (data.startsWith('superlike_')) {
const targetUserId = data.replace('superlike_', '');
await this.handleSuperlike(chatId, telegramId, targetUserId);
} else if (data.startsWith('view_profile_')) {
const targetUserId = data.replace('view_profile_', '');
await this.handleViewProfile(chatId, telegramId, targetUserId);
} else if (data.startsWith('more_photos_')) {
const targetUserId = data.replace('more_photos_', '');
await this.handleMorePhotos(chatId, telegramId, targetUserId);
}
// Обработка лайков и ответных лайков из уведомлений
else if (data.startsWith('like_back:')) {
const targetUserId = data.replace('like_back:', '');
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
}
// Матчи и чаты
else if (data === 'view_matches') {
await this.handleViewMatches(chatId, telegramId);
} else if (data === 'open_chats') {
await this.handleOpenChats(chatId, telegramId);
} else if (data === 'native_chats') {
await this.enhancedChatHandlers.showChatsNative(chatId, telegramId);
} else if (data.startsWith('open_native_chat_')) {
const matchId = data.replace('open_native_chat_', '');
await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId);
} else if (data.startsWith('chat_history_')) {
const matchId = data.replace('chat_history_', '');
await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId);
} else if (data.startsWith('chat_')) {
const matchId = data.replace('chat_', '');
await this.handleOpenChat(chatId, telegramId, matchId);
} else if (data.startsWith('send_message_')) {
const matchId = data.replace('send_message_', '');
await this.handleSendMessage(chatId, telegramId, matchId);
} else if (data.startsWith('view_chat_profile_')) {
const matchId = data.replace('view_chat_profile_', '');
await this.handleViewChatProfile(chatId, telegramId, matchId);
} else if (data.startsWith('unmatch_')) {
const matchId = data.replace('unmatch_', '');
await this.handleUnmatch(chatId, telegramId, matchId);
} else if (data.startsWith('confirm_unmatch_')) {
const matchId = data.replace('confirm_unmatch_', '');
await this.handleConfirmUnmatch(chatId, telegramId, matchId);
}
// Настройки
else if (data === 'settings') {
await this.handleSettings(chatId, telegramId);
} else if (data === 'search_settings') {
await this.handleSearchSettings(chatId, telegramId);
} else if (data === 'notification_settings') {
await this.handleNotificationSettings(chatId, telegramId);
} else if (data === 'view_stats') {
await this.handleViewStats(chatId, telegramId);
} else if (data === 'view_profile_viewers') {
await this.handleViewProfileViewers(chatId, telegramId);
} else if (data === 'hide_profile') {
await this.handleHideProfile(chatId, telegramId);
} else if (data === 'delete_profile') {
await this.handleDeleteProfile(chatId, telegramId);
} else if (data === 'main_menu') {
await this.handleMainMenu(chatId, telegramId);
} else if (data === 'confirm_delete_profile') {
await this.handleConfirmDeleteProfile(chatId, telegramId);
}
// Информация
else if (data === 'how_it_works') {
await this.handleHowItWorks(chatId);
} else if (data === 'back_to_browsing') {
await this.handleStartBrowsing(chatId, telegramId);
} else if (data === 'get_vip') {
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 if (data === 'notifications') {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
await this.handleNotificationSettings(chatId, telegramId);
}
}
// Обработка переключения настроек уведомлений
else if (data.startsWith('notif_toggle:') ||
data === 'notif_time' ||
data.startsWith('notif_time_set:') ||
data === 'notif_dnd' ||
data.startsWith('notif_dnd_set:') ||
data === 'notif_dnd_time' ||
data.startsWith('notif_dnd_time_set:') ||
data === 'notif_dnd_time_custom') {
// Делегируем обработку в NotificationHandlers, если он доступен
if (this.notificationHandlers) {
// Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем
// NotificationHandlers уже зарегистрировал свои обработчики в register()
} else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция настройки уведомлений недоступна.',
show_alert: true
});
}
}
else {
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция в разработке!',
show_alert: false
});
return;
}
await this.bot.answerCallbackQuery(query.id);
} catch (error) {
console.error('Callback handler error:', error);
await this.bot.answerCallbackQuery(query.id, {
text: 'Произошла ошибка. Попробуйте еще раз.',
show_alert: true
});
}
}
// Добавим все необходимые методы для обработки коллбэков
async handleCreateProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise<void> {
// Заглушка метода
}
async handleViewMyProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleManagePhotos(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handlePreviewProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditName(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditAge(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditBio(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditHobbies(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditJob(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditEducation(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditHeight(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditReligion(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDatingGoal(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditLifestyle(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditSearchPreferences(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleAddPhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeletePhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetMainPhoto(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
// Заглушка метода
}
async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
// Заглушка метода
}
async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
// Заглушка метода
}
async handleEditSmoking(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDrinking(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditKids(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise<void> {
// Заглушка метода
}
async handleEditAgeRange(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleEditDistance(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise<void> {
// Заглушка метода
}
async handleVipSearch(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
// Заглушка метода
}
async handleNextCandidate(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
// Заглушка метода
}
async handleViewMatches(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleOpenChats(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise<void> {
// Заглушка метода
}
async handleSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSearchSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleViewStats(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleViewProfileViewers(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleHideProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleDeleteProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleMainMenu(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleHowItWorks(chatId: number): Promise<void> {
// Заглушка метода
}
async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
// Заглушка метода
}
async handleLanguageSettings(chatId: number, telegramId: string): Promise<void> {
// Заглушка метода
}
async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise<void> {
// Заглушка метода
}
async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise<void> {
// Заглушка метода
}
// Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers)
async handleNotificationSettings(chatId: number, telegramId: string): Promise<void> {
try {
if (this.notificationHandlers) {
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
// Вызываем метод из notificationHandlers для получения настроек и отображения меню
const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId);
await this.notificationHandlers.sendNotificationSettings(chatId, settings);
} else {
// Если NotificationHandlers недоступен, показываем сообщение об ошибке
await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.');
await this.handleSettings(chatId, telegramId);
}
} catch (error) {
console.error('Error handling notification settings:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.');
}
}
}

View File

@@ -2,16 +2,20 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-ap
import { ProfileService } from '../services/profileService';
import { MatchingService } from '../services/matchingService';
import { Profile } from '../models/Profile';
import { getUserTranslation } from '../services/localizationService';
import { NotificationHandlers } from './notificationHandlers';
export class CommandHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private matchingService: MatchingService;
private notificationHandlers: NotificationHandlers;
constructor(bot: TelegramBot) {
this.bot = bot;
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.notificationHandlers = new NotificationHandlers(bot);
}
register(): void {
@@ -22,6 +26,12 @@ export class CommandHandlers {
this.bot.onText(/\/matches/, (msg: Message) => this.handleMatches(msg));
this.bot.onText(/\/settings/, (msg: Message) => this.handleSettings(msg));
this.bot.onText(/\/create_profile/, (msg: Message) => this.handleCreateProfile(msg));
// Регистрация обработчика настроек уведомлений
this.bot.onText(/\/notifications/, (msg: Message) => this.notificationHandlers.handleNotificationsCommand(msg));
// Регистрируем обработчики для уведомлений
this.notificationHandlers.register();
}
async handleStart(msg: Message): Promise<void> {
@@ -43,7 +53,8 @@ export class CommandHandlers {
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
],
[
{ text: '⚙️ Настройки', callback_data: 'settings' }
{ text: '⚙️ Настройки', callback_data: 'settings' },
{ text: '🔔 Уведомления', callback_data: 'notifications' }
]
]
};
@@ -83,6 +94,7 @@ export class CommandHandlers {
/browse - Просмотр анкет
/matches - Ваши матчи
/settings - Настройки
/notifications - Настройки уведомлений
/help - Эта справка
<EFBFBD> Как использовать:
@@ -104,15 +116,18 @@ export class CommandHandlers {
const profile = await this.profileService.getProfileByTelegramId(userId);
if (!profile) {
const createProfileText = await getUserTranslation(userId, 'profile.create');
const noProfileText = await getUserTranslation(userId, 'profile.noProfile');
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: '🚀 Создать профиль', callback_data: 'create_profile' }]
[{ text: createProfileText, callback_data: 'create_profile' }]
]
};
await this.bot.sendMessage(
msg.chat.id,
'❌ У вас пока нет профиля.\nСоздайте его для начала использования бота!',
noProfileText,
{ reply_markup: keyboard }
);
return;
@@ -129,9 +144,11 @@ export class CommandHandlers {
const profile = await this.profileService.getProfileByTelegramId(userId);
if (!profile) {
const createFirstText = await getUserTranslation(userId, 'profile.createFirst');
await this.bot.sendMessage(
msg.chat.id,
'❌ Сначала создайте профиль!\nИспользуйте команду /start'
createFirstText
);
return;
}
@@ -185,7 +202,7 @@ export class CommandHandlers {
inline_keyboard: [
[
{ text: '🔍 Настройки поиска', callback_data: 'search_settings' },
{ text: '🔔 Уведомления', callback_data: 'notification_settings' }
{ text: '🔔 Уведомления', callback_data: 'notifications' }
],
[
{ text: '🚫 Скрыть профиль', callback_data: 'hide_profile' },
@@ -236,7 +253,10 @@ export class CommandHandlers {
{ text: '✏️ Редактировать', callback_data: 'edit_profile' },
{ text: '📸 Фото', callback_data: 'manage_photos' }
],
[{ text: '🔍 Начать поиск', callback_data: 'start_browsing' }]
[
{ text: '🔍 Начать поиск', callback_data: 'start_browsing' },
{ text: '🔔 Уведомления', callback_data: 'notifications' }
]
]
} : {
inline_keyboard: [

View File

@@ -168,25 +168,24 @@ export class EnhancedChatHandlers {
// ===== СИСТЕМА УВЕДОМЛЕНИЙ =====
// Отправить уведомление о новом сообщении
// Отправить уведомление о новом сообщении - теперь используем NotificationService
async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise<void> {
try {
const receiverChatId = parseInt(receiverTelegramId);
// Получаем идентификаторы пользователей для использования в NotificationService
const receiverUserId = await this.profileService.getUserIdByTelegramId(receiverTelegramId);
const sender = await this.chatService.getMatchInfo(matchId, receiverTelegramId);
await this.bot.sendMessage(
receiverChatId,
`💌 *Новое сообщение от ${senderName}*\n\n` +
`"${this.escapeMarkdown(messagePreview)}"\n\n` +
'👆 Нажмите "Открыть чат" для ответа',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[{ text: '💬 Открыть чат', callback_data: `open_native_chat_${matchId}` }],
[{ text: '📱 Все чаты', callback_data: 'native_chats' }]
]
}
}
if (!receiverUserId || !sender?.otherUserId) {
console.error('Failed to get user IDs for notification');
return;
}
// Используем сервис уведомлений для отправки более красивого уведомления
await this.notificationService.sendMessageNotification(
receiverUserId,
sender.otherUserId,
messagePreview,
matchId
);
} catch (error) {
console.error('Error sending message notification:', error);
@@ -218,9 +217,10 @@ export class EnhancedChatHandlers {
const messageId = await this.chatService.sendMessage(
matchId,
telegramId,
msg.text || '[Медиа]',
msg.photo ? 'photo' : 'text',
msg.photo ? msg.photo[msg.photo.length - 1].file_id : undefined
msg.photo ?
(msg.caption || '[Фото]') + ' [file_id: ' + msg.photo[msg.photo.length - 1].file_id + ']' :
(msg.text || '[Медиа]'),
msg.photo ? 'photo' : 'text'
);
if (messageId) {

View File

@@ -0,0 +1,76 @@
import TelegramBot from 'node-telegram-bot-api';
import { ProfileService } from '../services/profileService';
import { MatchingService } from '../services/matchingService';
export class LikeBackHandler {
private bot: TelegramBot;
private profileService: ProfileService;
private matchingService: MatchingService;
constructor(bot: TelegramBot) {
this.bot = bot;
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
}
// Функция для обработки обратного лайка из уведомления
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
try {
// Получаем информацию о пользователях
const [userId, targetProfile] = await Promise.all([
this.profileService.getUserIdByTelegramId(telegramId),
this.profileService.getProfileByUserId(targetUserId)
]);
if (!userId || !targetProfile) {
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
return;
}
// Проверяем, есть ли уже свайп
const existingSwipe = await this.matchingService.getSwipeBetweenUsers(userId, targetUserId);
if (existingSwipe) {
await this.bot.sendMessage(chatId, '❓ Вы уже оценили этот профиль ранее.');
return;
}
// Создаем свайп (лайк)
const result = await this.matchingService.createSwipe(userId, targetUserId, 'like');
if (result.isMatch) {
// Это матч!
await this.bot.sendMessage(
chatId,
'🎉 *Поздравляем! Это взаимно!*\n\n' +
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
'Теперь вы можете начать общение.',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
]
}
}
);
} else {
await this.bot.sendMessage(
chatId,
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
'Если вы также понравитесь этому пользователю, будет создан матч.',
{
reply_markup: {
inline_keyboard: [
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
]
}
}
);
}
} catch (error) {
console.error('Error in handleLikeBack:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
}
}
}

View File

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

View File

@@ -0,0 +1,644 @@
import TelegramBot from 'node-telegram-bot-api';
import { v4 as uuidv4 } from 'uuid';
import { query } from '../database/connection';
import { NotificationService } from '../services/notificationService';
interface NotificationSettings {
newMatches: boolean;
newMessages: boolean;
newLikes: boolean;
reminders: boolean;
dailySummary: boolean;
timePreference: 'morning' | 'afternoon' | 'evening' | 'night';
doNotDisturb: boolean;
doNotDisturbStart?: string;
doNotDisturbEnd?: string;
}
export class NotificationHandlers {
private bot: TelegramBot;
private notificationService: NotificationService;
constructor(bot: TelegramBot) {
this.bot = bot;
this.notificationService = new NotificationService(bot);
}
// Метод для получения экземпляра сервиса уведомлений
getNotificationService(): NotificationService {
return this.notificationService;
}
// Обработка команды /notifications
async handleNotificationsCommand(msg: TelegramBot.Message): Promise<void> {
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
const settings = await this.notificationService.getNotificationSettings(userId);
await this.sendNotificationSettings(msg.chat.id, settings as NotificationSettings);
} catch (error) {
console.error('Error handling notifications command:', error);
await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при загрузке настроек уведомлений.');
}
}
// Отправка меню настроек уведомлений
async sendNotificationSettings(chatId: number, settings: NotificationSettings): Promise<void> {
const message = `
🔔 *Настройки уведомлений*
Выберите, какие уведомления вы хотите получать:
${settings.newMatches ? '✅' : '❌'} Новые матчи
${settings.newMessages ? '✅' : '❌'} Новые сообщения
${settings.newLikes ? '✅' : '❌'} Новые лайки
${settings.reminders ? '✅' : '❌'} Напоминания
${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки
⏰ Предпочтительное время: ${this.getTimePreferenceText(settings.timePreference)}
${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить": ${settings.doNotDisturb ? 'Включен' : 'Выключен'}
${settings.doNotDisturb && settings.doNotDisturbStart && settings.doNotDisturbEnd ?
`с ${settings.doNotDisturbStart} до ${settings.doNotDisturbEnd}` : ''}
Нажмите на кнопку, чтобы изменить настройку:
`;
await this.bot.sendMessage(chatId, message, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: `${settings.newMatches ? '✅' : '❌'} Новые матчи`, callback_data: 'notif_toggle:newMatches' },
{ text: `${settings.newMessages ? '✅' : '❌'} Новые сообщения`, callback_data: 'notif_toggle:newMessages' }
],
[
{ text: `${settings.newLikes ? '✅' : '❌'} Новые лайки`, callback_data: 'notif_toggle:newLikes' },
{ text: `${settings.reminders ? '✅' : '❌'} Напоминания`, callback_data: 'notif_toggle:reminders' }
],
[
{ text: `${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки`, callback_data: 'notif_toggle:dailySummary' }
],
[
{ text: `⏰ Время: ${this.getTimePreferenceText(settings.timePreference)}`, callback_data: 'notif_time' }
],
[
{ text: `${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить"`, callback_data: 'notif_dnd' }
],
[
{ text: '↩️ Назад', callback_data: 'settings' }
]
]
}
});
}
// Обработка переключения настройки уведомления
async handleNotificationToggle(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_toggle:settingName
const settingName = callbackQuery.data?.split(':')[1];
if (!settingName) return;
const settings = await this.notificationService.getNotificationSettings(userId);
let updatedSettings: Partial<NotificationSettings> = { ...settings };
// Инвертируем значение настройки
if (settingName in updatedSettings) {
switch(settingName) {
case 'newMatches':
updatedSettings.newMatches = !updatedSettings.newMatches;
break;
case 'newMessages':
updatedSettings.newMessages = !updatedSettings.newMessages;
break;
case 'newLikes':
updatedSettings.newLikes = !updatedSettings.newLikes;
break;
case 'reminders':
updatedSettings.reminders = !updatedSettings.reminders;
break;
case 'dailySummary':
updatedSettings.dailySummary = !updatedSettings.dailySummary;
break;
}
}
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Настройка "${this.getSettingName(settingName)}" ${updatedSettings[settingName as keyof NotificationSettings] ? 'включена' : 'отключена'}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings as NotificationSettings);
} catch (error) {
console.error('Error handling notification toggle:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении настроек.' });
}
}
// Обработка выбора времени для уведомлений
async handleTimePreference(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('⏰ *Выберите предпочтительное время для уведомлений:*', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '🌅 Утро (9:00)', callback_data: 'notif_time_set:morning' },
{ text: '☀️ День (13:00)', callback_data: 'notif_time_set:afternoon' }
],
[
{ text: '🌆 Вечер (19:00)', callback_data: 'notif_time_set:evening' },
{ text: '🌙 Ночь (22:00)', callback_data: 'notif_time_set:night' }
],
[
{ text: '↩️ Назад', callback_data: 'notifications' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка установки времени для уведомлений
async handleTimePreferenceSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_time_set:timePreference
const timePreference = callbackQuery.data?.split(':')[1] as 'morning' | 'afternoon' | 'evening' | 'night';
if (!timePreference) return;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
timePreference
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Время уведомлений установлено на ${this.getTimePreferenceText(timePreference)}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
} catch (error) {
console.error('Error handling time preference set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении времени уведомлений.' });
}
}
// Обработка режима "Не беспокоить"
async handleDndMode(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('🔕 *Режим "Не беспокоить"*\n\nВыберите действие:', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Включить', callback_data: 'notif_dnd_set:on' },
{ text: '❌ Выключить', callback_data: 'notif_dnd_set:off' }
],
[
{ text: '⏰ Настроить время', callback_data: 'notif_dnd_time' }
],
[
{ text: '↩️ Назад', callback_data: 'notifications' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка установки режима "Не беспокоить"
async handleDndModeSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_dnd_set:on/off
const mode = callbackQuery.data?.split(':')[1];
if (!mode) return;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужное поле
const existingSettings = settings as NotificationSettings;
let updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: mode === 'on'
};
// Если включаем режим "Не беспокоить", но не задано время, ставим дефолтные значения
if (mode === 'on' && (!updatedSettings.doNotDisturbStart || !updatedSettings.doNotDisturbEnd)) {
updatedSettings.doNotDisturbStart = '23:00';
updatedSettings.doNotDisturbEnd = '08:00';
}
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Режим "Не беспокоить" ${mode === 'on' ? 'включен' : 'выключен'}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
} catch (error) {
console.error('Error handling DND mode set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении режима "Не беспокоить".' });
}
}
// Настройка времени для режима "Не беспокоить"
async handleDndTimeSetup(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('⏰ *Настройка времени для режима "Не беспокоить"*\n\nВыберите один из предустановленных вариантов или введите свой:', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '🌙 23:00 - 08:00', callback_data: 'notif_dnd_time_set:23:00:08:00' }
],
[
{ text: '🌙 22:00 - 07:00', callback_data: 'notif_dnd_time_set:22:00:07:00' }
],
[
{ text: '🌙 00:00 - 09:00', callback_data: 'notif_dnd_time_set:00:00:09:00' }
],
[
{ text: '✏️ Ввести свой вариант', callback_data: 'notif_dnd_time_custom' }
],
[
{ text: '↩️ Назад', callback_data: 'notif_dnd' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Установка предустановленного времени для режима "Не беспокоить"
async handleDndTimeSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_dnd_time_set:startTime:endTime
const parts = callbackQuery.data?.split(':');
if (parts && parts.length >= 4) {
const startTime = `${parts[2]}:${parts[3]}`;
const endTime = `${parts[4]}:${parts[5]}`;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: true,
doNotDisturbStart: startTime,
doNotDisturbEnd: endTime
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
}
} catch (error) {
console.error('Error handling DND time set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при настройке времени "Не беспокоить".' });
}
}
// Запрос пользовательского времени для режима "Не беспокоить"
async handleDndTimeCustom(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
// Устанавливаем ожидание пользовательского ввода
const userId = callbackQuery.from?.id.toString();
if (userId) {
await this.setUserState(userId, 'waiting_dnd_time');
}
await this.bot.editMessageText('⏰ *Введите время для режима "Не беспокоить"*\n\nУкажите время в формате:\n`с [ЧЧ:ММ] до [ЧЧ:ММ]`\n\nНапример: `с 23:30 до 07:00`', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '↩️ Отмена', callback_data: 'notif_dnd_time' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка пользовательского ввода времени для режима "Не беспокоить"
async handleDndTimeInput(msg: TelegramBot.Message): Promise<void> {
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
// Очищаем состояние ожидания
await this.clearUserState(telegramId);
// Парсим введенное время
const timeRegex = /с\s+(\d{1,2}[:\.]\d{2})\s+до\s+(\d{1,2}[:\.]\d{2})/i;
const match = msg.text?.match(timeRegex);
if (match && match.length >= 3) {
let startTime = match[1].replace('.', ':');
let endTime = match[2].replace('.', ':');
// Проверяем и форматируем время
if (this.isValidTime(startTime) && this.isValidTime(endTime)) {
startTime = this.formatTime(startTime);
endTime = this.formatTime(endTime);
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: true,
doNotDisturbStart: startTime,
doNotDisturbEnd: endTime
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
await this.bot.sendMessage(msg.chat.id, `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`);
await this.sendNotificationSettings(msg.chat.id, updatedSettings);
} else {
await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат времени. Пожалуйста, используйте формат ЧЧ:ММ (например, 23:30).');
}
} else {
await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат ввода. Пожалуйста, введите время в формате "с [ЧЧ:ММ] до [ЧЧ:ММ]" (например, "с 23:30 до 07:00").');
}
} catch (error) {
console.error('Error handling DND time input:', error);
await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при настройке времени "Не беспокоить".');
}
}
// Проверка валидности времени
private isValidTime(time: string): boolean {
const regex = /^(\d{1,2}):(\d{2})$/;
const match = time.match(regex);
if (match) {
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
return false;
}
// Форматирование времени в формат ЧЧ:ММ
private formatTime(time: string): string {
const [hours, minutes] = time.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
// Получение текстового представления времени
private getTimePreferenceText(preference: string): string {
switch (preference) {
case 'morning': return 'Утро (9:00)';
case 'afternoon': return 'День (13:00)';
case 'evening': return 'Вечер (19:00)';
case 'night': return 'Ночь (22:00)';
default: return 'Вечер (19:00)';
}
}
// Получение названия настройки
private getSettingName(setting: string): string {
switch (setting) {
case 'newMatches': return 'Новые матчи';
case 'newMessages': return 'Новые сообщения';
case 'newLikes': return 'Новые лайки';
case 'reminders': return 'Напоминания';
case 'dailySummary': return 'Ежедневные сводки';
default: return setting;
}
}
// Получение ID пользователя по Telegram ID
private async getUserIdByTelegramId(telegramId: string): Promise<string | null> {
try {
const result = await query(
'SELECT id FROM users WHERE telegram_id = $1',
[parseInt(telegramId)]
);
return result.rows.length > 0 ? result.rows[0].id : null;
} catch (error) {
console.error('Error getting user by telegram ID:', error);
return null;
}
}
// Установка состояния ожидания пользователя
private async setUserState(telegramId: string, state: string): Promise<void> {
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) return;
// Сначала проверяем, существуют ли столбцы state и state_data
const checkColumnResult = await query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'state'
`);
if (checkColumnResult.rows.length === 0) {
console.log('Adding state and state_data columns to users table...');
// Добавляем столбцы, если их нет
await query(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb;
`);
}
// Теперь устанавливаем состояние
await query(
`UPDATE users
SET state = $1,
state_data = jsonb_set(COALESCE(state_data, '{}'::jsonb), '{timestamp}', to_jsonb(NOW()))
WHERE telegram_id = $2`,
[state, parseInt(telegramId)]
);
} catch (error) {
console.error('Error setting user state:', error);
}
}
// Очистка состояния ожидания пользователя
private async clearUserState(telegramId: string): Promise<void> {
try {
await query(
'UPDATE users SET state = NULL WHERE telegram_id = $1',
[parseInt(telegramId)]
);
} catch (error) {
console.error('Error clearing user state:', error);
}
}
// Регистрация обработчиков уведомлений
register(): void {
// Команда настройки уведомлений
this.bot.onText(/\/notifications/, this.handleNotificationsCommand.bind(this));
// Обработчик для кнопки настроек уведомлений в меню настроек
this.bot.on('callback_query', async (callbackQuery) => {
if (callbackQuery.data === 'notifications') {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
const settings = await this.notificationService.getNotificationSettings(userId);
await this.sendNotificationSettings(callbackQuery.message.chat.id, settings as NotificationSettings);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error handling notifications callback:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при загрузке настроек уведомлений.' });
}
}
else if (callbackQuery.data?.startsWith('notif_toggle:')) {
await this.handleNotificationToggle(callbackQuery);
}
else if (callbackQuery.data === 'notif_time') {
await this.handleTimePreference(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_time_set:')) {
await this.handleTimePreferenceSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd') {
await this.handleDndMode(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_dnd_set:')) {
await this.handleDndModeSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd_time') {
await this.handleDndTimeSetup(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_dnd_time_set:')) {
await this.handleDndTimeSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd_time_custom') {
await this.handleDndTimeCustom(callbackQuery);
}
});
// Обработчик пользовательского ввода для времени "Не беспокоить"
this.bot.on('message', async (msg) => {
if (!msg.text) return;
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
// Сначала проверяем, существует ли столбец state
const checkColumnResult = await query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'state'
`);
if (checkColumnResult.rows.length === 0) {
console.log('State column does not exist in users table. Skipping state check.');
return;
}
// Теперь проверяем состояние пользователя
const result = await query(
'SELECT state FROM users WHERE telegram_id = $1',
[parseInt(telegramId)]
);
if (result.rows.length > 0 && result.rows[0].state === 'waiting_dnd_time') {
await this.handleDndTimeInput(msg);
}
} catch (error) {
console.error('Error checking user state:', error);
}
});
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More