diff --git a/backend/app/api/v1/assignments.py b/backend/app/api/v1/assignments.py
index 7ce7bfb..089c545 100644
--- a/backend/app/api/v1/assignments.py
+++ b/backend/app/api/v1/assignments.py
@@ -354,7 +354,8 @@ async def create_dispute(
db,
user_id=assignment.participant.user_id,
marathon_title=marathon.title,
- challenge_title=assignment.challenge.title
+ challenge_title=assignment.challenge.title,
+ assignment_id=assignment_id
)
# Load relationships for response
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index cea12b1..8d8db03 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -23,6 +23,9 @@ class Settings(BaseSettings):
TELEGRAM_BOT_USERNAME: str = ""
TELEGRAM_LINK_TOKEN_EXPIRE_MINUTES: int = 10
+ # Frontend
+ FRONTEND_URL: str = "http://localhost:3000"
+
# Uploads
UPLOAD_DIR: str = "uploads"
MAX_IMAGE_SIZE: int = 15 * 1024 * 1024 # 15 MB
diff --git a/backend/app/main.py b/backend/app/main.py
index 4903c09..b9737e7 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,6 +1,13 @@
+import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pathlib import Path
diff --git a/backend/app/services/telegram_notifier.py b/backend/app/services/telegram_notifier.py
index 3f3def7..eda328c 100644
--- a/backend/app/services/telegram_notifier.py
+++ b/backend/app/services/telegram_notifier.py
@@ -22,7 +22,8 @@ class TelegramNotifier:
self,
chat_id: int,
text: str,
- parse_mode: str = "HTML"
+ parse_mode: str = "HTML",
+ reply_markup: dict | None = None
) -> bool:
"""Send a message to a Telegram chat."""
if not self.bot_token:
@@ -31,13 +32,17 @@ class TelegramNotifier:
try:
async with httpx.AsyncClient() as client:
+ payload = {
+ "chat_id": chat_id,
+ "text": text,
+ "parse_mode": parse_mode
+ }
+ if reply_markup:
+ payload["reply_markup"] = reply_markup
+
response = await client.post(
f"{self.api_url}/sendMessage",
- json={
- "chat_id": chat_id,
- "text": text,
- "parse_mode": parse_mode
- },
+ json=payload,
timeout=10.0
)
if response.status_code == 200:
@@ -53,7 +58,8 @@ class TelegramNotifier:
self,
db: AsyncSession,
user_id: int,
- message: str
+ message: str,
+ reply_markup: dict | None = None
) -> bool:
"""Send notification to a user by user_id."""
result = await db.execute(
@@ -61,10 +67,16 @@ class TelegramNotifier:
)
user = result.scalar_one_or_none()
- if not user or not user.telegram_id:
+ if not user:
+ logger.warning(f"[Notify] User {user_id} not found")
return False
- return await self.send_message(user.telegram_id, message)
+ if not user.telegram_id:
+ logger.warning(f"[Notify] User {user_id} ({user.nickname}) has no telegram_id")
+ return False
+
+ logger.info(f"[Notify] Sending to user {user.nickname} (telegram_id={user.telegram_id})")
+ return await self.send_message(user.telegram_id, message, reply_markup=reply_markup)
async def notify_marathon_participants(
self,
@@ -171,16 +183,41 @@ class TelegramNotifier:
db: AsyncSession,
user_id: int,
marathon_title: str,
- challenge_title: str
+ challenge_title: str,
+ assignment_id: int
) -> bool:
"""Notify user about dispute raised on their assignment."""
- message = (
- f"⚠️ На твоё задание подан спор\n\n"
- f"Марафон: {marathon_title}\n"
- f"Задание: {challenge_title}\n\n"
- f"Зайди на сайт, чтобы ответить на спор."
- )
- return await self.notify_user(db, user_id, message)
+ logger.info(f"[Dispute] Sending notification to user_id={user_id} for assignment_id={assignment_id}")
+
+ dispute_url = f"{settings.FRONTEND_URL}/assignments/{assignment_id}"
+ logger.info(f"[Dispute] URL: {dispute_url}")
+
+ # Telegram requires HTTPS for inline keyboard URLs
+ use_inline_button = dispute_url.startswith("https://")
+
+ if use_inline_button:
+ message = (
+ f"⚠️ На твоё задание подан спор\n\n"
+ f"Марафон: {marathon_title}\n"
+ f"Задание: {challenge_title}"
+ )
+ reply_markup = {
+ "inline_keyboard": [[
+ {"text": "Открыть спор", "url": dispute_url}
+ ]]
+ }
+ else:
+ message = (
+ f"⚠️ На твоё задание подан спор\n\n"
+ f"Марафон: {marathon_title}\n"
+ f"Задание: {challenge_title}\n\n"
+ f"🔗 {dispute_url}"
+ )
+ reply_markup = None
+
+ result = await self.notify_user(db, user_id, message, reply_markup=reply_markup)
+ logger.info(f"[Dispute] Notification result: {result}")
+ return result
async def notify_dispute_resolved(
self,