init commit

This commit is contained in:
2025-08-12 21:02:23 +09:00
parent a64b348631
commit 29aca4e7e9
37 changed files with 1318 additions and 176 deletions

40
.drone.yml Normal file
View File

@@ -0,0 +1,40 @@
kind: pipeline
type: docker
name: build_test_local
steps:
- name: build
image: docker:27
volumes:
- name: dockersock
path: /var/run
commands:
- docker compose -f docker-compose.yml build --pull
- name: migrate
image: docker:27
volumes:
- name: dockersock
path: /var/run
commands:
- docker compose up -d db
- |
for i in $(seq 1 60); do
if docker compose ps db | grep -q "(healthy)"; then
echo "DB healthy"; break
fi
echo "Waiting DB... $i"
sleep 2
done
- docker compose run --rm bot alembic upgrade head
- name: lint
image: python:3.12-slim
commands:
- pip install ruff==0.5.7
- ruff check services/bot
volumes:
- name: dockersock
host:
path: /var/run/docker.sock

13
.env Normal file
View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=soulmate_bot_v1
TZ=Asia/Seoul
MYSQL_DATABASE=seoulmate
MYSQL_USER=match_admin
MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3
MYSQL_ROOT_PASSWORD=eiKVznWb13FZMCoXvlFRIfozzP752qWkMTEE7kzlus3vSUdPtg
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

176
.gitignore vendored
View File

@@ -1,176 +0,0 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=korea-marriage-bot
TZ=Asia/Seoul
MYSQL_DATABASE=match_agency
MYSQL_USER=match_user
MYSQL_PASSWORD=match_password_change_me
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=replace_with_your_bot_token
PRIMARY_ADMIN_TELEGRAM_ID=123456789
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=korea-marriage-bot
TZ=Asia/Seoul
MYSQL_DATABASE=match_agency
MYSQL_USER=match_user
MYSQL_PASSWORD=match_password_change_me
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=replace_with_your_bot_token
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=korea-marriage-bot
TZ=Asia/Seoul
MYSQL_DATABASE=match_agency
MYSQL_USER=match_user
MYSQL_PASSWORD=match_password_change_me
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=soulmate_bot_v1
TZ=Asia/Seoul
MYSQL_DATABASE=match_agency
MYSQL_USER=match_user
MYSQL_PASSWORD=match_password_change_me
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=soulmate_bot_v1
TZ=Asia/Seoul
MYSQL_DATABASE=seoulmate
MYSQL_USER=match_admin
MYSQL_PASSWORD=match_password_change_
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=soulmate_bot_v1
TZ=Asia/Seoul
MYSQL_DATABASE=seoulmate
MYSQL_USER=match_admin
MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3
MYSQL_ROOT_PASSWORD=root_password_change_me
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
COMPOSE_PROJECT_NAME=soulmate_bot_v1
TZ=Asia/Seoul
MYSQL_DATABASE=seoulmate
MYSQL_USER=match_admin
MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3
MYSQL_ROOT_PASSWORD=eiKVznWb13FZMCoXvlFRIfozzP752qWkMTEE7kzlus3vSUdPtg
DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
PRIMARY_ADMIN_TELEGRAM_ID=556399210
PYTHONUNBUFFERED=1

View File

@@ -0,0 +1,13 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db
.history

View File

View File

