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"
>
+