Add notification settings
This commit is contained in:
@@ -73,6 +73,21 @@ class TelegramStatsResponse(BaseModel):
|
||||
best_streak: int
|
||||
|
||||
|
||||
class TelegramNotificationSettings(BaseModel):
|
||||
notify_events: bool = True
|
||||
notify_disputes: bool = True
|
||||
notify_moderation: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TelegramNotificationSettingsUpdate(BaseModel):
|
||||
notify_events: bool | None = None
|
||||
notify_disputes: bool | None = None
|
||||
notify_moderation: bool | None = None
|
||||
|
||||
|
||||
# Endpoints
|
||||
@router.post("/generate-link-token", response_model=TelegramLinkToken)
|
||||
async def generate_link_token(current_user: CurrentUser):
|
||||
@@ -391,3 +406,46 @@ async def get_user_stats(telegram_id: int, db: DbSession, _: BotSecretDep):
|
||||
total_points=total_points,
|
||||
best_streak=best_streak
|
||||
)
|
||||
|
||||
|
||||
@router.get("/notifications/{telegram_id}", response_model=TelegramNotificationSettings | None)
|
||||
async def get_notification_settings(telegram_id: int, db: DbSession, _: BotSecretDep):
|
||||
"""Get user's notification settings by Telegram ID."""
|
||||
result = await db.execute(
|
||||
select(User).where(User.telegram_id == telegram_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
return TelegramNotificationSettings.model_validate(user)
|
||||
|
||||
|
||||
@router.patch("/notifications/{telegram_id}", response_model=TelegramNotificationSettings | None)
|
||||
async def update_notification_settings(
|
||||
telegram_id: int,
|
||||
data: TelegramNotificationSettingsUpdate,
|
||||
db: DbSession,
|
||||
_: BotSecretDep
|
||||
):
|
||||
"""Update user's notification settings by Telegram ID."""
|
||||
result = await db.execute(
|
||||
select(User).where(User.telegram_id == telegram_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if data.notify_events is not None:
|
||||
user.notify_events = data.notify_events
|
||||
if data.notify_disputes is not None:
|
||||
user.notify_disputes = data.notify_disputes
|
||||
if data.notify_moderation is not None:
|
||||
user.notify_moderation = data.notify_moderation
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
return TelegramNotificationSettings.model_validate(user)
|
||||
|
||||
@@ -9,7 +9,8 @@ from app.models.assignment import AssignmentStatus
|
||||
from app.models.marathon import MarathonStatus
|
||||
from app.schemas import (
|
||||
UserPublic, UserPrivate, UserUpdate, TelegramLink, MessageResponse,
|
||||
PasswordChange, UserStats, UserProfilePublic,
|
||||
PasswordChange, UserStats, UserProfilePublic, NotificationSettings,
|
||||
NotificationSettingsUpdate,
|
||||
)
|
||||
from app.services.storage import storage_service
|
||||
|
||||
@@ -189,6 +190,32 @@ async def change_password(
|
||||
return MessageResponse(message="Пароль успешно изменен")
|
||||
|
||||
|
||||
@router.get("/me/notifications", response_model=NotificationSettings)
|
||||
async def get_notification_settings(current_user: CurrentUser):
|
||||
"""Get current user's notification settings"""
|
||||
return NotificationSettings.model_validate(current_user)
|
||||
|
||||
|
||||
@router.patch("/me/notifications", response_model=NotificationSettings)
|
||||
async def update_notification_settings(
|
||||
data: NotificationSettingsUpdate,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
):
|
||||
"""Update current user's notification settings"""
|
||||
if data.notify_events is not None:
|
||||
current_user.notify_events = data.notify_events
|
||||
if data.notify_disputes is not None:
|
||||
current_user.notify_disputes = data.notify_disputes
|
||||
if data.notify_moderation is not None:
|
||||
current_user.notify_moderation = data.notify_moderation
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
|
||||
return NotificationSettings.model_validate(current_user)
|
||||
|
||||
|
||||
@router.get("/me/stats", response_model=UserStats)
|
||||
async def get_my_stats(current_user: CurrentUser, db: DbSession):
|
||||
"""Получить свою статистику"""
|
||||
|
||||
@@ -34,6 +34,11 @@ class User(Base):
|
||||
banned_by_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
ban_reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# Notification settings (all enabled by default)
|
||||
notify_events: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
notify_disputes: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
notify_moderation: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
created_marathons: Mapped[list["Marathon"]] = relationship(
|
||||
"Marathon",
|
||||
|
||||
@@ -9,6 +9,8 @@ from app.schemas.user import (
|
||||
PasswordChange,
|
||||
UserStats,
|
||||
UserProfilePublic,
|
||||
NotificationSettings,
|
||||
NotificationSettingsUpdate,
|
||||
)
|
||||
from app.schemas.marathon import (
|
||||
MarathonCreate,
|
||||
@@ -115,6 +117,8 @@ __all__ = [
|
||||
"PasswordChange",
|
||||
"UserStats",
|
||||
"UserProfilePublic",
|
||||
"NotificationSettings",
|
||||
"NotificationSettingsUpdate",
|
||||
# Marathon
|
||||
"MarathonCreate",
|
||||
"MarathonUpdate",
|
||||
|
||||
@@ -47,6 +47,10 @@ class UserPrivate(UserPublic):
|
||||
telegram_username: str | None = None
|
||||
telegram_first_name: str | None = None
|
||||
telegram_last_name: str | None = None
|
||||
# Notification settings
|
||||
notify_events: bool = True
|
||||
notify_disputes: bool = True
|
||||
notify_moderation: bool = True
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
@@ -83,3 +87,20 @@ class UserProfilePublic(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationSettings(BaseModel):
|
||||
"""Notification settings for Telegram bot"""
|
||||
notify_events: bool = True
|
||||
notify_disputes: bool = True
|
||||
notify_moderation: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationSettingsUpdate(BaseModel):
|
||||
"""Update notification settings"""
|
||||
notify_events: bool | None = None
|
||||
notify_disputes: bool | None = None
|
||||
notify_moderation: bool | None = None
|
||||
|
||||
@@ -83,9 +83,15 @@ class TelegramNotifier:
|
||||
db: AsyncSession,
|
||||
marathon_id: int,
|
||||
message: str,
|
||||
exclude_user_id: int | None = None
|
||||
exclude_user_id: int | None = None,
|
||||
check_setting: str | None = None
|
||||
) -> int:
|
||||
"""Send notification to all marathon participants with linked Telegram."""
|
||||
"""Send notification to all marathon participants with linked Telegram.
|
||||
|
||||
Args:
|
||||
check_setting: If provided, only send to users with this setting enabled.
|
||||
Options: 'notify_events', 'notify_disputes', 'notify_moderation'
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.join(Participant, Participant.user_id == User.id)
|
||||
@@ -100,6 +106,10 @@ class TelegramNotifier:
|
||||
for user in users:
|
||||
if exclude_user_id and user.id == exclude_user_id:
|
||||
continue
|
||||
# Check notification setting if specified
|
||||
if check_setting and not getattr(user, check_setting, True):
|
||||
logger.info(f"[Notify] Skipping user {user.nickname} - {check_setting} is disabled")
|
||||
continue
|
||||
if await self.send_message(user.telegram_id, message):
|
||||
sent_count += 1
|
||||
|
||||
@@ -113,7 +123,7 @@ class TelegramNotifier:
|
||||
event_type: str,
|
||||
marathon_title: str
|
||||
) -> int:
|
||||
"""Notify participants about event start."""
|
||||
"""Notify participants about event start (respects notify_events setting)."""
|
||||
event_messages = {
|
||||
"golden_hour": f"🌟 <b>Начался Golden Hour</b> в «{marathon_title}»!\n\nВсе очки x1.5 в течение часа!",
|
||||
"jackpot": f"🎰 <b>JACKPOT</b> в «{marathon_title}»!\n\nОчки x3 за следующий сложный челлендж!",
|
||||
@@ -128,7 +138,9 @@ class TelegramNotifier:
|
||||
f"📌 Новое событие в «{marathon_title}»!"
|
||||
)
|
||||
|
||||
return await self.notify_marathon_participants(db, marathon_id, message)
|
||||
return await self.notify_marathon_participants(
|
||||
db, marathon_id, message, check_setting='notify_events'
|
||||
)
|
||||
|
||||
async def notify_event_end(
|
||||
self,
|
||||
@@ -137,7 +149,7 @@ class TelegramNotifier:
|
||||
event_type: str,
|
||||
marathon_title: str
|
||||
) -> int:
|
||||
"""Notify participants about event end."""
|
||||
"""Notify participants about event end (respects notify_events setting)."""
|
||||
event_names = {
|
||||
"golden_hour": "Golden Hour",
|
||||
"jackpot": "Jackpot",
|
||||
@@ -150,7 +162,9 @@ class TelegramNotifier:
|
||||
event_name = event_names.get(event_type, "Событие")
|
||||
message = f"⏰ <b>{event_name}</b> в «{marathon_title}» завершён"
|
||||
|
||||
return await self.notify_marathon_participants(db, marathon_id, message)
|
||||
return await self.notify_marathon_participants(
|
||||
db, marathon_id, message, check_setting='notify_events'
|
||||
)
|
||||
|
||||
async def notify_marathon_start(
|
||||
self,
|
||||
@@ -186,7 +200,14 @@ class TelegramNotifier:
|
||||
challenge_title: str,
|
||||
assignment_id: int
|
||||
) -> bool:
|
||||
"""Notify user about dispute raised on their assignment."""
|
||||
"""Notify user about dispute raised on their assignment (respects notify_disputes setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_disputes:
|
||||
logger.info(f"[Dispute] Skipping user {user.nickname} - notify_disputes is disabled")
|
||||
return False
|
||||
|
||||
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}"
|
||||
@@ -227,7 +248,14 @@ class TelegramNotifier:
|
||||
challenge_title: str,
|
||||
is_valid: bool
|
||||
) -> bool:
|
||||
"""Notify user about dispute resolution."""
|
||||
"""Notify user about dispute resolution (respects notify_disputes setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_disputes:
|
||||
logger.info(f"[Dispute] Skipping user {user.nickname} - notify_disputes is disabled")
|
||||
return False
|
||||
|
||||
if is_valid:
|
||||
message = (
|
||||
f"❌ <b>Спор признан обоснованным</b>\n\n"
|
||||
@@ -251,7 +279,14 @@ class TelegramNotifier:
|
||||
marathon_title: str,
|
||||
game_title: str
|
||||
) -> bool:
|
||||
"""Notify user that their proposed game was approved."""
|
||||
"""Notify user that their proposed game was approved (respects notify_moderation setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_moderation:
|
||||
logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled")
|
||||
return False
|
||||
|
||||
message = (
|
||||
f"✅ <b>Твоя игра одобрена!</b>\n\n"
|
||||
f"Марафон: {marathon_title}\n"
|
||||
@@ -267,7 +302,14 @@ class TelegramNotifier:
|
||||
marathon_title: str,
|
||||
game_title: str
|
||||
) -> bool:
|
||||
"""Notify user that their proposed game was rejected."""
|
||||
"""Notify user that their proposed game was rejected (respects notify_moderation setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_moderation:
|
||||
logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled")
|
||||
return False
|
||||
|
||||
message = (
|
||||
f"❌ <b>Твоя игра отклонена</b>\n\n"
|
||||
f"Марафон: {marathon_title}\n"
|
||||
@@ -284,7 +326,14 @@ class TelegramNotifier:
|
||||
game_title: str,
|
||||
challenge_title: str
|
||||
) -> bool:
|
||||
"""Notify user that their proposed challenge was approved."""
|
||||
"""Notify user that their proposed challenge was approved (respects notify_moderation setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_moderation:
|
||||
logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled")
|
||||
return False
|
||||
|
||||
message = (
|
||||
f"✅ <b>Твой челлендж одобрен!</b>\n\n"
|
||||
f"Марафон: {marathon_title}\n"
|
||||
@@ -302,7 +351,14 @@ class TelegramNotifier:
|
||||
game_title: str,
|
||||
challenge_title: str
|
||||
) -> bool:
|
||||
"""Notify user that their proposed challenge was rejected."""
|
||||
"""Notify user that their proposed challenge was rejected (respects notify_moderation setting)."""
|
||||
# Check user's notification settings
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.notify_moderation:
|
||||
logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled")
|
||||
return False
|
||||
|
||||
message = (
|
||||
f"❌ <b>Твой челлендж отклонён</b>\n\n"
|
||||
f"Марафон: {marathon_title}\n"
|
||||
|
||||
Reference in New Issue
Block a user