- 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.
202 lines
5.9 KiB
Python
202 lines
5.9 KiB
Python
"""Group duty pin: current duty message text, next shift end, pin CRUD. All accept session."""
|
|
|
|
from datetime import datetime, timezone
|
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from duty_teller.cache import duty_pin_cache
|
|
from duty_teller.db.repository import (
|
|
get_current_duty,
|
|
get_next_shift_end,
|
|
get_group_duty_pin,
|
|
save_group_duty_pin,
|
|
delete_group_duty_pin,
|
|
get_all_group_duty_pin_chat_ids,
|
|
is_trusted_group,
|
|
add_trusted_group,
|
|
remove_trusted_group,
|
|
)
|
|
from duty_teller.i18n import t
|
|
from duty_teller.utils.dates import parse_utc_iso
|
|
|
|
|
|
def get_pin_refresh_data(
|
|
session: Session, chat_id: int, tz_name: str, lang: str = "en"
|
|
) -> tuple[int | None, str, datetime | None]:
|
|
"""Get all data needed for pin refresh in a single DB session.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
tz_name: Timezone name for display.
|
|
lang: Language code for i18n.
|
|
|
|
Returns:
|
|
(message_id, duty_message_text, next_shift_end_utc).
|
|
message_id is None if no pin record. next_shift_end_utc is naive UTC or None.
|
|
"""
|
|
pin = get_group_duty_pin(session, chat_id)
|
|
message_id = pin.message_id if pin else None
|
|
if message_id is None:
|
|
return (None, t(lang, "duty.no_duty"), None)
|
|
now = datetime.now(timezone.utc)
|
|
text = get_duty_message_text(session, tz_name, lang)
|
|
next_end = get_next_shift_end(session, now)
|
|
return (message_id, text, next_end)
|
|
|
|
|
|
def format_duty_message(duty, user, tz_name: str, lang: str = "en") -> str:
|
|
"""Build the text for the pinned duty message.
|
|
|
|
Args:
|
|
duty: Duty instance or None.
|
|
user: User instance or None.
|
|
tz_name: Timezone name for display (e.g. Europe/Moscow).
|
|
lang: Language code for i18n ('ru' or 'en').
|
|
|
|
Returns:
|
|
Formatted message string; "No duty" if duty or user is None.
|
|
"""
|
|
if duty is None or user is None:
|
|
return t(lang, "duty.no_duty")
|
|
try:
|
|
tz = ZoneInfo(tz_name)
|
|
except ZoneInfoNotFoundError:
|
|
tz = ZoneInfo("Europe/Moscow")
|
|
tz_name = "Europe/Moscow"
|
|
start_dt = parse_utc_iso(duty.start_at)
|
|
end_dt = parse_utc_iso(duty.end_at)
|
|
start_local = start_dt.astimezone(tz)
|
|
end_local = end_dt.astimezone(tz)
|
|
offset_sec = (
|
|
start_local.utcoffset().total_seconds() if start_local.utcoffset() else 0
|
|
)
|
|
sign = "+" if offset_sec >= 0 else "-"
|
|
h, r = divmod(abs(int(offset_sec)), 3600)
|
|
m = r // 60
|
|
tz_hint = f"UTC{sign}{h:d}:{m:02d}, {tz_name}"
|
|
time_range = (
|
|
f"{start_local.strftime('%d.%m.%Y %H:%M')} — "
|
|
f"{end_local.strftime('%d.%m.%Y %H:%M')} ({tz_hint})"
|
|
)
|
|
label = t(lang, "duty.label")
|
|
lines = [
|
|
f"🕐 {label} {time_range}",
|
|
f"👤 {user.full_name}",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def get_duty_message_text(session: Session, tz_name: str, lang: str = "en") -> str:
|
|
"""Get current duty from DB and return formatted message text. Cached 90s."""
|
|
cache_key = ("duty_message_text", tz_name, lang)
|
|
text, found = duty_pin_cache.get(cache_key)
|
|
if found:
|
|
return text
|
|
now = datetime.now(timezone.utc)
|
|
result = get_current_duty(session, now)
|
|
if result is None:
|
|
text = t(lang, "duty.no_duty")
|
|
else:
|
|
duty, user = result
|
|
text = format_duty_message(duty, user, tz_name, lang)
|
|
duty_pin_cache.set(cache_key, text)
|
|
return text
|
|
|
|
|
|
def get_next_shift_end_utc(session: Session) -> datetime | None:
|
|
"""Return next shift end as naive UTC datetime for job scheduling. Cached 90s."""
|
|
cache_key = ("next_shift_end",)
|
|
value, found = duty_pin_cache.get(cache_key)
|
|
if found:
|
|
return value
|
|
result = get_next_shift_end(session, datetime.now(timezone.utc))
|
|
duty_pin_cache.set(cache_key, result)
|
|
return result
|
|
|
|
|
|
def save_pin(session: Session, chat_id: int, message_id: int) -> None:
|
|
"""Save or update the pinned duty message record for a chat.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
message_id: Message id to store.
|
|
"""
|
|
save_group_duty_pin(session, chat_id, message_id)
|
|
|
|
|
|
def delete_pin(session: Session, chat_id: int) -> None:
|
|
"""Remove the pinned message record for the chat (e.g. when bot leaves).
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
"""
|
|
delete_group_duty_pin(session, chat_id)
|
|
|
|
|
|
def get_message_id(session: Session, chat_id: int) -> int | None:
|
|
"""Return message_id for the pinned duty message in this chat.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
|
|
Returns:
|
|
Message id or None if no pin record.
|
|
"""
|
|
pin = get_group_duty_pin(session, chat_id)
|
|
return pin.message_id if pin else None
|
|
|
|
|
|
def get_all_pin_chat_ids(session: Session) -> list[int]:
|
|
"""Return all chat_ids that have a pinned duty message.
|
|
|
|
Used to restore update jobs on bot startup.
|
|
|
|
Args:
|
|
session: DB session.
|
|
|
|
Returns:
|
|
List of chat ids.
|
|
"""
|
|
return get_all_group_duty_pin_chat_ids(session)
|
|
|
|
|
|
def is_group_trusted(session: Session, chat_id: int) -> bool:
|
|
"""Check if the group is in the trusted list.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
|
|
Returns:
|
|
True if the group is trusted.
|
|
"""
|
|
return is_trusted_group(session, chat_id)
|
|
|
|
|
|
def trust_group(
|
|
session: Session, chat_id: int, added_by_user_id: int | None = None
|
|
) -> None:
|
|
"""Add the group to the trusted list.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
added_by_user_id: Telegram user id of the admin who added the group (optional).
|
|
"""
|
|
add_trusted_group(session, chat_id, added_by_user_id)
|
|
|
|
|
|
def untrust_group(session: Session, chat_id: int) -> None:
|
|
"""Remove the group from the trusted list.
|
|
|
|
Args:
|
|
session: DB session.
|
|
chat_id: Telegram chat id.
|
|
"""
|
|
remove_trusted_group(session, chat_id)
|