- Introduced a new `trusted_groups` table to store groups authorized to receive duty information. - Implemented functions to add, remove, and check trusted groups in the database. - Enhanced command handlers to manage trusted groups, including `/trust_group` and `/untrust_group` commands for admin users. - Updated internationalization messages to support new commands and group status notifications. - Added unit tests for trusted groups repository functions to ensure correct behavior and data integrity.
206 lines
6.0 KiB
Python
206 lines
6.0 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}",
|
|
]
|
|
if user.phone:
|
|
lines.append(f"📞 {user.phone}")
|
|
if user.username:
|
|
lines.append(f"@{user.username}")
|
|
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)
|