Calendar reminders as push notifications
A job every minute finds the event reminders that fell due — repeating events and moved occurrences expanded as the calendar view does — and pushes each once to every device with notifications on (sdk.push, new: plugins can notify through the new-mail push delivery). A reminder missed while the worker was down is still sent within 10 minutes; cancelled events and declined invitations stay quiet. Title, then "In 15 minutes, 14:00 · Room 2" in the user's zone and clock format; it stays until dismissed and shows even with the app open; clicking it opens that day (/p/calendar?date=…, new deep link). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,12 @@ class PluginSdk:
|
||||
CalDAV/CardDAV/ICS. Returns status + headers + body."""
|
||||
return PluginHttp()
|
||||
|
||||
# --- notifications ------------------------------------------------------
|
||||
@property
|
||||
def push(self) -> PluginPush:
|
||||
"""Notifications on the user's devices (Web Push) — calendar reminders."""
|
||||
return PluginPush(self.name)
|
||||
|
||||
# --- secret vault -------------------------------------------------------
|
||||
@property
|
||||
def vault(self) -> PluginVault:
|
||||
@@ -187,6 +193,39 @@ class PluginSdk:
|
||||
return PluginVault()
|
||||
|
||||
|
||||
class PluginPush:
|
||||
"""``sdk.push``: show a notification on every device where the user turned
|
||||
notifications on (Settings → Accounts → Notifications)."""
|
||||
|
||||
def __init__(self, plugin: str) -> None:
|
||||
self._plugin = plugin
|
||||
|
||||
async def notify(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
*,
|
||||
title: str,
|
||||
body: str = "",
|
||||
url: str = "/mail",
|
||||
tag: str | None = None,
|
||||
sticky: bool = False,
|
||||
) -> int:
|
||||
"""Send one notification; returns how many devices it went to. ``tag``
|
||||
replaces an earlier notification with the same tag (it is prefixed with
|
||||
the plugin's name); ``sticky`` keeps it on screen until dismissed."""
|
||||
from app.push.service import deliver
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"type": f"plugin.{self._plugin}",
|
||||
"title": title[:200],
|
||||
"body": body[:500],
|
||||
"url": url if url.startswith("/") else "/mail",
|
||||
"tag": f"{self._plugin}-{tag}" if tag else self._plugin,
|
||||
"sticky": sticky,
|
||||
}
|
||||
return await deliver(user_id, [payload])
|
||||
|
||||
|
||||
def sdk(name: str) -> PluginSdk:
|
||||
"""Return the SDK facade bound to plugin ``name`` (SPEC §11)."""
|
||||
return PluginSdk(name)
|
||||
|
||||
@@ -200,6 +200,26 @@ async def notify(user_id: uuid.UUID, message_ids: list[uuid.UUID]) -> int:
|
||||
pushes were delivered to push services."""
|
||||
from app.db import session_scope
|
||||
|
||||
async with session_scope() as db:
|
||||
if not await db.scalar(
|
||||
select(PushSubscription.id).where(PushSubscription.user_id == user_id).limit(1)
|
||||
):
|
||||
return 0
|
||||
messages = await _news(db, user_id, message_ids)
|
||||
if not messages:
|
||||
return 0
|
||||
payloads = _payloads(messages, await _unread(db, user_id))
|
||||
return await deliver(user_id, payloads)
|
||||
|
||||
|
||||
async def deliver(user_id: uuid.UUID, payloads: list[dict[str, object]]) -> int:
|
||||
"""Push ``payloads`` to every device of the user that is still signed in
|
||||
(for new mail, and for plugins: calendar reminders); returns how many were
|
||||
handed to push services. Devices that are gone are forgotten."""
|
||||
from app.db import session_scope
|
||||
|
||||
if not payloads:
|
||||
return 0
|
||||
async with session_scope() as db:
|
||||
# Plain values: the rows expire when the key pair is first saved below.
|
||||
subs = [
|
||||
@@ -210,10 +230,6 @@ async def notify(user_id: uuid.UUID, message_ids: list[uuid.UUID]) -> int:
|
||||
]
|
||||
if not subs:
|
||||
return 0
|
||||
messages = await _news(db, user_id, message_ids)
|
||||
if not messages:
|
||||
return 0
|
||||
payloads = _payloads(messages, await _unread(db, user_id))
|
||||
key = await vapid(db)
|
||||
subject = _subject(await db.get(AppSettings, 1))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user