11 Commits

Author SHA1 Message Date
9106af4f8e pre=deploy 2025-11-24 17:00:20 +09:00
240864617f geo distance meter 2025-11-06 15:34:51 +09:00
0bbeb0767b geo detection 2025-11-06 15:09:15 +09:00
88d9ccd75d fix(database): Исправлены критические ошибки БД - job, state, looking_for
- Добавлена колонка job в profiles (устраняет ошибку column job does not exist)
- Добавлена колонка state в users (устраняет предупреждения State column does not exist)
- Исправлен триггер create_initial_profile() для включения looking_for
- Колонка looking_for сделана nullable с DEFAULT 'both'
- Добавлена колонка interested_in как современный синоним для looking_for
- Созданы индексы для производительности: idx_profiles_job, idx_users_state, idx_profiles_interested_in

Патчи:
- sql/fix_looking_for_column.sql
- sql/add_job_and_state_columns.sql

Утилиты:
- bin/apply_all_patches.sh - автоматическое применение всех патчей

Документация:
- docs/DATABASE_FIXES.md - подробное описание исправлений
- docs/HEALTH_CHECK.md - чеклист проверки здоровья бота
- docs/FIXES_SUMMARY_2025-11-06.md - краткая сводка изменений

Fixes: #job-column-error #state-column-warning #looking-for-constraint
2025-11-06 10:30:35 +09:00
9281388959 MakeFile created. 2025-09-18 18:43:39 +09:00
0566901fa4 migrations fix 2025-09-18 17:00:48 +09:00
e907dffe8c migrations fix 2025-09-18 16:52:03 +09:00
fdd0580554 docker fix 2025-09-18 16:47:07 +09:00
29d6255f22 Merge pull request 'docker image fix' (#5) from dev into main
Reviewed-on: #5
2025-09-18 07:38:26 +00:00
d02a742278 Merge pull request 'docker-compose fix' (#4) from dev into main
Reviewed-on: #4
2025-09-18 06:31:33 +00:00
155e4d3b7b Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2025-09-18 05:21:26 +00:00
73 changed files with 5878 additions and 954 deletions

View File

@@ -46,6 +46,13 @@ JWT_SECRET=your_jwt_secret_here
# Encryption key for sensitive data
ENCRYPTION_KEY=your_encryption_key_here
# === KAKAO MAPS API ===
# Kakao REST API Key for location services (Get from https://developers.kakao.com/)
# You can use either KAKAO_REST_API_KEY or KAKAO_MAP_REST_KEY
KAKAO_REST_API_KEY=your_kakao_rest_api_key_here
# KAKAO_MAP_REST_KEY=your_kakao_rest_api_key_here
# === ADVANCED SETTINGS ===
# Notification check interval in milliseconds (default: 60000 - 1 minute)

107
Makefile Normal file
View File

@@ -0,0 +1,107 @@
# Makefile для Telegram Tinder Bot
.PHONY: help install update run migrate fix-docker clean clear-interactions
# Значения по умолчанию
DB_HOST ?= db
DB_PORT ?= 5432
DB_NAME ?= telegram_tinder_bot
DB_USERNAME ?= postgres
DB_PASSWORD ?= postgres
# Основные команды
help:
@echo "========== Telegram Tinder Bot Makefile =========="
@echo "make install - Установка зависимостей"
@echo "make update - Обновление кода из репозитория"
@echo "make run - Запуск бота в контейнере"
@echo "make migrate - Применение миграций базы данных"
@echo "make fix-docker - Исправление проблем с Docker"
@echo "make clear-interactions - Очистка матчей, свайпов и сообщений"
@echo "make clean - Очистка и остановка контейнеров"
install:
@echo "Установка зависимостей..."
@if ! command -v docker &> /dev/null; then \
echo "Установка Docker..."; \
echo "Удаление конфликтующих пакетов..."; \
sudo apt remove -y docker.io containerd runc 2>/dev/null || true; \
sudo apt update; \
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common; \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \
echo "deb [arch=$$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null; \
sudo apt update; \
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin; \
sudo systemctl start docker; \
sudo systemctl enable docker; \
sudo usermod -aG docker $$USER; \
echo "Docker установлен. Перелогиньтесь для применения группы docker."; \
fi
@if [ ! -f .env ]; then \
echo "Создание .env файла..."; \
cp .env.example .env 2>/dev/null || cp .env.production .env 2>/dev/null || echo "NODE_ENV=production" > .env; \
echo "Пожалуйста, отредактируйте файл .env!"; \
fi
@mkdir -p logs uploads && chmod -R 777 logs uploads
update:
@echo "Обновление кода..."
@git fetch --all
@git pull origin main || git pull origin master || echo "Не удалось обновить код"
@if [ -f package.json ]; then npm ci || npm install; fi
@echo "Пересборка контейнеров..."
@docker compose down || docker-compose down || true
@docker compose build || docker-compose build
@docker compose up -d || docker-compose up -d
@echo "Применение миграций к базе данных..."
@sleep 5
@make migrate
@echo "✅ Обновление завершено! Бот перезапущен с новой версией."
run:
@echo "Запуск бота..."
@docker-compose down || true
@make fix-docker
@docker-compose build
@docker-compose up -d
@echo "Бот запущен! Для просмотра логов: docker-compose logs -f"
migrate:
@echo "Применение миграций к базе данных..."
@if [ ! -f .env ]; then \
echo "❌ Файл .env не найден! Создайте его перед применением миграций."; \
exit 1; \
fi
@. ./.env && export $$(cat .env | grep -v '^#' | xargs) && \
echo "Подключение к БД: $$DB_HOST:$$DB_PORT/$$DB_NAME ($$DB_USERNAME)" && \
echo "Создание расширения uuid-ossp..." && \
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
-c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" 2>/dev/null || true && \
echo "Применение consolidated.sql..." && \
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
-f sql/consolidated.sql 2>&1 | grep -E "(ERROR|CREATE|ALTER)" || true && \
echo "Применение дополнительных миграций..." && \
for sql_file in sql/add_*.sql; do \
[ -f "$$sql_file" ] && echo " - Применение $$(basename $$sql_file)..." && \
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
-f "$$sql_file" 2>&1 | grep -v "NOTICE" || true; \
done && \
echo "✅ Миграции применены успешно!"
fix-docker:
@echo "Исправление Docker конфигурации..."
@if [ -f Dockerfile ] && grep -q "RUN npm run build" Dockerfile; then \
sed -i 's/RUN npm run build/RUN npm run build:linux/g' Dockerfile; \
fi
@docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>/dev/null || true
@docker system prune -f --volumes >/dev/null 2>&1 || true
clear-interactions:
@echo "Очистка взаимодействий пользователей..."
@bash bin/clear_interactions.sh
clean:
@echo "Очистка..."
@docker-compose down || true
@rm -rf temp_migrations node_modules/.cache
@echo "Очистка завершена"

View File

@@ -0,0 +1,49 @@
{
"noProfile": {
"message": "❌ У вас пока нет профиля.\\nСоздайте его для начала использования бота!",
"createButton": "🚀 Создать профиль"
},
"noMatches": {
"message": "💔 У вас пока нет матчей.\\n\\n🔍 Попробуйте просмотреть больше анкет!\\nИспользуйте /browse для поиска.",
"browsing": "🔍 Найти еще"
},
"matches": {
"title": "💕 Ваши матчи:",
"openChats": "💬 Открыть чаты",
"nativeChats": "📱 Нативные чаты",
"findMore": "🔍 Найти еще",
"cityNotSpecified": "Не указан"
},
"profileCreation": {
"start": "👋 Давайте создадим ваш профиль!\\n\\n📝 Сначала напишите ваше имя:",
"enterName": "❌ Пожалуйста, отправьте текстовое сообщение с вашим именем",
"enterAge": "📅 Отлично! Теперь укажите ваш возраст:",
"ageNotNumber": "❌ Пожалуйста, отправьте число",
"ageInvalid": "❌ Возраст должен быть числом от 18 до 100",
"enterCity": "📍 Прекрасно! В каком городе вы живете?",
"cityText": "❌ Пожалуйста, отправьте название города",
"enterBio": "📝 Теперь расскажите немного о себе (био):\\n\\n💡 Например: хобби, интересы, что ищете в отношениях и т.д.",
"bioText": "❌ Пожалуйста, отправьте текстовое описание",
"enterPhoto": "📸 Отлично! Теперь отправьте ваше фото:\\n\\n💡 Лучше использовать качественное фото лица",
"photoRequired": "❌ Пожалуйста, отправьте фотографию",
"error": "❌ Произошла ошибка. Попробуйте еще раз.",
"createError": "❌ Ошибка при создании профиля. Попробуйте еще раз позже."
},
"profileView": {
"cityNotSpecified": "Не указан",
"bioNotSpecified": "Описание не указано",
"editProfile": "✏️ Редактировать",
"managePhotos": "📸 Фото",
"startBrowsing": "🔍 Начать поиск",
"backToBrowsing": "👈 Назад"
},
"browsing": {
"noMoreProfiles": "🎉 Вы просмотрели всех доступных кандидатов!\\n\\n⏰ Попробуйте позже - возможно появятся новые анкеты!",
"needProfile": "❌ Сначала создайте профиль!\\nИспользуйте команду /start",
"cityNotSpecified": "Не указан"
},
"general": {
"greeting": "Привет! 👋\\n\\nИспользуйте команды для навигации:\\n/start - Главное меню\\n/help - Справка\\n/profile - Мой профиль\\n/browse - Поиск анкет",
"photoManagement": "📸 Для управления фотографиями используйте:"
}
}

View File

@@ -0,0 +1,85 @@
# Скрипт очистки взаимодействий пользователей
## Описание
Этот скрипт удаляет все взаимодействия между пользователями, оставляя только сами профили. Полезно для тестирования или сброса состояния приложения.
## Что удаляется
-**Messages** - все сообщения в чатах
-**Matches** - все матчи между пользователями
-**Profile Views** - все просмотры профилей
-**Swipes** - все свайпы (лайки, дизлайки, суперлайки)
-**Notifications** - все уведомления
## Что НЕ удаляется
-**Users** - пользователи остаются
-**Profiles** - профили пользователей остаются
## Использование
### Способ 1: Через Makefile (рекомендуется)
```bash
make clear-interactions
```
### Способ 2: Прямой запуск скрипта
```bash
./bin/clear_interactions.sh
```
### Способ 3: Прямое выполнение SQL
```bash
PGPASSWORD='your_password' psql -h host -U username -d database -f sql/clear_interactions.sql
```
## Подтверждение
Скрипт запросит подтверждение перед выполнением:
```
Вы уверены, что хотите продолжить? (yes/no):
```
Введите `yes` для продолжения или `no` для отмены.
## Требования
- Файл `.env` должен существовать и содержать переменные:
- `DB_HOST`
- `DB_PORT`
- `DB_NAME`
- `DB_USERNAME`
- `DB_PASSWORD`
## Вывод
После успешного выполнения скрипт покажет статистику:
```
table_name | remaining_records
-------------------+-------------------
messages | 0
matches | 0
profile_views | 0
swipes | 0
notifications | 0
users | 2
profiles | 2
```
## Безопасность
- Скрипт использует транзакцию (BEGIN/COMMIT) для безопасности
- Все операции выполняются атомарно
- В случае ошибки изменения откатываются
## Примечания
- ⚠️ **Необратимая операция!** Удаленные данные нельзя восстановить
- 💡 Рекомендуется делать резервную копию БД перед запуском
- 🔒 Убедитесь, что у вас есть права на удаление данных в БД

54
bin/QUICK_FIX.md Normal file
View File

@@ -0,0 +1,54 @@
# Быстрое исправление проблем с миграциями
## Проблема
При запуске миграций возникают ошибки с TypeScript-файлами и проблемы с модульными разрешениями.
## Быстрое решение
1. **Примените прямые SQL-миграции (рекомендуемый способ)**:
```bash
chmod +x bin/apply_direct_sql.sh
./bin/apply_direct_sql.sh
```
Этот скрипт создаст и применит консолидированную SQL-миграцию, которая создаст все необходимые таблицы.
2. **Создайте консолидированную JS-миграцию**:
```bash
chmod +x bin/create_consolidated_migration.sh
./bin/create_consolidated_migration.sh
```
Затем примените её:
```bash
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up
```
## Проверка результата
После выполнения миграций проверьте наличие таблиц в базе данных:
```bash
export PGPASSWORD=$DB_PASSWORD
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "\dt"
```
## Если проблемы сохраняются
1. **Проверьте доступность базы данных**:
```bash
export PGPASSWORD=$DB_PASSWORD
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "SELECT 1"
```
2. **Проверьте правильность переменных окружения**:
```bash
echo "DB_HOST: $DB_HOST"
echo "DB_PORT: $DB_PORT"
echo "DB_NAME: $DB_NAME"
echo "DB_USERNAME: $DB_USERNAME"
```
3. **Установите PostgreSQL-клиент**, если он отсутствует:
```bash
apt-get update
apt-get install -y postgresql-client
```

View File

@@ -2,6 +2,24 @@
Этот документ описывает процесс автоматического обновления бота с помощью созданных скриптов.
## Доступные скрипты
### apply_all_patches.sh
Применяет все SQL патчи к базе данных в правильном порядке:
- Основная схема (consolidated.sql)
- Исправление триггера looking_for
- Добавление колонок job и state
```bash
./bin/apply_all_patches.sh
```
### apply_migrations.sh
Применяет Node.js миграции через node-pg-migrate.
### apply_direct_sql.sh
Применяет SQL файлы напрямую через psql.
## Скрипт обновления
Скрипт обновления выполняет следующие действия:

76
bin/apply_all_patches.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# Применение всех патчей базы данных
# Использование: ./bin/apply_all_patches.sh
set -e # Остановка при ошибке
# Загрузка переменных окружения
if [ -f .env ]; then
source .env
else
echo "❌ Файл .env не найден!"
exit 1
fi
# Проверка обязательных переменных
if [ -z "$DB_HOST" ] || [ -z "$DB_PORT" ] || [ -z "$DB_NAME" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then
echo "❌ Не все переменные DB_* заданы в .env"
exit 1
fi
echo "🔧 Применение патчей к базе данных..."
echo "📍 Сервер: $DB_HOST:$DB_PORT"
echo "📂 База данных: $DB_NAME"
echo ""
# Функция применения патча
apply_patch() {
local patch_file=$1
local description=$2
if [ ! -f "$patch_file" ]; then
echo "⚠️ Патч $patch_file не найден, пропуск..."
return
fi
echo "📝 Применение: $description"
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" -f "$patch_file" > /dev/null 2>&1; then
echo "✅ Патч применен: $patch_file"
else
echo "⚠️ Ошибка при применении: $patch_file (возможно уже применен)"
fi
echo ""
}
# Применение патчей в правильном порядке
apply_patch "sql/consolidated.sql" "Основная схема БД (16 таблиц)"
apply_patch "sql/fix_looking_for_column.sql" "Исправление триггера и колонки looking_for"
apply_patch "sql/add_job_and_state_columns.sql" "Добавление колонок job и state"
echo "🎉 Все патчи обработаны!"
echo ""
echo "🔍 Проверка применения патчей..."
# Проверка критичных колонок
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" << 'EOF'
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profiles' AND column_name = 'job')
THEN '✅ profiles.job существует'
ELSE '❌ profiles.job НЕ НАЙДЕНА'
END as status_job,
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'state')
THEN '✅ users.state существует'
ELSE '❌ users.state НЕ НАЙДЕНА'
END as status_state,
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profiles' AND column_name = 'interested_in')
THEN '✅ profiles.interested_in существует'
ELSE '❌ profiles.interested_in НЕ НАЙДЕНА'
END as status_interested_in;
EOF
echo ""
echo "✅ Готово! Теперь можно перезапустить бота:"
echo " docker compose restart bot"

208
bin/apply_direct_sql.sh Normal file
View File

@@ -0,0 +1,208 @@
#!/bin/bash
# apply_direct_sql.sh - Прямое выполнение SQL-миграций с помощью psql
echo "🚀 Прямое выполнение SQL-миграций..."
# Загрузка переменных окружения из .env
if [ -f .env ]; then
echo "📝 Загрузка переменных окружения из .env..."
set -o allexport
source .env
set +o allexport
else
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
export DB_HOST="localhost"
export DB_PORT="5432"
export DB_NAME="telegram_tinder_bot"
export DB_USERNAME="postgres"
export DB_PASSWORD="postgres"
fi
# Создаем консолидированный SQL-файл
echo "📝 Создание консолидированного SQL-файла..."
consolidated_sql="consolidated_migration.sql"
cat > "$consolidated_sql" << EOL
-- Консолидированная миграция для Telegram Tinder Bot
-- Создана автоматически: $(date)
-- Создаем таблицу migrations, если её еще нет
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Основная структура базы данных
-- Таблица пользователей
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
language_code VARCHAR(10),
is_bot BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
state VARCHAR(50) DEFAULT 'START',
state_data JSONB,
gender VARCHAR(10),
looking_for VARCHAR(10),
bio TEXT,
age INTEGER,
location VARCHAR(255),
photos JSONB DEFAULT '[]'::jsonb,
interests TEXT[],
premium BOOLEAN DEFAULT FALSE,
premium_expires_at TIMESTAMP
);
-- Таблица профилей
CREATE TABLE IF NOT EXISTS profiles (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255),
age INTEGER,
gender VARCHAR(10),
bio TEXT,
photos JSONB DEFAULT '[]'::jsonb,
interests TEXT[],
location VARCHAR(255),
religion VARCHAR(50),
education VARCHAR(255),
job VARCHAR(255),
height INTEGER,
smoking VARCHAR(50),
drinking VARCHAR(50),
looking_for VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица лайков
CREATE TABLE IF NOT EXISTS likes (
id SERIAL PRIMARY KEY,
from_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
to_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
is_like BOOLEAN NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(from_user_id, to_user_id)
);
-- Таблица матчей
CREATE TABLE IF NOT EXISTS matches (
id SERIAL PRIMARY KEY,
user1_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
user2_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT TRUE,
UNIQUE(user1_id, user2_id)
);
-- Таблица сообщений
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
match_id INTEGER REFERENCES matches(id) ON DELETE CASCADE,
sender_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
message_text TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица просмотров профилей
CREATE TABLE IF NOT EXISTS profile_views (
id SERIAL PRIMARY KEY,
viewer_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
viewed_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(viewer_id, viewed_id)
);
-- Таблица уведомлений
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
scheduled_for TIMESTAMP,
processed BOOLEAN DEFAULT FALSE
);
-- Индексы для оптимизации запросов
-- Индексы для таблицы пользователей
CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
CREATE INDEX IF NOT EXISTS users_gender_idx ON users(gender);
CREATE INDEX IF NOT EXISTS users_looking_for_idx ON users(looking_for);
CREATE INDEX IF NOT EXISTS users_premium_idx ON users(premium);
-- Индексы для таблицы лайков
CREATE INDEX IF NOT EXISTS likes_from_user_id_idx ON likes(from_user_id);
CREATE INDEX IF NOT EXISTS likes_to_user_id_idx ON likes(to_user_id);
CREATE INDEX IF NOT EXISTS likes_is_like_idx ON likes(is_like);
-- Индексы для таблицы матчей
CREATE INDEX IF NOT EXISTS matches_user1_id_idx ON matches(user1_id);
CREATE INDEX IF NOT EXISTS matches_user2_id_idx ON matches(user2_id);
CREATE INDEX IF NOT EXISTS matches_active_idx ON matches(active);
-- Индексы для таблицы сообщений
CREATE INDEX IF NOT EXISTS messages_match_id_idx ON messages(match_id);
CREATE INDEX IF NOT EXISTS messages_sender_id_idx ON messages(sender_id);
-- Индексы для таблицы профилей
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id);
CREATE INDEX IF NOT EXISTS profiles_gender_idx ON profiles(gender);
CREATE INDEX IF NOT EXISTS profiles_looking_for_idx ON profiles(looking_for);
-- Индексы для таблицы просмотров профилей
CREATE INDEX IF NOT EXISTS profile_views_viewer_id_idx ON profile_views(viewer_id);
CREATE INDEX IF NOT EXISTS profile_views_viewed_id_idx ON profile_views(viewed_id);
-- Индексы для таблицы уведомлений
CREATE INDEX IF NOT EXISTS notifications_user_id_idx ON notifications(user_id);
CREATE INDEX IF NOT EXISTS notifications_scheduled_for_idx ON notifications(scheduled_for);
CREATE INDEX IF NOT EXISTS notifications_processed_idx ON notifications(processed);
-- Запись о выполнении миграции
INSERT INTO migrations (name) VALUES ('consolidated_migration.sql')
ON CONFLICT DO NOTHING;
EOL
echo "✅ Консолидированный SQL-файл создан: $consolidated_sql"
# Вывод информации о подключении
echo "🔍 Используемые параметры подключения:"
echo "DB_HOST: $DB_HOST"
echo "DB_PORT: $DB_PORT"
echo "DB_NAME: $DB_NAME"
echo "DB_USERNAME: $DB_USERNAME"
echo "DB_PASSWORD: ********"
# Проверка наличия psql
if command -v psql >/dev/null; then
echo "✅ Найдена команда psql, продолжаем..."
else
echo "⚠️ Команда psql не найдена, установите PostgreSQL клиент:"
echo "apt-get update && apt-get install -y postgresql-client"
exit 1
fi
# Применение миграции
echo "🔄 Применение консолидированной миграции..."
export PGPASSWORD=$DB_PASSWORD
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$consolidated_sql"
# Проверка результата
if [ $? -eq 0 ]; then
echo "✅ Миграция успешно применена!"
else
echo "❌ Ошибка при применении миграции!"
exit 1
fi
# Удаление временного файла
rm -f "$consolidated_sql"
echo "🚀 Миграция базы данных завершена!"

