feat: enhance duty information handling with contact details and current duty view

- Added `bot_username` to settings for dynamic retrieval of the bot's username.
- Implemented `_resolve_bot_username` function to fetch the bot's username if not set, improving user experience in group chats.
- Updated `DutyWithUser` schema to include optional `phone` and `username` fields for enhanced duty information.
- Enhanced API responses to include contact details for users, ensuring better communication.
- Introduced a new current duty view in the web app, displaying active duty information along with contact options.
- Updated CSS styles for better presentation of contact information in duty cards.
- Added unit tests to verify the inclusion of contact details in API responses and the functionality of the current duty view.
This commit is contained in:
2026-03-02 16:09:08 +03:00
parent f8aceabab5
commit e3240d0981
25 changed files with 1126 additions and 44 deletions

View File

@@ -6,7 +6,7 @@ from datetime import datetime, timezone
from typing import Literal
import duty_teller.config as config
from telegram import Update
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatMemberStatus
from telegram.error import BadRequest, Forbidden
from telegram.ext import ChatMemberHandler, CommandHandler, ContextTypes
@@ -81,6 +81,23 @@ def _sync_trust_group(chat_id: int, added_by_user_id: int | None) -> bool:
return False
def _get_contact_button_markup(lang: str) -> InlineKeyboardMarkup | None:
"""Return inline keyboard with 'View contacts' URL button, or None if BOT_USERNAME not set.
Uses a t.me Mini App deep link so the app opens inside Telegram. Uses url (not web_app):
InlineKeyboardButton with web_app is allowed only in private chats, so in groups
Telegram returns Button_type_invalid. A plain URL button works everywhere.
"""
if not config.BOT_USERNAME:
return None
url = f"https://t.me/{config.BOT_USERNAME}?startapp=duty"
button = InlineKeyboardButton(
text=t(lang, "pin_duty.view_contacts"),
url=url,
)
return InlineKeyboardMarkup([[button]])
def _sync_untrust_group(chat_id: int) -> tuple[bool, int | None]:
"""Remove group from trusted list.
@@ -180,7 +197,11 @@ async def _refresh_pin_for_chat(
return "no_message"
old_message_id = message_id
try:
msg = await context.bot.send_message(chat_id=chat_id, text=text)
msg = await context.bot.send_message(
chat_id=chat_id,
text=text,
reply_markup=_get_contact_button_markup(config.DEFAULT_LANGUAGE),
)
except (BadRequest, Forbidden) as e:
logger.warning(
"Failed to send duty message for pin refresh chat_id=%s: %s", chat_id, e
@@ -260,7 +281,11 @@ async def my_chat_member_handler(
None, lambda: _get_duty_message_text_sync(lang)
)
try:
msg = await context.bot.send_message(chat_id=chat_id, text=text)
msg = await context.bot.send_message(
chat_id=chat_id,
text=text,
reply_markup=_get_contact_button_markup(lang),
)
except (BadRequest, Forbidden) as e:
logger.warning("Failed to send duty message in chat_id=%s: %s", chat_id, e)
return
@@ -334,7 +359,11 @@ async def pin_duty_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
None, lambda: _get_duty_message_text_sync(lang)
)
try:
msg = await context.bot.send_message(chat_id=chat_id, text=text)
msg = await context.bot.send_message(
chat_id=chat_id,
text=text,
reply_markup=_get_contact_button_markup(lang),
)
except (BadRequest, Forbidden) as e:
logger.warning(
"Failed to send duty message for pin_duty chat_id=%s: %s", chat_id, e
@@ -422,7 +451,11 @@ async def trust_group_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
None, lambda: _get_duty_message_text_sync(lang)
)
try:
msg = await context.bot.send_message(chat_id=chat_id, text=text)
msg = await context.bot.send_message(
chat_id=chat_id,
text=text,
reply_markup=_get_contact_button_markup(lang),
)
except (BadRequest, Forbidden) as e:
logger.warning(
"Failed to send duty message after trust_group chat_id=%s: %s",