# retoor <retoor@molodetz.nl>
import asyncio
import logging
from datetime import datetime, timezone
from devplacepy.database import (
get_table,
notification_enabled,
get_silenced_uids,
)
from devplacepy.services.background import background
from devplacepy.utils.text import generate_uid, extract_mentions
logger = logging.getLogger(__name__)
PUSH_ICON = "/static/apple-touch-icon.png"
DEFAULT_PUSH_URL = "/notifications"
_push_tasks: set[asyncio.Task] = set()
async def _safe_notify(user_uid: str, payload: dict[str, str]) -> None:
from devplacepy import push
try:
await push.notify_user(user_uid, payload)
except Exception as e:
logger.warning("Push delivery failed for %s: %s", user_uid, e)
def _schedule_push(user_uid: str, message: str, target_url: str | None) -> None:
payload = {
"title": "DevPlace",
"message": message,
"icon": PUSH_ICON,
"url": target_url or DEFAULT_PUSH_URL,
}
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = background.loop
if loop is None:
logger.warning("no event loop available for push delivery to %s", user_uid)
return
asyncio.run_coroutine_threadsafe(_safe_notify(user_uid, payload), loop)
return
task = loop.create_task(_safe_notify(user_uid, payload))
_push_tasks.add(task)
task.add_done_callback(_push_tasks.discard)
def _schedule_telegram(user_uid: str, message: str) -> None:
from devplacepy.services.telegram import store as telegram_store
try:
telegram_store.enqueue_outbox(user_uid, message)
except Exception as e:
logger.warning("Telegram delivery enqueue failed for %s: %s", user_uid, e)
def create_notification(
user_uid: str,
notification_type: str,
message: str,
related_uid: str,
target_url: str | None = None,
) -> None:
background.submit(
_deliver_notification,
user_uid,
notification_type,
message,
related_uid,
target_url,
)
def _deliver_notification(
user_uid: str,
notification_type: str,
message: str,
related_uid: str,
target_url: str | None = None,
) -> None:
from devplacepy.templating import clear_unread_cache
if related_uid and related_uid in get_silenced_uids(user_uid):
return
in_app = notification_enabled(user_uid, notification_type, "in_app")
push = notification_enabled(user_uid, notification_type, "push")
telegram = notification_enabled(user_uid, notification_type, "telegram")
if in_app:
get_table("notifications").insert(
{
"uid": generate_uid(),
"user_uid": user_uid,
"type": notification_type,
"message": message,
"related_uid": related_uid,
"target_url": target_url,
"read": False,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
clear_unread_cache(user_uid)
if push:
_schedule_push(user_uid, message, target_url)
if telegram:
_schedule_telegram(user_uid, message)
from devplacepy.services.audit import record as audit
audit.record_system(
"notification.create",
actor_kind="system",
target_type="notification",
target_uid=related_uid,
summary=f"notification created: {message}",
metadata={
"type": notification_type,
"in_app": in_app,
"push": push,
"telegram": telegram,
},
links=[
audit.recipient(user_uid),
audit.link("source", "notification", related_uid),
],
)
def create_mention_notifications(content: str, actor_uid: str, target_url: str) -> None:
background.submit(_deliver_mention_notifications, content, actor_uid, target_url)
def _deliver_mention_notifications(
content: str, actor_uid: str, target_url: str
) -> None:
usernames = list(dict.fromkeys(extract_mentions(content)))
if not usernames:
return
users = get_table("users")
actor = users.find_one(uid=actor_uid)
if not actor:
return
actor_username = actor["username"]
for mentioned in users.find(users.table.columns.username.in_(usernames)):
if mentioned["uid"] != actor_uid:
create_notification(
mentioned["uid"],
"mention",
f"@{actor_username} mentioned you",
actor_uid,
target_url,
)