75
bin/apply_migrations.sh Normal file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
# apply_migrations.sh - Скрипт для ручного применения миграций
echo "🔄 Ручное применение миграций базы данных..."
# Загрузка переменных окружения из .env
if [ -f .env ]; then
echo "📝 Загрузка переменных окружения из .env..."
export $(grep -v '^#' .env | xargs)
else
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
export DB_HOST="localhost"
export DB_PORT="5432"
export DB_NAME="telegram_tinder_bot"
export DB_USERNAME="postgres"
export DB_PASSWORD="postgres"
fi
# Проверка на существование директории миграций
if [ ! -d "migrations" ] && [ ! -d "src/database/migrations" ]; then
echo "❌ Не найдены директории с миграциями!"
exit 1
fi
# Вывод информации о подключении
echo "🔍 Используемые параметры подключения:"
echo "DB_HOST: $DB_HOST"
echo "DB_PORT: $DB_PORT"
echo "DB_NAME: $DB_NAME"
echo "DB_USERNAME: $DB_USERNAME"
echo "DB_PASSWORD: ********"
# Проверка подключения к базе данных
echo "🔍 Проверка подключения к базе данных..."
if command -v pg_isready >/dev/null; then
pg_isready -h $DB_HOST -p $DB_PORT -U $DB_USERNAME
if [ $? -ne 0 ]; then
echo "❌ Не удалось подключиться к базе данных!"
exit 1
fi
else
echo "⚠️ Утилита pg_isready не найдена, пропускаем проверку"
fi
# Копирование миграций JS в отдельную директорию
echo "📂 Копирование только JS-миграций во временную директорию..."
mkdir -p temp_migrations
find migrations -name "*.js" -exec cp {} temp_migrations/ \;
# Применение миграций
echo "🔄 Применение миграций с помощью node-pg-migrate..."
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up --migrations-dir=temp_migrations
# Проверка результата
if [ $? -eq 0 ]; then
echo "✅ Миграции успешно применены!"
else
echo "❌ Ошибка при применении миграций!"
echo "⚠️ Пытаемся применить миграции из других источников..."
# Попробуем применить SQL-миграции напрямую
if [ -d "src/database/migrations" ]; then
echo "📂 Найдены SQL-миграции. Пытаемся применить их напрямую..."
for sql_file in src/database/migrations/*.sql; do
if [ -f "$sql_file" ]; then
echo "🔄 Применение миграции $sql_file..."
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$sql_file" || echo "⚠️ Ошибка при применении $sql_file"
fi
done
fi
fi
# Очистка временных файлов
echo "🧹 Очистка временных файлов..."
rm -rf temp_migrations

71
bin/clear_interactions.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# Скрипт для очистки всех взаимодействий между пользователями
# Использование: ./clear_interactions.sh
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}================================================${NC}"
echo -e "${YELLOW} Скрипт очистки взаимодействий пользователей${NC}"
echo -e "${YELLOW}================================================${NC}"
echo ""
echo -e "${RED}ВНИМАНИЕ!${NC} Будут удалены:"
echo " - Все сообщения (messages)"
echo " - Все матчи (matches)"
echo " - Все просмотры профилей (profile_views)"
echo " - Все свайпы (swipes)"
echo " - Все уведомления (notifications)"
echo ""
echo -e "Профили пользователей ${GREEN}НЕ${NC} будут удалены."
echo ""
# Запрос подтверждения
read -p "Вы уверены, что хотите продолжить? (yes/no): " confirmation
if [ "$confirmation" != "yes" ]; then
echo -e "${YELLOW}Операция отменена.${NC}"
exit 0
fi
echo ""
echo -e "${YELLOW}Загрузка переменных окружения...${NC}"
# Загрузка переменных из .env файла
if [ -f .env ]; then
export $(cat .env | grep -v '^#' | xargs)
else
echo -e "${RED}Ошибка: файл .env не найден!${NC}"
exit 1
fi
# Проверка наличия необходимых переменных
if [ -z "$DB_HOST" ] || [ -z "$DB_PORT" ] || [ -z "$DB_NAME" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then
echo -e "${RED}Ошибка: не все переменные БД определены в .env${NC}"
exit 1
fi
echo -e "${GREEN}Переменные загружены успешно.${NC}"
echo ""
echo -e "${YELLOW}Выполнение SQL скрипта...${NC}"
# Выполнение SQL скрипта
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" -f sql/clear_interactions.sql
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}================================================${NC}"
echo -e "${GREEN} ✅ Очистка выполнена успешно!${NC}"
echo -e "${GREEN}================================================${NC}"
else
echo ""
echo -e "${RED}================================================${NC}"
echo -e "${RED} ❌ Ошибка при выполнении очистки!${NC}"
echo -e "${RED}================================================${NC}"
exit 1
fi

View File

@@ -0,0 +1,52 @@
#!/bin/bash
# compile_ts_migrations.sh - Скрипт для компиляции TS миграций в JS
echo "🔄 Компиляция TypeScript миграций в JavaScript..."
# Проверка наличия TypeScript файлов
if [ ! -f "migrations/*.ts" ] && [ ! -d "node_modules/typescript" ]; then
echo "📦 Установка TypeScript..."
npm install --no-save typescript
fi
# Создание временного tsconfig для миграций
echo "📝 Создание временного tsconfig.json для миграций..."
cat > migrations/tsconfig.json << EOL
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": false,
"outDir": "../temp_migrations",
"baseUrl": "..",
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["./*.ts"]
}
EOL
# Компиляция TS файлов
echo "🔄 Компиляция TypeScript миграций..."
npx tsc -p migrations/tsconfig.json
# Подтверждение
if [ $? -eq 0 ]; then
echo "✅ Миграции успешно скомпилированы в директорию temp_migrations/"
# Проверка, были ли созданы файлы
file_count=$(find temp_migrations -name "*.js" | wc -l)
echo "📊 Скомпилировано файлов: $file_count"
else
echo "❌ Ошибка при компиляции миграций!"
exit 1
fi
# Очистка временных файлов
rm migrations/tsconfig.json

View File

@@ -0,0 +1,188 @@
#!/bin/bash
# create_consolidated_migration.sh - Создание консолидированной миграции из всех источников
echo "🚀 Создание консолидированной миграции..."
# Создаем каталог для миграций если его нет
mkdir -p migrations
# Текущее время для имени файла
timestamp=$(date +%s)
# Путь к консолидированной миграции
consolidated_file="migrations/${timestamp}_consolidated_migration.js"
echo "📝 Создание файла $consolidated_file..."
# Создаем JS-миграцию
cat > "$consolidated_file" << EOL
/* eslint-disable camelcase */
exports.shorthands = undefined;
exports.up = pgm => {
// Консолидированная миграция, созданная автоматически
// Создаем таблицу migrations, если её ещё нет
pgm.sql(\`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
\`);
// Создаем основную структуру базы данных
pgm.sql(\`
-- Таблица пользователей
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
language_code VARCHAR(10),
is_bot BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
state VARCHAR(50) DEFAULT 'START',
state_data JSONB,
gender VARCHAR(10),
looking_for VARCHAR(10),
bio TEXT,
age INTEGER,
location VARCHAR(255),
photos JSONB DEFAULT '[]'::jsonb,
interests TEXT[],
premium BOOLEAN DEFAULT FALSE,
premium_expires_at TIMESTAMP
);
-- Таблица профилей
CREATE TABLE IF NOT EXISTS profiles (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255),
age INTEGER,
gender VARCHAR(10),
bio TEXT,
photos JSONB DEFAULT '[]'::jsonb,
interests TEXT[],
location VARCHAR(255),
religion VARCHAR(50),
education VARCHAR(255),
job VARCHAR(255),
height INTEGER,
smoking VARCHAR(50),
drinking VARCHAR(50),
looking_for VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица лайков
CREATE TABLE IF NOT EXISTS likes (
id SERIAL PRIMARY KEY,
from_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
to_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
is_like BOOLEAN NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(from_user_id, to_user_id)
);
-- Таблица матчей
CREATE TABLE IF NOT EXISTS matches (
id SERIAL PRIMARY KEY,
user1_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
user2_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT TRUE,
UNIQUE(user1_id, user2_id)
);
-- Таблица сообщений
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
match_id INTEGER REFERENCES matches(id) ON DELETE CASCADE,
sender_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
message_text TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица просмотров профилей
CREATE TABLE IF NOT EXISTS profile_views (
id SERIAL PRIMARY KEY,
viewer_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
viewed_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(viewer_id, viewed_id)
);
-- Таблица уведомлений
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
data JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
scheduled_for TIMESTAMP,
processed BOOLEAN DEFAULT FALSE
);
\`);
// Индексы для оптимизации запросов
pgm.sql(\`
-- Индексы для таблицы пользователей
CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
CREATE INDEX IF NOT EXISTS users_gender_idx ON users(gender);
CREATE INDEX IF NOT EXISTS users_looking_for_idx ON users(looking_for);
CREATE INDEX IF NOT EXISTS users_premium_idx ON users(premium);
-- Индексы для таблицы лайков
CREATE INDEX IF NOT EXISTS likes_from_user_id_idx ON likes(from_user_id);
CREATE INDEX IF NOT EXISTS likes_to_user_id_idx ON likes(to_user_id);
CREATE INDEX IF NOT EXISTS likes_is_like_idx ON likes(is_like);
-- Индексы для таблицы матчей
CREATE INDEX IF NOT EXISTS matches_user1_id_idx ON matches(user1_id);
CREATE INDEX IF NOT EXISTS matches_user2_id_idx ON matches(user2_id);
CREATE INDEX IF NOT EXISTS matches_active_idx ON matches(active);
-- Индексы для таблицы сообщений
CREATE INDEX IF NOT EXISTS messages_match_id_idx ON messages(match_id);
CREATE INDEX IF NOT EXISTS messages_sender_id_idx ON messages(sender_id);
-- Индексы для таблицы профилей
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id);
CREATE INDEX IF NOT EXISTS profiles_gender_idx ON profiles(gender);
CREATE INDEX IF NOT EXISTS profiles_looking_for_idx ON profiles(looking_for);
-- Индексы для таблицы просмотров профилей
CREATE INDEX IF NOT EXISTS profile_views_viewer_id_idx ON profile_views(viewer_id);
CREATE INDEX IF NOT EXISTS profile_views_viewed_id_idx ON profile_views(viewed_id);
-- Индексы для таблицы уведомлений
CREATE INDEX IF NOT EXISTS notifications_user_id_idx ON notifications(user_id);
CREATE INDEX IF NOT EXISTS notifications_scheduled_for_idx ON notifications(scheduled_for);
CREATE INDEX IF NOT EXISTS notifications_processed_idx ON notifications(processed);
\`);
};
exports.down = pgm => {
// Эта функция не будет фактически использоваться,
// но для полноты оставляем возможность отката
pgm.sql(\`
DROP TABLE IF EXISTS notifications;
DROP TABLE IF EXISTS profile_views;
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS matches;
DROP TABLE IF EXISTS likes;
DROP TABLE IF EXISTS profiles;
DROP TABLE IF EXISTS users;
\`);
};
EOL
echo "✅ Консолидированная миграция создана: $consolidated_file"
echo ""
echo "🚀 Для применения миграции выполните:"
echo "DATABASE_URL=postgres://\${DB_USERNAME}:\${DB_PASSWORD}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME} npx node-pg-migrate up"

60
bin/find_hardcoded_texts.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Скрипт для поиска всех хардкод-текстов на русском языке в TypeScript файлах
echo "========================================="
echo "🔍 Поиск хардкод-текстов в проекте"
echo "========================================="
echo ""
# Директория для поиска
SEARCH_DIR="src"
# Подсчет общего количества хардкод-текстов
echo "📊 Общая статистика:"
echo "-------------------"
single_quotes=$(grep -rn "'[А-Яа-яЁё]" $SEARCH_DIR --include="*.ts" | wc -l)
double_quotes=$(grep -rn '"[А-Яа-яЁё]' $SEARCH_DIR --include="*.ts" | wc -l)
total=$((single_quotes + double_quotes))
echo "Тексты в одинарных кавычках: $single_quotes"
echo "Тексты в двойных кавычках: $double_quotes"
echo "ВСЕГО: $total"
echo ""
# Топ-10 файлов с наибольшим количеством хардкода
echo "📁 Топ-10 файлов с хардкод-текстами:"
echo "-----------------------------------"
grep -rn "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" $SEARCH_DIR --include="*.ts" | \
cut -d: -f1 | \
sort | \
uniq -c | \
sort -rn | \
head -10 | \
awk '{printf "%3d тексто в: %s\n", $1, $2}'
echo ""
# Детальная информация по каждому файлу
echo "📄 Детальная статистика по файлам:"
echo "----------------------------------"
for file in $(grep -rl "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" $SEARCH_DIR --include="*.ts" | sort); do
count=$(grep -n "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" "$file" | wc -l)
if [ $count -gt 0 ]; then
printf "%-60s %3d текстов\n" "$file" "$count"
fi
done
echo ""
echo "========================================="
echo "✅ Анализ завершен"
echo "========================================="
echo ""
echo "Рекомендации:"
echo "1. Начните с файлов, содержащих больше всего текстов"
echo "2. Используйте команду для просмотра конкретных строк:"
echo " grep -n \"'[А-Яа-яЁё]\\|\\\"[А-Яа-яЁё]\" <файл>"
echo "3. Замените тексты на локализационные ключи"
echo "4. Добавьте переводы в файлы src/locales/*.json"
echo ""

100
bin/fix_docker.bat Normal file
View File

@@ -0,0 +1,100 @@
@echo off
REM fix_docker.bat - Скрипт для устранения проблемы ContainerConfig в Windows
echo 🔧 Устранение проблемы с Docker контейнерами...
REM Остановка всех контейнеров проекта
echo 📥 Остановка всех контейнеров проекта...
docker-compose down -v
REM Принудительное удаление контейнеров по имени
echo 🗑️ Принудительное удаление оставшихся контейнеров...
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>NUL
REM Очистка неиспользуемых томов и сетей
echo 🧹 Очистка неиспользуемых томов и сетей...
docker system prune -f --volumes
REM Очистка кеша Docker
echo 🧼 Очистка кеша Docker...
docker builder prune -f
REM Исправление docker-compose.yml
echo 📝 Создание обновленного docker-compose.yml...
REM Создаем обновленный docker-compose.yml с использованием PowerShell
powershell -Command "& {
$content = @'
version: '3.8'
services:
bot:
build: .
container_name: telegram-tinder-bot
restart: unless-stopped
env_file: .env
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
- DB_USERNAME=${DB_USERNAME:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
volumes:
- ./uploads:/app/uploads:rw
- ./logs:/app/logs:rw
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
container_name: postgres-tinder
restart: unless-stopped
environment:
- POSTGRES_DB=${DB_NAME:-telegram_tinder_bot}
- POSTGRES_USER=${DB_USERNAME:-postgres}
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- \"5433:5432\"
networks:
- bot-network
healthcheck:
test: [\"CMD-SHELL\", \"pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-telegram_tinder_bot}\"]
interval: 10s
timeout: 5s
retries: 5
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
'@
Set-Content -Path 'docker-compose.yml' -Value $content
}"
echo ✅ docker-compose.yml обновлен!
echo 🚀 Готово! Теперь вы можете запустить контейнеры снова с помощью команды:
echo docker-compose up -d
pause

117
bin/fix_docker.sh Normal file
View File

@@ -0,0 +1,117 @@
#!/bin/bash
# fix_docker.sh - Скрипт для устранения проблемы ContainerConfig
echo "🔧 Устранение проблемы с Docker контейнерами..."
# Остановка всех контейнеров проекта
echo "📥 Остановка всех контейнеров проекта..."
docker-compose down -v
# Принудительное удаление контейнеров по имени
echo "🗑️ Принудительное удаление оставшихся контейнеров..."
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>/dev/null || true
# Очистка неиспользуемых томов и сетей
echo "🧹 Очистка неиспользуемых томов и сетей..."
docker system prune -f --volumes
# Очистка кеша Docker
echo "🧼 Очистка кеша Docker..."
docker builder prune -f
# Исправление docker-compose.yml
echo "📝 Создание обновленного docker-compose.yml..."
cat > docker-compose.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=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
- DB_USERNAME=${DB_USERNAME:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
volumes:
- ./uploads:/app/uploads:rw
- ./logs:/app/logs:rw
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
EOL
# Если используем внешнюю базу данных, добавляем только adminer
if [ "${DB_HOST:-db}" != "db" ]; then
cat >> docker-compose.yml << EOL
adminer:
image: adminer:latest
container_name: adminer-tinder
restart: unless-stopped
ports:
- "8080:8080"
networks:
- bot-network
EOL
else
# Если используем локальную базу данных, добавляем PostgreSQL и adminer
cat >> docker-compose.yml << EOL
db:
image: postgres:15-alpine
container_name: postgres-tinder
restart: unless-stopped
environment:
- POSTGRES_DB=\${DB_NAME:-telegram_tinder_bot}
- POSTGRES_USER=\${DB_USERNAME:-postgres}
- POSTGRES_PASSWORD=\${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5433:5432"
networks:
- bot-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \${DB_USERNAME:-postgres} -d \${DB_NAME:-telegram_tinder_bot}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
adminer:
image: adminer:latest
container_name: adminer-tinder
restart: unless-stopped
ports:
- "8080:8080"
networks:
- bot-network
EOL
fi
# Завершаем файл docker-compose.yml
cat >> docker-compose.yml << EOL
volumes:
postgres_data:
networks:
bot-network:
driver: bridge
EOL
echo "✅ docker-compose.yml обновлен!"
echo "🚀 Готово! Теперь вы можете запустить контейнеры снова с помощью команды:"
echo "docker-compose up -d"

15
bin/fix_line_endings.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# fix_line_endings.sh - Script to fix line endings in shell scripts
echo "🔧 Fixing line endings in shell scripts..."
# Fix shell scripts
for file in $(find . -name "*.sh"); do
echo "📄 Processing $file..."
tr -d '\r' < "$file" > "$file.fixed"
mv "$file.fixed" "$file"
chmod +x "$file"
echo "✅ Fixed $file"
done
echo "🚀 All shell scripts fixed!"

17
bin/fix_permissions.sh Normal file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# fix_permissions.sh - Устанавливает права на выполнение для всех скриптов
echo "🔧 Установка прав на выполнение для всех скриптов..."
# Находим все .sh файлы и устанавливаем права на выполнение
find . -name "*.sh" -type f -exec chmod +x {} \;
echo "✅ Права на выполнение установлены!"
# Исправление переносов строк
echo "🔧 Исправление переносов строк..."
find . -name "*.sh" -type f -exec sh -c 'tr -d "\r" < "$1" > "$1.fixed" && mv "$1.fixed" "$1"' -- {} \;
echo "✅ Переносы строк исправлены!"
echo "🚀 Готово! Все скрипты готовы к использованию."

20
bin/init_database.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# init_database.sh - Initialize database with schema
set -e
echo "🗄️ Initializing database..."
# Wait for database to be ready
echo "⏳ Waiting for database..."
sleep 3
# Create UUID extension
echo "📦 Creating UUID extension..."
docker compose exec -T db psql -U postgres -d telegram_tinder_bot -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" || true
# Apply consolidated schema
echo "📋 Applying database schema..."
docker compose exec -T db psql -U postgres -d telegram_tinder_bot < sql/consolidated.sql
echo "✅ Database initialized successfully!"

37
bin/run_full_migration.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# run_full_migration.sh - Полный процесс миграции с компиляцией TypeScript
echo "🚀 Запуск полного процесса миграции..."
# Проверка наличия файлов TS
if find migrations -name "*.ts" -quit; then
echo "📋 Обнаружены TypeScript миграции. Компилируем их..."
# Компиляция TS файлов
./bin/compile_ts_migrations.sh
# Проверка результата
if [ $? -ne 0 ]; then
echo "❌ Ошибка компиляции TS миграций!"
exit 1
fi
else
echo " TypeScript миграции не обнаружены, пропускаем компиляцию."
mkdir -p temp_migrations
fi
# Копирование JS миграций
echo "📂 Копирование JS-миграций..."
find migrations -name "*.js" -exec cp {} temp_migrations/ \;
# Запуск миграций
echo "🔄 Применение всех миграций..."
./bin/apply_migrations.sh
# Проверка результата
if [ $? -eq 0 ]; then
echo "✅ Процесс миграции успешно завершен!"
else
echo "❌ Ошибка в процессе миграции."
exit 1
fi

101
bin/run_sql_migrations.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
# run_sql_migrations.sh - Ручное применение SQL-миграций
echo "🚀 Запуск SQL-миграций..."
# Загрузка переменных окружения из .env
if [ -f .env ]; then
echo "📝 Загрузка переменных окружения из .env..."
set -o allexport
source .env
set +o allexport
else
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
export DB_HOST="localhost"
export DB_PORT="5432"
export DB_NAME="telegram_tinder_bot"
export DB_USERNAME="postgres"
export DB_PASSWORD="postgres"
fi
# Вывод информации о подключении
echo "🔍 Используемые параметры подключения:"
echo "DB_HOST: $DB_HOST"
echo "DB_PORT: $DB_PORT"
echo "DB_NAME: $DB_NAME"
echo "DB_USERNAME: $DB_USERNAME"
echo "DB_PASSWORD: ********"
# Функция для применения SQL файлов из директории
apply_sql_files() {
local directory=$1
echo "🔍 Ищем SQL-файлы в директории $directory..."
if [ -d "$directory" ]; then
# Получаем список файлов в порядке времени создания
files=$(find "$directory" -name "*.sql" | sort)
if [ -z "$files" ]; then
echo "⚠️ SQL-файлы не найдены в $directory"
return 0
fi
for sql_file in $files; do
echo "🔄 Применение миграции $sql_file..."
# Проверяем, есть ли уже запись о миграции в таблице migrations
filename=$(basename "$sql_file")
exists=$(PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -t -c "SELECT EXISTS(SELECT 1 FROM migrations WHERE name='$filename')" 2>/dev/null)
# Если таблицы migrations не существует, создаем её
if [ $? -ne 0 ]; then
echo "📝 Таблица migrations не найдена. Создаем..."
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
" 2>/dev/null
exists=" f"
fi
# Если миграция уже применена, пропускаем
if [[ "$exists" == *"t"* ]]; then
echo "⏭️ Миграция $filename уже применена, пропускаем"
continue
fi
# Применяем миграцию
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$sql_file"
if [ $? -eq 0 ]; then
echo "✅ Миграция $filename успешно применена"
# Записываем в таблицу migrations
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "
INSERT INTO migrations (name) VALUES ('$filename')
" 2>/dev/null
else
echo "❌ Ошибка при применении миграции $filename"
return 1
fi
done
else
echo "⚠️ Директория $directory не найдена"
return 0
fi
return 0
}
# Применяем SQL миграции из всех возможных папок
echo "🔄 Применение SQL-миграций из src/database/migrations..."
apply_sql_files "src/database/migrations"
echo "🔄 Применение SQL-миграций из migrations/sql..."
apply_sql_files "migrations/sql"
echo "🔄 Применение SQL-миграций из migrations (если есть)..."
apply_sql_files "migrations"
echo "✅ Процесс применения SQL-миграций завершен!"

230
deploy.sh
View File

@@ -1,63 +1,217 @@
#!/bin/bash
# deploy.sh - Скрипт для деплоя Telegram Tinder Bot
# deploy.sh - Улучшенный скрипт для деплоя Telegram Tinder Bot
echo "🚀 Деплой Telegram Tinder Bot..."
set -e # Выход при ошибке
# Проверяем наличие 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
# Определение цветов для вывода
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 Deploy ${NC}"
echo -e "${BLUE}========================================${NC}"
# Определяем рабочую директорию
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
# Функция для проверки наличия команды
check_command() {
if ! command -v "$1" &> /dev/null; then
echo -e "${RED}❌ Команда $1 не найдена!${NC}"
return 1
else
echo -e "${GREEN}✓ Команда $1 найдена${NC}"
return 0
fi
}
# Шаг 1: Проверка и установка зависимостей
echo -e "\n${BLUE}Шаг 1: Проверка и установка зависимостей...${NC}"
# Проверяем наличие Docker и Docker Compose
if ! check_command docker || ! check_command docker-compose; then
echo -e "${YELLOW}Установка Docker и Docker Compose...${NC}"
# Проверяем, запущен ли скрипт от имени root
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}❌ Этот скрипт должен быть запущен с правами root для установки Docker.${NC}"
echo -e "Пожалуйста, запустите: ${YELLOW}sudo $0${NC}"
exit 1
fi
# Устанавливаем Docker и Docker Compose
if [ -f bin/install_docker.sh ]; then
bash bin/install_docker.sh
else
echo -e "${YELLOW}Установка Docker с помощью apt...${NC}"
apt update
apt install -y docker.io docker-compose
fi
fi
# Проверяем наличие Git
if ! check_command git; then
echo -e "${YELLOW}Установка Git...${NC}"
apt update && apt install -y git
fi
# Проверяем наличие Node.js (для локальных операций)
if ! check_command node || ! check_command npm; then
echo -e "${YELLOW}Установка Node.js...${NC}"
apt update
apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
fi
# Шаг 2: Получение последних изменений из репозитория
echo -e "\n${BLUE}Шаг 2: Получение последних изменений из репозитория...${NC}"
# Сохраняем локальные изменения, если они есть
git stash save "Auto-stash before deploy: $(date)" || true
# Получаем последние изменения
echo "📥 Получение последних изменений..."
git pull origin main
git fetch --all
git checkout main || git checkout master
git pull origin "$(git rev-parse --abbrev-ref HEAD)"
echo -e "${GREEN}✓ Получены последние изменения${NC}"
# Шаг 3: Проверка и создание файлов конфигурации
echo -e "\n${BLUE}Шаг 3: Проверка и настройка конфигурационных файлов...${NC}"
# Проверяем наличие .env файла
if [ ! -f .env ]; then
echo "📝 Создание .env файла из .env.production..."
cp .env.production .env
echo "⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!"
exit 1
echo -e "${YELLOW}⚠️ Файл .env не найден!${NC}"
# Пытаемся найти шаблон .env файла
if [ -f .env.production ]; then
echo -e "${YELLOW}Создание .env файла из .env.production...${NC}"
cp .env.production .env
elif [ -f .env.example ]; then
echo -e "${YELLOW}Создание .env файла из .env.example...${NC}"
cp .env.example .env
else
echo -e "${RED}❌ Шаблон .env файла не найден! Создаем базовый .env файл...${NC}"
cat > .env << EOL
# Базовый .env файл
NODE_ENV=production
PORT=3000
DB_HOST=db
DB_PORT=5432
DB_NAME=telegram_tinder_bot
DB_USERNAME=postgres
DB_PASSWORD=postgres
TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN
EOL
fi
echo -e "${YELLOW}⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!${NC}"
echo -e "${YELLOW}⚠️ Особенно важно указать TELEGRAM_BOT_TOKEN${NC}"
read -p "Продолжить деплой? (y/n): " continue_deploy
if [[ ! $continue_deploy =~ ^[Yy]$ ]]; then
echo -e "${RED}Деплой отменен. Пожалуйста, настройте .env файл и запустите скрипт снова.${NC}"
exit 1
fi
fi
# Проверяем и исправляем проблему с командой сборки в Dockerfile
echo "🔧 Проверка конфигурации Dockerfile..."
if grep -q "RUN npm run build" Dockerfile; then
echo "⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux..."
# Проверяем наличие docker-compose.override.yml
if [ ! -f docker-compose.override.yml ] && [ -f docker-compose.override.yml.example ]; then
echo -e "${YELLOW}Создание docker-compose.override.yml из примера...${NC}"
cp docker-compose.override.yml.example docker-compose.override.yml
fi
# Шаг 4: Исправление проблем с Docker
echo -e "\n${BLUE}Шаг 4: Проверка и исправление проблем с Docker...${NC}"
# Исправляем проблему с командой сборки в Dockerfile
if [ -f Dockerfile ] && grep -q "RUN npm run build" Dockerfile; then
echo -e "${YELLOW}⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux...${NC}"
sed -i 's/RUN npm run build/RUN npm run build:linux/g' Dockerfile
echo "✅ Dockerfile обновлен"
echo -e "${GREEN} Dockerfile обновлен${NC}"
fi
# Запускаем Docker Compose
echo "🐳 Сборка и запуск контейнеров Docker..."
docker-compose down
# Исправление прав доступа к файлам в Unix-системах
if [ -f bin/fix_permissions.sh ]; then
echo -e "${YELLOW}Исправление прав доступа к файлам...${NC}"
bash bin/fix_permissions.sh
fi
# Шаг 5: Запуск с Docker Compose
echo -e "\n${BLUE}Шаг 5: Сборка и запуск Docker контейнеров...${NC}"
# Остановка и удаление старых контейнеров
echo -e "${YELLOW}Остановка и удаление старых контейнеров...${NC}"
docker-compose down || true
# Проверка наличия скрипта для исправления Docker
if [ -f bin/fix_docker.sh ]; then
echo -e "${YELLOW}Запуск скрипта исправления Docker...${NC}"
bash bin/fix_docker.sh
fi
# Создание необходимых директорий с правильными правами доступа
echo -e "${YELLOW}Создание необходимых директорий...${NC}"
mkdir -p logs uploads
chmod -R 777 logs uploads
# Сборка и запуск контейнеров
echo -e "${YELLOW}Сборка контейнеров...${NC}"
docker-compose build
echo -e "${YELLOW}Запуск контейнеров...${NC}"
docker-compose up -d
# Шаг 6: Применение миграций
echo -e "\n${BLUE}Шаг 6: Применение миграций базы данных...${NC}"
# Ждем инициализации базы данных
echo -e "${YELLOW}Ожидание инициализации базы данных...${NC}"
sleep 10
# Выбор способа миграции
if [ -f bin/run_full_migration.sh ]; then
echo -e "${YELLOW}Запуск полной миграции базы данных...${NC}"
docker-compose exec bot bash -c "cd /app && ./bin/run_full_migration.sh" || true
elif [ -f bin/apply_migrations.sh ]; then
echo -e "${YELLOW}Применение миграций базы данных...${NC}"
docker-compose exec bot bash -c "cd /app && ./bin/apply_migrations.sh" || true
else
echo -e "${YELLOW}Миграционные скрипты не найдены, пропускаем этап миграции${NC}"
fi
# Шаг 7: Проверка работоспособности
echo -e "\n${BLUE}Шаг 7: Проверка работоспособности...${NC}"
# Проверяем статус контейнеров
echo "🔍 Проверка статуса контейнеров..."
echo -e "${YELLOW}Проверка статуса контейнеров...${NC}"
docker-compose ps
echo "✅ Деплой успешно завершен! Бот должен быть доступен через Telegram."
# Ждем запуска API
echo -e "${YELLOW}Ожидание запуска API...${NC}"
sleep 5
docker-compose exec bot curl -s http://localhost:3000/health || echo "⚠️ Сервис не отвечает на проверку здоровья"
# Вывод информации о деплое
echo -e "\n${GREEN}✅ Деплой успешно завершен!${NC}"
echo -e "${GREEN}✅ Бот должен быть доступен через Telegram.${NC}"
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 -e "${BLUE}📊 Полезные команды:${NC}"
echo -e "- ${YELLOW}Просмотр логов:${NC} docker-compose logs -f bot"
echo -e "- ${YELLOW}Перезапуск сервисов:${NC} docker-compose restart"
echo -e "- ${YELLOW}Остановка всех сервисов:${NC} docker-compose down"
echo -e "- ${YELLOW}Доступ к базе данных:${NC} docker-compose exec db psql -U postgres -d telegram_tinder_bot"
echo -e "- ${YELLOW}Проверка состояния бота:${NC} 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"
echo -e "${BLUE}🌟 Для администрирования базы данных:${NC}"
echo -e "Adminer доступен по адресу: http://ваш_сервер:8080"
echo -e " - ${YELLOW}Система:${NC} PostgreSQL"
echo -e " - ${YELLOW}Сервер:${NC} db"
echo -e " - ${YELLOW}Пользователь:${NC} postgres"
echo -e " - ${YELLOW}Пароль:${NC} (из переменной DB_PASSWORD в .env)"
echo -e " - ${YELLOW}База данных:${NC} telegram_tinder_bot"
echo ""
echo -e "${YELLOW}⚠️ При возникновении проблем проверьте файлы в директории bin/ для дополнительных утилит исправления.${NC}"

View File

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

View File

@@ -1,45 +0,0 @@
[36mВведите параметры подключения к внешней базе данных:[0m
Хост (например, localhost): Порт (например, 5432): Имя базы данных: Имя пользователя: Пароль: [33mМодифицируем docker-compose.yml для работы с внешней базой данных...[0m
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><><E4A0A9><EFBFBD>: 0.
version: '3.8'
services:
bot:
build: .
container_name: telegram-tinder-bot
restart: unless-stopped
env_file: .env
environment:
- NODE_ENV=production
- DB_HOST=
- DB_PORT=
- DB_NAME=
- DB_USERNAME=
- DB_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

View File

@@ -1,23 +1,19 @@
version: '3.8'
services:
bot:
build: .
container_name: telegram-tinder-bot
restart: unless-stopped
depends_on:
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_HOST=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
- DB_USERNAME=${DB_USERNAME:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
volumes:
- ./uploads:/app/uploads
- ./logs:/app/logs
- ./uploads:/app/uploads:rw
- ./logs:/app/logs:rw
networks:
- bot-network
healthcheck:
@@ -32,9 +28,9 @@ services:
container_name: postgres-tinder
restart: unless-stopped
environment:
- POSTGRES_DB=telegram_tinder_bot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD:-password123}
- POSTGRES_DB=${DB_NAME:-telegram_tinder_bot}
- POSTGRES_USER=${DB_USERNAME:-postgres}
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
@@ -42,19 +38,18 @@ services:
networks:
- bot-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-telegram_tinder_bot}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
adminer:
image: adminer:latest
container_name: adminer-tinder
restart: unless-stopped
ports:
- "8080:8080"
depends_on:
- db
networks:
- bot-network

176
docs/DATABASE_FIXES.md Normal file
View File

@@ -0,0 +1,176 @@
# Database Schema Fixes
## Обзор исправлений
Этот документ описывает исправления схемы базы данных, примененные к проекту Telegram Tinder Bot для устранения критических ошибок и предупреждений.
## Дата последнего обновления: 2025-11-06
---
## Исправление 1: Колонка `looking_for` и триггер создания профиля
### Проблема
```
ERROR: null value in column "looking_for" of relation "profiles" violates not-null constraint
```
### Причина
Триггер `create_initial_profile()` не устанавливал значение для обязательного поля `looking_for` при автоматическом создании профиля.
### Решение
Применен патч: `sql/fix_looking_for_column.sql`
**Изменения:**
1. Обновлен триггер для включения `looking_for = 'both'` и `interested_in = 'both'`
2. Сделана колонка `looking_for` необязательной (nullable) с DEFAULT 'both'
3. Добавлена колонка `interested_in` как современный синоним для `looking_for`
4. Создан индекс для поиска: `idx_profiles_interested_in`
**Применение:**
```bash
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/fix_looking_for_column.sql
```
---
## Исправление 2: Колонка `job` и `state`
### Проблема 1: Column "job" does not exist
```
ERROR: column "job" of relation "profiles" does not exist
```
**Причина:** Код использует `job`, но в БД создана колонка `occupation`.
### Проблема 2: State column does not exist
```
WARNING: State column does not exist in users table. Skipping state check.
```
**Причина:** Таблица `users` не содержит колонку `state` для отслеживания состояния диалога.
### Решение
Применен патч: `sql/add_job_and_state_columns.sql`
**Изменения:**
1. Добавлена колонка `job VARCHAR(255)` в таблицу `profiles`
2. Скопированы данные из `occupation``job` для обратной совместимости
3. Добавлена колонка `state VARCHAR(50)` в таблицу `users`
4. Созданы индексы: `idx_profiles_job`, `idx_users_state`
**Применение:**
```bash
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/add_job_and_state_columns.sql
```
---
## Полная последовательность применения патчей
Для нового развертывания применяйте патчи в следующем порядке:
```bash
# 1. Основная схема (если еще не применена)
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/consolidated.sql
# 2. Исправление looking_for
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/fix_looking_for_column.sql
# 3. Добавление job и state
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/add_job_and_state_columns.sql
```
Или используйте автоматизированную команду:
```bash
make migrate
```
---
## Проверка применения исправлений
### Проверка триггера looking_for
```sql
SELECT proname, prosrc
FROM pg_proc
WHERE proname = 'create_initial_profile';
```
Должен содержать: `looking_for = 'both', interested_in = 'both'`
### Проверка колонок
```sql
-- Проверка profiles
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'profiles'
AND column_name IN ('job', 'occupation', 'looking_for', 'interested_in')
ORDER BY column_name;
-- Проверка users
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name = 'state';
```
### Проверка индексов
```sql
SELECT indexname, tablename, indexdef
FROM pg_indexes
WHERE tablename IN ('profiles', 'users')
AND indexname IN ('idx_profiles_job', 'idx_users_state', 'idx_profiles_interested_in')
ORDER BY tablename, indexname;
```
---
## Известные предупреждения (неопасные)
### ES Module в миграциях
```
SyntaxError: Unexpected token 'export'
```
**Статус:** Не критично. Миграции применяются через psql напрямую, а не через node-pg-migrate.
**Причина:** Файлы миграций в `/migrations` используют ES6 синтаксис, несовместимый с node-pg-migrate в режиме CommonJS.
**Решение:** Используйте `make migrate` или применяйте SQL патчи напрямую через psql.
### DEEPSEEK_API_KEY not found
```
⚠️ DEEPSEEK_API_KEY not found in environment variables
```
**Статус:** Не критично. Это опциональная AI-функция.
**Решение:** Добавьте `DEEPSEEK_API_KEY=your_key` в `.env` если хотите использовать AI-фичи.
---
## Mapping колонок (для справки)
| Код (TypeScript) | База данных | Комментарий |
|------------------|-------------|-------------|
| `job` | `job` | Основная колонка (новая) |
| `job` | `occupation` | Устаревшая, оставлена для совместимости |
| `interestedIn` | `interested_in` | Основная колонка (новая) |
| `lookingFor` | `looking_for` | Устаревшая, nullable |
| - | `state` | Новая колонка для users.state |
---
## Контакты для поддержки
При возникновении проблем с миграциями:
1. Проверьте логи бота: `docker compose logs bot --tail 50`
2. Проверьте применение всех патчей (см. раздел "Проверка")
3. Убедитесь, что `.env` содержит правильные DB_* переменные
4. Попробуйте применить патчи вручную через psql
**Версия документа:** 1.0
**Автор:** GitHub Copilot
**Дата:** 2025-11-06

View File

@@ -0,0 +1,173 @@
# Сводка исправлений от 2025-11-06
## 🎯 Цель
Устранить критические ошибки базы данных, блокирующие создание профилей пользователей.
---
## 🐛 Исправленные ошибки
### 1. ❌ Column "job" does not exist
**Ошибка:**
```
ERROR: column "job" of relation "profiles" does not exist
Code: 42703
```
**Причина:** Код использует поле `job`, но в БД существует только `occupation`.
**Решение:** Добавлена колонка `job` в таблицу `profiles`.
**Файл патча:** `sql/add_job_and_state_columns.sql`
**Статус:** ✅ ИСПРАВЛЕНО
---
### 2. ⚠️ State column does not exist in users table
**Предупреждение:**
```
State column does not exist in users table. Skipping state check.
```
**Причина:** Код пытается проверить состояние диалога пользователя через колонку `state`, которой нет в таблице `users`.
**Решение:** Добавлена колонка `state VARCHAR(50)` в таблицу `users`.
**Файл патча:** `sql/add_job_and_state_columns.sql`
**Статус:** ✅ ИСПРАВЛЕНО
---
### 3. ❌ null value in column "looking_for" violates not-null constraint
**Ошибка:**
```
ERROR: null value in column "looking_for" of relation "profiles" violates not-null constraint
Code: 23502
```
**Причина:** Триггер `create_initial_profile()` не устанавливал значение для обязательного поля `looking_for`.
**Решение:**
- Обновлен триггер для включения `looking_for = 'both'`
- Колонка сделана nullable с DEFAULT 'both'
- Добавлена колонка `interested_in` как современный синоним
**Файл патча:** `sql/fix_looking_for_column.sql`
**Статус:** ✅ ИСПРАВЛЕНО
---
## 📦 Применённые патчи
| # | Файл | Описание | Дата |
|---|------|----------|------|
| 1 | `sql/fix_looking_for_column.sql` | Исправление триггера и колонки looking_for | 2025-11-06 |
| 2 | `sql/add_job_and_state_columns.sql` | Добавление колонок job и state | 2025-11-06 |
---
## 🔧 Применение патчей
### Автоматически (рекомендуется):
```bash
./bin/apply_all_patches.sh
```
### Вручную:
```bash
# Патч 1: looking_for
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
-f sql/fix_looking_for_column.sql
# Патч 2: job и state
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
-f sql/add_job_and_state_columns.sql
# Перезапуск бота
docker compose restart bot
```
---
## ✅ Результаты после исправлений
### Проверка логов (нет критичных ошибок):
```bash
docker compose logs bot --since 5m | grep -E "(State column|column.*job|does not exist)"
```
**Результат:** Пусто (0 строк) ✅
### Проверка структуры БД:
```
table_name | column_name | data_type | nullable | column_default
------------+---------------+-------------------+----------+--------------------
profiles | interested_in | character varying | NULL | 'both'
profiles | job | character varying | NULL |
profiles | looking_for | character varying | NULL | 'both'
profiles | occupation | character varying | NULL |
users | state | character varying | NULL |
```
**Результат:** Все колонки присутствуют ✅
### Статус бота:
```
🎉 Bot initialized successfully!
🤖 Bot is running and ready to match people!
📱 Bot username: @seoulmate_officialbot
```
**Результат:** Бот запущен успешно ✅
---
## 📊 Статистика изменений
- **Добавлено колонок:** 3 (`job`, `state`, `interested_in`)
- **Обновлено триггеров:** 1 (`create_initial_profile`)
- **Создано индексов:** 3 (`idx_profiles_job`, `idx_users_state`, `idx_profiles_interested_in`)
- **Файлов патчей:** 2
- **Создано документации:** 3 файла (DATABASE_FIXES.md, HEALTH_CHECK.md, этот файл)
- **Создано утилит:** 1 (`bin/apply_all_patches.sh`)
---
## 🚀 Дальнейшие действия
### Обязательно:
- ✅ Протестировать создание профиля через бота
- ✅ Проверить обновление профиля (поле job)
- ✅ Убедиться что свайпы работают
### Опционально:
- ⚠️ Рассмотреть объединение `job` и `occupation` в одну колонку
- ⚠️ Рассмотреть объединение `looking_for` и `interested_in` в одну колонку
- ⚠️ Исправить ES module warnings в миграциях (низкий приоритет)
- ⚠️ Настроить DEEPSEEK_API_KEY если нужны AI-фичи
---
## 📚 Связанная документация
- `/docs/DATABASE_FIXES.md` - Подробное описание всех исправлений БД
- `/docs/HEALTH_CHECK.md` - Чеклист проверки здоровья бота
- `/bin/README.md` - Описание утилит и скриптов
- `/bin/apply_all_patches.sh` - Скрипт автоматического применения патчей
---
## 👤 Авторство
**Дата:** 2025-11-06
**Автор:** GitHub Copilot
**Проект:** Telegram Tinder Bot
**Версия:** 2.0
---
## ✨ Заключение
Все критические ошибки устранены. Бот готов к работе в production-среде с внешним PostgreSQL сервером (192.168.0.102:5432).
**Статус проекта:** 🟢 РАБОТОСПОСОБЕН

199
docs/HEALTH_CHECK.md Normal file
View File

@@ -0,0 +1,199 @@
# Быстрая проверка здоровья бота
Используйте этот чеклист после развёртывания или обновления бота.
## ✅ Чеклист проверки
### 1. Проверка контейнера
```bash
docker compose ps
# Ожидается: telegram-tinder-bot в состоянии "running" (healthy)
```
### 2. Проверка логов (нет критичных ошибок)
```bash
docker compose logs bot --tail 50
# ✅ Должно быть: "Bot initialized successfully"
# ✅ Должно быть: "Bot username: @your_bot_name"
# ❌ НЕ должно быть: "column X does not exist" (критическая ошибка)
```
### 3. Проверка схемы БД
```bash
# Проверка критичных колонок
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name << 'EOF'
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_name IN ('users', 'profiles')
AND column_name IN ('state', 'job', 'looking_for', 'interested_in', 'occupation')
ORDER BY table_name, column_name;
EOF
```
**Ожидаемый результат:**
```
table_name | column_name | data_type | is_nullable
------------+----------------+--------------------+-------------
profiles | interested_in | character varying | YES
profiles | job | character varying | YES
profiles | looking_for | character varying | YES
profiles | occupation | character varying | YES
users | state | character varying | YES
```
### 4. Проверка триггера
```bash
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -c \
"SELECT proname FROM pg_proc WHERE proname = 'create_initial_profile';"
```
**Ожидается:** `create_initial_profile` (1 строка)
### 5. Проверка индексов
```bash
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name << 'EOF'
SELECT indexname
FROM pg_indexes
WHERE indexname IN ('idx_profiles_job', 'idx_users_state', 'idx_profiles_interested_in');
EOF
```
**Ожидается:** 3 строки с названиями индексов
---
## 🔧 Типичные проблемы и решения
### Проблема: "column job does not exist"
**Решение:**
```bash
./bin/apply_all_patches.sh
docker compose restart bot
```
### Проблема: "State column does not exist" (много раз)
**Решение:**
```bash
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
-c "ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(50);"
docker compose restart bot
```
### Проблема: "looking_for violates not-null constraint"
**Решение:**
```bash
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
-f sql/fix_looking_for_column.sql
docker compose restart bot
```
### Проблема: Бот не запускается (exit code 1)
**Диагностика:**
```bash
docker compose logs bot --tail 100
# Ищите строки с ERROR или "does not exist"
```
**Решения:**
1. Проверьте `.env` - все переменные DB_* заданы?
2. Проверьте подключение к БД: `PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name -c "SELECT 1;"`
3. Примените все патчи: `./bin/apply_all_patches.sh`
4. Пересоберите контейнер: `make update`
---
## 📊 Быстрая диагностика одной командой
```bash
# Создайте alias для удобства
alias bot-health='docker compose ps && echo "=== LOGS ===" && docker compose logs bot --tail 20'
# Использование
bot-health
```
---
## 🚀 Команды для разработки
```bash
# Полное обновление (pull + rebuild + migrate + restart)
make update
# Применение миграций
make migrate
# Только перезапуск
docker compose restart bot
# Пересборка с нуля
make clean && make install
# Проверка синтаксиса TypeScript
npm run build
# Запуск в режиме разработки (локально)
npm run dev
```
---
## 📝 Переменные окружения (.env)
Обязательные:
```env
DB_HOST=192.168.0.102
DB_PORT=5432
DB_NAME=telegram_tinder_bot
DB_USERNAME=trevor
DB_PASSWORD=your_secure_password
TELEGRAM_BOT_TOKEN=your_bot_token
JWT_SECRET=your_jwt_secret
APP_SECRET=your_app_secret
NODE_ENV=production
PORT=3000
```
Опциональные:
```env
DEEPSEEK_API_KEY=your_deepseek_key # Для AI фич
LOG_LEVEL=info # debug | info | warn | error
```
---
## 🆘 Экстренное восстановление
Если бот полностью сломан:
```bash
# 1. Остановить всё
docker compose down
# 2. Сделать бэкап БД
./bin/backup_db.sh
# 3. Откатить к последнему коммиту
git reset --hard HEAD
# 4. Применить все патчи заново
./bin/apply_all_patches.sh
# 5. Пересобрать
make install
# 6. Запустить
docker compose up -d
```
---
**Версия:** 1.0
**Дата:** 2025-11-06
**Для:** Telegram Tinder Bot v2.0

View File

@@ -0,0 +1,225 @@
# Чеклист локализации
## Фаза 1: Инфраструктура ✅ ЗАВЕРШЕНО
- [x] Добавить колонку `lang` в таблицу `users`
- [x] Создать миграцию `sql/add_user_language.sql`
- [x] Применить миграцию к БД
- [x] Создать `LanguageHandlers`
- [x] Добавить методы в `ProfileService` (getUserLanguage, updateUserLanguage)
- [x] Интегрировать выбор языка в `/start`
- [x] Добавить обработку callback `set_lang_*`
- [x] Обновить `ru.json` (секция language)
- [x] Обновить `en.json` (секция language)
- [x] Создать документацию
- [x] Создать скрипт поиска хардкода
- [x] Протестировать выбор языка
## Фаза 2: Замена хардкод-текстов ⚠️ В ПРОЦЕССЕ
### Приоритет 1: Обработчики (HIGH)
#### callbackHandlers.ts (90 текстов)
- [ ] Просмотр профилей (showProfile, showNextCandidate) - ~15 текстов
- [ ] Редактирование профиля (edit_name, edit_age, edit_bio) - ~20 текстов
- [ ] Лайки и матчи (handleLike, handleMatch) - ~10 текстов
- [ ] VIP функции (handleVIPSearch, translateProfile) - ~15 текстов
- [ ] Меню и кнопки - ~20 текстов
- [ ] Прочие обработчики - ~10 текстов
**Прогресс:** 0/90 (0%)
#### messageHandlers.ts (21 текст)
- [ ] Создание профиля (handleCreateProfile) - ~8 текстов
- [ ] Ввод данных профиля (name, age, city, bio) - ~8 текстов
- [ ] Валидация ввода - ~5 текстов
**Прогресс:** 0/21 (0%)
#### notificationHandlers.ts (31 текст)
- [ ] Настройки уведомлений - ~15 текстов
- [ ] Меню уведомлений - ~10 текстов
- [ ] Обработка включения/выключения - ~6 текстов
**Прогресс:** 0/31 (0%)
### Приоритет 2: Сервисы (MEDIUM)
#### notificationService.ts (22 текста)
- [ ] Уведомления о лайках - ~8 текстов
- [ ] Уведомления о матчах - ~8 текстов
- [ ] Уведомления о сообщениях - ~6 текстов
**Прогресс:** 0/22 (0%)
### Приоритет 3: Контроллеры (MEDIUM)
#### vipController.ts (21 текст)
- [ ] VIP поиск - ~10 текстов
- [ ] Фильтры - ~8 текстов
- [ ] Перевод анкет - ~3 текста
**Прогресс:** 0/21 (0%)
#### profileEditController.ts (21 текст)
- [ ] Редактирование полей - ~15 текстов
- [ ] Валидация - ~6 текстов
**Прогресс:** 0/21 (0%)
### Приоритет 4: Команды (LOW)
#### commandHandlers.ts (6 текстов)
- [ ] Справка (/help) - ~3 текста
- [ ] Команды - ~3 текста
**Прогресс:** 0/6 (0%)
### Прочие файлы (LOW)
- [ ] enhancedChatHandlers.ts (4 текста)
- [ ] likeBackHandler.ts (2 текста)
**Прогресс:** 0/6 (0%)
**ИТОГО ФАЗА 2:** 0/239 (0%)
## Фаза 3: Переводы ⏳ НЕ НАЧАТО
### Базовые секции (для всех 9 языков)
- [ ] **language.*** - Секция управления языком
- [ ] es (Español)
- [ ] fr (Français)
- [ ] de (Deutsch)
- [ ] it (Italiano)
- [ ] pt (Português)
- [ ] ko (한국어)
- [ ] zh (中文)
- [ ] ja (日本語)
- [ ] kk (Қазақша)
- [ ] uz (O'zbek)
### Полные переводы
После завершения Фазы 2, перевести все новые ключи:
- [ ] **es.json** (Español) - 0% готовности
- [ ] **fr.json** (Français) - 0% готовности
- [ ] **de.json** (Deutsch) - 0% готовности
- [ ] **it.json** (Italiano) - 0% готовности
- [ ] **pt.json** (Português) - 0% готовности
- [ ] **ko.json** (한국어) - 0% готовности
- [ ] **zh.json** (中文) - 0% готовности
- [ ] **ja.json** (日本語) - 0% готовности
- [ ] **kk.json** (Қазақша) - 0% готовности
- [ ] **uz.json** (O'zbek) - 0% готовности
**Примечание:** Нанять native speakers или использовать профессиональные сервисы перевода.
## Фаза 4: Дополнительные функции ⏳ НЕ НАЧАТО
- [ ] Добавить кнопку "🌍 Язык" в настройки
- [ ] Создать команду `/language`
- [ ] Добавить автоопределение языка по `msg.from.language_code` (опционально)
- [ ] Написать тесты для локализации
- [ ] Создать админ-панель для управления переводами (опционально)
## Прогресс по файлам
| Файл | Текстов | Заменено | % | Статус |
|------|---------|----------|---|--------|
| callbackHandlers.ts | 90 | 0 | 0% | ⏳ Не начато |
| notificationHandlers.ts | 31 | 0 | 0% | ⏳ Не начато |
| notificationService.ts | 22 | 0 | 0% | ⏳ Не начато |
| messageHandlers.ts | 21 | 0 | 0% | ⏳ Не начато |
| vipController.ts | 21 | 0 | 0% | ⏳ Не начато |
| profileEditController.ts | 21 | 0 | 0% | ⏳ Не начато |
| commandHandlers.ts | 6 | 0 | 0% | ⏳ Не начато |
| enhancedChatHandlers.ts | 4 | 0 | 0% | ⏳ Не начато |
| likeBackHandler.ts | 2 | 0 | 0% | ⏳ Не начато |
**ОБЩИЙ ПРОГРЕСС:** 0/218 (0%)
*(Исключены скрипты: cleanDb.ts, createTestData.ts, getDatabaseInfo.ts, enhanceNotifications.ts, add-premium-columns.ts)*
## Оценка времени
| Фаза | Задача | Время | Статус |
|------|--------|-------|--------|
| 1 | Инфраструктура | 4-6 ч | ✅ Завершено |
| 2 | Замена хардкода | 15-22 ч | ⏳ 0% |
| 3 | Переводы (9 языков) | 18-27 ч | ⏳ 0% |
| 4 | Доп. функции | 3-5 ч | ⏳ 0% |
**ИТОГО:** 40-60 часов работы
**ВЫПОЛНЕНО:** ~5 часов (инфраструктура)
**ОСТАЛОСЬ:** ~35-55 часов
## Еженедельные цели
### Неделя 1 (текущая)
- [x] Создать инфраструктуру локализации
- [ ] Заменить тексты в callbackHandlers.ts (90 текстов)
- [ ] Заменить тексты в messageHandlers.ts (21 текст)
**Цель:** Завершить 111 замен (46% от общего)
### Неделя 2
- [ ] Заменить тексты в notificationHandlers.ts (31 текст)
- [ ] Заменить тексты в notificationService.ts (22 текста)
- [ ] Заменить тексты в контроллерах (42 текста)
**Цель:** Завершить 95 замен (40% от общего)
### Неделя 3
- [ ] Заменить оставшиеся тексты (12 текстов)
- [ ] Начать переводы базовых секций (language.*)
- [ ] Перевести 3-4 языка
**Цель:** Завершить замены (100%), переводы (40%)
### Неделя 4
- [ ] Завершить переводы всех 9 языков
- [ ] Добавить кнопку смены языка в настройки
- [ ] Финальное тестирование
- [ ] Деплой в production
**Цель:** 100% готовности системы локализации
## Метрики качества
- [ ] Все хардкод-тексты заменены (0/218)
- [ ] Все языковые файлы содержат одинаковые ключи (0/10)
- [ ] Нет TypeScript ошибок
- [ ] Нет runtime ошибок при переключении языков
- [ ] Все кнопки и меню работают на всех языках
- [ ] Документация обновлена
- [ ] Написаны тесты (опционально)
## Как обновлять этот файл
После замены текстов в файле, обновите прогресс:
```markdown
#### callbackHandlers.ts (90 текстов)
- [x] Просмотр профилей (showProfile, showNextCandidate) - ~15 текстов ✅
- [ ] Редактирование профиля (edit_name, edit_age, edit_bio) - ~20 текстов
...
**Прогресс:** 15/90 (17%) ⚠️ В процессе
```
## Примечания
- **⏳** = Не начато
- **⚠️** = В процессе
- **✅** = Завершено
- **❌** = Заблокировано
---
**Последнее обновление:** 06.11.2025
**Статус проекта:** Фаза 1 завершена, Фаза 2 готова к старту
**Общий прогресс:** ~10% (инфраструктура готова)

View File

@@ -0,0 +1,430 @@
# План замены хардкод-текстов на локализационные ключи
## Текущее состояние
**Реализовано:**
- Система локализации с i18next
- Выбор языка при первом запуске
- 10 поддерживаемых языков
- Сохранение языка в БД
⚠️ **Требуется:**
- Извлечение и замена ~500+ хардкод-текстов в коде
- Дополнение языковых файлов
## Стратегия замены
### Фаза 1: Критически важные пользовательские тексты (СНАЧАЛА)
#### Приоритет: HIGH
Файлы с наибольшим количеством пользовательских сообщений:
1. **src/handlers/messageHandlers.ts** (~150 текстов)
- Создание профиля
- Ввод данных (имя, возраст, город, био)
- Валидация ввода
- Сообщения об ошибках
2. **src/handlers/callbackHandlers.ts** (~200 текстов)
- Кнопки меню
- Просмотр профилей
- Лайки/дислайки
- Настройки профиля
- VIP функции
3. **src/handlers/commandHandlers.ts** (~50 текстов)
- Команды бота
- Главное меню
- Справка
### Фаза 2: Второстепенные тексты
#### Приоритет: MEDIUM
4. **src/services/notificationService.ts** (~30 текстов)
- Уведомления о лайках
- Уведомления о матчах
- Уведомления о сообщениях
5. **src/handlers/notificationHandlers.ts** (~20 текстов)
- Настройки уведомлений
### Фаза 3: Служебные тексты
#### Приоритет: LOW
6. **src/services/profileService.ts** (~10 текстов)
- Сообщения об ошибках валидации
7. **src/services/matchingService.ts** (~5 текстов)
- Логирование и отладка
## Процесс замены (пошаговый)
### Шаг 1: Анализ файла
```bash
# Найти все хардкод-тексты
grep -n "'[А-Яа-яЁё]" src/handlers/messageHandlers.ts
grep -n '"[А-Яа-яЁё]' src/handlers/messageHandlers.ts
```
### Шаг 2: Создание ключей локализации
Для каждого найденного текста:
1. **Определить категорию:**
- `profile.*` - профиль
- `buttons.*` - кнопки
- `errors.*` - ошибки
- `messages.*` - сообщения
- `commands.*` - команды
- `search.*` - поиск
- `matches.*` - матчи
- `settings.*` - настройки
- `notifications.*` - уведомления
2. **Создать понятный ключ:**
```
Плохо: "text1", "msg2"
Хорошо: "profile.namePrompt", "errors.invalidAge"
```
3. **Добавить в ru.json:**
```json
{
"profile": {
"namePrompt": "👤 Введите ваше имя:",
"agePrompt": "🎂 Сколько вам лет?",
"cityPrompt": "🌍 В каком городе вы находитесь?"
}
}
```
### Шаг 3: Замена в коде
#### Было:
```typescript
await bot.sendMessage(chatId, '👤 Введите ваше имя:');
```
#### Стало:
```typescript
const userId = msg.from?.id.toString();
const lang = await profileService.getUserLanguage(userId);
localizationService.setLanguage(lang);
await bot.sendMessage(chatId, localizationService.t('profile.namePrompt'));
```
#### Оптимизация (для методов класса):
```typescript
// В начале метода
private async sendLocalizedMessage(
chatId: number,
userId: string,
key: string,
options?: any
): Promise<void> {
const lang = await this.profileService.getUserLanguage(userId);
this.localizationService.setLanguage(lang);
const text = this.localizationService.t(key, options);
await this.bot.sendMessage(chatId, text);
}
// Использование
await this.sendLocalizedMessage(chatId, userId, 'profile.namePrompt');
```
### Шаг 4: Перевод на другие языки
После добавления ключа в `ru.json`, добавить переводы:
**en.json:**
```json
{
"profile": {
"namePrompt": "👤 Enter your name:",
"agePrompt": "🎂 How old are you?",
"cityPrompt": "🌍 What city are you in?"
}
}
```
**ko.json:**
```json
{
"profile": {
"namePrompt": "👤 이름을 입력하세요:",
"agePrompt": "🎂 나이가 어떻게 되세요?",
"cityPrompt": "🌍 어느 도시에 계세요?"
}
}
```
И так для всех 10 языков.
## Примеры типичных замен
### 1. Простое сообщение
**Было:**
```typescript
await bot.sendMessage(chatId, 'Анкеты закончились! Попробуйте позже.');
```
**Стало:**
```typescript
await bot.sendMessage(chatId, localizationService.t('search.noProfiles'));
```
### 2. Сообщение с параметрами
**Было:**
```typescript
await bot.sendMessage(chatId, `Привет, ${name}! С возвращением!`);
```
**Стало:**
```json
// ru.json
{
"welcome": {
"greeting": "Привет, {{name}}! С возвращением!"
}
}
```
```typescript
await bot.sendMessage(
chatId,
localizationService.t('welcome.greeting', { name })
);
```
### 3. Кнопки
**Было:**
```typescript
const keyboard = {
inline_keyboard: [
[{ text: '❤️ Нравится', callback_data: 'like' }],
[{ text: '👎 Не нравится', callback_data: 'dislike' }]
]
};
```
**Стало:**
```json
// ru.json
{
"buttons": {
"like": "❤️ Нравится",
"dislike": "👎 Не нравится"
}
}
```
```typescript
const keyboard = {
inline_keyboard: [
[{
text: localizationService.t('buttons.like'),
callback_data: 'like'
}],
[{
text: localizationService.t('buttons.dislike'),
callback_data: 'dislike'
}]
]
};
```
### 4. Множественное число (плюрализация)
**Было:**
```typescript
const text = count === 1
? `У вас ${count} новый матч`
: `У вас ${count} новых матчей`;
```
**Стало:**
```json
// ru.json
{
"matches": {
"newCount_one": "У вас {{count}} новый матч",
"newCount_few": "У вас {{count}} новых матча",
"newCount_many": "У вас {{count}} новых матчей"
}
}
```
```typescript
await bot.sendMessage(
chatId,
localizationService.t('matches.newCount', { count })
);
```
## Инструменты для автоматизации
### Скрипт поиска хардкод-текстов
```bash
#!/bin/bash
# find_hardcoded_texts.sh
echo "Поиск русских текстов в кавычках..."
grep -rn "'[А-Яа-яЁё]" src/ --include="*.ts" | wc -l
grep -rn '"[А-Яа-яЁё]' src/ --include="*.ts" | wc -l
echo "Топ-10 файлов с наибольшим количеством хардкода:"
grep -rn "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/ --include="*.ts" | \
cut -d: -f1 | \
sort | \
uniq -c | \
sort -rn | \
head -10
```
### Скрипт проверки покрытия переводами
```typescript
// scripts/check-translations.ts
import * as fs from 'fs';
import * as path from 'path';
const localesPath = path.join(__dirname, '..', 'src', 'locales');
const ruFile = JSON.parse(fs.readFileSync(path.join(localesPath, 'ru.json'), 'utf8'));
function getAllKeys(obj: any, prefix = ''): string[] {
let keys: string[] = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
keys = keys.concat(getAllKeys(obj[key], fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
const ruKeys = getAllKeys(ruFile);
const languages = ['en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko'];
languages.forEach(lang => {
const langFile = JSON.parse(fs.readFileSync(path.join(localesPath, `${lang}.json`), 'utf8'));
const langKeys = getAllKeys(langFile);
const missing = ruKeys.filter(key => !langKeys.includes(key));
console.log(`\n${lang}.json:`);
console.log(` Всего ключей: ${langKeys.length}/${ruKeys.length}`);
if (missing.length > 0) {
console.log(` Отсутствуют: ${missing.length}`);
console.log(` Пример: ${missing.slice(0, 5).join(', ')}`);
} else {
console.log(`Все ключи присутствуют`);
}
});
```
## Контрольный список (Checklist)
### Перед началом замены файла:
- [ ] Сделать backup файла или создать новую ветку в Git
- [ ] Прочитать весь файл, понять структуру
- [ ] Составить список всех текстов для замены
### В процессе замены:
- [ ] Заменять по 10-20 текстов за раз
- [ ] Тестировать после каждой замены
- [ ] Проверять TypeScript ошибки: `npm run build`
- [ ] Коммитить изменения: `git commit -m "localize: messageHandlers profile section"`
### После замены файла:
- [ ] Убедиться, что нет TypeScript ошибок
- [ ] Протестировать все функции файла в боте
- [ ] Обновить переводы для всех 10 языков
- [ ] Запустить скрипт проверки покрытия
- [ ] Создать Pull Request для review
## Рекомендации
1. **Начинайте с самого используемого функционала:**
- Регистрация (messageHandlers.ts - createProfile)
- Просмотр анкет (callbackHandlers.ts - showNextCandidate)
- Главное меню (commandHandlers.ts - handleStart)
2. **Группируйте ключи логически:**
```json
{
"profile": {
"prompts": {
"name": "...",
"age": "...",
"city": "..."
},
"validation": {
"nameLength": "...",
"ageRange": "...",
"cityRequired": "..."
}
}
}
```
3. **Используйте консистентную нотацию:**
- Всегда camelCase для ключей
- Всегда точки для разделения уровней
- Prefix для категории (profile, button, error)
4. **Не переводите:**
- Технические логи
- Callback_data значения
- Имена переменных и функций
5. **Делайте переводы качественными:**
- Нанимайте native speakers для перевода
- Используйте контекст культуры (эмодзи, формальность)
- Учитывайте длину текста (для кнопок)
## Оценка объема работ
### Время на замену (приблизительно):
- **messageHandlers.ts**: 4-6 часов
- **callbackHandlers.ts**: 6-8 часов
- **commandHandlers.ts**: 2-3 часа
- **notificationService.ts**: 1-2 часа
- **Прочие файлы**: 2-3 часа
**Итого на замену:** ~15-22 часа
### Время на переводы:
- **1 язык (native speaker)**: 2-3 часа
- **9 языков**: 18-27 часов
**ОБЩИЙ ОБЪЕМ:** ~33-49 часов
## Следующий шаг
Начните с файла **src/handlers/messageHandlers.ts**, секция создания профиля:
```bash
# Создайте ветку для работы
git checkout -b localization-phase1-message-handlers
# Начните замену
code src/handlers/messageHandlers.ts
```
Удачи! 🚀

View File

@@ -0,0 +1,226 @@
# Быстрый старт: Система локализации
## Что уже работает ✅
1. **Выбор языка при первом запуске** - новые пользователи видят меню из 10 языков
2. **Сохранение языка в БД** - колонка `users.lang` хранит выбор пользователя
3. **10 поддерживаемых языков** - ru, en, es, fr, de, it, pt, ko, zh, ja
4. **Инфраструктура i18next** - готова к использованию
## Что нужно сделать ⚠️
### ГЛАВНАЯ ЗАДАЧА: Заменить 255 хардкод-текстов
**Файлы по приоритету:**
1. `src/handlers/callbackHandlers.ts` - 90 текстов (кнопки, меню)
2. `src/handlers/notificationHandlers.ts` - 31 текст (уведомления)
3. `src/services/notificationService.ts` - 22 текста (сервис уведомлений)
4. `src/handlers/messageHandlers.ts` - 21 текст (создание профиля)
5. Остальные файлы - ~91 текст
## Как использовать локализацию в коде
### Вариант 1: Через LocalizationService
```typescript
import LocalizationService from '../services/localizationService';
// В методе класса:
const locService = LocalizationService.getInstance();
const userId = msg.from?.id.toString();
const lang = await this.profileService.getUserLanguage(userId);
locService.setLanguage(lang);
const text = locService.t('profile.namePrompt');
await this.bot.sendMessage(chatId, text);
```
### Вариант 2: Через getUserTranslation (рекомендуется)
```typescript
import { getUserTranslation } from '../services/localizationService';
const userId = msg.from?.id.toString();
const text = await getUserTranslation(userId, 'profile.namePrompt');
await this.bot.sendMessage(chatId, text);
```
### Вариант 3: С параметрами
```typescript
// В ru.json:
{
"welcome": {
"greeting": "Привет, {{name}}! Добро пожаловать!"
}
}
// В коде:
const text = await getUserTranslation(userId, 'welcome.greeting', { name: userName });
```
## Процесс замены текста
### ШАГ 1: Найти хардкод-текст
```bash
# Найти все тексты в файле
grep -n "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/handlers/callbackHandlers.ts
```
### ШАГ 2: Добавить ключ в ru.json
**Было в коде:**
```typescript
await bot.sendMessage(chatId, '👤 Введите ваше имя:');
```
**Добавляем в ru.json:**
```json
{
"profile": {
"prompts": {
"name": "👤 Введите ваше имя:"
}
}
}
```
### ШАГ 3: Заменить в коде
```typescript
const text = await getUserTranslation(userId, 'profile.prompts.name');
await bot.sendMessage(chatId, text);
```
### ШАГ 4: Добавить перевод в en.json
```json
{
"profile": {
"prompts": {
"name": "👤 Enter your name:"
}
}
}
```
### ШАГ 5: Протестировать
```bash
# Пересобрать
docker compose up -d --build bot
# Проверить
docker compose logs bot --tail 20
```
## Полезные команды
```bash
# Найти все хардкод-тексты
./bin/find_hardcoded_texts.sh
# Посмотреть тексты в конкретном файле
grep -n "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/handlers/callbackHandlers.ts
# Собрать и запустить бота
docker compose up -d --build bot
# Проверить логи
docker compose logs bot --tail 50 -f
# Применить миграцию БД (если еще не применена)
PGPASSWORD='Cl0ud_1985!' psql -h 192.168.0.102 -p 5432 -U trevor \
-d telegram_tinder_bot -f sql/add_user_language.sql
```
## Структура ключей (рекомендуется)
```json
{
"language": { ... }, // Управление языком
"welcome": { ... }, // Приветствия
"profile": {
"prompts": { ... }, // Запросы ввода
"validation": { ... }, // Ошибки валидации
"labels": { ... } // Метки полей
},
"buttons": { ... }, // Кнопки
"errors": { ... }, // Общие ошибки
"commands": { ... }, // Команды бота
"search": { ... }, // Поиск анкет
"matches": { ... }, // Матчи
"notifications": { ... }, // Уведомления
"settings": { ... }, // Настройки
"vip": { ... } // VIP функции
}
```
## Пример: Замена кнопок
**Было:**
```typescript
const keyboard = {
inline_keyboard: [
[{ text: '❤️ Нравится', callback_data: 'like' }],
[{ text: '👎 Не нравится', callback_data: 'dislike' }]
]
};
```
**Добавили в ru.json:**
```json
{
"buttons": {
"like": "❤️ Нравится",
"dislike": "👎 Не нравится"
}
}
```
**Стало:**
```typescript
const userId = msg.from?.id.toString();
const lang = await this.profileService.getUserLanguage(userId);
this.localizationService.setLanguage(lang);
const keyboard = {
inline_keyboard: [
[{
text: this.localizationService.t('buttons.like'),
callback_data: 'like'
}],
[{
text: this.localizationService.t('buttons.dislike'),
callback_data: 'dislike'
}]
]
};
```
## Документация
- **docs/LOCALIZATION_SYSTEM.md** - Полное описание системы
- **docs/LOCALIZATION_MIGRATION_PLAN.md** - Детальный план замены текстов
- **docs/LOCALIZATION_REPORT.md** - Отчет о выполненной работе
## Следующий шаг
**Начните с самого крупного файла:**
```bash
# Посмотреть все тексты
grep -n "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/handlers/callbackHandlers.ts
# Открыть файл
code src/handlers/callbackHandlers.ts
```
**Заменяйте по 10-20 текстов за раз, тестируйте после каждой замены!**
---
Удачи! 🚀

377
docs/LOCALIZATION_REPORT.md Normal file
View File

@@ -0,0 +1,377 @@
# Отчет о реализации системы локализации
**Дата:** 06.11.2025
**Ветка:** localization
**Статус:** ✅ Система локализации внедрена и работает
## Выполненные задачи
### 1. ✅ База данных
**Файл:** `sql/add_user_language.sql`
- Добавлена колонка `lang VARCHAR(5) DEFAULT 'ru' NOT NULL` в таблицу `users`
- Создан индекс `idx_users_lang` для оптимизации запросов
- Все существующие пользователи получили язык `ru` по умолчанию
- Миграция успешно применена к production БД
**Результат:**
```sql
SELECT COUNT(*), lang FROM users GROUP BY lang;
-- 2 пользователя с lang='ru'
```
### 2. ✅ Обработчик языков
**Файл:** `src/handlers/languageHandlers.ts` (НОВЫЙ)
Реализован класс `LanguageHandlers` с методами:
- `showLanguageSelection()` - показать меню из 10 языков с флагами
- `handleSetLanguage()` - обработать выбор языка пользователем
- `checkAndShowLanguageSelection()` - автоматически показывать выбор новым пользователям
**Функционал:**
- Интеграция с `ProfileService` для сохранения языка
- Интеграция с `LocalizationService` для смены языка
- Автоматическое удаление меню выбора после установки языка
- Показ приветственного сообщения на выбранном языке
### 3. ✅ Расширение ProfileService
**Файл:** `src/services/profileService.ts` (ОБНОВЛЕН)
Добавлены методы:
```typescript
async ensureUser(telegramId, userData, language = 'ru'): Promise<string>
async updateUserLanguage(telegramId, language): Promise<void>
async getUserLanguage(telegramId): Promise<string>
```
**Изменения:**
- `INSERT INTO users` теперь включает колонку `lang`
- UPSERT сохраняет существующий язык пользователя (не перезаписывает)
### 4. ✅ Интеграция в основной бот
**Файл:** `src/bot.ts` (ОБНОВЛЕН)
- Добавлен import `LanguageHandlers`
- Создан экземпляр `this.languageHandlers = new LanguageHandlers(this.bot)`
- Инициализация происходит при старте бота
**Файл:** `src/handlers/commandHandlers.ts` (ОБНОВЛЕН)
- В метод `handleStart()` добавлена проверка:
```typescript
const languageSelectionShown = await this.languageHandlers.checkAndShowLanguageSelection(userId, chatId);
if (languageSelectionShown) {
return; // Показываем выбор языка и выходим
}
```
- Новым пользователям сначала показывается выбор языка, затем приветствие
**Файл:** `src/handlers/callbackHandlers.ts` (ОБНОВЛЕН)
- Добавлена обработка callback `set_lang_{код}`:
```typescript
if (data.startsWith('set_lang_')) {
const languageHandlers = new LanguageHandlers(this.bot);
await languageHandlers.handleSetLanguage(query);
return;
}
```
### 5. ✅ Локализационные файлы
Обновлены файлы:
- `src/locales/ru.json` - добавлена секция `language`
- `src/locales/en.json` - добавлена секция `language`
**Структура секции:**
```json
{
"language": {
"select": "🌍 Выберите язык интерфейса:...",
"changed": "✅ Язык изменен на Русский",
"ru": "🇷🇺 Русский",
"en": "🇬🇧 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어"
}
}
```
### 6. ✅ Документация
Созданы документы:
1. **docs/LOCALIZATION_SYSTEM.md** - полное описание системы локализации
2. **docs/LOCALIZATION_MIGRATION_PLAN.md** - план миграции хардкод-текстов
3. **bin/find_hardcoded_texts.sh** - скрипт поиска хардкод-текстов
### 7. ✅ Тестирование
- Docker build: успешно ✅
- Запуск бота: успешно ✅
- Логи: `✅ Localization service initialized successfully`
- Бот работает: @seoulmate_officialbot
## Поддерживаемые языки (10)
| Код | Язык | Файл | Статус |
|-----|-----------|-----------|--------|
| ru | Русский | ru.json | ✅ Базовые ключи |
| en | English | en.json | ✅ Базовые ключи |
| es | Español | es.json | ⚠️ Требуется дополнение |
| fr | Français | fr.json | ⚠️ Требуется дополнение |
| de | Deutsch | de.json | ⚠️ Требуется дополнение |
| it | Italiano | it.json | ⚠️ Требуется дополнение |
| pt | Português | pt.json | ⚠️ Требуется дополнение |
| ko | 한국어 | ko.json | ⚠️ Требуется дополнение |
| zh | 中文 | zh.json | ⚠️ Требуется дополнение |
| ja | 日本語 | ja.json | ⚠️ Требуется дополнение |
## Пользовательский опыт (UX Flow)
### Новый пользователь:
```
Пользователь → /start
Бот проверяет: есть ли профиль?
НЕТ
Показывает меню выбора из 10 языков
Пользователь нажимает, например, "🇰🇷 한국어"
Callback: set_lang_ko
UPDATE users SET lang='ko' WHERE telegram_id=...
Localization сервис переключается на корейский
Приветственное сообщение на корейском
Кнопка "Создать профиль" на корейском
```
### Существующий пользователь:
```
Пользователь → /start
Бот загружает язык из БД (например, 'en')
Устанавливает язык в LocalizationService
Показывает главное меню на английском
```
## Статистика хардкод-текстов
**Результат анализа (`./bin/find_hardcoded_texts.sh`):**
```
Тексты в одинарных кавычках: 217
Тексты в двойных кавычках: 38
ВСЕГО: 255
```
**Топ-5 файлов для замены:**
| Файл | Количество текстов |
|------|-------------------|
| callbackHandlers.ts | 90 |
| notificationHandlers.ts | 31 |
| notificationService.ts | 22 |
| messageHandlers.ts | 21 |
| vipController.ts | 21 |
## Следующие шаги
### Фаза 2: Замена хардкод-текстов (ПРИОРИТЕТ)
**Оценка времени:** 15-22 часа
1. **messageHandlers.ts** (21 текст) - 2-3 часа
- Регистрация пользователя
- Создание профиля
- Валидация ввода
2. **callbackHandlers.ts** (90 текстов) - 6-8 часов
- Кнопки меню
- Просмотр профилей
- Лайки/дислайки
- Настройки
3. **notificationHandlers.ts + notificationService.ts** (53 текста) - 3-4 часа
- Уведомления о лайках
- Уведомления о матчах
- Настройки уведомлений
4. **commandHandlers.ts** (6 текстов) - 1 час
- Команды бота
- Справка
5. **Контроллеры** (42 текста) - 3-4 часа
- vipController.ts
- profileEditController.ts
### Фаза 3: Переводы (ПОСЛЕ ЗАМЕНЫ)
**Оценка времени:** 18-27 часов (2-3 часа на язык × 9 языков)
Необходимо перевести все новые ключи на 9 языков:
- es, fr, de, it, pt - Европейские языки
- ko, zh, ja - Азиатские языки
**Рекомендация:** Нанять native speakers или использовать профессиональные переводческие сервисы.
### Фаза 4: Дополнительные функции
1. Добавить кнопку "🌍 Язык / Language" в настройки
2. Добавить команду `/language` для быстрой смены языка
3. Автоопределение языка по `msg.from.language_code` (опционально)
## Технические детали
### Database Schema
```sql
-- Колонка добавлена в таблицу users
lang VARCHAR(5) DEFAULT 'ru' NOT NULL
-- Индекс создан для оптимизации
CREATE INDEX idx_users_lang ON users(lang);
```
### Callback Data Format
Все callback для выбора языка имеют формат:
```
set_lang_{код_ISO_639-1}
```
Примеры:
- `set_lang_ru` → Русский
- `set_lang_en` → English
- `set_lang_ko` → 한국어
### Localization Keys Structure
```json
{
"language.*": "Управление языком",
"welcome.*": "Приветствия",
"profile.*": "Профиль",
"buttons.*": "Кнопки",
"errors.*": "Ошибки",
"commands.*": "Команды",
"search.*": "Поиск",
"matches.*": "Матчи",
"notifications.*": "Уведомления",
"settings.*": "Настройки",
"vip.*": "VIP функции"
}
```
## Проблемы и решения
### Проблема 1: Инициализация LanguageHandlers
**Проблема:** TypeScript ошибка "свойство не имеет инициализатора"
**Решение:** Добавлена инициализация в конструктор `this.languageHandlers = new LanguageHandlers(bot)`
### Проблема 2: Новый пользователь vs существующий
**Проблема:** Как определить, когда показывать выбор языка?
**Решение:** Метод `checkAndShowLanguageSelection()` проверяет наличие профиля
### Проблема 3: Сохранение выбранного языка
**Проблема:** Где хранить язык пользователя?
**Решение:** Колонка `lang` в таблице `users`, методы в `ProfileService`
## Выводы
### Что работает ✅
1. **Автоматический выбор языка для новых пользователей**
- Показывается меню из 10 языков
- Язык сохраняется в БД
- Приветствие показывается на выбранном языке
2. **Сохранение языка пользователя**
- Язык хранится в колонке `users.lang`
- Загружается при каждом запросе
- Используется для всех сообщений
3. **Инфраструктура локализации**
- `LocalizationService` работает с i18next
- 10 языковых файлов готовы
- Методы `t()`, `setLanguage()`, `getCurrentLanguage()` работают
### Что требует доработки ⚠️
1. **Замена 255 хардкод-текстов**
- Основная работа впереди
- Требуется систематическая замена
- Оценка: ~20 часов работы
2. **Переводы для 9 языков**
- Только `ru.json` и `en.json` содержат секцию `language`
- Остальные 8 языков требуют перевода
- Оценка: ~20 часов (с переводчиками)
3. **Кнопка смены языка в настройках**
- Пока можно сменить только через `/start` (для новых)
- Нужна кнопка в меню настроек
- Оценка: 1-2 часа
## Команды для работы
### Применить миграцию БД:
```bash
PGPASSWORD='Cl0ud_1985!' psql -h 192.168.0.102 -p 5432 -U trevor -d telegram_tinder_bot -f sql/add_user_language.sql
```
### Найти хардкод-тексты:
```bash
./bin/find_hardcoded_texts.sh
```
### Посмотреть тексты в файле:
```bash
grep -n "'[А-Яа-яЁё]\|\"[А-Яа-яЁё]" src/handlers/callbackHandlers.ts
```
### Собрать и запустить бота:
```bash
docker compose up -d --build bot
```
### Проверить логи:
```bash
docker compose logs bot --tail 50
```
## Итог
✅ **Система локализации полностью внедрена и работает!**
Бот теперь:
- Спрашивает язык у новых пользователей
- Сохраняет язык в базе данных
- Поддерживает 10 языков
- Готов к замене всех хардкод-текстов
**Следующий шаг:** Начать систематическую замену хардкод-текстов, начиная с `callbackHandlers.ts` (90 текстов).
---
**Разработчик:** GitHub Copilot
**Заказчик:** Trevor
**Дата завершения:** 06.11.2025
**Статус:** ✅ ГОТОВО К ИСПОЛЬЗОВАНИЮ

329
docs/LOCALIZATION_SYSTEM.md Normal file
View File

@@ -0,0 +1,329 @@
# Система локализации Telegram Tinder Bot
## Обзор
Реализована полноценная система мультиязычной поддержки бота с возможностью выбора языка интерфейса.
## Реализованные функции
### 1. База данных
**Миграция:** `sql/add_user_language.sql`
Добавлена колонка `lang` в таблицу `users`:
- Тип: `VARCHAR(5)`
- Значение по умолчанию: `'ru'` (Русский)
- NOT NULL constraint
- Индекс для быстрого поиска: `idx_users_lang`
```sql
ALTER TABLE users
ADD COLUMN IF NOT EXISTS lang VARCHAR(5) DEFAULT 'ru' NOT NULL;
CREATE INDEX IF NOT EXISTS idx_users_lang ON users(lang);
```
### 2. Поддерживаемые языки
Бот поддерживает **10 языков**:
| Код | Язык | Флаг | Файл локализации |
|------|-----------|------|------------------|
| `ru` | Русский | 🇷🇺 | `ru.json` |
| `en` | English | 🇬🇧 | `en.json` |
| `es` | Español | 🇪🇸 | `es.json` |
| `fr` | Français | 🇫🇷 | `fr.json` |
| `de` | Deutsch | 🇩🇪 | `de.json` |
| `it` | Italiano | 🇮🇹 | `it.json` |
| `pt` | Português | 🇵🇹 | `pt.json` |
| `ko` | 한국어 | 🇰🇷 | `ko.json` |
| `zh` | 中文 | 🇨🇳 | `zh.json` |
| `ja` | 日本語 | 🇯🇵 | `ja.json` |
### 3. Архитектура
#### LocalizationService (`src/services/localizationService.ts`)
Сервис на базе `i18next`:
- Singleton pattern
- Автоматическая загрузка всех языковых файлов при инициализации
- Методы:
- `initialize()` - инициализация сервиса
- `t(key, options)` - получение перевода по ключу
- `setLanguage(lang)` - смена языка
- `getCurrentLanguage()` - получение текущего языка
- `getSupportedLanguages()` - список поддерживаемых языков
- `getTranslation(key, lang, options)` - получить перевод для конкретного языка без смены текущего
#### LanguageHandlers (`src/handlers/languageHandlers.ts`)
Обработчик выбора и управления языком:
- `showLanguageSelection(chatId, messageId?)` - показать меню выбора языка
- `handleSetLanguage(query)` - обработать установку языка
- `checkAndShowLanguageSelection(userId, chatId)` - проверить, нужно ли показывать выбор языка
#### ProfileService - Расширение (`src/services/profileService.ts`)
Добавлены методы для работы с языком пользователя:
- `ensureUser(telegramId, userData, language)` - создание/обновление пользователя с сохранением языка
- `updateUserLanguage(telegramId, language)` - обновление языка пользователя
- `getUserLanguage(telegramId)` - получение языка пользователя
### 4. Пользовательский опыт (UX)
#### Новый пользователь
1. Пользователь отправляет `/start`
2. **Автоматически показывается меню выбора языка** (10 кнопок с флагами)
3. После выбора языка:
- Язык сохраняется в БД
- Показывается приветственное сообщение на выбранном языке
- Предлагается создать профиль
#### Существующий пользователь
1. Пользователь отправляет `/start`
2. Бот использует сохраненный язык из БД
3. Показывается главное меню на выбранном языке
#### Изменение языка в настройках
Запланировано: добавить кнопку "🌍 Язык / Language" в раздел "⚙️ Настройки"
### 5. Структура локализационных файлов
Каждый файл `src/locales/{lang}.json` содержит:
```json
{
"language": {
"select": "🌍 Выберите язык...",
"changed": "✅ Язык изменен на...",
"ru": "🇷🇺 Русский",
"en": "🇬🇧 English",
...
},
"welcome": {
"greeting": "Добро пожаловать...",
"description": "...",
"getStarted": "..."
},
"profile": { ... },
"search": { ... },
"buttons": { ... },
"errors": { ... },
...
}
```
### 6. Интеграция в код
#### Импорт функции перевода
```typescript
import { getUserTranslation } from '../services/localizationService';
```
#### Использование в коде
```typescript
// Асинхронный вызов
const text = await getUserTranslation(userId, 'welcome.greeting');
// Или через сервис
const locService = LocalizationService.getInstance();
const text = locService.t('welcome.greeting');
```
#### Callback для выбора языка
Все callback_data для выбора языка имеют формат:
```
set_lang_{код_языка}
```
Например:
- `set_lang_ru` - установить русский
- `set_lang_en` - установить английский
- `set_lang_ko` - установить корейский
### 7. Запуск миграции
```bash
# Применить миграцию добавления колонки lang
PGPASSWORD='Cl0ud_1985!' psql -h 192.168.0.102 -p 5432 -U trevor -d telegram_tinder_bot -f sql/add_user_language.sql
```
### 8. Тестирование
#### Проверка выбора языка для нового пользователя:
1. Удалите свой профиль из БД:
```sql
DELETE FROM profiles WHERE user_id IN (
SELECT id FROM users WHERE telegram_id = YOUR_TELEGRAM_ID
);
DELETE FROM users WHERE telegram_id = YOUR_TELEGRAM_ID;
```
2. Отправьте `/start` боту
3. Должно появиться меню выбора из 10 языков
4. Выберите любой язык
5. Проверьте, что приветствие отображается на выбранном языке
#### Проверка сохранения языка:
```sql
SELECT telegram_id, username, lang FROM users;
```
## Следующие шаги
### Приоритет 1: Замена всех хардкод-текстов
Необходимо заменить все хардкод-тексты в следующих файлах:
1. **`src/handlers/messageHandlers.ts`** (профиль, регистрация)
2. **`src/handlers/callbackHandlers.ts`** (кнопки, меню)
3. **`src/handlers/commandHandlers.ts`** (команды)
4. **`src/services/notificationService.ts`** (уведомления)
### Приоритет 2: Дополнение языковых файлов
Текущие файлы содержат только базовые ключи. Нужно:
1. Извлечь все существующие тексты из кода
2. Добавить ключи в `ru.json` (эталонный файл)
3. Перевести ключи для остальных 9 языков
### Приоритет 3: Кнопка смены языка в настройках
Добавить в меню "⚙️ Настройки" кнопку:
```typescript
{ text: '🌍 Язык / Language', callback_data: 'change_language' }
```
## Состояние реализации
**Выполнено:**
- Добавлена колонка `lang` в таблицу `users`
- Создан `LanguageHandlers` для управления языком
- Интегрирован выбор языка в `/start` для новых пользователей
- Обновлен `LocalizationService`
- Добавлены секции `language` в `ru.json` и `en.json`
- Методы работы с языком в `ProfileService`
⚠️ **В процессе:**
- Замена хардкод-текстов на локализационные ключи
- Дополнение всех языковых файлов
📋 **Планируется:**
- Кнопка смены языка в настройках
- Полный перевод всех 10 языков
- Тесты локализации
## Технические детали
### Callback Query Flow
```
Пользователь нажимает кнопку "🇷🇺 Русский"
callback_data: 'set_lang_ru'
callbackHandlers.handleCallback() перехватывает
if (data.startsWith('set_lang_'))
languageHandlers.handleSetLanguage(query)
profileService.updateUserLanguage(userId, 'ru')
localizationService.setLanguage('ru')
Показ приветственного сообщения на русском
```
### Database Schema
```sql
CREATE TABLE 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),
lang VARCHAR(5) DEFAULT 'ru' NOT NULL, -- ← НОВАЯ КОЛОНКА
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
last_active_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_lang ON users(lang);
```
## Примеры использования
### Пример 1: Приветственное сообщение
```typescript
const lang = await profileService.getUserLanguage(userId);
localizationService.setLanguage(lang);
const greeting = localizationService.t('welcome.greeting');
const description = localizationService.t('welcome.description');
await bot.sendMessage(chatId, `${greeting}\n\n${description}`);
```
### Пример 2: Кнопки с переводом
```typescript
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{
text: localizationService.t('buttons.save'),
callback_data: 'save_profile'
}],
[{
text: localizationService.t('buttons.cancel'),
callback_data: 'cancel'
}]
]
};
```
### Пример 3: Ошибки
```typescript
try {
// ... код
} catch (error) {
const errorMsg = localizationService.t('errors.serverError');
await bot.sendMessage(chatId, errorMsg);
}
```
## Поддержка
Если нужно добавить новый язык:
1. Создайте файл `src/locales/{код}.json`
2. Скопируйте структуру из `ru.json`
3. Переведите все ключи
4. Добавьте язык в `LocalizationService.initialize()`:
```typescript
const newLangTranslations = JSON.parse(
fs.readFileSync(path.join(localesPath, овый_код.json'), 'utf8')
);
```
5. Добавьте в `resources` объект
6. Добавьте в `getSupportedLanguages()`
7. Добавьте кнопку в `LanguageHandlers.showLanguageSelection()`
---
**Статус:** ✅ Система локализации активна и работает
**Версия:** 1.0.0
**Дата:** 06.11.2025

80
docs/docker_fix.md Normal file
View File

@@ -0,0 +1,80 @@
# Решение проблемы с Docker-контейнерами
## Проблема
При запуске контейнеров через Docker Compose возникает ошибка `KeyError: 'ContainerConfig'`. Эта ошибка появляется из-за несовместимости между версиями Docker, Docker Compose и структурой docker-compose.yml.
## Решение
### 1. Очистка окружения Docker
На сервере выполните следующие команды, чтобы полностью очистить окружение Docker:
```bash
# Остановка и удаление контейнеров
docker-compose down -v
# Принудительное удаление контейнеров по имени
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot
# Очистка неиспользуемых томов и сетей
docker system prune -f --volumes
# Очистка кеша Docker
docker builder prune -f
```
### 2. Исправление проблем с переносами строк
Файлы, созданные в Windows и перенесенные в Linux, могут содержать неправильные символы переноса строки.
```bash
# Исправление переносов строк в shell-скриптах
find . -name "*.sh" -type f -exec sh -c 'tr -d "\r" < "$1" > "$1.fixed" && mv "$1.fixed" "$1" && chmod +x "$1"' -- {} \;
```
### 3. Обновление docker-compose.yml
Создайте новый docker-compose.yml с исправленной структурой:
```bash
# Запустите скрипт для исправления проблем с Docker
./bin/fix_docker.sh
```
### 4. Запуск с полностью чистым окружением
После выполнения всех исправлений запустите контейнеры заново:
```bash
docker-compose up -d
```
## Альтернативное решение
Если проблема сохраняется, можно попробовать запустить контейнеры по отдельности:
```bash
# Сначала запустить базу данных (если она нужна)
docker-compose up -d db
# Дождаться запуска базы данных
sleep 10
# Запустить бота
docker-compose up -d bot
# Запустить adminer
docker-compose up -d adminer
```
## Проверка работы миграций
После запуска контейнеров проверьте, что миграции базы данных применяются правильно:
```bash
# Просмотр логов контейнера бота
docker logs telegram-tinder-bot
# Если миграции не применяются, можно запустить их вручную внутри контейнера
docker exec -it telegram-tinder-bot sh -c "DATABASE_URL=postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME npx node-pg-migrate up"
```

88
docs/migrations_fix.md Normal file
View File

@@ -0,0 +1,88 @@
# Решение проблемы с миграциями базы данных
## Проблемы
При попытке применить миграции были обнаружены следующие проблемы:
1. **Ошибка с TypeScript файлами**: Node.js не может напрямую выполнять файлы `.ts` без компиляции их в JavaScript.
2. **Предупреждения о ES модулях**: Файлы используют синтаксис ES модулей, но не имеют расширения `.mjs` или настроек в package.json.
3. **Неверный порядок миграций**: Миграции могут выполняться в неправильном порядке.
## Решения
### Для быстрого применения миграций
Используйте один из следующих сценариев:
```bash
# Полный процесс миграции с компиляцией TypeScript
./bin/run_full_migration.sh
# Только SQL-миграции (минуя node-pg-migrate)
./bin/run_sql_migrations.sh
```
### Пошаговое решение
1. **Компиляция TypeScript миграций в JavaScript**:
```bash
./bin/compile_ts_migrations.sh
```
2. **Применение JS-миграций**:
```bash
./bin/apply_migrations.sh
```
3. **Ручное применение SQL-миграций**:
```bash
./bin/run_sql_migrations.sh
```
## Описание скриптов
- **run_full_migration.sh**: Полный процесс миграции, включающий компиляцию TypeScript и применение всех миграций.
- **compile_ts_migrations.sh**: Только компиляция TypeScript миграций в JavaScript.
- **apply_migrations.sh**: Применение JS-миграций через node-pg-migrate.
- **run_sql_migrations.sh**: Прямое применение SQL-миграций через psql.
## Проверка результатов
После выполнения миграций проверьте состояние базы данных:
```bash
# Подключение к базе данных
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME
# Проверка таблиц
\dt
# Проверка примененных миграций
SELECT * FROM migrations ORDER BY executed_at;
```
## Если проблемы сохраняются
1. **Очистить директорию миграций**:
```bash
# Создание резервной копии
mkdir -p backup_migrations
cp -r migrations/* backup_migrations/
# Оставить только JS-миграции
rm -f migrations/*.ts
```
2. **Инициализировать миграции заново**:
```bash
npx node-pg-migrate init
```
3. **Применить специальную консолидированную миграцию**:
```bash
# Создание консолидированной миграции
cat src/database/migrations/*.sql > consolidated.sql
# Применение консолидированной миграции
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f consolidated.sql
```

BIN
new_docker-keyring.gpg Normal file

Binary file not shown.

View File

@@ -1,15 +1,16 @@
// add-hobbies-column.js
// Скрипт для добавления колонки hobbies в таблицу profiles
require('dotenv').config();
const { Pool } = require('pg');
// Настройки подключения к базе данных
// Настройки подключения к базе данных из переменных окружения
const pool = new Pool({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!'
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
});
async function addHobbiesColumn() {

View File

@@ -1,4 +1,5 @@
// Исправленный код для создания профиля
require('dotenv').config();
const { Client } = require('pg');
const { v4: uuidv4 } = require('uuid');
@@ -18,13 +19,13 @@ if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) {
process.exit(1);
}
// Устанавливаем соединение с базой данных
// Устанавливаем соединение с базой данных из переменных окружения
const client = new Client({
host: '192.168.0.102',
port: 5432,
user: 'trevor',
password: 'Cl0ud_1985!',
database: 'telegram_tinder_bot'
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
user: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'telegram_tinder_bot'
});
async function createProfile() {

View File

@@ -2,17 +2,18 @@
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
// Используется для синхронизации существующей базы с миграциями
require('dotenv').config();
const { Client } = require('pg');
const fs = require('fs');
const path = require('path');
// Подключение к базе данных
// Подключение к базе данных из переменных окружения
const client = new Client({
host: '192.168.0.102',
port: 5432,
database: 'telegram_tinder_bot',
user: 'trevor',
password: 'Cl0ud_1985!'
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
});
async function syncMigrations() {

View File

@@ -27,7 +27,56 @@ sleep 5
# Run database migrations
echo "🔄 Running database migrations..."
node dist/database/migrateOnStartup.js
# Create migrations directory structure
mkdir -p dist/database/migrations
# Copy any available migrations
if [ -d "src/database/migrations" ]; then
echo "<22> Found SQL migrations. Copying..."
cp -R src/database/migrations/* dist/database/migrations/ 2>/dev/null || echo "No SQL migrations to copy"
fi
# Copy JS migrations if available
if [ -d "migrations" ]; then
echo "📂 Found JS migrations. Copying..."
mkdir -p migrations-temp
cp migrations/*.js migrations-temp/ 2>/dev/null || echo "No JS migrations to copy"
# Move JS migrations to dist/database/migrations
cp migrations-temp/*.js dist/database/migrations/ 2>/dev/null || echo "No JS migrations to copy to dist"
fi
# Display environment variables for debugging (without passwords)
echo "🔍 Environment variables for database connection:"
echo "DB_HOST: $DB_HOST"
echo "DB_PORT: $DB_PORT"
echo "DB_NAME: $DB_NAME"
echo "DB_USERNAME: $DB_USERNAME"
# Run migrations using node-pg-migrate
echo "🔄 Running migrations with node-pg-migrate..."
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up
# Verify connection to database
echo "🔍 Verifying database connection..."
node -e "
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
});
pool.query('SELECT NOW()', (err, res) => {
if (err) {
console.error('❌ Database connection failed:', err.message);
process.exit(1);
} else {
console.log('✅ Database connection successful:', res.rows[0].now);
pool.end();
}
});" || echo "❌ Failed to verify database connection"
# Start the bot
echo "✅ Starting the bot..."

View File

@@ -0,0 +1,36 @@
-- Добавление колонок job и state
-- Дата: 2025-11-06
-- Исправляет ошибки: "column job does not exist" и "State column does not exist"
-- 1. Добавляем колонку job в таблицу profiles (синоним для occupation)
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS job VARCHAR(255);
-- 2. Копируем существующие данные из occupation в job
UPDATE profiles SET job = occupation WHERE occupation IS NOT NULL AND job IS NULL;
-- 3. Добавляем колонку state в таблицу users для отслеживания состояния диалога
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(50);
-- 4. Создаём индексы для производительности
CREATE INDEX IF NOT EXISTS idx_profiles_job ON profiles(job);
CREATE INDEX IF NOT EXISTS idx_users_state ON users(state);
-- 5. Добавляем комментарии для документации
COMMENT ON COLUMN profiles.job IS 'Профессия/работа пользователя (синоним для occupation)';
COMMENT ON COLUMN profiles.occupation IS 'Профессия/работа пользователя (устаревшее, используйте job)';
COMMENT ON COLUMN users.state IS 'Текущее состояние пользователя в диалоге с ботом';
-- Проверка результата
SELECT
'profiles.job' as column_name,
CASE WHEN EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'profiles' AND column_name = 'job'
) THEN '✅ Существует' ELSE 'Не найдена' END as status
UNION ALL
SELECT
'users.state' as column_name,
CASE WHEN EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'state'
) THEN '✅ Существует' ELSE 'Не найдена' END as status;

View File

@@ -0,0 +1,17 @@
-- Миграция: Добавление колонок для хранения координат местоположения
-- Дата: 2025-01-20
-- Описание: Добавляет location_lat и location_lon для хранения GPS-координат,
-- полученных через Kakao Maps API, для расчета расстояния между пользователями
-- Добавляем колонки для широты и долготы
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location_lat DECIMAL(10, 8);
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location_lon DECIMAL(11, 8);
-- Создаем индекс для быстрого поиска по координатам
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;
-- Комментарии для документации
COMMENT ON COLUMN profiles.location_lat IS 'Широта местоположения пользователя (из Kakao Maps)';
COMMENT ON COLUMN profiles.location_lon IS 'Долгота местоположения пользователя (из Kakao Maps)';

24
sql/add_user_language.sql Normal file
View File

@@ -0,0 +1,24 @@
-- Добавление колонки lang в таблицу users
-- Эта миграция добавляет поддержку мультиязычности
-- Добавляем колонку lang с дефолтным значением 'ru'
ALTER TABLE users
ADD COLUMN IF NOT EXISTS lang VARCHAR(5) DEFAULT 'ru' NOT NULL;
-- Создаем индекс для быстрого поиска по языку
CREATE INDEX IF NOT EXISTS idx_users_lang ON users(lang);
-- Обновляем всех существующих пользователей, устанавливая русский язык
UPDATE users SET lang = 'ru' WHERE lang IS NULL OR lang = '';
-- Добавляем комментарий к колонке
COMMENT ON COLUMN users.lang IS 'User interface language (ISO 639-1 code)';
-- Проверка результата
SELECT
COUNT(*) as total_users,
lang,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() as percentage
FROM users
GROUP BY lang
ORDER BY total_users DESC;

View File

@@ -0,0 +1,42 @@
-- Скрипт для очистки всех взаимодействий между пользователями
-- Удаляет матчи, сообщения, свайпы и показы анкет
-- Оставляет только пользователей и их профили
-- Начало транзакции
BEGIN;
-- Удаление всех сообщений в чатах
DELETE FROM messages;
-- Удаление всех матчей
DELETE FROM matches;
-- Удаление всех просмотров профилей
DELETE FROM profile_views;
-- Удаление всех свайпов (лайки, дизлайки, суперлайки)
DELETE FROM swipes;
-- Удаление всех уведомлений
DELETE FROM notifications;
-- Фиксация транзакции
COMMIT;
-- Вывод статистики после очистки
SELECT
'messages' as table_name,
COUNT(*) as remaining_records
FROM messages
UNION ALL
SELECT 'matches', COUNT(*) FROM matches
UNION ALL
SELECT 'profile_views', COUNT(*) FROM profile_views
UNION ALL
SELECT 'swipes', COUNT(*) FROM swipes
UNION ALL
SELECT 'notifications', COUNT(*) FROM notifications
UNION ALL
SELECT 'users', COUNT(*) FROM users
UNION ALL
SELECT 'profiles', COUNT(*) FROM profiles;

View File

@@ -0,0 +1,43 @@
-- Исправление триггера create_initial_profile и колонки looking_for
-- Дата: 2025-11-06
-- 1. Удаляем старую функцию триггера
DROP FUNCTION IF EXISTS create_initial_profile() CASCADE;
-- 2. Создаём исправленную функцию с полем looking_for
CREATE OR REPLACE FUNCTION create_initial_profile()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO profiles (user_id, name, age, gender, looking_for, interested_in)
VALUES (NEW.id, COALESCE(NEW.first_name, 'User'), 18, 'other', 'both', 'both');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 3. Пересоздаём триггер
DROP TRIGGER IF EXISTS create_profile_on_user_insert ON users;
DROP TRIGGER IF EXISTS create_profile_trigger ON users;
CREATE TRIGGER create_profile_on_user_insert
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION create_initial_profile();
-- 4. Делаем looking_for необязательным с дефолтным значением
ALTER TABLE profiles ALTER COLUMN looking_for DROP NOT NULL;
ALTER TABLE profiles ALTER COLUMN looking_for SET DEFAULT 'both';
-- 5. Добавляем interested_in как синоним для looking_for
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS interested_in VARCHAR(20) DEFAULT 'both'
CHECK (interested_in IN ('male', 'female', 'both'));
-- 6. Обновляем существующие записи
UPDATE profiles SET looking_for = 'both' WHERE looking_for IS NULL;
UPDATE profiles SET interested_in = COALESCE(looking_for, 'both') WHERE interested_in IS NULL;
-- 7. Создаём индекс для поиска по interested_in
CREATE INDEX IF NOT EXISTS idx_profiles_interested_in ON profiles(interested_in);
COMMENT ON COLUMN profiles.looking_for IS 'Предпочитаемый пол для знакомства (устаревшее, используйте interested_in)';
COMMENT ON COLUMN profiles.interested_in IS 'Предпочитаемый пол для знакомства: male, female, both';

82
sql/fix_match_trigger.sql Normal file
View File

@@ -0,0 +1,82 @@
-- Исправление триггера create_match_on_mutual_like
-- Дата: 2025-11-06
-- Проблемы:
-- 1. Использовались неправильные имена полей: target_id вместо target_user_id, action вместо type
-- 2. Использовались несуществующие колонки в notifications: content и reference_id вместо data
-- Удаляем старую функцию
DROP FUNCTION IF EXISTS create_match_on_mutual_like() CASCADE;
-- Создаем исправленную функцию
CREATE OR REPLACE FUNCTION create_match_on_mutual_like()
RETURNS TRIGGER AS $$
DECLARE
reverse_like_exists BOOLEAN;
match_id_var UUID;
BEGIN
-- Проверяем только лайки и суперлайки (игнорируем pass)
IF NEW.type != 'like' AND NEW.type != 'superlike' THEN
RETURN NEW;
END IF;
-- Проверяем есть ли обратный лайк (правильные имена полей: target_user_id, type)
SELECT EXISTS (
SELECT 1
FROM swipes
WHERE user_id = NEW.target_user_id
AND target_user_id = NEW.user_id
AND type IN ('like', 'superlike')
) INTO reverse_like_exists;
-- Если есть взаимный лайк, создаем матч
IF reverse_like_exists THEN
-- Создаем матч и получаем его ID
INSERT INTO matches (user_id_1, user_id_2, created_at, is_active)
VALUES (
LEAST(NEW.user_id, NEW.target_user_id),
GREATEST(NEW.user_id, NEW.target_user_id),
NOW(),
true
)
ON CONFLICT (user_id_1, user_id_2) DO UPDATE SET is_active = true
RETURNING id INTO match_id_var;
-- Создаем уведомления для обоих пользователей
-- Используем data (jsonb) вместо content и reference_id
INSERT INTO notifications (user_id, type, data, is_read, created_at)
VALUES (
NEW.user_id,
'new_match',
jsonb_build_object('message', 'У вас новый матч!', 'match_id', match_id_var),
false,
NOW()
);
INSERT INTO notifications (user_id, type, data, is_read, created_at)
VALUES (
NEW.target_user_id,
'new_match',
jsonb_build_object('message', 'У вас новый матч!', 'match_id', match_id_var),
false,
NOW()
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Пересоздаем триггер
DROP TRIGGER IF EXISTS create_match_trigger ON swipes;
CREATE TRIGGER create_match_trigger
AFTER INSERT ON swipes
FOR EACH ROW
EXECUTE FUNCTION create_match_on_mutual_like();
-- Проверка
SELECT 'Триггер create_match_on_mutual_like успешно исправлен!' as status;
COMMENT ON FUNCTION create_match_on_mutual_like() IS
'Триггер автоматически создает матч при взаимном лайке.
Исправлено: использование правильных имен полей (target_user_id, type)
и правильной структуры notifications (data jsonb).';

View File

@@ -0,0 +1,49 @@
-- Исправление триггера notify_new_message для использования правильной схемы notifications
-- Проблема: триггер использует content и reference_id вместо data (jsonb)
-- Удаляем старый триггер и функцию
DROP TRIGGER IF EXISTS notify_new_message_trigger ON messages;
DROP FUNCTION IF EXISTS notify_new_message();
-- Создаём новую функцию с правильной схемой
CREATE OR REPLACE FUNCTION notify_new_message()
RETURNS TRIGGER AS $$
DECLARE
recipient_id UUID;
BEGIN
-- Определяем получателя сообщения (второго участника матча)
SELECT CASE
WHEN m.user_id_1 = NEW.sender_id THEN m.user_id_2
ELSE m.user_id_1
END INTO recipient_id
FROM matches m
WHERE m.id = NEW.match_id;
-- Создаём уведомление с правильной структурой (data jsonb)
IF recipient_id IS NOT NULL THEN
INSERT INTO notifications (user_id, type, data, created_at)
VALUES (
recipient_id,
'new_message',
jsonb_build_object(
'message_id', NEW.id,
'match_id', NEW.match_id,
'sender_id', NEW.sender_id,
'content_preview', LEFT(NEW.content, 50)
),
NOW()
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Создаём триггер заново
CREATE TRIGGER notify_new_message_trigger
AFTER INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION notify_new_message();
-- Проверка
SELECT 'Trigger notify_new_message fixed successfully' AS status;

View File

@@ -0,0 +1,46 @@
-- Исправление триггера update_last_active для работы с messages и swipes
-- Проблема: в messages есть sender_id, а в swipes есть user_id
-- Удаляем старые триггеры
DROP TRIGGER IF EXISTS update_last_active_on_message ON messages;
DROP TRIGGER IF EXISTS update_last_active_on_swipe ON swipes;
DROP FUNCTION IF EXISTS update_last_active();
-- Создаём функцию для обновления last_active для отправителя сообщения
CREATE OR REPLACE FUNCTION update_last_active_on_message()
RETURNS TRIGGER AS $$
BEGIN
UPDATE profiles
SET last_active = CURRENT_TIMESTAMP
WHERE user_id = NEW.sender_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Создаём функцию для обновления last_active при свайпе
CREATE OR REPLACE FUNCTION update_last_active_on_swipe()
RETURNS TRIGGER AS $$
BEGIN
UPDATE profiles
SET last_active = CURRENT_TIMESTAMP
WHERE user_id = NEW.user_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Создаём триггер для messages
CREATE TRIGGER update_last_active_on_message
AFTER INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION update_last_active_on_message();
-- Создаём триггер для swipes
CREATE TRIGGER update_last_active_on_swipe
AFTER INSERT ON swipes
FOR EACH ROW
EXECUTE FUNCTION update_last_active_on_swipe();
-- Проверка
SELECT 'Triggers update_last_active fixed successfully' AS status;

View File

@@ -9,6 +9,7 @@ import { CommandHandlers } from './handlers/commandHandlers';
import { CallbackHandlers } from './handlers/callbackHandlers';
import { MessageHandlers } from './handlers/messageHandlers';
import { NotificationHandlers } from './handlers/notificationHandlers';
import { LanguageHandlers } from './handlers/languageHandlers';
class TelegramTinderBot {
@@ -21,6 +22,7 @@ class TelegramTinderBot {
private callbackHandlers: CallbackHandlers;
private messageHandlers: MessageHandlers;
private notificationHandlers: NotificationHandlers;
private languageHandlers: LanguageHandlers;
constructor() {
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
@@ -34,9 +36,10 @@ class TelegramTinderBot {
this.localizationService = LocalizationService.getInstance();
this.commandHandlers = new CommandHandlers(this.bot);
this.messageHandlers = new MessageHandlers(this.bot);
this.messageHandlers = new MessageHandlers(this.bot, this.notificationService);
this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers);
this.notificationHandlers = new NotificationHandlers(this.bot);
this.languageHandlers = new LanguageHandlers(this.bot);
this.setupErrorHandling();
this.setupPeriodicTasks();

View File

@@ -31,7 +31,9 @@ export class CallbackHandlers {
this.bot = bot;
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.chatService = new ChatService();
// Получаем notificationService из messageHandlers (если есть)
const notificationService = (messageHandlers as any).notificationService;
this.chatService = new ChatService(notificationService);
this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
@@ -47,6 +49,21 @@ export class CallbackHandlers {
this.likeBackHandler = new LikeBackHandler(bot);
}
// Вспомогательный метод для получения перевода с учетом языка пользователя
private async getTranslation(userId: string, key: string, options?: any): Promise<string> {
try {
const lang = await this.profileService.getUserLanguage(userId);
const LocalizationService = require('../services/localizationService').default;
const locService = LocalizationService.getInstance();
locService.setLanguage(lang);
return locService.t(key, options);
} catch (error) {
console.error('Translation error:', error);
// Возвращаем ключ как fallback
return key;
}
}
register(): void {
this.bot.on('callback_query', (query) => this.handleCallback(query));
}
@@ -59,6 +76,14 @@ export class CallbackHandlers {
const data = query.data;
try {
// Обработка выбора языка
if (data.startsWith('set_lang_')) {
const LanguageHandlers = require('./languageHandlers').LanguageHandlers;
const languageHandlers = new LanguageHandlers(this.bot);
await languageHandlers.handleSetLanguage(query);
return;
}
// Основные действия профиля
if (data === 'create_profile') {
await this.handleCreateProfile(chatId, telegramId);
@@ -86,6 +111,119 @@ export class CallbackHandlers {
await this.handleEditHobbies(chatId, telegramId);
} else if (data === 'edit_city') {
await this.handleEditCity(chatId, telegramId);
} else if (data === 'confirm_city') {
try {
const states = (this.messageHandlers as any).userStates as Map<string, any>;
const userState = states ? states.get(telegramId) : null;
if (userState && userState.step === 'confirm_city') {
// Подтверждаем город и переводим пользователя к вводу био
userState.step = 'waiting_bio';
console.log(`User ${telegramId} confirmed city: ${userState.data.city}`);
// Убираем inline-кнопки из сообщения с подтверждением
try {
// clear inline keyboard
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
} catch (e) {
// ignore
}
await this.bot.sendMessage(chatId, `✅ Город подтверждён: *${userState.data.city}*\n\n📝 Теперь расскажите немного о себе (био):`, { parse_mode: 'Markdown' });
} else {
const errorText = await this.getTranslation(telegramId, 'errors.contextNotFound');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} catch (error) {
console.error('Error confirming city via callback:', error);
const errorText = await this.getTranslation(telegramId, 'errors.cityConfirmError');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} else if (data === 'edit_city_manual') {
try {
const states = (this.messageHandlers as any).userStates as Map<string, any>;
const userState = states ? states.get(telegramId) : null;
if (userState) {
userState.step = 'waiting_city';
console.log(`User ${telegramId} chose to enter city manually`);
try {
// clear inline keyboard
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
} catch (e) { }
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную:', { reply_markup: { remove_keyboard: true } });
} else {
const errorText = await this.getTranslation(telegramId, 'errors.contextNotFound');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} catch (error) {
console.error('Error switching to manual city input via callback:', error);
const errorText = await this.getTranslation(telegramId, 'errors.generalError');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} else if (data === 'confirm_city_edit') {
try {
const editState = this.messageHandlers.profileEditStates.get(telegramId);
if (editState && editState.field === 'city' && editState.tempCity) {
console.log(`User ${telegramId} confirmed city edit: ${editState.tempCity}`);
// Обновляем город в профиле
await this.messageHandlers.updateProfileField(telegramId, 'city', editState.tempCity);
// Обновляем координаты, если они есть
if (editState.tempLocation) {
console.log(`User ${telegramId} updating location: lat=${editState.tempLocation.latitude}, lon=${editState.tempLocation.longitude}`);
await this.messageHandlers.updateProfileField(telegramId, 'location', editState.tempLocation);
}
// Очищаем состояние
this.messageHandlers.clearProfileEditState(telegramId);
// Убираем inline-кнопки
try {
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
} catch (e) { }
await this.bot.sendMessage(chatId, '✅ Город успешно обновлён!');
setTimeout(async () => {
const keyboard = {
inline_keyboard: [
[
{ text: '✏️ Продолжить редактирование', callback_data: 'edit_profile' },
{ text: '👀 Предпросмотр', callback_data: 'preview_profile' }
],
[{ text: '🏠 Главное меню', callback_data: 'main_menu' }]
]
};
const selectActionText = await this.getTranslation(telegramId, 'buttons.selectAction');
await this.bot.sendMessage(chatId, selectActionText, { reply_markup: keyboard });
}, 500);
} else {
const errorText = await this.getTranslation(telegramId, 'errors.contextNotFound');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} catch (error) {
console.error('Error confirming city edit via callback:', error);
const errorText = await this.getTranslation(telegramId, 'errors.cityConfirmError');
await this.bot.answerCallbackQuery(query.id, { text: errorText });
}
} else if (data === 'edit_city_manual_edit') {
try {
const editState = this.messageHandlers.profileEditStates.get(telegramId);
if (editState && editState.field === 'city') {
console.log(`User ${telegramId} chose to re-enter city during edit`);
// Очищаем временный город, но оставляем состояние редактирования
delete editState.tempCity;
try {
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
} catch (e) { }
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную или отправьте геолокацию:', {
reply_markup: {
keyboard: [
[{ text: '📍 Отправить геолокацию', request_location: true }]
],
resize_keyboard: true,
one_time_keyboard: true
}
});
} else {
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
}
} catch (error) {
console.error('Error switching to re-enter city during edit via callback:', error);
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка' });
}
} else if (data === 'edit_job') {
await this.handleEditJob(chatId, telegramId);
} else if (data === 'edit_education') {
@@ -184,6 +322,12 @@ export class CallbackHandlers {
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
}
// Быстрый переход в чат из уведомлений
else if (data.startsWith('open_chat:')) {
const matchId = data.replace('open_chat:', '');
await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId);
}
// Матчи и чаты
else if (data === 'view_matches') {
await this.handleViewMatches(chatId, telegramId);
@@ -309,15 +453,17 @@ export class CallbackHandlers {
// Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем
// NotificationHandlers уже зарегистрировал свои обработчики в register()
} else {
const errorText = await this.getTranslation(telegramId, 'notifications.unavailable');
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция настройки уведомлений недоступна.',
text: errorText,
show_alert: true
});
}
}
else {
const devText = await this.getTranslation(telegramId, 'notifications.inDevelopment');
await this.bot.answerCallbackQuery(query.id, {
text: 'Функция в разработке!',
text: devText,
show_alert: false
});
return;
@@ -327,8 +473,9 @@ export class CallbackHandlers {
} catch (error) {
console.error('Callback handler error:', error);
const errorText = await this.getTranslation(telegramId, 'errors.tryAgain');
await this.bot.answerCallbackQuery(query.id, {
text: 'Произошла ошибка. Попробуйте еще раз.',
text: errorText,
show_alert: true
});
}
@@ -426,11 +573,12 @@ export class CallbackHandlers {
]
};
const matchText = await this.getTranslation(telegramId, 'matches.mutualLike', {
name: targetProfile?.name || await this.getTranslation(telegramId, 'common.thisUser')
});
await this.bot.sendMessage(
chatId,
'🎉 ЭТО МАТЧ! 💕\n\n' +
'Вы понравились друг другу с ' + (targetProfile?.name || 'этим пользователем') + '!\n\n' +
'Теперь вы можете начать общение!',
'🎉 ЭТО МАТЧ! 💕\n\n' + matchText,
{ reply_markup: keyboard }
);
} else {
@@ -496,11 +644,12 @@ export class CallbackHandlers {
]
};
const superMatchText = await this.getTranslation(telegramId, 'matches.superLikeMatch', {
name: targetProfile?.name || await this.getTranslation(telegramId, 'common.thisUser')
});
await this.bot.sendMessage(
chatId,
'💖 СУПЕР МАТЧ! ⭐\n\n' +
'Ваш супер лайк произвел впечатление на ' + (targetProfile?.name || 'этого пользователя') + '!\n\n' +
'Начните общение первыми!',
'💖 СУПЕР МАТЧ! ⭐\n\n' + superMatchText,
{ reply_markup: keyboard }
);
} else {
@@ -952,7 +1101,20 @@ export class CallbackHandlers {
const mainPhotoFileId = profile.photos[0]; // Первое фото - главное
let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n';
profileText += '📍 ' + (profile.city || 'Не указан') + '\n';
profileText += '📍 ' + (profile.city || 'Не указан');
// Добавляем расстояние, если это не владелец профиля и есть viewerId
if (!isOwner && viewerId) {
const viewerProfile = await this.profileService.getProfileByTelegramId(viewerId);
if (viewerProfile && viewerProfile.location && profile.location) {
const distance = viewerProfile.getDistanceTo(profile);
if (distance !== null) {
profileText += ` (${Math.round(distance)} км)`;
}
}
}
profileText += '\n';
if (profile.job) profileText += '💼 ' + profile.job + '\n';
if (profile.education) profileText += '🎓 ' + profile.education + '\n';
if (profile.height) profileText += '📏 ' + profile.height + ' см\n';
@@ -975,14 +1137,22 @@ export class CallbackHandlers {
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
// Хобби с хэштегами
if (profile.hobbies && profile.hobbies.trim()) {
const hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
profileText += '\n🎯 ' + formattedHobbies + '\n';
if (profile.hobbies) {
let hobbiesArray: string[] = [];
if (typeof profile.hobbies === 'string') {
hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
} else if (Array.isArray(profile.hobbies)) {
hobbiesArray = (profile.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
}
if (hobbiesArray.length > 0) {
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
profileText += '\n🎯 ' + formattedHobbies + '\n';
}
}
if (profile.interests.length > 0) {
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
profileText += '\n💡 Интересы: ' + profile.interests.join(', ');
}
let keyboard: InlineKeyboardMarkup;
@@ -1087,8 +1257,21 @@ export class CallbackHandlers {
const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное
// Получаем профиль текущего пользователя для вычисления расстояния
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
let candidateText = candidate.name + ', ' + candidate.age + '\n';
candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n';
candidateText += '📍 ' + (candidate.city || 'Не указан');
// Добавляем расстояние, если есть координаты у обоих пользователей
if (userProfile && userProfile.location && candidate.location) {
const distance = userProfile.getDistanceTo(candidate);
if (distance !== null) {
candidateText += ` (${Math.round(distance)} км)`;
}
}
candidateText += '\n';
if (candidate.job) candidateText += '💼 ' + candidate.job + '\n';
if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n';
if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n';
@@ -1192,8 +1375,15 @@ export class CallbackHandlers {
// Редактирование города
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
parse_mode: 'Markdown'
await this.bot.sendMessage(chatId, '🏙️ *Укажите ваш город:*\n\nВыберите один из вариантов:', {
parse_mode: 'Markdown',
reply_markup: {
keyboard: [
[{ text: '📍 Отправить геолокацию', request_location: true }]
],
resize_keyboard: true,
one_time_keyboard: true
}
});
}
@@ -1762,10 +1952,18 @@ export class CallbackHandlers {
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
// Хобби с хэштегами
if (candidate.hobbies && candidate.hobbies.trim()) {
const hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
candidateText += '\n🎯 ' + formattedHobbies + '\n';
if (candidate.hobbies) {
let hobbiesArray: string[] = [];
if (typeof candidate.hobbies === 'string') {
hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
} else if (Array.isArray(candidate.hobbies)) {
hobbiesArray = (candidate.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
}
if (hobbiesArray.length > 0) {
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
candidateText += '\n🎯 ' + formattedHobbies + '\n';
}
}
if (candidate.interests.length > 0) {

View File

@@ -4,10 +4,12 @@ import { MatchingService } from '../services/matchingService';
import { Profile } from '../models/Profile';
import { getUserTranslation } from '../services/localizationService';
import { NotificationHandlers } from './notificationHandlers';
import { LanguageHandlers } from './languageHandlers';
export class CommandHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private languageHandlers: LanguageHandlers;
private matchingService: MatchingService;
private notificationHandlers: NotificationHandlers;
@@ -16,6 +18,7 @@ export class CommandHandlers {
this.profileService = new ProfileService();
this.matchingService = new MatchingService();
this.notificationHandlers = new NotificationHandlers(bot);
this.languageHandlers = new LanguageHandlers(bot);
}
register(): void {
@@ -38,6 +41,12 @@ export class CommandHandlers {
const userId = msg.from?.id.toString();
if (!userId) return;
// Проверяем, нужно ли показать выбор языка новому пользователю
const languageSelectionShown = await this.languageHandlers.checkAndShowLanguageSelection(userId, msg.chat.id);
if (languageSelectionShown) {
return; // Показали выбор языка, ждем ответа пользователя
}
// Проверяем есть ли у пользователя профиль
const existingProfile = await this.profileService.getProfileByTelegramId(userId);

View File

@@ -11,9 +11,9 @@ export class EnhancedChatHandlers {
constructor(bot: TelegramBot) {
this.bot = bot;
this.chatService = new ChatService();
this.profileService = new ProfileService();
this.notificationService = new NotificationService(bot);
this.chatService = new ChatService(this.notificationService);
}
// ===== НАТИВНЫЙ ИНТЕРФЕЙС ЧАТОВ =====

View File

@@ -0,0 +1,167 @@
import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api';
import { ProfileService } from '../services/profileService';
import LocalizationService from '../services/localizationService';
export class LanguageHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private localizationService: LocalizationService;
constructor(bot: TelegramBot) {
this.bot = bot;
this.profileService = new ProfileService();
this.localizationService = LocalizationService.getInstance();
}
/**
* Показать меню выбора языка
*/
async showLanguageSelection(chatId: number, messageId?: number): Promise<void> {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '🇷🇺 Русский', callback_data: 'set_lang_ru' },
{ text: '🇬🇧 English', callback_data: 'set_lang_en' }
],
[
{ text: '🇪🇸 Español', callback_data: 'set_lang_es' },
{ text: '🇫🇷 Français', callback_data: 'set_lang_fr' }
],
[
{ text: '🇩🇪 Deutsch', callback_data: 'set_lang_de' },
{ text: '🇮🇹 Italiano', callback_data: 'set_lang_it' }
],
[
{ text: '🇵🇹 Português', callback_data: 'set_lang_pt' },
{ text: '🇰🇷 한국어', callback_data: 'set_lang_ko' }
],
[
{ text: '🇨🇳 中文', callback_data: 'set_lang_zh' },
{ text: '🇯🇵 日本語', callback_data: 'set_lang_ja' }
]
]
};
const text =
'🌍 Choose your language / Выберите язык:\n\n' +
'🇷🇺 Русский\n' +
'🇬🇧 English\n' +
'🇪🇸 Español\n' +
'🇫🇷 Français\n' +
'🇩🇪 Deutsch\n' +
'🇮🇹 Italiano\n' +
'🇵🇹 Português\n' +
'🇰🇷 한국어\n' +
'🇨🇳 中文\n' +
'🇯🇵 日本語';
if (messageId) {
// Обновляем существующее сообщение
await this.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
} else {
// Отправляем новое сообщение
await this.bot.sendMessage(chatId, text, { reply_markup: keyboard });
}
}
/**
* Обработать установку языка
*/
async handleSetLanguage(query: CallbackQuery): Promise<void> {
const chatId = query.message?.chat.id;
const userId = query.from.id.toString();
const messageId = query.message?.message_id;
if (!chatId || !userId) return;
// Извлекаем код языка из callback_data (например, 'set_lang_ru' -> 'ru')
const langCode = query.data?.replace('set_lang_', '');
if (!langCode) return;
try {
// Проверяем, поддерживается ли язык
const supportedLanguages = this.localizationService.getSupportedLanguages();
if (!supportedLanguages.includes(langCode)) {
await this.bot.answerCallbackQuery(query.id, {
text: '❌ Unsupported language / Язык не поддерживается'
});
return;
}
// Обновляем язык пользователя в базе данных
await this.profileService.updateUserLanguage(userId, langCode);
// Устанавливаем язык в сервисе локализации
this.localizationService.setLanguage(langCode);
// Получаем переведенное сообщение об успехе
const successMessage = this.localizationService.t('language.changed');
// Показываем уведомление об успехе
await this.bot.answerCallbackQuery(query.id, {
text: successMessage,
show_alert: false
});
// Удаляем сообщение с выбором языка
if (messageId) {
await this.bot.deleteMessage(chatId, messageId);
}
// Показываем приветственное сообщение на выбранном языке
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[{ text: this.localizationService.t('start.createProfile'), callback_data: 'create_profile' }],
[{ text: this.localizationService.t('start.howItWorks'), callback_data: 'how_it_works' }]
]
};
await this.bot.sendMessage(
chatId,
this.localizationService.t('start.welcomeNew'),
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Error setting language:', error);
await this.bot.answerCallbackQuery(query.id, {
text: '❌ Error / Ошибка'
});
}
}
/**
* Проверить, нужно ли показать выбор языка новому пользователю
*/
async checkAndShowLanguageSelection(userId: string, chatId: number): Promise<boolean> {
try {
// Получаем текущий язык пользователя
const currentLang = await this.profileService.getUserLanguage(userId);
// Если язык уже установлен, не показываем выбор
if (currentLang && currentLang !== 'ru') {
return false;
}
// Проверяем, есть ли у пользователя профиль
const profile = await this.profileService.getProfileByTelegramId(userId);
// Показываем выбор языка только новым пользователям без профиля
if (!profile) {
await this.showLanguageSelection(chatId);
return true;
}
return false;
} catch (error) {
console.error('Error checking language selection:', error);
return false;
}
}
}
export default LanguageHandlers;

