mirror of
https://github.com/Shadik23/telegram_greeting_bot.git
synced 2025-12-10 05:19:39 +05:00
feat: Add GitHub Actions CI and professional README
This commit is contained in:
17
.devcontainer/devcontainer.json
Normal file
17
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Telegram Bot Dev Environment",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "bot",
|
||||
"workspaceFolder": "/app",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python"
|
||||
},
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN
|
||||
|
||||
POSTGRES_DB=greeting_db
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
|
||||
WEB_USERNAME=admin
|
||||
WEB_PASSWORD=secure_password
|
||||
6
.env.test
Normal file
6
.env.test
Normal file
@@ -0,0 +1,6 @@
|
||||
BOT_TOKEN=dummy_token_for_testing
|
||||
POSTGRES_DB=greeting_db_test
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
WEB_USERNAME=admin
|
||||
WEB_PASSWORD=supersecret
|
||||
52
.github/workflows/main.yml
vendored
Normal file
52
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# .github/workflows/main.yml
|
||||
|
||||
name: CI Pipeline
|
||||
|
||||
# --- Triggers ---
|
||||
# This workflow runs on pushes to the 'main' branch and on pull requests targeting 'main'.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# --- Jobs ---
|
||||
# We define a single job called 'build-and-test'.
|
||||
jobs:
|
||||
build-and-test:
|
||||
# The type of virtual machine to run the job on. 'ubuntu-latest' is a standard choice.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# --- Steps ---
|
||||
# A sequence of tasks that will be executed as part of the job.
|
||||
steps:
|
||||
# Step 1: Check out the repository code
|
||||
# This action checks out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Step 2: Set up Docker Buildx
|
||||
# This is a required step for building multi-platform images and using advanced Docker features.
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Step 3: Run the test suite
|
||||
# This is the core of our CI. It executes the same script we used locally.
|
||||
# We provide a dummy BOT_TOKEN because the tests don't need a real one.
|
||||
- name: Run integration tests
|
||||
run: ./run_tests.sh
|
||||
env:
|
||||
# We need to provide the BOT_TOKEN variable, as the script expects it,
|
||||
# but it can be a dummy value since it's only used for the bot service
|
||||
# which is idle during tests.
|
||||
BOT_TOKEN: "dummy_token_for_ci"
|
||||
|
||||
# Step 4 (Optional but recommended): Clean up Docker resources
|
||||
# This ensures that any lingering containers or images are removed to free up space on the GitHub runner.
|
||||
- name: Docker cleanup
|
||||
if: always() # This step runs even if previous steps fail.
|
||||
run: |
|
||||
docker-compose down -v --remove-orphans
|
||||
docker system prune -af --volumes
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.vscode/
|
||||
venv/
|
||||
.DS_Store
|
||||
db_data/
|
||||
132
README.md
Normal file
132
README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Telegram Greeting Bot
|
||||
|
||||
[](https://github.com/averageencoreenjoer/telegram_greeting_bot/actions/workflows/main.yml)
|
||||
|
||||
A fully containerized Telegram bot project featuring a dynamic, database-driven greeting message that can be updated in real-time via a simple web admin panel. This project is built with a senior-level approach, emphasizing clean architecture, separation of concerns, containerization, and automated testing.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Dynamic Greetings**: The `/start` command greeting is fetched directly from a PostgreSQL database.
|
||||
- **Real-time Updates**: No need to restart the bot. Changes made in the admin panel are reflected instantly.
|
||||
- **Mini Admin Panel**: A secure, password-protected web interface (built with FastAPI) to update the greeting text.
|
||||
- **Fully Containerized**: Uses Docker and Docker Compose to run the entire stack (Bot, Web, DB) with a single command.
|
||||
- **Automated Integration Tests**: A robust test suite using Pytest ensures the entire system works correctly.
|
||||
- **CI/CD Ready**: Includes a GitHub Actions workflow to automatically run tests on every push and pull request.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python"/>
|
||||
<img src="https://img.shields.io/badge/Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram"/>
|
||||
<img src="https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI"/>
|
||||
<img src="https://img.shields.io/badge/PostgreSQL-4169E1?style=for-the-badge&logo=postgresql&logoColor=white" alt="PostgreSQL"/>
|
||||
<img src="https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"/>
|
||||
<img src="https://img.shields.io/badge/Pytest-0A9B71?style=for-the-badge&logo=pytest&logoColor=white" alt="Pytest"/>
|
||||
<img src="https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white" alt="GitHub Actions"/>
|
||||
</p>
|
||||
|
||||
- **Bot Framework**: `aiogram`
|
||||
- **Web Framework**: `FastAPI`
|
||||
- **Database**: `PostgreSQL` with `asyncpg` driver
|
||||
- **Containerization**: `Docker` & `Docker Compose`
|
||||
- **Testing**: `Pytest`, `pytest-asyncio`, `httpx`
|
||||
- **CI/CD**: `GitHub Actions`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://www.docker.com/products/docker-desktop/) installed and running.
|
||||
- A Telegram Bot Token obtained from [@BotFather](https://t.me/BotFather).
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/averageencoreenjoer/telegram_greeting_bot.git
|
||||
cd telegram_greeting_bot
|
||||
```
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Create a local environment file by copying the example.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Now, open the `.env` file and fill in your details:
|
||||
|
||||
```ini
|
||||
# .env
|
||||
|
||||
# 1. Your unique Telegram Bot Token from @BotFather
|
||||
BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u1234567
|
||||
|
||||
# 2. Credentials for the database (can be left as default for local development)
|
||||
POSTGRES_DB=greeting_db
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
|
||||
# 3. Credentials for the web admin panel (you will use these to log in)
|
||||
WEB_USERNAME=admin
|
||||
WEB_PASSWORD=supersecret
|
||||
```
|
||||
|
||||
### 3. Run the Application
|
||||
|
||||
Launch the entire stack using Docker Compose.
|
||||
|
||||
```bash
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
The services will start in detached mode (`-d`). You can view the logs with `docker-compose logs -f`.
|
||||
|
||||
### 4. Interact with the System
|
||||
|
||||
- **Telegram Bot**: Find your bot on Telegram and send the `/start` command. It will reply with the default greeting.
|
||||
- **Admin Panel**: Open your browser and navigate to `http://localhost:8000/docs`.
|
||||
- You will be prompted for authentication. Use the `WEB_USERNAME` and `WEB_PASSWORD` from your `.env` file.
|
||||
- Use the `/update_greeting` endpoint to change the greeting text.
|
||||
- Go back to Telegram and send `/start` again. The bot will now use the new greeting!
|
||||
|
||||
### 5. Stopping the Application
|
||||
|
||||
To stop all services, run:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
To stop services and remove the database volume (delete all data), run:
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Running Tests
|
||||
|
||||
This project comes with a fully automated integration test suite that spins up a separate, isolated environment to validate the system's behavior.
|
||||
|
||||
To run the tests, execute the provided script from the project root:
|
||||
|
||||
```bash
|
||||
./run_tests.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Start the services using a dedicated test database (`.env.test`).
|
||||
2. Wait for the database to be fully ready.
|
||||
3. Install test dependencies.
|
||||
4. Run the Pytest suite, which makes real API calls.
|
||||
5. Shut down and clean up all test-related containers and volumes.
|
||||
|
||||
This is the same script that runs in the GitHub Actions CI pipeline.
|
||||
14
bot/Dockerfile
Normal file
14
bot/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл зависимостей из папки bot
|
||||
COPY bot/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем исходный код из папки bot/src
|
||||
COPY bot/src/. /app/
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
3
bot/requirements.txt
Normal file
3
bot/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
aiogram==3.*
|
||||
asyncpg==0.*
|
||||
python-dotenv==1.*
|
||||
0
bot/src/__init__.py
Normal file
0
bot/src/__init__.py
Normal file
17
bot/src/config.py
Normal file
17
bot/src/config.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class BotConfig:
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
DB_NAME = os.getenv("DB_NAME", "greeting_db")
|
||||
DB_USER = os.getenv("DB_USER", "user")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
|
||||
|
||||
@classmethod
|
||||
def get_db_url(cls):
|
||||
return f"postgresql://{cls.DB_USER}:{cls.DB_PASSWORD}@{cls.DB_HOST}/{cls.DB_NAME}"
|
||||
|
||||
28
bot/src/db.py
Normal file
28
bot/src/db.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import asyncpg
|
||||
from config import BotConfig
|
||||
|
||||
|
||||
async def get_db_pool():
|
||||
return await asyncpg.create_pool(BotConfig.get_db_url())
|
||||
|
||||
|
||||
async def get_greeting(pool):
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT greeting_text FROM greetings ORDER BY id LIMIT 1")
|
||||
return row['greeting_text'] if row else "Привет! Добро пожаловать!"
|
||||
|
||||
|
||||
async def set_greeting(pool, new_text: str):
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"UPDATE greetings SET greeting_text = $1 WHERE id = 1",
|
||||
new_text
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO greetings (id, greeting_text)
|
||||
VALUES (1, $1)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
""",
|
||||
new_text
|
||||
)
|
||||
11
bot/src/handlers.py
Normal file
11
bot/src/handlers.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from aiogram import Router, types
|
||||
from aiogram.filters import CommandStart
|
||||
from db import get_greeting
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def command_start_handler(message: types.Message, db_pool) -> None:
|
||||
greeting_text = await get_greeting(db_pool)
|
||||
await message.answer(greeting_text + "!!!")
|
||||
0
bot/src/keyboards.py
Normal file
0
bot/src/keyboards.py
Normal file
29
bot/src/main.py
Normal file
29
bot/src/main.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from aiogram import Bot, Dispatcher
|
||||
from config import BotConfig
|
||||
from handlers import router
|
||||
from db import get_db_pool
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
bot = Bot(token=BotConfig.BOT_TOKEN)
|
||||
dp = Dispatcher()
|
||||
|
||||
db_pool = await get_db_pool()
|
||||
dp["db_pool"] = db_pool
|
||||
|
||||
dp.include_router(router)
|
||||
|
||||
logging.info("Starting bot...")
|
||||
await dp.start_polling(bot)
|
||||
|
||||
await db_pool.close()
|
||||
logging.info("Bot stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
8
db/init.sql
Normal file
8
db/init.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS greetings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
greeting_text TEXT NOT NULL DEFAULT 'Привет! Добро пожаловать!'
|
||||
);
|
||||
|
||||
INSERT INTO greetings (greeting_text)
|
||||
SELECT 'Привет! Добро пожаловать!'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM greetings WHERE id = 1);
|
||||
5
docker-compose.override.yml
Normal file
5
docker-compose.override.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
command: tail -f /dev/null
|
||||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready && psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c 'SELECT 1'" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: bot/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
BOT_TOKEN: ${BOT_TOKEN}
|
||||
DB_HOST: db
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_USER: ${POSTGRES_USER}
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- ./bot/src:/app
|
||||
tty: true
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: web/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_USER: ${POSTGRES_USER}
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
WEB_USERNAME: ${WEB_USERNAME}
|
||||
WEB_PASSWORD: ${WEB_PASSWORD}
|
||||
volumes:
|
||||
- ./web/src:/app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
tty: true
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
4
requirements.test.txt
Normal file
4
requirements.test.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pytest
|
||||
pytest-asyncio
|
||||
httpx
|
||||
python-dotenv
|
||||
33
run_tests.sh
Executable file
33
run_tests.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
set -e
|
||||
|
||||
echo "--- 1. Запуск тестового окружения (используя .env.test) ---"
|
||||
docker-compose --env-file .env.test up -d --build --force-recreate
|
||||
|
||||
echo "--- 2. Ожидание готовности Базы Данных ---"
|
||||
|
||||
retries=15
|
||||
while [ $retries -gt 0 ]; do
|
||||
docker-compose exec -T db pg_isready -U user -d greeting_db_test && break
|
||||
retries=$((retries-1))
|
||||
echo "Ожидание БД... Осталось попыток: $retries"
|
||||
sleep 2
|
||||
done
|
||||
if [ $retries -eq 0 ]; then
|
||||
echo "!!! База Данных не запустилась вовремя. Проверьте логи: docker-compose logs db"
|
||||
exit 1
|
||||
fi
|
||||
echo "--- База Данных готова! ---"
|
||||
|
||||
echo "--- 3. Установка Python-зависимостей для тестов ---"
|
||||
|
||||
pip3 install -r requirements.test.txt
|
||||
|
||||
echo "--- 4. ЗАПУСК ТЕСТОВ ---"
|
||||
pytest tests/
|
||||
|
||||
TEST_EXIT_CODE=$?
|
||||
|
||||
echo "--- 5. Очистка: остановка и удаление тестового окружения ---"
|
||||
docker-compose --env-file .env.test down -v
|
||||
|
||||
exit $TEST_EXIT_CODE
|
||||
30
tests/test_integration.py
Normal file
30
tests/test_integration.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
import httpx
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv(dotenv_path='.env.test')
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
ADMIN_USER = os.getenv("WEB_USERNAME")
|
||||
ADMIN_PASSWORD = os.getenv("WEB_PASSWORD")
|
||||
AUTH = (ADMIN_USER, ADMIN_PASSWORD)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_greeting_cycle():
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response_before = await client.get(BASE_URL + "/", auth=AUTH)
|
||||
assert response_before.status_code == 200
|
||||
assert response_before.json()["current_greeting_text"] == "Привет! Добро пожаловать!"
|
||||
|
||||
new_text = "Автотест прошел успешно!"
|
||||
update_payload = {"new_greeting_text": new_text}
|
||||
response_update = await client.post(BASE_URL + "/update_greeting", json=update_payload, auth=AUTH)
|
||||
assert response_update.status_code == 200
|
||||
assert response_update.json()["current_greeting_text"] == new_text
|
||||
|
||||
response_after = await client.get(BASE_URL + "/", auth=AUTH)
|
||||
assert response_after.status_code == 200
|
||||
assert response_after.json()["current_greeting_text"] == new_text
|
||||
|
||||
14
web/Dockerfile
Normal file
14
web/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл зависимостей из папки web
|
||||
COPY web/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем исходный код из папки web/src
|
||||
COPY web/src/. /app/
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
4
web/requirements.txt
Normal file
4
web/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi==0.*
|
||||
uvicorn==0.*
|
||||
asyncpg==0.*
|
||||
python-dotenv==1.*
|
||||
0
web/src/__init__.py
Normal file
0
web/src/__init__.py
Normal file
18
web/src/config.py
Normal file
18
web/src/config.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class WebConfig:
|
||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
DB_NAME = os.getenv("DB_NAME", "greeting_db")
|
||||
DB_USER = os.getenv("DB_USER", "user")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
|
||||
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
|
||||
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "secure_password")
|
||||
|
||||
@classmethod
|
||||
def get_db_url(cls):
|
||||
return f"postgresql://{cls.DB_USER}:{cls.DB_PASSWORD}@{cls.DB_HOST}/{cls.DB_NAME}"
|
||||
|
||||
94
web/src/main.py
Normal file
94
web/src/main.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import asyncpg
|
||||
from fastapi import FastAPI, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from config import WebConfig
|
||||
from schemas import GreetingUpdate, GreetingResponse
|
||||
import secrets
|
||||
|
||||
|
||||
app = FastAPI(title="Greeting Bot Admin Panel")
|
||||
security = HTTPBasic()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
db_pool = None
|
||||
|
||||
|
||||
async def get_db_pool():
|
||||
global db_pool
|
||||
if db_pool is None:
|
||||
for i in range(5):
|
||||
try:
|
||||
logging.info(f"Attempting to connect to DB (try {i+1}/5)...")
|
||||
db_pool = await asyncpg.create_pool(WebConfig.get_db_url())
|
||||
logging.info("DB pool created successfully.")
|
||||
break
|
||||
except (ConnectionRefusedError, asyncpg.exceptions.InvalidPasswordError) as e:
|
||||
logging.warning(f"DB connection failed: {e}. Retrying in 3 seconds...")
|
||||
if i == 4:
|
||||
raise
|
||||
await asyncio.sleep(3)
|
||||
return db_pool
|
||||
|
||||
|
||||
async def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
correct_username = secrets.compare_digest(credentials.username, WebConfig.WEB_USERNAME)
|
||||
correct_password = secrets.compare_digest(credentials.password, WebConfig.WEB_PASSWORD)
|
||||
if not (correct_username and correct_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
await get_db_pool()
|
||||
logging.info("Web service started and DB pool initialized.")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
global db_pool
|
||||
if db_pool:
|
||||
await db_pool.close()
|
||||
logging.info("DB pool closed.")
|
||||
|
||||
|
||||
@app.get("/", response_model=GreetingResponse)
|
||||
async def get_greeting_text(username: str = Depends(get_current_username)):
|
||||
pool = await get_db_pool()
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT greeting_text FROM greetings ORDER BY id LIMIT 1")
|
||||
if row:
|
||||
return GreetingResponse(current_greeting_text=row['greeting_text'])
|
||||
else:
|
||||
return GreetingResponse(current_greeting_text="Привет! Добро пожаловать!")
|
||||
|
||||
|
||||
@app.post("/update_greeting", response_model=GreetingResponse)
|
||||
async def update_greeting_text(
|
||||
greeting_update: GreetingUpdate,
|
||||
username: str = Depends(get_current_username)
|
||||
):
|
||||
pool = await get_db_pool()
|
||||
async with pool.acquire() as conn:
|
||||
existing_greeting = await conn.fetchrow("SELECT id FROM greetings WHERE id = 1")
|
||||
|
||||
if existing_greeting:
|
||||
await conn.execute(
|
||||
"UPDATE greetings SET greeting_text = $1 WHERE id = 1",
|
||||
greeting_update.new_greeting_text
|
||||
)
|
||||
else:
|
||||
await conn.execute(
|
||||
"INSERT INTO greetings (id, greeting_text) VALUES (1, $1)",
|
||||
greeting_update.new_greeting_text
|
||||
)
|
||||
logging.info(f"Greeting updated to: {greeting_update.new_greeting_text}")
|
||||
return GreetingResponse(current_greeting_text=greeting_update.new_greeting_text)
|
||||
|
||||
9
web/src/schemas.py
Normal file
9
web/src/schemas.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GreetingUpdate(BaseModel):
|
||||
new_greeting_text: str
|
||||
|
||||
|
||||
class GreetingResponse(BaseModel):
|
||||
current_greeting_text: str
|
||||
Reference in New Issue
Block a user