|
# 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,
|
|
)
|