diff --git a/migrations.py b/migrations.py index 6461d57..a968549 100644 --- a/migrations.py +++ b/migrations.py @@ -277,3 +277,8 @@ async def m012_chat_schedules_seen_and_notification_jobs(db): ); """ ) + + +async def m013_persistent_notifications(db): + await db.execute("ALTER TABLE chat.categories ADD COLUMN persistent_notifications BOOLEAN NOT NULL DEFAULT FALSE;") + await db.execute("ALTER TABLE chat.chats ADD COLUMN last_admin_notification_at TIMESTAMP;") diff --git a/models.py b/models.py index 8646bcc..907235f 100644 --- a/models.py +++ b/models.py @@ -18,6 +18,7 @@ class CreateCategories(BaseModel): claim_split: float | None = 0 guest_notifications: bool | None = False public_note: str | None = DEFAULT_PUBLIC_NOTE + persistent_notifications: bool = False notify_telegram: str | None = None notify_nostr: str | None = None notify_email: str | None = None @@ -46,6 +47,7 @@ class Categories(BaseModel): claim_split: float | None = 0 guest_notifications: bool | None = False public_note: str | None = DEFAULT_PUBLIC_NOTE + persistent_notifications: bool = False notify_telegram: str | None = None notify_nostr: str | None = None notify_email: str | None = None @@ -152,6 +154,7 @@ class ChatSession(BaseModel): participants: list[dict] = Field(default_factory=list) messages: list[dict] = Field(default_factory=list) last_message_at: datetime | None = None + last_admin_notification_at: datetime | None = None created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) diff --git a/services.py b/services.py index f139c28..08dc95e 100644 --- a/services.py +++ b/services.py @@ -289,6 +289,7 @@ async def _notify_new_chat( message, "chat.new", ) + chat.last_admin_notification_at = datetime.now(timezone.utc) async def _notify_chat_reply( @@ -566,7 +567,41 @@ def _sanitize_public_chat(chat: ChatSession) -> ChatSession: return sanitized -async def _append_message(chat: ChatSession, message: ChatMessage, unread: bool) -> ChatSession: +async def _notify_persistent_message(chat: ChatSession, message: ChatMessage) -> None: + if message.sender_role != "public" or message.message_type != "message" or chat.resolved: + return + previous = [m for m in chat.messages if m.get("message_type", "message") == "message"] + if not previous or previous[0].get("sender_id") != message.sender_id: + return + if any(m.get("sender_role") == "admin" for m in previous): + return + category = await get_categories_by_id(chat.categories_id) + if not category or not category.persistent_notifications: + return + if not (category.notify_telegram or category.notify_nostr or category.notify_email): + return + last_notification = chat.last_admin_notification_at + if last_notification is None: + last_notification = datetime.fromisoformat(previous[0]["created_at"]) + if last_notification.tzinfo is None: + last_notification = last_notification.replace(tzinfo=timezone.utc) + if message.created_at - last_notification < timedelta(minutes=15): + return + await send_notification( + category.notify_telegram, + [category.notify_nostr] if category.notify_nostr else [], + _parse_notify_emails(category.notify_email), + f'Unanswered chat: "{message.message}" {_build_chat_link(None, chat)}', + "chat.reminder", + ) + chat.last_admin_notification_at = message.created_at + + +async def _append_message( + chat: ChatSession, message: ChatMessage, unread: bool, notify_persistent: bool = True +) -> ChatSession: + if notify_persistent: + await _notify_persistent_message(chat, message) payload = _serialize_message(message) chat.messages.append(payload) chat.last_message_at = message.created_at @@ -614,7 +649,7 @@ async def _handle_lnurlp_drawdown( ) if not chat.messages and not after_hours: await _notify_new_chat(category, chat, base_url, data.message) - await _append_message(chat, message, unread=True) + await _append_message(chat, message, unread=True, notify_persistent=not after_hours) if after_hours: await _notify_after_hours_admin(category, chat, message, base_url) else: @@ -689,7 +724,7 @@ async def _send_free_message( ) if not chat.messages and not after_hours: await _notify_new_chat(category, chat, base_url, data.message) - await _append_message(chat, message, unread=True) + await _append_message(chat, message, unread=True, notify_persistent=not after_hours) if after_hours: await _notify_after_hours_admin(category, chat, message, base_url) elif data.sender_role == "public" and not user_id: diff --git a/static/index.js b/static/index.js index 1c003b4..f231524 100644 --- a/static/index.js +++ b/static/index.js @@ -17,6 +17,7 @@ window.PageChat = { denomination: 'sat', claim_split: 0, guest_notifications: false, + persistent_notifications: false, public_note: 'we aim to reply as soon as possible but it may take up to 24hrs for a reply', notify_telegram: null, @@ -262,6 +263,7 @@ window.PageChat = { denomination: 'sat', claim_split: 0, guest_notifications: false, + persistent_notifications: false, public_note: 'we aim to reply as soon as possible but it may take up to 24hrs for a reply', notify_telegram: null, diff --git a/static/index.vue b/static/index.vue index 3318b41..895b5cc 100644 --- a/static/index.vue +++ b/static/index.vue @@ -544,6 +544,23 @@ label="Email address (comma separated)" hint="Optional notification target" > + +
+ Notify again when the chat starter sends another message at least 15 + minutes after the last notification, until support replies. +