FlowDeck CI / lint (push) Successful in 43s
FlowDeck CI / test (push) Successful in 4m2s
FlowDeck CI / lint (pull_request) Successful in 42s
FlowDeck CI / test (pull_request) Successful in 4m3s
FlowDeck CI / docker (push) Successful in 1m2s
FlowDeck CI / docker (pull_request) Successful in 35s
tests/conftest.py: mutate the settings singleton (instead of rebinding) so DB + backup dir are isolated per test -> pytest-xdist safe. Real backup tests (snapshot/prune/admin API) and OAuth mock tests (Gitea/GitHub/link) replace the previous skips. init_db() now also creates webhook_subscriptions (full schema without the FastAPI lifespan). ruff check is clean; .eslintrc.json migrated to eslint.config.mjs (flat config). CI: lint job (ruff + eslint), parallel tests (-n auto), run on every branch push. VERSION 5.11.1.
148 lines
4.6 KiB
Python
148 lines
4.6 KiB
Python
"""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(
|
|
f"SELECT id, login, email FROM users WHERE lower(login) IN ({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
|