45 lines
1.8 KiB
Python
45 lines
1.8 KiB
Python
import asyncpg
|
|
from aiogram import BaseMiddleware
|
|
from aiogram.types import Update
|
|
|
|
|
|
def _chat_id(update: Update) -> int | None:
|
|
if update.message is not None:
|
|
return update.message.chat.id
|
|
if update.callback_query is not None and update.callback_query.message is not None:
|
|
return update.callback_query.message.chat.id
|
|
return None
|
|
|
|
|
|
class DedupMiddleware(BaseMiddleware):
|
|
"""Один update_id обрабатывается один раз, даже если Telegram прислал повтор.
|
|
|
|
Повторная доставка на другую реплику упирается в первичный ключ.
|
|
Сообщения одного чата сериализуются advisory-lock'ом в Postgres.
|
|
"""
|
|
|
|
def __init__(self, pool: asyncpg.Pool) -> None:
|
|
self.pool = pool
|
|
|
|
async def __call__(self, handler, event, data):
|
|
if not isinstance(event, Update):
|
|
return await handler(event, data)
|
|
|
|
async with self.pool.acquire() as conn:
|
|
inserted = await conn.fetchrow(
|
|
"INSERT INTO telegram_updates (update_id) VALUES ($1) "
|
|
"ON CONFLICT DO NOTHING RETURNING update_id",
|
|
event.update_id,
|
|
)
|
|
if inserted is None:
|
|
return None
|
|
try:
|
|
async with conn.transaction():
|
|
chat_id = _chat_id(event)
|
|
if chat_id is not None:
|
|
await conn.execute("SELECT pg_advisory_xact_lock($1)", chat_id)
|
|
return await handler(event, data)
|
|
except Exception:
|
|
await conn.execute("DELETE FROM telegram_updates WHERE update_id = $1", event.update_id)
|
|
raise
|