View File

@@ -2,6 +2,7 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-ap
import { ProfileService } from '../services/profileService';
import { ChatService } from '../services/chatService';
import { EnhancedChatHandlers } from './enhancedChatHandlers';
import { KakaoMapService } from '../services/kakaoMapService';
// Состояния пользователей для создания профилей
interface UserState {
@@ -19,6 +20,8 @@ interface ChatState {
interface ProfileEditState {
waitingForInput: boolean;
field: string;
tempCity?: string; // Временное хранение города для подтверждения
tempLocation?: { latitude: number; longitude: number }; // Временное хранение координат
}
export class MessageHandlers {
@@ -26,15 +29,27 @@ export class MessageHandlers {
private profileService: ProfileService;
private chatService: ChatService;
private enhancedChatHandlers: EnhancedChatHandlers;
private notificationService: any;
private kakaoMapService: KakaoMapService | null = null;
private userStates: Map<string, UserState> = new Map();
private chatStates: Map<string, ChatState> = new Map();
private profileEditStates: Map<string, ProfileEditState> = new Map();
public profileEditStates: Map<string, ProfileEditState> = new Map();
constructor(bot: TelegramBot) {
constructor(bot: TelegramBot, notificationService?: any) {
this.bot = bot;
this.profileService = new ProfileService();
this.chatService = new ChatService();
this.notificationService = notificationService;
this.chatService = new ChatService(notificationService);
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
// Инициализируем Kakao Maps, если есть API ключ
const kakaoApiKey = process.env.KAKAO_REST_API_KEY || process.env.KAKAO_MAP_REST_KEY;
if (kakaoApiKey) {
this.kakaoMapService = new KakaoMapService(kakaoApiKey);
console.log('✅ Kakao Maps service initialized');
} else {
console.warn('⚠️ KAKAO_REST_API_KEY or KAKAO_MAP_REST_KEY not found, location features will be limited');
}
}
register(): void {
@@ -55,7 +70,7 @@ export class MessageHandlers {
const profileEditState = this.profileEditStates.get(userId);
// Проверяем на нативные чаты (прямые сообщения в контексте чата)
if (msg.text && await this.enhancedChatHandlers.handleIncomingChatMessage(msg.chat.id, msg.text)) {
if (await this.enhancedChatHandlers.handleIncomingChatMessage(msg, userId)) {
return; // Сообщение обработано как сообщение в чате
}
@@ -127,22 +142,68 @@ export class MessageHandlers {
userState.data.age = age;
userState.step = 'waiting_city';
await this.bot.sendMessage(chatId, '📍 Прекрасно! В каком городе вы живете?');
// Запрашиваем геолокацию или текст
await this.bot.sendMessage(
chatId,
'📍 Прекрасно! В каком городе вы живете?\n\n' +
'💡 Вы можете:\n' +
'• Отправить геолокацию 📍 (кнопка ниже)\n' +
'• Написать название города вручную',
{
reply_markup: {
keyboard: [
[{ text: '📍 Отправить геолокацию', request_location: true }]
],
one_time_keyboard: true,
resize_keyboard: true
}
}
);
break;
case 'waiting_city':
if (!msg.text) {
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города');
// Обработка геолокации
if (msg.location) {
await this.handleLocationForCity(msg, userId, userState);
return;
}
userState.data.city = msg.text.trim();
userState.step = 'waiting_bio';
// Обработка текста
if (!msg.text) {
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города или геолокацию');
return;
}
const cityInput = msg.text.trim();
console.log(`User ${userId} entered city manually: ${cityInput}`);
// Временно сохраняем город и запрашиваем подтверждение
userState.data.city = cityInput;
userState.step = 'confirm_city';
await this.bot.sendMessage(
chatId,
'📝 Теперь расскажите немного о себе (био):\n\n' +
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
chatId,
`<EFBFBD> Вы указали город: *${cityInput}*\n\n` +
'Подтвердите или введите заново.',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual' }
]
]
}
}
);
break;
case 'confirm_city':
// Этот случай обрабатывается через callback_data, но на случай если пользователь напишет текст
await this.bot.sendMessage(
chatId,
'⚠️ Пожалуйста, используйте кнопки для подтверждения города.'
);
break;
@@ -208,6 +269,7 @@ export class MessageHandlers {
interestedIn: interestedIn,
bio: profileData.bio,
city: profileData.city,
location: profileData.location, // Добавляем координаты
photos: profileData.photos,
interests: [],
searchPreferences: {
@@ -428,6 +490,47 @@ export class MessageHandlers {
}
value = distance;
break;
case 'hobbies':
// Разбиваем строку с запятыми на массив
if (typeof value === 'string') {
value = value.split(',').map(hobby => hobby.trim()).filter(hobby => hobby.length > 0);
}
break;
case 'city':
// Обработка города: поддержка геолокации и текстового ввода
if (msg.location) {
// Обработка геолокации
await this.handleLocationForCityEdit(msg, userId);
return; // Выходим из функции, так как требуется подтверждение
} else if (msg.text) {
// Обработка текстового ввода города
const cityInput = msg.text.trim();
console.log(`User ${userId} entered city manually during edit: ${cityInput}`);
// Сохраняем временно в состояние редактирования
const editState = this.profileEditStates.get(userId);
if (editState) {
editState.tempCity = cityInput;
}
// Требуем подтверждения
await this.bot.sendMessage(chatId, `📍 Вы указали город: *${cityInput}*\n\одтвердите или введите заново.`, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual_edit' }
]
]
}
});
return; // Выходим, ждем подтверждения
} else {
isValid = false;
errorMessage = '❌ Пожалуйста, отправьте название города или геолокацию';
break;
}
}
if (!isValid) {
@@ -497,6 +600,10 @@ export class MessageHandlers {
// В БД поле называется 'city' (не 'location')
updates.city = value;
break;
case 'location':
// Обновляем координаты
updates.location = value;
break;
case 'job':
// В БД поле называется 'occupation', но мы используем job в модели
updates.job = value;
@@ -537,4 +644,202 @@ export class MessageHandlers {
await this.profileService.updateProfile(profile.userId, updates);
}
}
// Обработка геолокации для определения города
private async handleLocationForCity(msg: Message, userId: string, userState: UserState): Promise<void> {
const chatId = msg.chat.id;
if (!msg.location) {
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
return;
}
try {
// Показываем индикатор загрузки
await this.bot.sendChatAction(chatId, 'typing');
if (!this.kakaoMapService) {
// Если Kakao Maps не настроен, используем координаты как есть
await this.bot.sendMessage(
chatId,
'⚠️ Автоматическое определение города недоступно.\n' +
'Пожалуйста, введите название вашего города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
return;
}
// Показываем индикатор загрузки
await this.bot.sendChatAction(chatId, 'typing');
if (!this.kakaoMapService) {
// Если Kakao Maps не настроен, используем координаты как есть
console.warn(`KakaoMaps not configured - user ${userId} sent coords: ${msg.location.latitude},${msg.location.longitude}`);
await this.bot.sendMessage(
chatId,
'⚠️ Автоматическое определение города недоступно.\n' +
'Пожалуйста, введите название вашего города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
return;
}
// Логируем входные координаты
console.log(`Processing coordinates for user ${userId}: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
// Получаем адрес через Kakao Maps
const address = await this.kakaoMapService.getAddressFromCoordinates(
msg.location.latitude,
msg.location.longitude
);
if (!address) {
console.warn(`KakaoMaps returned no address for user ${userId} coords: ${msg.location.latitude},${msg.location.longitude}`);
await this.bot.sendMessage(
chatId,
'❌ Не удалось определить город по вашей геолокации.\n' +
'Пожалуйста, введите название города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
return;
}
// Получаем название города для сохранения
const cityName = this.kakaoMapService.getCityNameForProfile(address);
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
// Логируем результат
console.log(`KakaoMaps resolved for user ${userId}: city=${cityName}, address=${displayAddress}`);
// Временно сохраняем город И координаты (пока не подтверждены пользователем)
userState.data.city = cityName;
userState.data.location = {
latitude: msg.location.latitude,
longitude: msg.location.longitude
};
userState.step = 'confirm_city';
// Отправляем пользователю информацию с кнопками подтверждения
await this.bot.sendMessage(
chatId,
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
'Пожалуйста, подтвердите город проживания или введите название вручную.',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual' }
]
]
}
}
);
await this.bot.sendMessage(
chatId,
'📝 Теперь расскажите немного о себе (био):\n\n' +
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
);
} catch (error) {
console.error('Error handling location for city:', error);
await this.bot.sendMessage(
chatId,
'❌ Произошла ошибка при определении города.\n' +
'Пожалуйста, введите название города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
}
}
// Обработка геолокации для определения города при редактировании профиля
private async handleLocationForCityEdit(msg: Message, userId: string): Promise<void> {
const chatId = msg.chat.id;
if (!msg.location) {
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
return;
}
try {
// Показываем индикатор загрузки
await this.bot.sendChatAction(chatId, 'typing');
if (!this.kakaoMapService) {
console.warn(`KakaoMaps not configured - user ${userId} sent coords during edit: ${msg.location.latitude},${msg.location.longitude}`);
await this.bot.sendMessage(
chatId,
'⚠️ Автоматическое определение города недоступно.\n' +
'Пожалуйста, введите название вашего города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
return;
}
// Логируем входные координаты
console.log(`Processing coordinates for user ${userId} during edit: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
// Получаем адрес через Kakao Maps
const address = await this.kakaoMapService.getAddressFromCoordinates(
msg.location.latitude,
msg.location.longitude
);
if (!address) {
console.warn(`KakaoMaps returned no address for user ${userId} during edit coords: ${msg.location.latitude},${msg.location.longitude}`);
await this.bot.sendMessage(
chatId,
'❌ Не удалось определить город по вашей геолокации.\n' +
'Пожалуйста, введите название города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
return;
}
// Получаем название города для сохранения
const cityName = this.kakaoMapService.getCityNameForProfile(address);
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
// Логируем результат
console.log(`KakaoMaps resolved for user ${userId} during edit: city=${cityName}, address=${displayAddress}`);
// Временно сохраняем город И координаты в состояние редактирования
const editState = this.profileEditStates.get(userId);
if (editState) {
editState.tempCity = cityName;
editState.tempLocation = {
latitude: msg.location.latitude,
longitude: msg.location.longitude
};
}
// Отправляем пользователю информацию с кнопками подтверждения
await this.bot.sendMessage(
chatId,
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
'Пожалуйста, подтвердите город проживания или введите название вручную.',
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual_edit' }
]
]
}
}
);
} catch (error) {
console.error('Error handling location for city during edit:', error);
await this.bot.sendMessage(
chatId,
'❌ Произошла ошибка при определении города.\n' +
'Пожалуйста, введите название города вручную.',
{ reply_markup: { remove_keyboard: true } }
);
}
}
}

View File

@@ -1,4 +1,18 @@
{
"language": {
"select": "🌍 Select interface language:\n\nYou can change the language later in settings.",
"changed": "✅ Language changed to English",
"ru": "🇷🇺 Русский",
"en": "🇬🇧 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어"
},
"welcome": {
"greeting": "Welcome to Telegram Tinder Bot! 💕",
"description": "Find your soulmate right here!",

View File

@@ -1,4 +1,18 @@
{
"language": {
"select": "🌍 Выберите язык интерфейса:\n\nВы сможете изменить язык позже в настройках.",
"changed": "✅ Язык изменен на Русский",
"ru": "🇷🇺 Русский",
"en": "🇬🇧 English",
"es": "🇪🇸 Español",
"fr": "🇫🇷 Français",
"de": "🇩🇪 Deutsch",
"it": "🇮🇹 Italiano",
"pt": "🇵🇹 Português",
"zh": "🇨🇳 中文",
"ja": "🇯🇵 日本語",
"ko": "🇰🇷 한국어"
},
"welcome": {
"greeting": "Добро пожаловать в Telegram Tinder Bot! 💕",
"description": "Найди свою вторую половинку прямо здесь!",
@@ -83,7 +97,12 @@
"matches": "Взаимности",
"premium": "Премиум",
"settings": "Настройки",
"help": "Помощь"
"help": "Помощь",
"notifications": "Уведомления"
},
"notifications": {
"unavailable": "Функция настройки уведомлений недоступна.",
"inDevelopment": "Функция в разработке!"
},
"buttons": {
"back": "« Назад",
@@ -94,7 +113,8 @@
"edit": "Редактировать",
"delete": "Удалить",
"yes": "Да",
"no": "Нет"
"no": "Нет",
"selectAction": "Выберите действие:"
},
"errors": {
"profileNotFound": "Анкета не найдена",
@@ -102,13 +122,29 @@
"ageInvalid": "Введите корректный возраст (18-100)",
"photoRequired": "Добавьте хотя бы одну фотографию",
"networkError": "Ошибка сети. Попробуйте позже.",
"serverError": "Ошибка сервера. Попробуйте позже."
"serverError": "Ошибка сервера. Попробуйте позже.",
"contextNotFound": "Контекст не найден. Повторите, пожалуйста.",
"cityConfirmError": "Ошибка при подтверждении города",
"generalError": "Ошибка",
"tryAgain": "Произошла ошибка. Попробуйте еще раз."
},
"common": {
"back": "👈 Назад"
"back": "👈 Назад",
"thisUser": "этим пользователем"
},
"matches": {
"noMatches": "✨ У вас пока нет матчей.\n\n🔍 Попробуйте просмотреть больше анкет!\nИспользуйте /browse для поиска."
"noMatches": "✨ У вас пока нет матчей.\n\n🔍 Попробуйте просмотреть больше анкет!\nИспользуйте /browse для поиска.",
"title": "Ваши матчи ({count})",
"mutualLike": "Вы понравились друг другу с {name}!\n\nТеперь вы можете начать общение!",
"superLikeMatch": "Ваш супер лайк произвел впечатление на {name}!\n\nНачните общение первыми!",
"likeBackMatch": "Теперь вы можете начать общение.",
"likeNotification": "Если вы также понравитесь этому пользователю, будет создан матч.",
"tryMoreProfiles": "Попробуйте просмотреть больше анкет!",
"startBrowsing": "Начните просматривать анкеты и получите первые матчи!",
"newMatch": "Новый матч",
"youSaid": "Вы",
"unmatchConfirm": "Вы больше не увидите этого пользователя в своих матчах.",
"bioMissing": "Описание отсутствует"
},
"start": {
"welcomeBack": "🎉 С возвращением, {name}!\n\n💖 Telegram Tinder Bot готов к работе!\n\nЧто хотите сделать?",

View File

@@ -6,9 +6,11 @@ import { v4 as uuidv4 } from 'uuid';
export class ChatService {
private profileService: ProfileService;
private notificationService: any; // Добавим позже правильный тип
constructor() {
constructor(notificationService?: any) {
this.profileService = new ProfileService();
this.notificationService = notificationService;
}
// Получить все чаты (матчи) пользователя
@@ -149,7 +151,7 @@ export class ChatService {
}
const row = messageResult.rows[0];
return new Message({
const message = new Message({
id: row.id,
matchId: row.match_id,
senderId: row.sender_id,
@@ -158,6 +160,26 @@ export class ChatService {
isRead: row.is_read,
createdAt: new Date(row.created_at)
});
// Определяем получателя (второго участника матча)
const match = matchResult.rows[0];
const receiverId = match.user_id_1 === senderId ? match.user_id_2 : match.user_id_1;
// Отправляем уведомление получателю о новом сообщении
if (this.notificationService) {
try {
await this.notificationService.sendMessageNotification(
receiverId,
senderId,
content,
matchId
);
} catch (notifError) {
console.error('Error sending message notification:', notifError);
}
}
return message;
} catch (error) {
console.error('Error sending message:', error);
return null;

View File

@@ -0,0 +1,162 @@
import axios from 'axios';
export interface KakaoCoordinates {
latitude: number;
longitude: number;
}
export interface KakaoAddress {
addressName: string; // Полный адрес
region1: string; // Область (예: 서울특별시)
region2: string; // Район (예: 강남구)
region3: string; // Подрайон (예: 역삼동)
city?: string; // Город (обработанный)
}
export class KakaoMapService {
private readonly apiKey: string;
private readonly baseUrl = 'https://dapi.kakao.com/v2/local';
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('Kakao Maps API key is required');
}
this.apiKey = apiKey;
}
/**
* Получить адрес по координатам (Reverse Geocoding)
*/
async getAddressFromCoordinates(latitude: number, longitude: number): Promise<KakaoAddress | null> {
try {
const response = await axios.get(`${this.baseUrl}/geo/coord2address.json`, {
headers: {
'Authorization': `KakaoAK ${this.apiKey}`
},
params: {
x: longitude,
y: latitude,
input_coord: 'WGS84'
}
});
if (response.data && response.data.documents && response.data.documents.length > 0) {
const doc = response.data.documents[0];
const address = doc.address || doc.road_address;
if (!address) {
return null;
}
// Извлекаем город из региона
const city = this.extractCity(address.region_1depth_name, address.region_2depth_name);
return {
addressName: address.address_name || '',
region1: address.region_1depth_name || '',
region2: address.region_2depth_name || '',
region3: address.region_3depth_name || '',
city
};
}
return null;
} catch (error) {
console.error('Error getting address from Kakao Maps:', error);
if (axios.isAxiosError(error)) {
console.error('Response:', error.response?.data);
}
return null;
}
}
/**
* Извлечь название города из регионов
* Примеры:
* - 서울특별시 → Seoul
* - 경기도 수원시 → Suwon
* - 부산광역시 → Busan
*/
private extractCity(region1: string, region2: string): string {
// Убираем суффиксы типов административных единиц
const cleanRegion1 = region1
.replace('특별시', '') // Особый город (Сеул)
.replace('광역시', '') // Метрополитен
.replace('특별자치시', '') // Особый автономный город
.replace('도', '') // Провинция
.trim();
const cleanRegion2 = region2
.replace('시', '') // Город
.replace('군', '') // Округ
.trim();
// Если region1 - это город (Сеул, Пусан, и т.д.), возвращаем его
const specialCities = ['서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종'];
if (specialCities.some(city => region1.includes(city))) {
return this.translateCityName(cleanRegion1);
}
// Иначе возвращаем region2 (город в провинции)
if (cleanRegion2) {
return this.translateCityName(cleanRegion2);
}
// Если не удалось определить, возвращаем очищенный region1
return this.translateCityName(cleanRegion1);
}
/**
* Перевод названий городов на английский/латиницу
*/
private translateCityName(koreanName: string): string {
const cityMap: { [key: string]: string } = {
'서울': 'Seoul',
'부산': 'Busan',
'대구': 'Daegu',
'인천': 'Incheon',
'광주': 'Gwangju',
'대전': 'Daejeon',
'울산': 'Ulsan',
'세종': 'Sejong',
'수원': 'Suwon',
'성남': 'Seongnam',
'고양': 'Goyang',
'용인': 'Yongin',
'창원': 'Changwon',
'청주': 'Cheongju',
'전주': 'Jeonju',
'천안': 'Cheonan',
'안산': 'Ansan',
'안양': 'Anyang',
'제주': 'Jeju',
'평택': 'Pyeongtaek',
'화성': 'Hwaseong',
'포항': 'Pohang',
'진주': 'Jinju',
'강릉': 'Gangneung',
'김해': 'Gimhae',
'춘천': 'Chuncheon',
'원주': 'Wonju'
};
return cityMap[koreanName] || koreanName;
}
/**
* Форматировать адрес для отображения пользователю
*/
formatAddressForDisplay(address: KakaoAddress): string {
if (address.city) {
return `${address.city}, ${address.region1}`;
}
return `${address.region2}, ${address.region1}`;
}
/**
* Получить краткое название города для сохранения в профиле
*/
getCityNameForProfile(address: KakaoAddress): string {
return address.city || address.region2;
}
}

View File

@@ -427,15 +427,20 @@ export class MatchingService {
const userId = userProfile.userId;
// Определяем, каким должен быть пол показываемых профилей
let targetGender: string;
let targetGender: string | null;
let genderCondition: string;
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
// Конкретный пол
targetGender = userProfile.interestedIn;
genderCondition = `AND p.gender = '${targetGender}'`;
} else {
// Если "both" или другое значение, показываем противоположный пол
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
// Если "both" - показываем всех кроме своего пола
targetGender = null;
genderCondition = `AND p.gender != '${userProfile.gender}'`;
}
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender || 'any (except self)'}, условие: ${genderCondition}`);
// Получаем список просмотренных профилей из новой таблицы profile_views
// и добавляем также профили из свайпов для полной совместимости
@@ -490,7 +495,7 @@ export class MatchingService {
FROM profiles p
JOIN users u ON p.user_id = u.id
WHERE p.is_visible = true
AND p.gender = '${targetGender}'
${genderCondition}
${excludeCondition}
`;
@@ -502,14 +507,14 @@ export class MatchingService {
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
// Используем определенный ранее targetGender для поиска
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender || 'any'}, возраст: ${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 = '${targetGender}'
${genderCondition}
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
${excludeCondition}
ORDER BY RANDOM()

View File

@@ -1118,10 +1118,10 @@ export class NotificationService {
const lastMessageTime = new Date(result.rows[0].created_at);
const now = new Date();
const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60);
const secondsSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / 1000;
// Считаем активным если последнее сообщение было менее 10 минут назад
return hoursSinceLastMessage < (10 / 60);
// Считаем активным если последнее сообщение было менее 30 секунд назад
return secondsSinceLastMessage < 30;
} catch (error) {
console.error('Error checking user activity:', error);
return false;

View File

@@ -46,17 +46,38 @@ export class ProfileService {
updatedAt: now
});
// Сохранение в базу данных
// Сохранение в базу данных (UPSERT для избежания конфликта с триггером)
await query(`
INSERT INTO profiles (
id, user_id, name, age, gender, interested_in, bio, photos,
city, education, job, height, religion, dating_goal,
city, education, job, height, location_lat, location_lon, religion, dating_goal,
is_verified, is_visible, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
ON CONFLICT (user_id) DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age,
gender = EXCLUDED.gender,
interested_in = EXCLUDED.interested_in,
bio = EXCLUDED.bio,
photos = EXCLUDED.photos,
city = EXCLUDED.city,
education = EXCLUDED.education,
job = EXCLUDED.job,
height = EXCLUDED.height,
location_lat = EXCLUDED.location_lat,
location_lon = EXCLUDED.location_lon,
religion = EXCLUDED.religion,
dating_goal = EXCLUDED.dating_goal,
is_verified = EXCLUDED.is_verified,
is_visible = EXCLUDED.is_visible,
updated_at = EXCLUDED.updated_at
`, [
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
profile.height, profile.religion, profile.datingGoal,
profile.bio, profile.photos, profile.city, profile.education, profile.job,
profile.height,
profile.location?.latitude || null,
profile.location?.longitude || null,
profile.religion, profile.datingGoal,
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
]);
@@ -126,26 +147,44 @@ export class ProfileService {
}
// Создание пользователя если не существует
async ensureUser(telegramId: string, userData: any): Promise<string> {
async ensureUser(telegramId: string, userData: any, language: string = 'ru'): Promise<string> {
// Используем UPSERT для избежания дублирования
const result = await query(`
INSERT INTO users (telegram_id, username, first_name, last_name)
VALUES ($1, $2, $3, $4)
INSERT INTO users (telegram_id, username, first_name, last_name, lang)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (telegram_id) DO UPDATE SET
username = EXCLUDED.username,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name
last_name = EXCLUDED.last_name,
lang = COALESCE(users.lang, EXCLUDED.lang)
RETURNING id
`, [
parseInt(telegramId),
userData.username || null,
userData.first_name || null,
userData.last_name || null
userData.last_name || null,
language
]);
return result.rows[0].id;
}
// Обновление языка пользователя
async updateUserLanguage(telegramId: string, language: string): Promise<void> {
await query(`
UPDATE users SET lang = $1 WHERE telegram_id = $2
`, [language, parseInt(telegramId)]);
}
// Получение языка пользователя
async getUserLanguage(telegramId: string): Promise<string> {
const result = await query(`
SELECT lang FROM users WHERE telegram_id = $1
`, [parseInt(telegramId)]);
return result.rows.length > 0 ? result.rows[0].lang : 'ru';
}
// Обновление профиля
async updateProfile(userId: string, updates: Partial<ProfileData>): Promise<Profile> {
const existingProfile = await this.getProfileByUserId(userId);
@@ -168,13 +207,20 @@ export class ProfileService {
switch (key) {
case 'photos':
case 'interests':
case 'hobbies':
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
updateValues.push(JSON.stringify(value));
// PostgreSQL принимает нативные массивы
updateValues.push(Array.isArray(value) ? value : [value]);
break;
case 'location':
// Пропускаем обработку местоположения, так как колонки location нет
console.log('Skipping location update - column does not exist');
// Сохраняем координаты в location_lat и location_lon
if (value && typeof value === 'object' && 'latitude' in value && 'longitude' in value) {
updateFields.push(`location_lat = $${paramIndex++}`);
updateValues.push(value.latitude);
updateFields.push(`location_lon = $${paramIndex++}`);
updateValues.push(value.longitude);
console.log(`Updating location: lat=${value.latitude}, lon=${value.longitude}`);
}
break;
case 'searchPreferences':
// Поля search preferences больше не хранятся в БД, пропускаем
@@ -458,7 +504,10 @@ export class ProfileService {
drinking: undefined,
kids: undefined
}, // Пропускаем lifestyle, так как этих колонок нет
location: undefined, // Пропускаем location, так как этих колонок нет
location: (entity.location_lat && entity.location_lon) ? {
latitude: parseFloat(entity.location_lat),
longitude: parseFloat(entity.location_lon)
} : undefined,
searchPreferences: {
minAge: 18,
maxAge: 50,

View File

@@ -0,0 +1,59 @@
import { query } from '../database/connection';
export class UserLanguageService {
// Получить язык пользователя
async getUserLanguage(telegramId: string): Promise<string> {
try {
const result = await query(
'SELECT language FROM users WHERE telegram_id = $1',
[telegramId]
);
if (result.rows.length > 0) {
return result.rows[0].language || 'ru';
}
return 'ru'; // Язык по умолчанию
} catch (error) {
console.error('Error getting user language:', error);
return 'ru';
}
}
// Установить язык пользователя
async setUserLanguage(telegramId: string, language: string): Promise<boolean> {
try {
await query(
'UPDATE users SET language = $1 WHERE telegram_id = $2',
[language, telegramId]
);
return true;
} catch (error) {
console.error('Error setting user language:', error);
return false;
}
}
// Получить поддерживаемые языки
getSupportedLanguages(): { [key: string]: string } {
return {
'ru': '🇷🇺 Русский',
'en': '🇺🇸 English',
'es': '🇪🇸 Español',
'fr': '🇫🇷 Français',
'de': '🇩🇪 Deutsch',
'it': '🇮🇹 Italiano',
'pt': '🇵🇹 Português',
'zh': '🇨🇳 中文',
'ja': '🇯🇵 日本語',
'ko': '🇰🇷 한국어',
'uz': '🇺🇿 O\'zbekcha',
'kk': '🇰🇿 Қазақша'
};
}
// Проверить, поддерживается ли язык
isLanguageSupported(language: string): boolean {
return Object.keys(this.getSupportedLanguages()).includes(language);
}
}

241
start.bat
View File

@@ -1,241 +0,0 @@
@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
View File

@@ -1,229 +0,0 @@
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 для выхода"

225
start.sh
View File

@@ -1,225 +0,0 @@
#!/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
# Проверяем и исправляем проблему с командой сборки в Dockerfile
echo -e "${YELLOW}Проверка конфигурации Dockerfile...${NC}"
if grep -q "RUN npm run build" Dockerfile && ! grep -q "RUN npm run build:linux" Dockerfile; then
echo -e "${YELLOW}⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux...${NC}"
sed -i "s/RUN npm run build/RUN npm run build:linux/g" Dockerfile
echo -e "${GREEN}✅ Dockerfile обновлен для использования команды сборки совместимой с Linux${NC}"
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}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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