Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""scheduled push notifications

Revision ID: ccf2b2be66fc
Revises: 27d034ace1bf
Create Date: 2026-07-06 22:26:06.383649

"""

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "ccf2b2be66fc"
down_revision = "27d034ace1bf"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"scheduled_push_notifications",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("message", sa.String(), nullable=False),
sa.Column("send_at", sa.DateTime(), nullable=False),
sa.Column("cancellation_key", sa.String(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="cascade"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_scheduled_push_notifications_cancellation_key"),
"scheduled_push_notifications",
["cancellation_key"],
unique=False,
)
op.create_index(
op.f("ix_scheduled_push_notifications_id"),
"scheduled_push_notifications",
["id"],
unique=False,
)
op.create_index(
op.f("ix_scheduled_push_notifications_send_at"),
"scheduled_push_notifications",
["send_at"],
unique=False,
)
op.create_index(
op.f("ix_scheduled_push_notifications_user_id"),
"scheduled_push_notifications",
["user_id"],
unique=False,
)
op.add_column(
"push_notification_subscriptions",
sa.Column(
"grants",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
server_default=sa.text(
'\'{"booking": false, "community": false, "reminder": false}\'::jsonb'
),
),
)


def downgrade() -> None:
op.drop_column("push_notification_subscriptions", "grants")
op.drop_index(
op.f("ix_scheduled_push_notifications_user_id"),
table_name="scheduled_push_notifications",
)
op.drop_index(
op.f("ix_scheduled_push_notifications_send_at"),
table_name="scheduled_push_notifications",
)
op.drop_index(
op.f("ix_scheduled_push_notifications_id"),
table_name="scheduled_push_notifications",
)
op.drop_index(
op.f("ix_scheduled_push_notifications_cancellation_key"),
table_name="scheduled_push_notifications",
)
op.drop_table("scheduled_push_notifications")
21 changes: 14 additions & 7 deletions rezervo/api/community.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from rezervo.api.common import get_db, token_auth_scheme
from rezervo.database import crud
from rezervo.database.crud import get_user_config_by_id
from rezervo.notify.push import notify_friend_request_web_push
from rezervo.schemas.community import (
Community,
Expand Down Expand Up @@ -44,11 +43,19 @@ def update_relationship(
)

if updated_relationship is UserRelationship.REQUEST_SENT:
receiver_push_subscriptions = get_user_config_by_id( # type: ignore
db, payload.user_id
).config.notifications.push_notification_subscriptions
if receiver_push_subscriptions is not None:
for subscription in receiver_push_subscriptions:
notify_friend_request_web_push(subscription, db_user.name)
receiver_config = crud.get_user_config_by_id(db, payload.user_id)
receiver_notifications = (
receiver_config.config.notifications
if receiver_config is not None
else None
)
if receiver_notifications is not None:
receiver_push_subscriptions = (
receiver_notifications.push_notification_subscriptions
)
if receiver_push_subscriptions is not None:
for subscription in receiver_push_subscriptions:
if subscription.grants.community:
notify_friend_request_web_push(subscription, db_user.name)

return updated_relationship
4 changes: 2 additions & 2 deletions rezervo/api/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


class Features(CamelModel):
class_reminder_notifications: bool
slack_connected: bool


@router.get("/features", response_model=Features)
Expand All @@ -27,7 +27,7 @@ def get_features(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
admin_config = AdminConfig(**db_user.admin_config)
return Features(
class_reminder_notifications=(
slack_connected=(
admin_config.notifications is not None
and admin_config.notifications.slack is not None
and admin_config.notifications.slack.user_id is not None
Expand Down
15 changes: 11 additions & 4 deletions rezervo/api/notifications/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from rezervo.api.common import get_db, token_auth_scheme
from rezervo.database import crud
from rezervo.schemas.config.app import AppConfig
from rezervo.schemas.config.config import PushNotificationSubscription, read_app_config
from rezervo.schemas.config.config import (
PushNotificationGrants,
PushNotificationSubscription,
read_app_config,
)

router = APIRouter()

Expand Down Expand Up @@ -56,14 +60,17 @@ def unsubscribe_from_push_notifications(
return None


@router.post("/notifications/push/verify", response_model=bool)
@router.post("/notifications/push/verify", response_model=PushNotificationGrants | None)
def verify_push_notifications_subscription(
subscription: PushNotificationSubscription,
token=Depends(token_auth_scheme),
db: Session = Depends(get_db),
app_config: AppConfig = Depends(read_app_config),
):
) -> PushNotificationGrants | None:
db_user = crud.user_from_token(db, app_config, token)
if db_user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return crud.verify_push_notification_subscription(db, db_user.id, subscription)
for existing in crud.get_user_push_notification_subscriptions(db, db_user.id):
if existing.endpoint == subscription.endpoint:
return existing.grants
return None
2 changes: 2 additions & 0 deletions rezervo/api/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from rezervo.api.common import get_db, token_auth_scheme
from rezervo.database import crud
from rezervo.notify.notify import reconcile_scheduled_push_reminders
from rezervo.schemas.config.app import AppConfig
from rezervo.schemas.config.config import read_app_config
from rezervo.schemas.config.user import UserPreferences
Expand Down Expand Up @@ -36,4 +37,5 @@ def upsert_user_preferences(
db_user.preferences = preferences.model_dump()
db.commit()
db.refresh(db_user)
reconcile_scheduled_push_reminders(db, db_user.id, preferences.notifications)
return db_user.preferences
5 changes: 5 additions & 0 deletions rezervo/chains/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from rezervo import models
from rezervo.chains.active import get_chain
from rezervo.database import crud
from rezervo.database.database import SessionLocal
from rezervo.errors import AuthenticationError, BookingError
from rezervo.notify.slack import delete_scheduled_dm_slack, notify_cancellation_slack
Expand Down Expand Up @@ -59,6 +60,10 @@ async def cancel_booking(
auth_data, _class, config, user_id
)
if res is None:
with SessionLocal() as db:
crud.delete_scheduled_push_notifications_for_class(
db, user_id, chain_identifier, _class.id
)
if config.notifications is not None and config.notifications.slack is not None:
update_slack_notifications_with_cancellation(
chain_identifier, _class, config.notifications.slack
Expand Down
9 changes: 9 additions & 0 deletions rezervo/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from rezervo.errors import AuthenticationError, BookingError
from rezervo.notify.apprise import aprs
from rezervo.notify.notify import notify_auth_failure, notify_booking_failure
from rezervo.notify.scheduled import send_due_scheduled_push_notifications
from rezervo.schemas.config.user import (
ChainIdentifier,
)
Expand Down Expand Up @@ -274,6 +275,14 @@ def purge_slack_receipts_cli():
log.debug("No expired Slack notification receipts")


@cli.command(name="process_scheduled_push")
def process_scheduled_push_cli():
"""
Dispatch scheduled web push notifications that are due (e.g. class reminders)
"""
send_due_scheduled_push_notifications()


@cli.command(name="extend_auth_sessions")
async def extend_auth_sessions_cli():
"""
Expand Down
6 changes: 6 additions & 0 deletions rezervo/cli/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ def initialize_cron():
schedule="0 0 * * *",
comment="purge slack receipts",
)
upsert_cli_cron_job(
crontab,
command="process_scheduled_push",
schedule="* * * * *",
comment="process scheduled push notifications",
)
upsert_cli_cron_job(
crontab,
command="purge_playwright",
Expand Down
Loading