"""FlowDeck — Notification service (v4.9.0 collaboration). Creates in-app notifications (mentions, comments, page changes) and triggers email delivery via :mod:`app.services.mailer` when the target user has opted in. """ from __future__ import annotations import json import re from app.db import get_conn from app.services import mailer def create_notification( user_id: int, actor_id: int | None, ntype: str, title: str, message: str, resource_type: str = "page", resource_id: int = 0, url: str = "", conn=None, commit: bool = True, ) -> int | None: """Insert a notification row. Returns the new id (or None if skipped). ``conn`` may be supplied to join an existing transaction (caller controls commit). Otherwise a dedicated connection is opened and committed. """ if not user_id: return None if conn is not None: cur = conn.execute( """INSERT INTO notifications (user_id, actor_id, ntype, title, message, resource_type, resource_id, url) VALUES (?,?,?,?,?,?,?,?)""", (user_id, actor_id, ntype, title, message, resource_type, resource_id, url), ) if commit: conn.commit() return cur.lastrowid with get_conn() as conn: cur = conn.execute( """INSERT INTO notifications (user_id, actor_id, ntype, title, message, resource_type, resource_id, url) VALUES (?,?,?,?,?,?,?,?)""", (user_id, actor_id, ntype, title, message, resource_type, resource_id, url), ) conn.commit() return cur.lastrowid _MENTION_RE = re.compile(r"(?:^|\s)@([\w\-\.]+)") def extract_mentions(text: str) -> list[str]: """Return the set of @login handles mentioned in ``text`` (lowercase).""" return list(dict.fromkeys(m.lower() for m in _MENTION_RE.findall(text or ""))) def process_mentions( text: str, actor_id: int, ntype: str, title: str, message: str, resource_type: str = "page", resource_id: int = 0, url: str = "", conn=None, ) -> list[int]: """Create notifications for every user @-mentioned in ``text``. Returns the list of mentioned user ids that were notified. """ handles = extract_mentions(text) if not handles: return [] notified = [] if conn is not None: _do_mentions(conn, handles, actor_id, ntype, title, message, resource_type, resource_id, url, notified) return notified with get_conn() as conn: _do_mentions(conn, handles, actor_id, ntype, title, message, resource_type, resource_id, url, notified) conn.commit() return notified def _do_mentions(conn, handles, actor_id, ntype, title, message, resource_type, resource_id, url, notified): placeholders = ",".join("?" * len(handles)) rows = conn.execute( "SELECT id, login, email FROM users WHERE lower(login) IN (%s)" % placeholders, handles, ).fetchall() for row in rows: if row["id"] == actor_id: continue create_notification( row["id"], actor_id, ntype, title, message, resource_type, resource_id, url, conn=conn, commit=False, ) notified.append(row["id"]) mailer.notify_user( row["id"], subject=title, body_text=message, cta_url=url or "", ) def get_user_prefs(user_id: int, conn=None) -> dict: if conn is not None: row = conn.execute( "SELECT notification_prefs FROM users WHERE id=?", (user_id,) ).fetchone() else: with get_conn() as conn: row = conn.execute( "SELECT notification_prefs FROM users WHERE id=?", (user_id,) ).fetchone() if not row or not row["notification_prefs"]: return {"comments": True, "mentions": True} try: prefs = json.loads(row["notification_prefs"]) except (TypeError, json.JSONDecodeError): prefs = {} return {"comments": bool(prefs.get("comments", True)), "mentions": bool(prefs.get("mentions", True))} def set_user_prefs(user_id: int, prefs: dict, conn=None) -> dict: if conn is not None: conn.execute( "UPDATE users SET notification_prefs=? WHERE id=?", (json.dumps(prefs), user_id), ) conn.commit() else: with get_conn() as conn: conn.execute( "UPDATE users SET notification_prefs=? WHERE id=?", (json.dumps(prefs), user_id), ) conn.commit() return prefs