mirror of
https://github.com/olegvodyanov/docbot.git
synced 2025-12-19 23:57:05 +03:00
add webhook
This commit is contained in:
parent
97bf38f0c0
commit
070f446159
@ -1,4 +0,0 @@
|
||||
from core.ai.ai_bot import detect_eyes
|
||||
|
||||
|
||||
detect_eyes("/Users/o.vodianov/IdeaProjects/docbot/image.png")
|
||||
@ -9,6 +9,8 @@ services:
|
||||
- ./db/init:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- docbot-network
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
@ -27,12 +29,10 @@ services:
|
||||
environment:
|
||||
- BOT_TOKEN=${BOT_TOKEN}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- ADMIN_API_KEY=${ADMIN_API_KEY}
|
||||
- LOGGING_LEVEL=${LOGGING_LEVEL:-INFO}
|
||||
- PRODAMUS_TOKEN=${PRODAMUS_TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./conversations.pkl:/app/conversations.pkl
|
||||
- ./data:/app/data:rw
|
||||
- ./.env:/app/.env
|
||||
- ./.env_db:/app/.env_db
|
||||
depends_on:
|
||||
@ -54,7 +54,6 @@ services:
|
||||
environment:
|
||||
- BOT_TOKEN=${BOT_TOKEN}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- ADMIN_API_KEY=${ADMIN_API_KEY}
|
||||
- LOGGING_LEVEL=${LOGGING_LEVEL:-INFO}
|
||||
- PRODAMUS_TOKEN=${PRODAMUS_TOKEN}
|
||||
ports:
|
||||
@ -68,6 +67,7 @@ services:
|
||||
profiles:
|
||||
- production
|
||||
- development
|
||||
- chat_only
|
||||
|
||||
networks:
|
||||
docbot-network:
|
||||
|
||||
16
poetry.lock
generated
16
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
@ -1086,6 +1086,18 @@ files = [
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
description = "A streaming multipart parser for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
|
||||
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-telegram-bot"
|
||||
version = "22.1"
|
||||
@ -1585,4 +1597,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "c50f083c47c32cf0ca58d1ccfec01aec63db5dc07be39cac76bb1e792c255951"
|
||||
content-hash = "337def2431e36d8fde59d4273d3db93edf87a73ac78c74307f5c44def8c0a04d"
|
||||
|
||||
@ -33,6 +33,7 @@ alembic_utils = "0.8.8"
|
||||
alembic-postgresql-enum = "1.7.0"
|
||||
pytz = "2025.2"
|
||||
opencv-python = "4.12.0.88"
|
||||
python-multipart = "0.0.20"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@ -6,7 +6,6 @@ class Settings(BaseSettings):
|
||||
|
||||
BOT_TOKEN: str
|
||||
DATABASE_URL: str
|
||||
ADMIN_API_KEY: str
|
||||
LOGGING_LEVEL: str
|
||||
PRODAMUS_TOKEN: str
|
||||
|
||||
|
||||
@ -63,3 +63,10 @@ def is_valid_url(text: str) -> bool:
|
||||
return all([result.scheme in ["http", "https"], result.netloc])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def make_a_payment_link(base_link: str, code: str, phone: str) -> str:
|
||||
if base_link and code and phone:
|
||||
return f"{base_link}/?order_id={code}&customer_phone={phone}&do=pay"
|
||||
else:
|
||||
return null
|
||||
@ -172,3 +172,10 @@ class PaymentMethod(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||
|
||||
doctor: Mapped["Doctors"] = relationship(back_populates="payment_methods")
|
||||
|
||||
|
||||
class PaymentsRegistered(Base):
|
||||
__tablename__ = "payments_registered"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False)
|
||||
mapped: Mapped[bool] = mapped_column(nullable=True)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from telegram import (
|
||||
Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
)
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.ext import (
|
||||
ContextTypes,
|
||||
ConversationHandler,
|
||||
@ -11,8 +12,6 @@ from telegram.ext import (
|
||||
)
|
||||
from docbot.handlers.utils.cancel_handler import get_cancel_handler
|
||||
from docbot.handlers.start_handler import get_start_handler
|
||||
from core.enums.dialog_helpers import ConfirmationMessage
|
||||
from core.utils import is_phone_correct
|
||||
from docbot.services.patients_service import (
|
||||
create_patient, update_patient_phone, get_patient_by_telegram_id,
|
||||
is_user_has_phone
|
||||
@ -22,7 +21,9 @@ from docbot.services.doctors_service import (
|
||||
)
|
||||
from docbot.services.session_service import create_session
|
||||
from core.logging import logger
|
||||
from core.exceptions import DatabaseError # Assuming a custom exception for database-related issues
|
||||
from core.exceptions import DatabaseError
|
||||
from core.enums.dialog_helpers import ConfirmationMessage
|
||||
from core.utils import is_phone_correct, make_a_payment_link
|
||||
|
||||
|
||||
SEND_ACKNOWLEDGEMENT_INFO = 1
|
||||
@ -304,8 +305,10 @@ async def pay_consultation(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
)
|
||||
return ConversationHandler.END
|
||||
|
||||
session_code = ""
|
||||
|
||||
try:
|
||||
await create_session(
|
||||
session_code = await create_session(
|
||||
telegram_id=user_id,
|
||||
phone=patient.phone,
|
||||
consultation_date_time=consultation_date_time,
|
||||
@ -319,20 +322,23 @@ async def pay_consultation(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
)
|
||||
return ConversationHandler.END
|
||||
|
||||
link = await get_doctors_payment_link(context.user_data['doctor_number'])
|
||||
base_link = await get_doctors_payment_link(context.user_data['doctor_number'])
|
||||
|
||||
if not link:
|
||||
if not base_link:
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.message.reply_text(
|
||||
"❌ Извините, но по введённому серийному номеру врача не найдено ссылки на оплату. Пожалуйста, проверьте правильность введённого номера и попробуйте снова.",
|
||||
"❌ Извините, но по введённому серийному номеру врача не найдено ссылки на оплату."
|
||||
"Пожалуйста, проверьте правильность введённого номера и попробуйте снова.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return ConversationHandler.END
|
||||
|
||||
payment_link = make_a_payment_link(base_link=base_link, code=session_code, phone=patient.phone)
|
||||
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.message.reply_text(
|
||||
f"Чтобы оплатить консультацию перейдите по ссылке {link}.",
|
||||
parse_mode="Markdown"
|
||||
f"Чтобы оплатить консультацию перейдите по ссылке {payment_link}.",
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
context.user_data.clear()
|
||||
|
||||
@ -16,7 +16,7 @@ from docbot.handlers.utils.cancel_handler import get_cancel_handler
|
||||
|
||||
|
||||
def main():
|
||||
persistence = PicklePersistence(filepath="conversations.pkl")
|
||||
persistence = PicklePersistence(filepath="data/conversations.pkl")
|
||||
logger.info("Запуск DocBot…")
|
||||
bot_instance = ExtBot(token=settings.BOT_TOKEN)
|
||||
app = (
|
||||
|
||||
@ -5,7 +5,10 @@ from datetime import datetime
|
||||
from sqlalchemy import select
|
||||
|
||||
from db.session import AsyncSessionLocal
|
||||
from db.models import Sessions, Patients, SessionStatusHistory, SessionDateTimeHistory
|
||||
from db.models import (
|
||||
Sessions, Patients, SessionStatusHistory, SessionDateTimeHistory,
|
||||
PaymentsRegistered
|
||||
)
|
||||
from core.utils import (generate_session_code, date_time_formatter)
|
||||
from core.logging import logger
|
||||
|
||||
@ -45,6 +48,17 @@ async def create_session(telegram_id: int, phone: str, consultation_date_time: s
|
||||
return code
|
||||
|
||||
|
||||
async def save_payment_completed_info_from_prodamus(code: str) -> bool:
|
||||
async with AsyncSessionLocal() as session:
|
||||
async with session.begin():
|
||||
payment_completed = PaymentsRegistered(
|
||||
code=code
|
||||
)
|
||||
session.add(payment_completed)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def mark_consulted(code: str) -> bool:
|
||||
"""
|
||||
Отмечает, что консультация получена (добавляет consulted_at).
|
||||
|
||||
@ -1,15 +1,96 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import FastAPI, Request, Header, HTTPException
|
||||
from starlette.background import BackgroundTask
|
||||
from typing import Any, Dict
|
||||
from docbot.services.session_service import save_payment_completed_info_from_prodamus
|
||||
import json
|
||||
import logging
|
||||
|
||||
app = FastAPI()
|
||||
log = logging.getLogger("webhook")
|
||||
|
||||
def _mask(s: str | None) -> str | None:
|
||||
if not s:
|
||||
return s
|
||||
# замаскируем телефон/почту/длинные токены
|
||||
if "@" in s:
|
||||
name, _, dom = s.partition("@")
|
||||
return (name[:1] + "***@" + dom) if dom else "***"
|
||||
if s.startswith("+") and len(s) > 5:
|
||||
return s[:3] + "****" + s[-2:]
|
||||
if len(s) > 16:
|
||||
return s[:6] + "…"
|
||||
return s
|
||||
|
||||
def normalize_payload(d: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Приведём продукты из строки к JSON (если пришли строкой)
|
||||
products = d.get("products")
|
||||
if isinstance(products, str):
|
||||
try:
|
||||
d["products"] = json.loads(products)
|
||||
except Exception:
|
||||
pass
|
||||
# Маскируем чувствительные поля
|
||||
for k in ("customer_phone", "customer_email"):
|
||||
if k in d:
|
||||
d[k] = _mask(str(d[k]))
|
||||
return d
|
||||
|
||||
@app.post("/webhook")
|
||||
async def receive_webhook(
|
||||
request: Request,
|
||||
sign: str | None = Header(default=None, alias="Sign"), # подпись от Prodamus
|
||||
):
|
||||
# 1) читаем «сырое» тело и тип
|
||||
ctype = request.headers.get("content-type", "").lower()
|
||||
raw = await request.body()
|
||||
|
||||
# 2) парсим тело независимо от формата
|
||||
data: Dict[str, Any] = {}
|
||||
try:
|
||||
if "application/json" in ctype:
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
elif "application/x-www-form-urlencoded" in ctype or "multipart/form-data" in ctype:
|
||||
form = await request.form()
|
||||
data = {k: v for k, v in form.multi_items()}
|
||||
else:
|
||||
# если тип неизвестен — попробуем как JSON, затем как форма
|
||||
try:
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
except Exception:
|
||||
form = await request.form()
|
||||
data = {k: v for k, v in form.multi_items()}
|
||||
except Exception as e:
|
||||
# не роняем хендпоинт
|
||||
log.exception("Webhook parse error: %s", e)
|
||||
raise HTTPException(status_code=400, detail="Invalid webhook payload")
|
||||
|
||||
# 3)Sign может прийти как "Sign: <hex>" — уберём префикс, если он там повторно
|
||||
if sign and sign.lower().startswith("sign:"):
|
||||
sign = sign.split(":", 1)[1].strip()
|
||||
|
||||
# 4) нормализуем, маскируем PII для логов
|
||||
safe = normalize_payload(dict(data))
|
||||
print(safe)
|
||||
|
||||
code=safe['order_id']
|
||||
|
||||
await save_payment_completed_info_from_prodamus(code=code)
|
||||
|
||||
# 5) ЛОГИ (для отладки — потом переведите на структурные логи)
|
||||
log.info("Prodamus webhook: sign=%s, payload=%s, ctype=%s", _mask(sign), safe, ctype)
|
||||
|
||||
# 6) (опционально) в фоне проверим подпись и обновим БД/уведомим в Телеграм,
|
||||
# чтобы ответить Prodamus быстро и не словить ретраи из-за таймаута.
|
||||
#def process_business():
|
||||
# TODO: verify signature HMAC по правилам Prodamus
|
||||
# TODO: валидация суммы/валюты/статуса
|
||||
# TODO: идемпотентно обновить платеж в Postgres
|
||||
# TODO: отправить уведомление в бот (create_task/queue)
|
||||
#pass
|
||||
|
||||
return {"ok": True}#, BackgroundTask(process_business)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home():
|
||||
return {"message": "Hello World"}
|
||||
|
||||
|
||||
@app.post("/webhook")
|
||||
async def receive_webhook(request: Request):
|
||||
data = await request.json()
|
||||
# Process your data here
|
||||
return {"message": "Webhook received!"}
|
||||
Loading…
x
Reference in New Issue
Block a user