@@ -0,0 +1,191 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,191 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.env.local
.env
node_modules/
*.log
docker-data/
volumes/
alembic/*.pyc
alembic/versions/__pycache__/
.DS_Store
Thumbs.db

View File

View File

View File

@@ -0,0 +1,108 @@
# 1) Создаём структуру Alembic
mkdir -p services/bot/alembic/versions
# 2) env.py
cat > services/bot/alembic/env.py <<'PY'
from __future__ import annotations
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
config = context.config
# Подменяем URL из переменных окружения контейнера
if os.getenv("DATABASE_URL"):
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
fileConfig(config.config_file_name)
# Метаданные моделей
from app.models.base import Base # noqa: E402
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True,
dialect_opts={"paramstyle": "named"})
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
PY
# 3) Первая миграция
cat > services/bot/alembic/versions/0001_init.py <<'PY'
from alembic import op
import sqlalchemy as sa
revision = '0001_init'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'admins',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('telegram_id', sa.Integer(), unique=True, index=True),
sa.Column('full_name', sa.String(length=120), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
)
op.create_table(
'candidates',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('telegram_id', sa.Integer(), unique=True, index=True),
sa.Column('username', sa.String(length=100), nullable=True),
sa.Column('full_name', sa.String(length=120), nullable=True),
sa.Column('gender', sa.String(length=20), nullable=True),
sa.Column('birth_date', sa.Date(), nullable=True),
sa.Column('height_cm', sa.Float(), nullable=True),
sa.Column('weight_kg', sa.Float(), nullable=True),
sa.Column('country', sa.String(length=80), nullable=True),
sa.Column('city', sa.String(length=120), nullable=True),
sa.Column('citizenship', sa.String(length=80), nullable=True),
sa.Column('visa_status', sa.String(length=60), nullable=True),
sa.Column('languages', sa.String(length=200), nullable=True),
sa.Column('education', sa.String(length=120), nullable=True),
sa.Column('occupation', sa.String(length=120), nullable=True),
sa.Column('income_range', sa.String(length=60), nullable=True),
sa.Column('religion', sa.String(length=80), nullable=True),
sa.Column('marital_status', sa.String(length=60), nullable=True),
sa.Column('has_children', sa.Boolean(), nullable=True),
sa.Column('children_notes', sa.String(length=200), nullable=True),
sa.Column('smoking', sa.String(length=20), nullable=True),
sa.Column('alcohol', sa.String(length=20), nullable=True),
sa.Column('health_notes', sa.String(length=300), nullable=True),
sa.Column('hobbies_tags', sa.String(length=300), nullable=True),
sa.Column('hobbies_free', sa.Text(), nullable=True),
sa.Column('goal', sa.String(length=120), nullable=True),
sa.Column('partner_prefs', sa.Text(), nullable=True),
sa.Column('avatar_file_id', sa.String(length=200), nullable=True),
sa.Column('gallery_file_ids', sa.Text(), nullable=True),
sa.Column('consent_personal', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('consent_policy', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('is_verified', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')),
)
def downgrade():
op.drop_table('candidates')
op.drop_table('admins')
PY

View File

@@ -0,0 +1,55 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u2462276MYSQL_USER -p2462276MYSQL_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

View File

@@ -0,0 +1,55 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u2462276MYSQL_USER -p2462276MYSQL_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

View File

@@ -0,0 +1,57 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

View File

@@ -0,0 +1,60 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

View File

@@ -0,0 +1,60 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

View File

@@ -0,0 +1,67 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test:
[
"CMD-SHELL",
"([ -x /usr/bin/mariadb-admin ] && mariadb-admin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \
([ -x /usr/bin/mysqladmin ] && mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \
(command -v bash >/dev/null 2>&1 && bash -c '</dev/tcp/127.0.0.1/3306' >/dev/null 2>&1)"
]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

29
Makefile Normal file
View File

@@ -0,0 +1,29 @@
SHELL := /bin/bash
.PHONY: up down logs build stop restart botsh dbsh migrate
up:
docker compose up -d
down:
docker compose down
build:
docker compose build --pull
logs:
docker compose logs -f --tail=200
stop:
docker compose stop
restart: down up
botsh:
docker compose exec bot bash || true
dbsh:
docker compose exec db bash || true
migrate:
docker compose run --rm bot alembic upgrade head

67
docker-compose.yml Normal file
View File

@@ -0,0 +1,67 @@
services:
db:
image: mariadb:11.6
restart: unless-stopped
environment:
TZ: ${TZ}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test:
[
"CMD-SHELL",
"([ -x /usr/bin/mariadb-admin ] && mariadb-admin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \
([ -x /usr/bin/mysqladmin ] && mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \
(command -v bash >/dev/null 2>&1 && bash -c '</dev/tcp/127.0.0.1/3306' >/dev/null 2>&1)"
]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
volumes:
- db_data:/var/lib/mysql
networks:
- appnet
bot:
build:
context: ./services/bot
dockerfile: Dockerfile
args:
- PY_VERSION=3.12
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
volumes:
- ./services/bot:/app
command: ["/app/entrypoint.sh"]
networks:
- appnet
adminer:
image: adminer:latest
ports:
- "8081:8080"
environment:
TZ: ${TZ}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:
driver: bridge

11
services/bot/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
ARG PY_VERSION=3.12
FROM python:${PY_VERSION}-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends tzdata \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN chmod +x entrypoint.sh
CMD ["./entrypoint.sh"]

3
services/bot/alembic.ini Normal file
View File

@@ -0,0 +1,3 @@
[alembic]
script_location = alembic
sqlalchemy.url = ${DATABASE_URL}

View File

@@ -0,0 +1,39 @@
from __future__ import annotations
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
config = context.config
# Подменяем URL из переменных окружения контейнера
if os.getenv("DATABASE_URL"):
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
fileConfig(config.config_file_name)
# Метаданные моделей
from app.models.base import Base # noqa: E402
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True,
dialect_opts={"paramstyle": "named"})
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,59 @@
from alembic import op
import sqlalchemy as sa
revision = '0001_init'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'admins',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('telegram_id', sa.Integer(), unique=True, index=True),
sa.Column('full_name', sa.String(length=120), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
)
op.create_table(
'candidates',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('telegram_id', sa.Integer(), unique=True, index=True),
sa.Column('username', sa.String(length=100), nullable=True),
sa.Column('full_name', sa.String(length=120), nullable=True),
sa.Column('gender', sa.String(length=20), nullable=True),
sa.Column('birth_date', sa.Date(), nullable=True),
sa.Column('height_cm', sa.Float(), nullable=True),
sa.Column('weight_kg', sa.Float(), nullable=True),
sa.Column('country', sa.String(length=80), nullable=True),
sa.Column('city', sa.String(length=120), nullable=True),
sa.Column('citizenship', sa.String(length=80), nullable=True),
sa.Column('visa_status', sa.String(length=60), nullable=True),
sa.Column('languages', sa.String(length=200), nullable=True),
sa.Column('education', sa.String(length=120), nullable=True),
sa.Column('occupation', sa.String(length=120), nullable=True),
sa.Column('income_range', sa.String(length=60), nullable=True),
sa.Column('religion', sa.String(length=80), nullable=True),
sa.Column('marital_status', sa.String(length=60), nullable=True),
sa.Column('has_children', sa.Boolean(), nullable=True),
sa.Column('children_notes', sa.String(length=200), nullable=True),
sa.Column('smoking', sa.String(length=20), nullable=True),
sa.Column('alcohol', sa.String(length=20), nullable=True),
sa.Column('health_notes', sa.String(length=300), nullable=True),
sa.Column('hobbies_tags', sa.String(length=300), nullable=True),
sa.Column('hobbies_free', sa.Text(), nullable=True),
sa.Column('goal', sa.String(length=120), nullable=True),
sa.Column('partner_prefs', sa.Text(), nullable=True),
sa.Column('avatar_file_id', sa.String(length=200), nullable=True),
sa.Column('gallery_file_ids', sa.Text(), nullable=True),
sa.Column('consent_personal', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('consent_policy', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('is_verified', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')),
)
def downgrade():
op.drop_table('candidates')
op.drop_table('admins')

View File

@@ -0,0 +1,5 @@
import os
class Settings:
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
DATABASE_URL = os.getenv("DATABASE_URL", "")
settings = Settings()

14
services/bot/app/main.py Normal file
View File

@@ -0,0 +1,14 @@
import asyncio
from telegram.ext import Application, CommandHandler
from app.core.config import settings
async def start(update, context):
await update.message.reply_text("Привет! Бот запущен.")
async def main():
app = Application.builder().token(settings.BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
await app.initialize()
await app.start()
await app.updater.start_polling()
await app.updater.wait_until_closed()
if __name__ == "__main__":
asyncio.run(main())

26
services/bot/entrypoint.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
if [ -f "/app/.env" ]; then
set -a; source /app/.env; set +a
fi
echo "Waiting for DB..."
python - <<'PY'
import os, time, pymysql
url = os.environ["DATABASE_URL"]
u = url.split("://",1)[1].split("@",1)
auth, host_db = u[0], u[1]
host, port_db = host_db.split("/",1)[0], host_db.split("/",1)[1]
host, port = host.split(":")[0], int(host.split(":")[1] or 3306)
user, password = auth.split(":")[0], ":".join(auth.split(":")[1:])
db = port_db.split("?")[0]
for i in range(60):
try:
conn = pymysql.connect(host=host, port=port, user=user, password=password, database=db)
conn.close()
break
except Exception as e:
print("Waiting DB...", e)
time.sleep(2)
PY
alembic upgrade head
exec python -m app.main

View File

@@ -0,0 +1,7 @@
python-telegram-bot==21.6
SQLAlchemy==2.0.30
alembic==1.13.2
pymysql==1.1.1
python-dotenv==1.0.1
tenacity==9.0.0
ruff==0.5.7