50
.env.example
50
.env.example
@@ -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
68
.env.production
Normal 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
|
||||
25
Dockerfile
25
Dockerfile
@@ -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"]
|
||||
|
||||
43
README.md
43
README.md
@@ -268,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
|
||||
# Клонировать репозиторий
|
||||
@@ -279,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
|
||||
|
||||
# Запуск бота
|
||||
|
||||
54
bin/backup_db.sh
Normal file
54
bin/backup_db.sh
Normal 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
72
bin/create_release.sh
Normal 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
58
bin/install_docker.sh
Normal 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!"
|
||||
@@ -1,14 +0,0 @@
|
||||
import { query } from './src/database/connection';
|
||||
|
||||
async function checkSchema() {
|
||||
try {
|
||||
const result = await query('SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1', ['messages']);
|
||||
console.log(result.rows);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkSchema();
|
||||
55
deploy.sh
Normal file
55
deploy.sh
Normal 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"
|
||||
69
docker-compose.override.yml.example
Normal file
69
docker-compose.override.yml.example
Normal 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
|
||||
@@ -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
|
||||
|
||||
264
docs/PRODUCTION_DEPLOYMENT.md
Normal file
264
docs/PRODUCTION_DEPLOYMENT.md
Normal 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 для веб-интерфейса
|
||||
- [ ] (Опционально) Настроено автоматическое обновление
|
||||
44
migrations/1631980000000_add_profile_views_table.ts
Normal file
44
migrations/1631980000000_add_profile_views_table.ts
Normal 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 });
|
||||
}
|
||||
@@ -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
|
||||
$$;
|
||||
`);
|
||||
};
|
||||
14
migrations/add_user_state_columns.sql
Normal file
14
migrations/add_user_state_columns.sql
Normal 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';
|
||||
@@ -22,6 +22,7 @@
|
||||
"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"
|
||||
|
||||
49
scripts/README.md
Normal file
49
scripts/README.md
Normal 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`.
|
||||
259
scripts/createNotificationTables.js
Normal file
259
scripts/createNotificationTables.js
Normal 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));
|
||||
86
scripts/createProfileViewsTable.js
Normal file
86
scripts/createProfileViewsTable.js
Normal 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();
|
||||
88
scripts/legacy/checkCallbackHandlers.js
Normal file
88
scripts/legacy/checkCallbackHandlers.js
Normal 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();
|
||||
66
scripts/legacy/checkDatabase.js
Normal file
66
scripts/legacy/checkDatabase.js
Normal 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();
|
||||
64
scripts/legacy/checkProfileViews.js
Normal file
64
scripts/legacy/checkProfileViews.js
Normal 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();
|
||||
74
scripts/legacy/checkUserTable.js
Normal file
74
scripts/legacy/checkUserTable.js
Normal 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();
|
||||
55
scripts/legacy/cleanDatabase.js
Normal file
55
scripts/legacy/cleanDatabase.js
Normal 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();
|
||||
66
scripts/legacy/clearDatabase.js
Normal file
66
scripts/legacy/clearDatabase.js
Normal 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();
|
||||
81
scripts/legacy/clearDatabase.mjs
Normal file
81
scripts/legacy/clearDatabase.mjs
Normal 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();
|
||||
26
scripts/legacy/clear_database.sql
Normal file
26
scripts/legacy/clear_database.sql
Normal 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';
|
||||
70
scripts/legacy/createProfileViewsTable.ts
Normal file
70
scripts/legacy/createProfileViewsTable.ts
Normal 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());
|
||||
142
scripts/legacy/fixCallbackHandlers.js
Normal file
142
scripts/legacy/fixCallbackHandlers.js
Normal 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);
|
||||
}
|
||||
170
scripts/legacy/fixDatabaseStructure.js
Normal file
170
scripts/legacy/fixDatabaseStructure.js
Normal 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();
|
||||
48
scripts/legacy/fix_all_notifications.js
Normal file
48
scripts/legacy/fix_all_notifications.js
Normal 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💡 Уведомления должны теперь работать корректно!');
|
||||
});
|
||||
});
|
||||
332
scripts/legacy/fix_notification_callbacks.js
Normal file
332
scripts/legacy/fix_notification_callbacks.js
Normal 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();
|
||||
85
scripts/legacy/testCallbacks.js
Normal file
85
scripts/legacy/testCallbacks.js
Normal 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();
|
||||
102
scripts/legacy/testMatching.js
Normal file
102
scripts/legacy/testMatching.js
Normal 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();
|
||||
98
scripts/legacy/testProfileViews.js
Normal file
98
scripts/legacy/testProfileViews.js
Normal 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();
|
||||
81
scripts/legacy/testVipMethod.js
Normal file
81
scripts/legacy/testVipMethod.js
Normal 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();
|
||||
75
scripts/legacy/testVipStatus.js
Normal file
75
scripts/legacy/testVipStatus.js
Normal 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();
|
||||
16
scripts/startup.sh
Normal file
16
scripts/startup.sh
Normal 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
|
||||
104
scripts/update_bot_with_notifications.js
Normal file
104
scripts/update_bot_with_notifications.js
Normal 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('🔔 Перезапустите бота для применения изменений');
|
||||
BIN
set-premium.js
BIN
set-premium.js
Binary file not shown.
404
sql/consolidated.sql
Normal file
404
sql/consolidated.sql
Normal 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;
|
||||
51
src/bot.ts
51
src/bot.ts
@@ -8,6 +8,8 @@ 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;
|
||||
@@ -18,7 +20,7 @@ class TelegramTinderBot {
|
||||
private commandHandlers: CommandHandlers;
|
||||
private callbackHandlers: CallbackHandlers;
|
||||
private messageHandlers: MessageHandlers;
|
||||
|
||||
private notificationHandlers: NotificationHandlers;
|
||||
constructor() {
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
if (!token) {
|
||||
@@ -34,6 +36,7 @@ class TelegramTinderBot {
|
||||
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();
|
||||
@@ -78,6 +81,7 @@ class TelegramTinderBot {
|
||||
{ command: 'browse', description: '💕 Смотреть анкеты' },
|
||||
{ command: 'matches', description: '💖 Мои матчи' },
|
||||
{ command: 'settings', description: '⚙️ Настройки' },
|
||||
{ command: 'notifications', description: '🔔 Настройки уведомлений' },
|
||||
{ command: 'help', description: '❓ Помощь' }
|
||||
];
|
||||
|
||||
@@ -94,6 +98,9 @@ class TelegramTinderBot {
|
||||
|
||||
// Сообщения
|
||||
this.messageHandlers.register();
|
||||
|
||||
// Обработчики уведомлений
|
||||
this.notificationHandlers.register();
|
||||
}
|
||||
|
||||
// Обработка ошибок
|
||||
@@ -137,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);
|
||||
}
|
||||
|
||||
// Очистка старых данных
|
||||
@@ -152,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`);
|
||||
|
||||
@@ -186,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) {
|
||||
@@ -229,4 +260,4 @@ if (require.main === module) {
|
||||
});
|
||||
}
|
||||
|
||||
export { TelegramTinderBot };
|
||||
export { TelegramTinderBot };
|
||||
|
||||
108
src/database/migrateOnStartup.ts
Normal file
108
src/database/migrateOnStartup.ts
Normal 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);
|
||||
});
|
||||
182
src/database/migrations/consolidated.sql
Normal file
182
src/database/migrations/consolidated.sql
Normal 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);
|
||||
@@ -4,12 +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;
|
||||
@@ -22,6 +24,8 @@ export class CallbackHandlers {
|
||||
private vipController: VipController;
|
||||
private vipService: VipService;
|
||||
private translationController: TranslationController;
|
||||
private notificationHandlers?: NotificationHandlers;
|
||||
private likeBackHandler: LikeBackHandler;
|
||||
|
||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||
this.bot = bot;
|
||||
@@ -34,6 +38,13 @@ export class CallbackHandlers {
|
||||
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 {
|
||||
@@ -167,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);
|
||||
@@ -263,6 +280,41 @@ export class CallbackHandlers {
|
||||
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: 'Функция в разработке!',
|
||||
@@ -385,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,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, '❌ Произошла ошибка при обработке лайка');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,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 = {
|
||||
@@ -766,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> {
|
||||
@@ -807,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';
|
||||
@@ -876,20 +1018,43 @@ 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1450,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: [
|
||||
[
|
||||
@@ -2112,4 +2277,27 @@ export class CallbackHandlers {
|
||||
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, '❌ Произошла ошибка при загрузке настроек уведомлений. Попробуйте позже.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
606
src/handlers/callbackHandlers.ts.backup-1758166633763
Normal file
606
src/handlers/callbackHandlers.ts.backup-1758166633763
Normal 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, '❌ Произошла ошибка при загрузке настроек уведомлений.');
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/handlers/callbackHandlers.ts.original
Normal file
BIN
src/handlers/callbackHandlers.ts.original
Normal file
Binary file not shown.
606
src/handlers/callbackHandlers.ts.stub
Normal file
606
src/handlers/callbackHandlers.ts.stub
Normal 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, '❌ Произошла ошибка при загрузке настроек уведомлений.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,19 @@ 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 {
|
||||
@@ -23,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> {
|
||||
@@ -44,7 +53,8 @@ export class CommandHandlers {
|
||||
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
|
||||
],
|
||||
[
|
||||
{ text: '⚙️ Настройки', callback_data: 'settings' }
|
||||
{ text: '⚙️ Настройки', callback_data: 'settings' },
|
||||
{ text: '🔔 Уведомления', callback_data: 'notifications' }
|
||||
]
|
||||
]
|
||||
};
|
||||
@@ -84,6 +94,7 @@ export class CommandHandlers {
|
||||
/browse - Просмотр анкет
|
||||
/matches - Ваши матчи
|
||||
/settings - Настройки
|
||||
/notifications - Настройки уведомлений
|
||||
/help - Эта справка
|
||||
|
||||
<EFBFBD> Как использовать:
|
||||
@@ -191,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' },
|
||||
@@ -242,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: [
|
||||
|
||||
@@ -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);
|
||||
|
||||
76
src/handlers/likeBackHandler.ts
Normal file
76
src/handlers/likeBackHandler.ts
Normal 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, '❌ Произошла ошибка при обработке лайка');
|
||||
}
|
||||
}
|
||||
}
|
||||
644
src/handlers/notificationHandlers.ts
Normal file
644
src/handlers/notificationHandlers.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
62
src/premium/README.md
Normal file
62
src/premium/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Модуль премиум-функций Telegram Tinder Bot
|
||||
|
||||
Этот каталог содержит модули и скрипты для управления премиум-функциями бота.
|
||||
|
||||
## Содержимое
|
||||
|
||||
- `add-premium-columns.js` - Добавление колонок для премиум-функций в базу данных (версия JavaScript)
|
||||
- `add-premium-columns.ts` - Добавление колонок для премиум-функций в базу данных (версия TypeScript)
|
||||
- `add-premium-columns-direct.js` - Прямое добавление премиум-колонок без миграций
|
||||
- `addPremiumColumn.js` - Добавление отдельной колонки премиум в таблицу пользователей
|
||||
- `setPremiumStatus.js` - Обновление статуса премиум для пользователей
|
||||
|
||||
## Премиум-функции
|
||||
|
||||
В боте реализованы следующие премиум-функции:
|
||||
|
||||
1. **Неограниченные лайки** - снятие дневного лимита на количество лайков
|
||||
2. **Супер-лайки** - возможность отправлять супер-лайки (повышенный приоритет)
|
||||
3. **Просмотр лайков** - возможность видеть, кто поставил лайк вашему профилю
|
||||
4. **Скрытый режим** - возможность скрывать свою активность
|
||||
5. **Расширенные фильтры** - дополнительные параметры для поиска
|
||||
|
||||
## Использование
|
||||
|
||||
### Добавление премиум-колонок в базу данных
|
||||
|
||||
```bash
|
||||
node src/premium/add-premium-columns.js
|
||||
```
|
||||
|
||||
### Изменение премиум-статуса пользователя
|
||||
|
||||
```typescript
|
||||
import { PremiumService } from '../services/premiumService';
|
||||
|
||||
// Установка премиум-статуса для пользователя
|
||||
const premiumService = new PremiumService();
|
||||
await premiumService.setPremiumStatus(userId, true, 30); // 30 дней премиума
|
||||
```
|
||||
|
||||
## Интеграция в основной код
|
||||
|
||||
Проверка премиум-статуса должна выполняться следующим образом:
|
||||
|
||||
```typescript
|
||||
// В классах контроллеров
|
||||
const isPremium = await this.premiumService.checkUserPremium(userId);
|
||||
|
||||
if (isPremium) {
|
||||
// Предоставить премиум-функцию
|
||||
} else {
|
||||
// Сообщить о необходимости премиум-подписки
|
||||
}
|
||||
```
|
||||
|
||||
## Период действия премиум-статуса
|
||||
|
||||
По умолчанию премиум-статус устанавливается на 30 дней. Для изменения срока используйте третий параметр в методе `setPremiumStatus`.
|
||||
|
||||
## Дополнительная информация
|
||||
|
||||
Более подробная информация о премиум-функциях содержится в документации проекта в каталоге `docs/VIP_FUNCTIONS.md`.
|
||||
58
src/premium/addPremiumColumn.js
Normal file
58
src/premium/addPremiumColumn.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// Скрипт для добавления колонки premium в таблицу users и установки premium для всех пользователей
|
||||
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 setAllUsersToPremium() {
|
||||
try {
|
||||
console.log('Проверяем наличие столбца premium в таблице users...');
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'premium'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!result.rows[0].exists) {
|
||||
console.log('🔄 Добавляем столбец premium...');
|
||||
await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`);
|
||||
console.log('✅ Столбец premium успешно добавлен');
|
||||
} else {
|
||||
console.log('✅ Столбец premium уже существует');
|
||||
}
|
||||
|
||||
console.log('Устанавливаем премиум-статус для всех пользователей...');
|
||||
|
||||
const updateResult = await pool.query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
WHERE true
|
||||
RETURNING id, telegram_id, premium
|
||||
`);
|
||||
|
||||
console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`);
|
||||
updateResult.rows.forEach(row => {
|
||||
console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`);
|
||||
});
|
||||
|
||||
console.log('🎉 Все пользователи теперь имеют премиум-статус!');
|
||||
} catch (error) {
|
||||
console.error('Ошибка при установке премиум-статуса:', error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
console.log('Соединение с базой данных закрыто');
|
||||
}
|
||||
}
|
||||
|
||||
setAllUsersToPremium();
|
||||
73
src/premium/setPremiumStatus.js
Normal file
73
src/premium/setPremiumStatus.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// Скрипт для установки премиум-статуса всем пользователям
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Проверяем и выводим параметры подключения
|
||||
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 setAllUsersToPremium() {
|
||||
try {
|
||||
console.log('Устанавливаем премиум-статус для всех пользователей...');
|
||||
|
||||
// Проверка соединения с БД
|
||||
console.log('Проверка соединения с БД...');
|
||||
const testResult = await pool.query('SELECT NOW()');
|
||||
console.log('✅ Соединение успешно:', testResult.rows[0].now);
|
||||
|
||||
// Проверка наличия столбца premium
|
||||
console.log('Проверяем наличие столбца premium в таблице users...');
|
||||
|
||||
const checkResult = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'premium'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!checkResult.rows[0].exists) {
|
||||
console.log('🔄 Добавляем столбец premium...');
|
||||
await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`);
|
||||
console.log('✅ Столбец premium успешно добавлен');
|
||||
} else {
|
||||
console.log('✅ Столбец premium уже существует');
|
||||
}
|
||||
|
||||
// Устанавливаем premium=true для всех пользователей
|
||||
const updateResult = await pool.query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
WHERE true
|
||||
RETURNING id, telegram_id, premium
|
||||
`);
|
||||
|
||||
console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`);
|
||||
updateResult.rows.forEach(row => {
|
||||
console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`);
|
||||
});
|
||||
|
||||
console.log('🎉 Все пользователи теперь имеют премиум-статус!');
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при установке премиум-статуса:', error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
console.log('Соединение с базой данных закрыто');
|
||||
}
|
||||
}
|
||||
|
||||
setAllUsersToPremium();
|
||||
133
src/scripts/enhanceNotifications.ts
Normal file
133
src/scripts/enhanceNotifications.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { query } from '../database/connection';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Загружаем переменные окружения
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Скрипт для обновления механизма уведомлений
|
||||
*/
|
||||
export async function enhanceNotifications() {
|
||||
try {
|
||||
console.log('Enhancing notifications system...');
|
||||
console.log('DB Connection Details:');
|
||||
console.log(`- Host: ${process.env.DB_HOST}`);
|
||||
console.log(`- Port: ${process.env.DB_PORT}`);
|
||||
console.log(`- Database: ${process.env.DB_NAME}`);
|
||||
|
||||
// 1. Создаем расширение для генерации UUID, если его нет
|
||||
await query(`
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"
|
||||
`);
|
||||
|
||||
// 2. Создаем таблицу для хранения типов уведомлений и шаблонов сообщений, если её нет
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS 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 NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// 2. Вставляем базовые шаблоны для различных типов уведомлений
|
||||
const templates = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_like',
|
||||
title: 'Новый лайк!',
|
||||
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
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' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'super_like',
|
||||
title: 'Супер-лайк!',
|
||||
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
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' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_match',
|
||||
title: 'Новый матч!',
|
||||
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_message',
|
||||
title: 'Новое сообщение!',
|
||||
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
// Вставляем шаблоны с проверкой на конфликты
|
||||
for (const template of templates) {
|
||||
await query(`
|
||||
INSERT INTO notification_templates
|
||||
(id, type, title, message_template, button_template, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (type) DO UPDATE
|
||||
SET title = $3,
|
||||
message_template = $4,
|
||||
button_template = $5
|
||||
`, [template.id, template.type, template.title, template.message_template, template.button_template]);
|
||||
}
|
||||
|
||||
console.log('✅ Notification templates updated successfully');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error enhancing notifications:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запускаем файл напрямую
|
||||
if (require.main === module) {
|
||||
enhanceNotifications().then(() => {
|
||||
console.log('Notification system enhancement completed');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,33 @@
|
||||
import { query } from '../database/connection';
|
||||
import { query, testConnection } from '../database/connection';
|
||||
|
||||
async function setAllUsersToPremium() {
|
||||
try {
|
||||
console.log('Setting premium status for all users...');
|
||||
|
||||
// Проверка соединения с базой данных
|
||||
console.log('Testing database connection...');
|
||||
const dbConnected = await testConnection();
|
||||
if (!dbConnected) {
|
||||
throw new Error('Failed to connect to database. Please check your database settings.');
|
||||
}
|
||||
console.log('Database connection successful!');
|
||||
|
||||
// Проверка наличия столбца premium
|
||||
const checkResult = await query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'premium'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!checkResult.rows[0].exists) {
|
||||
console.log('Adding premium column to users table...');
|
||||
await query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`);
|
||||
console.log('Premium column added successfully');
|
||||
}
|
||||
|
||||
const result = await query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
|
||||
@@ -17,24 +17,6 @@ export class MatchingService {
|
||||
}
|
||||
|
||||
// Выполнить свайп
|
||||
// Конвертация типов свайпов между API и БД
|
||||
private convertSwipeTypeToDirection(swipeType: SwipeType): string {
|
||||
switch (swipeType) {
|
||||
case 'like': return 'right';
|
||||
case 'pass': return 'left';
|
||||
case 'superlike': return 'super';
|
||||
default: return 'left';
|
||||
}
|
||||
}
|
||||
|
||||
private convertDirectionToSwipeType(direction: string): SwipeType {
|
||||
switch (direction) {
|
||||
case 'right': return 'like';
|
||||
case 'left': return 'pass';
|
||||
case 'super': return 'superlike';
|
||||
default: return 'pass';
|
||||
}
|
||||
}
|
||||
|
||||
async performSwipe(telegramId: string, targetTelegramId: string, swipeType: SwipeType): Promise<{
|
||||
swipe: Swipe;
|
||||
@@ -63,22 +45,21 @@ export class MatchingService {
|
||||
}
|
||||
|
||||
const swipeId = uuidv4();
|
||||
const direction = this.convertSwipeTypeToDirection(swipeType);
|
||||
let isMatch = false;
|
||||
let match: Match | undefined;
|
||||
|
||||
await transaction(async (client) => {
|
||||
// Создаем свайп
|
||||
await client.query(`
|
||||
INSERT INTO swipes (id, user_id, target_user_id, direction, created_at)
|
||||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [swipeId, userId, targetUserId, direction, new Date()]);
|
||||
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||||
|
||||
// Если это лайк или суперлайк, проверяем взаимность
|
||||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||
const reciprocalSwipe = await client.query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super')
|
||||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||||
`, [targetUserId, userId]);
|
||||
|
||||
if (reciprocalSwipe.rows.length > 0) {
|
||||
@@ -91,7 +72,7 @@ export class MatchingService {
|
||||
if (existingMatch.rows.length === 0) {
|
||||
isMatch = true;
|
||||
const matchId = uuidv4();
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||||
|
||||
// Упорядочиваем пользователей для консистентности
|
||||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||
@@ -152,6 +133,89 @@ export class MatchingService {
|
||||
|
||||
return this.mapEntityToSwipe(result.rows[0]);
|
||||
}
|
||||
|
||||
// Получить свайп между двумя пользователями (псевдоним для getSwipe)
|
||||
async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||||
return this.getSwipe(userId, targetUserId);
|
||||
}
|
||||
|
||||
// Создать свайп (лайк, дислайк или суперлайк)
|
||||
async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{
|
||||
swipe: Swipe;
|
||||
isMatch: boolean;
|
||||
match?: Match;
|
||||
}> {
|
||||
const swipeId = uuidv4();
|
||||
let isMatch = false;
|
||||
let match: Match | undefined;
|
||||
|
||||
await transaction(async (client) => {
|
||||
// Создаем свайп
|
||||
await client.query(`
|
||||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||||
|
||||
// Если это лайк или суперлайк, проверяем взаимность
|
||||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||
const reciprocalSwipe = await client.query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||||
`, [targetUserId, userId]);
|
||||
|
||||
if (reciprocalSwipe.rows.length > 0) {
|
||||
// Проверяем, что матч еще не существует
|
||||
const existingMatch = await client.query(`
|
||||
SELECT * FROM matches
|
||||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||
`, [userId, targetUserId]);
|
||||
|
||||
if (existingMatch.rows.length === 0) {
|
||||
isMatch = true;
|
||||
const matchId = uuidv4();
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||||
|
||||
// Упорядочиваем пользователей для консистентности
|
||||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||
|
||||
// Создаем матч
|
||||
await client.query(`
|
||||
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]);
|
||||
|
||||
match = new Match({
|
||||
id: matchId,
|
||||
userId1: user1Id,
|
||||
userId2: user2Id,
|
||||
createdAt: new Date(),
|
||||
isActive: true,
|
||||
isSuperMatch: isSuperMatch,
|
||||
unreadCount1: 0,
|
||||
unreadCount2: 0
|
||||
});
|
||||
|
||||
// Обновляем свайпы, отмечая что они образуют матч
|
||||
await client.query(`
|
||||
UPDATE swipes SET is_match = true
|
||||
WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1)
|
||||
`, [userId, targetUserId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const swipe = new Swipe({
|
||||
id: swipeId,
|
||||
userId,
|
||||
targetUserId,
|
||||
type: swipeType,
|
||||
timestamp: new Date(),
|
||||
isMatch
|
||||
});
|
||||
|
||||
return { swipe, isMatch, match };
|
||||
}
|
||||
|
||||
// Получить все матчи пользователя по telegram ID
|
||||
async getUserMatches(telegramId: string, limit: number = 50): Promise<Match[]> {
|
||||
@@ -217,7 +281,7 @@ export class MatchingService {
|
||||
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
||||
const result = await query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false
|
||||
WHERE target_user_id = $1 AND type IN ('like', 'superlike') AND is_match = false
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
@@ -236,10 +300,10 @@ export class MatchingService {
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const result = await query(`
|
||||
SELECT direction, COUNT(*) as count
|
||||
SELECT type, COUNT(*) as count
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1 AND created_at >= $2
|
||||
GROUP BY direction
|
||||
WHERE user_id = $1 AND created_at >= $2
|
||||
GROUP BY type
|
||||
`, [userId, today]);
|
||||
|
||||
const stats = {
|
||||
@@ -253,11 +317,11 @@ export class MatchingService {
|
||||
const count = parseInt(row.count);
|
||||
stats.total += count;
|
||||
|
||||
switch (row.direction) {
|
||||
switch (row.type) {
|
||||
case 'like':
|
||||
stats.likes = count;
|
||||
break;
|
||||
case 'super':
|
||||
case 'superlike':
|
||||
stats.superlikes = count;
|
||||
break;
|
||||
case 'pass':
|
||||
@@ -299,9 +363,9 @@ export class MatchingService {
|
||||
private mapEntityToSwipe(entity: any): Swipe {
|
||||
return new Swipe({
|
||||
id: entity.id,
|
||||
userId: entity.swiper_id,
|
||||
targetUserId: entity.swiped_id,
|
||||
type: this.convertDirectionToSwipeType(entity.direction),
|
||||
userId: entity.user_id || entity.swiper_id,
|
||||
targetUserId: entity.target_user_id || entity.swiped_id,
|
||||
type: entity.type || 'pass',
|
||||
timestamp: entity.created_at,
|
||||
isMatch: entity.is_match
|
||||
});
|
||||
@@ -329,8 +393,8 @@ export class MatchingService {
|
||||
FROM swipes s1
|
||||
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||||
WHERE s1.user_id = $1
|
||||
AND s1.direction IN ('right', 'super')
|
||||
AND s2.direction IN ('right', 'super')
|
||||
AND s1.type IN ('like', 'superlike')
|
||||
AND s2.type IN ('like', 'superlike')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM matches m
|
||||
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
|
||||
@@ -343,73 +407,156 @@ export class MatchingService {
|
||||
|
||||
// Получить следующего кандидата для просмотра
|
||||
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
||||
console.log(`[DEBUG] getNextCandidate вызван для telegramId=${telegramId}, isNewUser=${isNewUser}`);
|
||||
|
||||
// Сначала получаем профиль пользователя по telegramId
|
||||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!userProfile) {
|
||||
console.log(`[ERROR] Профиль пользователя с telegramId=${telegramId} не найден`);
|
||||
throw new BotError('User profile not found', 'PROFILE_NOT_FOUND');
|
||||
}
|
||||
console.log(`[DEBUG] Найден профиль пользователя:`, JSON.stringify({
|
||||
userId: userProfile.userId,
|
||||
gender: userProfile.gender,
|
||||
interestedIn: userProfile.interestedIn,
|
||||
minAge: userProfile.searchPreferences?.minAge,
|
||||
maxAge: userProfile.searchPreferences?.maxAge
|
||||
}));
|
||||
|
||||
// Получаем UUID пользователя
|
||||
const userId = userProfile.userId;
|
||||
|
||||
// Получаем список уже просмотренных пользователей
|
||||
const viewedUsers = await query(`
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM swipes
|
||||
WHERE user_id = $1
|
||||
`, [userId]);
|
||||
|
||||
const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id);
|
||||
viewedUserIds.push(userId); // Исключаем самого себя
|
||||
|
||||
// Если это новый пользователь или у пользователя мало просмотренных профилей,
|
||||
// показываем всех пользователей по очереди (исключая только себя)
|
||||
let excludeCondition = '';
|
||||
|
||||
if (!isNewUser) {
|
||||
excludeCondition = viewedUserIds.length > 0
|
||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
||||
: '';
|
||||
// Определяем, каким должен быть пол показываемых профилей
|
||||
let targetGender: string;
|
||||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||
targetGender = userProfile.interestedIn;
|
||||
} else {
|
||||
// Для новых пользователей исключаем только себя
|
||||
excludeCondition = `AND p.user_id != $2`;
|
||||
// Если "both" или другое значение, показываем противоположный пол
|
||||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
||||
|
||||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||||
// и добавляем также профили из свайпов для полной совместимости
|
||||
console.log(`[DEBUG] Запрашиваем просмотренные и свайпнутые профили для userId=${userId}`);
|
||||
const [viewedProfilesResult, swipedProfilesResult] = await Promise.all([
|
||||
query(`
|
||||
SELECT DISTINCT viewed_profile_id
|
||||
FROM profile_views
|
||||
WHERE viewer_id = $1
|
||||
`, [userId]),
|
||||
query(`
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM swipes
|
||||
WHERE user_id = $1
|
||||
`, [userId])
|
||||
]);
|
||||
console.log(`[DEBUG] Найдено ${viewedProfilesResult.rows.length} просмотренных и ${swipedProfilesResult.rows.length} свайпнутых профилей`);
|
||||
|
||||
// Объединяем просмотренные и свайпнутые профили в один список
|
||||
const viewedUserIds = [
|
||||
...viewedProfilesResult.rows.map((row: any) => row.viewed_profile_id),
|
||||
...swipedProfilesResult.rows.map((row: any) => row.target_user_id)
|
||||
];
|
||||
|
||||
// Всегда добавляем самого пользователя в список исключений
|
||||
viewedUserIds.push(userId);
|
||||
|
||||
// Удаляем дубликаты
|
||||
const uniqueViewedIds = [...new Set(viewedUserIds)];
|
||||
console.log(`[DEBUG] Всего ${uniqueViewedIds.length} уникальных исключаемых профилей`);
|
||||
|
||||
// Формируем параметры запроса
|
||||
let params: any[] = [];
|
||||
let excludeCondition: string = '';
|
||||
|
||||
// Для новых пользователей исключаем только себя
|
||||
if (isNewUser || uniqueViewedIds.length <= 1) {
|
||||
params = [userId];
|
||||
excludeCondition = 'AND p.user_id != $1';
|
||||
console.log(`[DEBUG] Режим нового пользователя: исключаем только самого себя`);
|
||||
} else {
|
||||
// Для остальных исключаем все просмотренные профили
|
||||
params = [...uniqueViewedIds];
|
||||
const placeholders = uniqueViewedIds.map((_: any, i: number) => `$${i + 1}`).join(', ');
|
||||
excludeCondition = `AND p.user_id NOT IN (${placeholders})`;
|
||||
console.log(`[DEBUG] Стандартный режим: исключаем ${uniqueViewedIds.length} профилей`);
|
||||
}
|
||||
|
||||
// Ищем подходящих кандидатов
|
||||
// Выполним предварительный запрос для проверки наличия доступных анкет
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = '${targetGender}'
|
||||
${excludeCondition}
|
||||
`;
|
||||
|
||||
console.log(`[DEBUG] Проверка наличия подходящих анкет...`);
|
||||
console.log(`[DEBUG] SQL запрос count: ${countQuery}`);
|
||||
console.log(`[DEBUG] Параметры count: ${JSON.stringify(params)}`);
|
||||
const countResult = await query(countQuery, params);
|
||||
const availableProfilesCount = parseInt(countResult.rows[0]?.count || '0');
|
||||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||||
|
||||
// Используем определенный ранее targetGender для поиска
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
|
||||
const candidateQuery = `
|
||||
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = $1
|
||||
AND p.gender = '${targetGender}'
|
||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||
${excludeCondition}
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 1
|
||||
`;
|
||||
console.log(`[DEBUG] SQL запрос: ${candidateQuery}`);
|
||||
console.log(`[DEBUG] Параметры: ${JSON.stringify(params)}`);
|
||||
|
||||
const params = [userProfile.interestedIn, ...viewedUserIds];
|
||||
const result = await query(candidateQuery, params);
|
||||
console.log(`[DEBUG] Результаты запроса: найдено ${result.rows.length} профилей`);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
console.log(`[DEBUG] Подходящие кандидаты не найдены`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidateData = result.rows[0];
|
||||
console.log(`[DEBUG] Найден подходящий кандидат: ${candidateData.name}, возраст: ${candidateData.age}`);
|
||||
|
||||
// Записываем просмотр профиля в новую таблицу profile_views
|
||||
try {
|
||||
const viewerTelegramId = telegramId;
|
||||
const viewedTelegramId = candidateData.telegram_id.toString();
|
||||
|
||||
console.log(`[DEBUG] Записываем просмотр профиля: viewer=${viewerTelegramId}, viewed=${viewedTelegramId}`);
|
||||
// Асинхронно записываем просмотр, но не ждем завершения
|
||||
this.profileService.recordProfileView(viewerTelegramId, viewedTelegramId, 'browse')
|
||||
.catch(err => console.error(`[ERROR] Ошибка записи просмотра профиля:`, err));
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Ошибка записи просмотра профиля:`, err);
|
||||
}
|
||||
|
||||
// Используем ProfileService для правильного маппинга данных
|
||||
return this.profileService.mapEntityToProfile(candidateData);
|
||||
const profile = this.profileService.mapEntityToProfile(candidateData);
|
||||
console.log(`[DEBUG] Профиль преобразован и возвращается клиенту`);
|
||||
return profile;
|
||||
}
|
||||
|
||||
// VIP функция: поиск кандидатов по цели знакомства
|
||||
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
||||
const swipedUsersResult = await query(`
|
||||
SELECT swiped_id
|
||||
SELECT target_user_id
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1
|
||||
WHERE user_id = $1
|
||||
`, [userProfile.userId]);
|
||||
|
||||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id);
|
||||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.target_user_id);
|
||||
swipedUserIds.push(userProfile.userId); // Исключаем себя
|
||||
|
||||
let candidateQuery = `
|
||||
@@ -417,7 +564,7 @@ export class MatchingService {
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.is_active = true
|
||||
AND u.is_active = true
|
||||
AND p.gender = $1
|
||||
AND p.dating_goal = $2
|
||||
AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1316
src/services/notificationService.ts.new
Normal file
1316
src/services/notificationService.ts.new
Normal file
File diff suppressed because it is too large
Load Diff
@@ -169,8 +169,8 @@ export class ProfileService {
|
||||
case 'photos':
|
||||
case 'interests':
|
||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||
// Для PostgreSQL массивы передаем как есть, не как JSON строки
|
||||
updateValues.push(value);
|
||||
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
||||
updateValues.push(JSON.stringify(value));
|
||||
break;
|
||||
case 'location':
|
||||
// Пропускаем обработку местоположения, так как колонки location нет
|
||||
@@ -496,7 +496,7 @@ export class ProfileService {
|
||||
// Удаляем связанные данные
|
||||
await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]);
|
||||
await client.query('DELETE FROM matches WHERE user_id_1 = $1 OR user_id_2 = $1', [userId]);
|
||||
await client.query('DELETE FROM swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]);
|
||||
await client.query('DELETE FROM swipes WHERE user_id = $1 OR target_user_id = $1', [userId]);
|
||||
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
|
||||
});
|
||||
return true;
|
||||
@@ -526,16 +526,38 @@ export class ProfileService {
|
||||
// Записать просмотр профиля
|
||||
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
|
||||
try {
|
||||
// Преобразуем строковые ID в числа для запросов
|
||||
const viewerTelegramId = typeof viewerId === 'string' ? parseInt(viewerId) : viewerId;
|
||||
const viewedTelegramId = typeof viewedProfileId === 'string' ? parseInt(viewedProfileId) : viewedProfileId;
|
||||
|
||||
// Получаем внутренние ID пользователей
|
||||
const viewerIdResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewerTelegramId]);
|
||||
if (viewerIdResult.rows.length === 0) {
|
||||
throw new Error(`User with telegram_id ${viewerId} not found`);
|
||||
}
|
||||
|
||||
const viewedUserResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewedTelegramId]);
|
||||
if (viewedUserResult.rows.length === 0) {
|
||||
throw new Error(`User with telegram_id ${viewedProfileId} not found`);
|
||||
}
|
||||
|
||||
const viewerUuid = viewerIdResult.rows[0].id;
|
||||
const viewedUuid = viewedUserResult.rows[0].id;
|
||||
|
||||
// Не записываем просмотры своего профиля
|
||||
if (viewerUuid === viewedUuid) {
|
||||
console.log('Skipping self-view record');
|
||||
return;
|
||||
}
|
||||
|
||||
await query(`
|
||||
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type)
|
||||
VALUES (
|
||||
(SELECT id FROM users WHERE telegram_id = $1),
|
||||
(SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)),
|
||||
$3
|
||||
)
|
||||
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 viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type
|
||||
`, [viewerId, viewedProfileId, viewType]);
|
||||
SET view_date = NOW(), view_type = $3
|
||||
`, [viewerUuid, viewedUuid, viewType]);
|
||||
|
||||
console.log(`Recorded profile view: ${viewerId} viewed ${viewedProfileId}`);
|
||||
} catch (error) {
|
||||
console.error('Error recording profile view:', error);
|
||||
}
|
||||
@@ -547,8 +569,7 @@ export class ProfileService {
|
||||
const result = await query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM profile_views pv
|
||||
JOIN profiles p ON pv.viewed_profile_id = p.id
|
||||
WHERE p.user_id = $1
|
||||
WHERE viewed_profile_id = $1
|
||||
`, [userId]);
|
||||
|
||||
return parseInt(result.rows[0].count) || 0;
|
||||
@@ -562,14 +583,12 @@ export class ProfileService {
|
||||
async getProfileViewers(userId: string, limit: number = 10): Promise<Profile[]> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||
SELECT DISTINCT p.*
|
||||
FROM profile_views pv
|
||||
JOIN profiles target_p ON pv.viewed_profile_id = target_p.id
|
||||
JOIN users viewer_u ON pv.viewer_id = viewer_u.id
|
||||
JOIN profiles p ON viewer_u.id = p.user_id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE target_p.user_id = $1
|
||||
ORDER BY pv.viewed_at DESC
|
||||
WHERE pv.viewed_profile_id = $1
|
||||
ORDER BY pv.view_date DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
|
||||
@@ -579,4 +598,22 @@ export class ProfileService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Получить список просмотренных профилей
|
||||
async getViewedProfiles(userId: string, limit: number = 50): Promise<string[]> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT viewed_profile_id
|
||||
FROM profile_views
|
||||
WHERE viewer_id = $1
|
||||
ORDER BY view_date DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
|
||||
return result.rows.map((row: any) => row.viewed_profile_id);
|
||||
} catch (error) {
|
||||
console.error('Error getting viewed profiles:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ export class VipService {
|
||||
// Проверить премиум статус пользователя
|
||||
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
|
||||
try {
|
||||
// Проверяем существование пользователя
|
||||
// Проверяем существование пользователя и получаем его премиум статус
|
||||
const result = await query(`
|
||||
SELECT id
|
||||
SELECT id, premium
|
||||
FROM users
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId]);
|
||||
@@ -35,12 +35,13 @@ export class VipService {
|
||||
throw new BotError('User not found', 'USER_NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
// Временно возвращаем false для всех пользователей, так как колонки premium нет
|
||||
// В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно
|
||||
// Получаем актуальное значение премиум статуса из базы данных
|
||||
const isPremium = result.rows[0].premium || false;
|
||||
|
||||
return {
|
||||
isPremium: false,
|
||||
expiresAt: undefined,
|
||||
daysLeft: undefined
|
||||
isPremium: isPremium,
|
||||
expiresAt: undefined, // Пока не используем дату истечения
|
||||
daysLeft: undefined // Пока не используем количество дней
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking premium status:', error);
|
||||
@@ -51,9 +52,17 @@ export class VipService {
|
||||
// Добавить премиум статус
|
||||
async addPremium(telegramId: string, durationDays: number = 30): Promise<void> {
|
||||
try {
|
||||
// Временно заглушка, так как колонок premium и premium_expires_at нет
|
||||
console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`);
|
||||
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
|
||||
console.log(`[VIP] Добавление премиум для ${telegramId} на ${durationDays} дней`);
|
||||
|
||||
// Обновляем статус premium в базе данных
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
WHERE telegram_id = $1
|
||||
RETURNING id, telegram_id, premium
|
||||
`, [telegramId]);
|
||||
|
||||
console.log(`[VIP] Премиум успешно добавлен для пользователя ${telegramId}`);
|
||||
} catch (error) {
|
||||
console.error('Error adding premium:', error);
|
||||
throw error;
|
||||
@@ -63,9 +72,17 @@ export class VipService {
|
||||
// Удалить премиум статус
|
||||
async removePremium(telegramId: string): Promise<void> {
|
||||
try {
|
||||
// Временно заглушка, так как колонок premium и premium_expires_at нет
|
||||
console.log(`[VIP] Попытка удалить премиум для ${telegramId}`);
|
||||
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
|
||||
console.log(`[VIP] Удаление премиум для ${telegramId}`);
|
||||
|
||||
// Обновляем статус premium в базе данных
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET premium = false
|
||||
WHERE telegram_id = $1
|
||||
RETURNING id, telegram_id, premium
|
||||
`, [telegramId]);
|
||||
|
||||
console.log(`[VIP] Премиум успешно удален для пользователя ${telegramId}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing premium:', error);
|
||||
throw error;
|
||||
|
||||
241
start.bat
Normal file
241
start.bat
Normal file
@@ -0,0 +1,241 @@
|
||||
@echo off
|
||||
:: start.bat - Скрипт для запуска Telegram Tinder Bot на Windows
|
||||
:: Позволяет выбрать между локальной БД в контейнере или внешней БД
|
||||
|
||||
echo ================================================
|
||||
echo Запуск Telegram Tinder Bot
|
||||
echo ================================================
|
||||
|
||||
:: Проверка наличия Docker и Docker Compose
|
||||
WHERE docker >nul 2>&1
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
echo [31mОШИБКА: Docker не установлен![0m
|
||||
echo Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Проверяем наличие .env файла
|
||||
IF NOT EXIST .env (
|
||||
echo [33mФайл .env не найден. Создаем из шаблона...[0m
|
||||
IF EXIST .env.example (
|
||||
copy .env.example .env
|
||||
echo [32mФайл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.[0m
|
||||
) ELSE (
|
||||
echo [31mОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.[0m
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
:: Спрашиваем про запуск базы данных
|
||||
set /p use_container_db="Запустить базу данных PostgreSQL в контейнере? (y/n): "
|
||||
|
||||
:: Функции для работы с docker-compose
|
||||
IF /I "%use_container_db%" NEQ "y" (
|
||||
:: Запрашиваем параметры подключения к внешней БД
|
||||
echo [36mВведите параметры подключения к внешней базе данных:[0m
|
||||
set /p db_host="Хост (например, localhost): "
|
||||
set /p db_port="Порт (например, 5432): "
|
||||
set /p db_name="Имя базы данных: "
|
||||
set /p db_user="Имя пользователя: "
|
||||
set /p db_password="Пароль: "
|
||||
|
||||
:: Модифицируем docker-compose.yml
|
||||
echo [33mМодифицируем docker-compose.yml для работы с внешней базой данных...[0m
|
||||
|
||||
:: Сохраняем оригинальную версию файла
|
||||
copy docker-compose.yml docker-compose.yml.bak
|
||||
|
||||
:: Создаем временный файл с модифицированным содержимым
|
||||
(
|
||||
echo version: '3.8'
|
||||
echo.
|
||||
echo services:
|
||||
echo bot:
|
||||
echo build: .
|
||||
echo container_name: telegram-tinder-bot
|
||||
echo restart: unless-stopped
|
||||
echo env_file: .env
|
||||
echo environment:
|
||||
echo - NODE_ENV=production
|
||||
echo - DB_HOST=%db_host%
|
||||
echo - DB_PORT=%db_port%
|
||||
echo - DB_NAME=%db_name%
|
||||
echo - DB_USERNAME=%db_user%
|
||||
echo - DB_PASSWORD=%db_password%
|
||||
echo volumes:
|
||||
echo - ./uploads:/app/uploads
|
||||
echo - ./logs:/app/logs
|
||||
echo networks:
|
||||
echo - bot-network
|
||||
echo healthcheck:
|
||||
echo test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
echo interval: 30s
|
||||
echo timeout: 5s
|
||||
echo retries: 3
|
||||
echo start_period: 10s
|
||||
echo.
|
||||
echo adminer:
|
||||
echo image: adminer:latest
|
||||
echo container_name: adminer-tinder
|
||||
echo restart: unless-stopped
|
||||
echo ports:
|
||||
echo - "8080:8080"
|
||||
echo networks:
|
||||
echo - bot-network
|
||||
echo.
|
||||
echo volumes:
|
||||
echo postgres_data:
|
||||
echo.
|
||||
echo networks:
|
||||
echo bot-network:
|
||||
echo driver: bridge
|
||||
) > docker-compose.temp.yml
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y docker-compose.temp.yml docker-compose.yml
|
||||
|
||||
echo [32mdocker-compose.yml обновлен для работы с внешней базой данных[0m
|
||||
|
||||
:: Обновляем .env файл
|
||||
echo [33mОбновляем файл .env с параметрами внешней базы данных...[0m
|
||||
|
||||
:: Создаем временный файл
|
||||
type NUL > .env.temp
|
||||
|
||||
:: Читаем .env построчно и заменяем нужные строки
|
||||
for /f "tokens=*" %%a in (.env) do (
|
||||
set line=%%a
|
||||
set line=!line:DB_HOST=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_HOST=%db_host%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_PORT=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PORT=%db_port%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_NAME=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_NAME=%db_name%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_USERNAME=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_USERNAME=%db_user%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_PASSWORD=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PASSWORD=%db_password%>> .env.temp
|
||||
) else (
|
||||
echo %%a>> .env.temp
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y .env.temp .env
|
||||
|
||||
echo [32mФайл .env обновлен с параметрами внешней базы данных[0m
|
||||
|
||||
:: Запускаем только контейнер с ботом
|
||||
echo [36mЗапускаем Telegram Bot без контейнера базы данных...[0m
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
echo [32mБот запущен и использует внешнюю базу данных: %db_host%:%db_port%/%db_name%[0m
|
||||
echo [33mAdminer доступен по адресу: http://localhost:8080/[0m
|
||||
echo [33mДанные для входа в Adminer:[0m
|
||||
echo [33mСистема: PostgreSQL[0m
|
||||
echo [33mСервер: %db_host%[0m
|
||||
echo [33mПользователь: %db_user%[0m
|
||||
echo [33mПароль: (введенный вами)[0m
|
||||
echo [33mБаза данных: %db_name%[0m
|
||||
) ELSE (
|
||||
:: Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
if exist docker-compose.yml.bak (
|
||||
move /y docker-compose.yml.bak docker-compose.yml
|
||||
echo [32mdocker-compose.yml восстановлен из резервной копии[0m
|
||||
)
|
||||
|
||||
echo [36mЗапускаем Telegram Bot с контейнером базы данных...[0m
|
||||
|
||||
:: Проверка, запущены ли контейнеры
|
||||
docker ps -q -f name=telegram-tinder-bot > tmp_containers.txt
|
||||
set /p containers=<tmp_containers.txt
|
||||
del tmp_containers.txt
|
||||
|
||||
if not "%containers%" == "" (
|
||||
set /p restart_containers="Контейнеры уже запущены. Перезапустить? (y/n): "
|
||||
if /I "%restart_containers%" == "y" (
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
echo [32mКонтейнеры перезапущены[0m
|
||||
) else (
|
||||
echo [36mПродолжаем работу с уже запущенными контейнерами[0m
|
||||
)
|
||||
) else (
|
||||
docker-compose up -d
|
||||
echo [32mКонтейнеры запущены[0m
|
||||
)
|
||||
|
||||
:: Проверка наличия пароля для БД в .env
|
||||
findstr /C:"DB_PASSWORD=" .env > tmp_db_password.txt
|
||||
set /p db_password_line=<tmp_db_password.txt
|
||||
del tmp_db_password.txt
|
||||
|
||||
:: Проверяем, есть ли значение после DB_PASSWORD=
|
||||
for /f "tokens=2 delims==" %%i in ("%db_password_line%") do (
|
||||
set db_password=%%i
|
||||
)
|
||||
|
||||
if "%db_password%" == "" (
|
||||
:: Генерируем случайный пароль
|
||||
set chars=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||
set random_password=
|
||||
for /L %%i in (1,1,16) do (
|
||||
set /a random_index=!random! %% 62
|
||||
for %%j in (!random_index!) do set random_password=!random_password!!chars:~%%j,1!
|
||||
)
|
||||
|
||||
:: Обновляем .env файл с новым паролем
|
||||
:: Создаем временный файл
|
||||
type NUL > .env.temp
|
||||
|
||||
:: Читаем .env построчно и заменяем строку с паролем
|
||||
for /f "tokens=*" %%a in (.env) do (
|
||||
set line=%%a
|
||||
set line=!line:DB_PASSWORD=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PASSWORD=%random_password%>> .env.temp
|
||||
) else (
|
||||
echo %%a>> .env.temp
|
||||
)
|
||||
)
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y .env.temp .env
|
||||
|
||||
echo [33mСгенерирован случайный пароль для базы данных и сохранен в .env[0m
|
||||
)
|
||||
|
||||
echo [32mTelegram Bot запущен с локальной базой данных[0m
|
||||
echo [33mAdminer доступен по адресу: http://localhost:8080/[0m
|
||||
echo [33mДанные для входа в Adminer:[0m
|
||||
echo [33mСистема: PostgreSQL[0m
|
||||
echo [33mСервер: db[0m
|
||||
echo [33mПользователь: postgres[0m
|
||||
echo [33mПароль: (из переменной DB_PASSWORD в .env)[0m
|
||||
echo [33mБаза данных: telegram_tinder_bot[0m
|
||||
)
|
||||
|
||||
:: Проверка статуса контейнеров
|
||||
echo [36mПроверка статуса контейнеров:[0m
|
||||
docker-compose ps
|
||||
|
||||
echo ================================================
|
||||
echo [32mПроцесс запуска Telegram Tinder Bot завершен![0m
|
||||
echo ================================================
|
||||
echo [33mДля просмотра логов используйте: docker-compose logs -f bot[0m
|
||||
echo [33mДля остановки: docker-compose down[0m
|
||||
|
||||
pause
|
||||
229
start.ps1
Normal file
229
start.ps1
Normal file
@@ -0,0 +1,229 @@
|
||||
function createModifiedDockerCompose {
|
||||
param (
|
||||
[string]$dbHost,
|
||||
[string]$dbPort,
|
||||
[string]$dbName,
|
||||
[string]$dbUser,
|
||||
[string]$dbPassword
|
||||
)
|
||||
|
||||
$dockerComposeContent = @"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=$dbHost
|
||||
- DB_PORT=$dbPort
|
||||
- DB_NAME=$dbName
|
||||
- DB_USERNAME=$dbUser
|
||||
- DB_PASSWORD=$dbPassword
|
||||
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
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
"@
|
||||
|
||||
return $dockerComposeContent
|
||||
}
|
||||
|
||||
function restoreDockerCompose {
|
||||
if (Test-Path -Path "docker-compose.yml.bak") {
|
||||
Copy-Item -Path "docker-compose.yml.bak" -Destination "docker-compose.yml" -Force
|
||||
Write-Host "docker-compose.yml восстановлен из резервной копии" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvFile {
|
||||
param (
|
||||
[string]$dbHost,
|
||||
[string]$dbPort,
|
||||
[string]$dbName,
|
||||
[string]$dbUser,
|
||||
[string]$dbPassword
|
||||
)
|
||||
|
||||
$envContent = Get-Content -Path ".env" -Raw
|
||||
|
||||
$envContent = $envContent -replace "DB_HOST=.*", "DB_HOST=$dbHost"
|
||||
$envContent = $envContent -replace "DB_PORT=.*", "DB_PORT=$dbPort"
|
||||
$envContent = $envContent -replace "DB_NAME=.*", "DB_NAME=$dbName"
|
||||
$envContent = $envContent -replace "DB_USERNAME=.*", "DB_USERNAME=$dbUser"
|
||||
$envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$dbPassword"
|
||||
|
||||
Set-Content -Path ".env" -Value $envContent
|
||||
|
||||
Write-Host "Файл .env обновлен с параметрами внешней базы данных" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function generateRandomPassword {
|
||||
$length = 16
|
||||
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
$bytes = New-Object Byte[] $length
|
||||
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
|
||||
$rng.GetBytes($bytes)
|
||||
|
||||
$password = ""
|
||||
for ($i = 0; $i -lt $length; $i++) {
|
||||
$password += $chars[$bytes[$i] % $chars.Length]
|
||||
}
|
||||
|
||||
return $password
|
||||
}
|
||||
|
||||
# Начало основного скрипта
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host " Запуск Telegram Tinder Bot" -ForegroundColor Cyan
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
|
||||
# Проверка наличия Docker
|
||||
try {
|
||||
docker --version | Out-Null
|
||||
} catch {
|
||||
Write-Host "ОШИБКА: Docker не установлен!" -ForegroundColor Red
|
||||
Write-Host "Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/"
|
||||
exit
|
||||
}
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if (-not (Test-Path -Path ".env")) {
|
||||
Write-Host "Файл .env не найден. Создаем из шаблона..." -ForegroundColor Yellow
|
||||
|
||||
if (Test-Path -Path ".env.example") {
|
||||
Copy-Item -Path ".env.example" -Destination ".env"
|
||||
Write-Host "Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную." -ForegroundColor Red
|
||||
exit
|
||||
}
|
||||
}
|
||||
|
||||
# Спрашиваем про запуск базы данных
|
||||
$useContainerDb = Read-Host "Запустить базу данных PostgreSQL в контейнере? (y/n)"
|
||||
|
||||
if ($useContainerDb -ne "y") {
|
||||
# Запрашиваем параметры подключения к внешней БД
|
||||
Write-Host "Введите параметры подключения к внешней базе данных:" -ForegroundColor Cyan
|
||||
$dbHost = Read-Host "Хост (например, localhost)"
|
||||
$dbPort = Read-Host "Порт (например, 5432)"
|
||||
$dbName = Read-Host "Имя базы данных"
|
||||
$dbUser = Read-Host "Имя пользователя"
|
||||
$dbPassword = Read-Host "Пароль" -AsSecureString
|
||||
$dbPasswordText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($dbPassword))
|
||||
|
||||
# Модифицируем docker-compose.yml
|
||||
Write-Host "Модифицируем docker-compose.yml для работы с внешней базой данных..." -ForegroundColor Yellow
|
||||
|
||||
# Сохраняем оригинальную версию файла
|
||||
Copy-Item -Path "docker-compose.yml" -Destination "docker-compose.yml.bak" -Force
|
||||
|
||||
# Создаем модифицированный docker-compose.yml
|
||||
$dockerComposeContent = createModifiedDockerCompose -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText
|
||||
Set-Content -Path "docker-compose.yml" -Value $dockerComposeContent
|
||||
|
||||
Write-Host "docker-compose.yml обновлен для работы с внешней базой данных" -ForegroundColor Green
|
||||
|
||||
# Обновляем .env файл
|
||||
Write-Host "Обновляем файл .env с параметрами внешней базы данных..." -ForegroundColor Yellow
|
||||
updateEnvFile -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText
|
||||
|
||||
# Запускаем только контейнер с ботом
|
||||
Write-Host "Запускаем Telegram Bot без контейнера базы данных..." -ForegroundColor Cyan
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
Write-Host "Бот запущен и использует внешнюю базу данных: $dbHost`:$dbPort/$dbName" -ForegroundColor Green
|
||||
Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow
|
||||
Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow
|
||||
Write-Host "Система: PostgreSQL" -ForegroundColor Yellow
|
||||
Write-Host "Сервер: $dbHost" -ForegroundColor Yellow
|
||||
Write-Host "Пользователь: $dbUser" -ForegroundColor Yellow
|
||||
Write-Host "Пароль: (введенный вами)" -ForegroundColor Yellow
|
||||
Write-Host "База данных: $dbName" -ForegroundColor Yellow
|
||||
} else {
|
||||
# Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
restoreDockerCompose
|
||||
|
||||
Write-Host "Запускаем Telegram Bot с контейнером базы данных..." -ForegroundColor Cyan
|
||||
|
||||
# Проверка, запущены ли контейнеры
|
||||
$containers = docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder
|
||||
|
||||
if ($containers) {
|
||||
$restartContainers = Read-Host "Контейнеры уже запущены. Перезапустить? (y/n)"
|
||||
if ($restartContainers -eq "y") {
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
Write-Host "Контейнеры перезапущены" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Продолжаем работу с уже запущенными контейнерами" -ForegroundColor Cyan
|
||||
}
|
||||
} else {
|
||||
docker-compose up -d
|
||||
Write-Host "Контейнеры запущены" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Проверка наличия пароля для БД в .env
|
||||
$envContent = Get-Content -Path ".env" -Raw
|
||||
$match = [Regex]::Match($envContent, "DB_PASSWORD=(.*)(\r?\n|$)")
|
||||
$dbPassword = if ($match.Success) { $match.Groups[1].Value.Trim() } else { "" }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($dbPassword)) {
|
||||
# Генерируем случайный пароль
|
||||
$randomPassword = generateRandomPassword
|
||||
|
||||
# Обновляем .env файл
|
||||
$envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$randomPassword"
|
||||
Set-Content -Path ".env" -Value $envContent
|
||||
|
||||
Write-Host "Сгенерирован случайный пароль для базы данных и сохранен в .env" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "Telegram Bot запущен с локальной базой данных" -ForegroundColor Green
|
||||
Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow
|
||||
Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow
|
||||
Write-Host "Система: PostgreSQL" -ForegroundColor Yellow
|
||||
Write-Host "Сервер: db" -ForegroundColor Yellow
|
||||
Write-Host "Пользователь: postgres" -ForegroundColor Yellow
|
||||
Write-Host "Пароль: (из переменной DB_PASSWORD в .env)" -ForegroundColor Yellow
|
||||
Write-Host "База данных: telegram_tinder_bot" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Проверка статуса контейнеров
|
||||
Write-Host "Проверка статуса контейнеров:" -ForegroundColor Cyan
|
||||
docker-compose ps
|
||||
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host "Процесс запуска Telegram Tinder Bot завершен!" -ForegroundColor Green
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host "Для просмотра логов используйте: docker-compose logs -f bot" -ForegroundColor Yellow
|
||||
Write-Host "Для остановки: docker-compose down" -ForegroundColor Yellow
|
||||
|
||||
Read-Host "Нажмите Enter для выхода"
|
||||
217
start.sh
Executable file
217
start.sh
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
# start.sh - Скрипт для запуска Telegram Tinder Bot
|
||||
# Позволяет выбрать между локальной БД в контейнере или внешней БД
|
||||
|
||||
# Цвета для вывода
|
||||
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 ${NC}"
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
|
||||
# Проверка наличия Docker и Docker Compose
|
||||
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
|
||||
echo -e "${RED}ОШИБКА: Docker и/или Docker Compose не установлены!${NC}"
|
||||
echo -e "Для установки Docker следуйте инструкции на: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${YELLOW}Файл .env не найден. Создаем из шаблона...${NC}"
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
echo -e "${GREEN}Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.${NC}"
|
||||
else
|
||||
echo -e "${RED}ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Спрашиваем про запуск базы данных
|
||||
read -p "Запустить базу данных PostgreSQL в контейнере? (y/n): " use_container_db
|
||||
|
||||
# Функция для изменения docker-compose.yml
|
||||
modify_docker_compose() {
|
||||
local host=$1
|
||||
local port=$2
|
||||
local user=$3
|
||||
local password=$4
|
||||
local db_name=$5
|
||||
|
||||
echo -e "${YELLOW}Модифицируем docker-compose.yml для работы с внешней базой данных...${NC}"
|
||||
|
||||
# Сохраняем оригинальную версию файла
|
||||
cp docker-compose.yml docker-compose.yml.bak
|
||||
|
||||
# Создаем временный файл с модифицированным содержимым
|
||||
cat > docker-compose.temp.yml << EOL
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${host}
|
||||
- DB_PORT=${port}
|
||||
- DB_NAME=${db_name}
|
||||
- DB_USERNAME=${user}
|
||||
- DB_PASSWORD=${password}
|
||||
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
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
EOL
|
||||
|
||||
# Заменяем оригинальный файл
|
||||
mv docker-compose.temp.yml docker-compose.yml
|
||||
|
||||
echo -e "${GREEN}docker-compose.yml обновлен для работы с внешней базой данных${NC}"
|
||||
}
|
||||
|
||||
# Функция для восстановления docker-compose.yml
|
||||
restore_docker_compose() {
|
||||
if [ -f docker-compose.yml.bak ]; then
|
||||
mv docker-compose.yml.bak docker-compose.yml
|
||||
echo -e "${GREEN}docker-compose.yml восстановлен из резервной копии${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Обработка выбора
|
||||
if [[ "$use_container_db" =~ ^[Nn]$ ]]; then
|
||||
# Запрашиваем параметры подключения к внешней БД
|
||||
echo -e "${BLUE}Введите параметры подключения к внешней базе данных:${NC}"
|
||||
read -p "Хост (например, localhost): " db_host
|
||||
read -p "Порт (например, 5432): " db_port
|
||||
read -p "Имя базы данных: " db_name
|
||||
read -p "Имя пользователя: " db_user
|
||||
read -p "Пароль: " db_password
|
||||
|
||||
# Модифицируем docker-compose.yml
|
||||
modify_docker_compose "$db_host" "$db_port" "$db_user" "$db_password" "$db_name"
|
||||
|
||||
# Обновляем .env файл
|
||||
echo -e "${YELLOW}Обновляем файл .env с параметрами внешней базы данных...${NC}"
|
||||
|
||||
# Используем sed для замены переменных в .env
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS требует другой синтаксис для sed
|
||||
sed -i '' "s/DB_HOST=.*/DB_HOST=${db_host}/" .env
|
||||
sed -i '' "s/DB_PORT=.*/DB_PORT=${db_port}/" .env
|
||||
sed -i '' "s/DB_NAME=.*/DB_NAME=${db_name}/" .env
|
||||
sed -i '' "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env
|
||||
sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env
|
||||
else
|
||||
# Linux и другие системы
|
||||
sed -i "s/DB_HOST=.*/DB_HOST=${db_host}/" .env
|
||||
sed -i "s/DB_PORT=.*/DB_PORT=${db_port}/" .env
|
||||
sed -i "s/DB_NAME=.*/DB_NAME=${db_name}/" .env
|
||||
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Файл .env обновлен с параметрами внешней базы данных${NC}"
|
||||
|
||||
# Запускаем только контейнер с ботом
|
||||
echo -e "${BLUE}Запускаем Telegram Bot без контейнера базы данных...${NC}"
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
echo -e "${GREEN}Бот запущен и использует внешнюю базу данных: ${db_host}:${db_port}/${db_name}${NC}"
|
||||
echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}"
|
||||
echo -e "${YELLOW}Данные для входа в Adminer:${NC}"
|
||||
echo -e "${YELLOW}Система: PostgreSQL${NC}"
|
||||
echo -e "${YELLOW}Сервер: ${db_host}${NC}"
|
||||
echo -e "${YELLOW}Пользователь: ${db_user}${NC}"
|
||||
echo -e "${YELLOW}Пароль: (введенный вами)${NC}"
|
||||
echo -e "${YELLOW}База данных: ${db_name}${NC}"
|
||||
else
|
||||
# Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
restore_docker_compose
|
||||
|
||||
echo -e "${BLUE}Запускаем Telegram Bot с контейнером базы данных...${NC}"
|
||||
|
||||
# Проверка, запущены ли контейнеры
|
||||
containers=$(docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder)
|
||||
if [ -n "$containers" ]; then
|
||||
echo -e "${YELLOW}Контейнеры уже запущены. Перезапустить? (y/n): ${NC}"
|
||||
read restart_containers
|
||||
if [[ "$restart_containers" =~ ^[Yy]$ ]]; then
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}Контейнеры перезапущены${NC}"
|
||||
else
|
||||
echo -e "${BLUE}Продолжаем работу с уже запущенными контейнерами${NC}"
|
||||
fi
|
||||
else
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}Контейнеры запущены${NC}"
|
||||
fi
|
||||
|
||||
# Проверка наличия пароля для БД в .env
|
||||
db_password=$(grep DB_PASSWORD .env | cut -d '=' -f2)
|
||||
if [ -z "$db_password" ]; then
|
||||
# Генерируем случайный пароль
|
||||
random_password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
|
||||
|
||||
# Обновляем .env файл
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS требует другой синтаксис для sed
|
||||
sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env
|
||||
else
|
||||
# Linux и другие системы
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Сгенерирован случайный пароль для базы данных и сохранен в .env${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Telegram Bot запущен с локальной базой данных${NC}"
|
||||
echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}"
|
||||
echo -e "${YELLOW}Данные для входа в Adminer:${NC}"
|
||||
echo -e "${YELLOW}Система: PostgreSQL${NC}"
|
||||
echo -e "${YELLOW}Сервер: db${NC}"
|
||||
echo -e "${YELLOW}Пользователь: postgres${NC}"
|
||||
echo -e "${YELLOW}Пароль: (из переменной DB_PASSWORD в .env)${NC}"
|
||||
echo -e "${YELLOW}База данных: telegram_tinder_bot${NC}"
|
||||
fi
|
||||
|
||||
# Проверка статуса контейнеров
|
||||
echo -e "${BLUE}Проверка статуса контейнеров:${NC}"
|
||||
docker-compose ps
|
||||
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
echo -e "${GREEN}Процесс запуска Telegram Tinder Bot завершен!${NC}"
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
echo -e "${YELLOW}Для просмотра логов используйте: docker-compose logs -f bot${NC}"
|
||||
echo -e "${YELLOW}Для остановки: docker-compose down${NC}"
|
||||
Reference in New Issue
Block a user