mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 08:41:00 +00:00
Start on push notification work
This commit is contained in:
parent
824f5b289c
commit
11e3ca12d4
10 changed files with 265 additions and 3 deletions
18
api/migrations/0003_token_push_subscription.py
Normal file
18
api/migrations/0003_token_push_subscription.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.1 on 2023-07-15 17:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0002_remove_token_code_token_revoked_alter_token_token_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="push_subscription",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,22 @@
|
|||
import urlman
|
||||
from django.db import models
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PushSubscriptionSchema(BaseModel):
|
||||
"""
|
||||
Basic validating schema for push data
|
||||
"""
|
||||
|
||||
class Keys(BaseModel):
|
||||
p256dh: str
|
||||
auth: str
|
||||
|
||||
endpoint: str
|
||||
keys: Keys
|
||||
alerts: dict[str, bool]
|
||||
policy: str
|
||||
server_key: str
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
|
@ -38,6 +55,8 @@ class Token(models.Model):
|
|||
updated = models.DateTimeField(auto_now=True)
|
||||
revoked = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
push_subscription = models.JSONField(blank=True, null=True)
|
||||
|
||||
class urls(urlman.Urls):
|
||||
edit = "/@{self.identity.handle}/settings/tokens/{self.id}/"
|
||||
|
||||
|
@ -49,3 +68,8 @@ class Token(models.Model):
|
|||
# TODO: Support granular scopes the other way?
|
||||
scope_prefix = scope.split(":")[0]
|
||||
return (scope in self.scopes) or (scope_prefix in self.scopes)
|
||||
|
||||
def set_push_subscription(self, data: dict):
|
||||
# Validate schema and assign
|
||||
self.push_subscription = PushSubscriptionSchema(**data).dict()
|
||||
self.save()
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from typing import Literal, Optional, Union
|
||||
|
||||
from django.conf import settings
|
||||
from hatchway import Field, Schema
|
||||
|
||||
from activities import models as activities_models
|
||||
from api import models as api_models
|
||||
from core.html import FediverseHtmlParser
|
||||
from users import models as users_models
|
||||
from users.services import IdentityService
|
||||
|
@ -15,6 +17,23 @@ class Application(Schema):
|
|||
client_id: str
|
||||
client_secret: str
|
||||
redirect_uri: str = Field(alias="redirect_uris")
|
||||
vapid_key: str | None
|
||||
|
||||
@classmethod
|
||||
def from_application(cls, application: api_models.Application) -> "Application":
|
||||
instance = cls.from_orm(application)
|
||||
instance.vapid_key = settings.SETUP.VAPID_PUBLIC_KEY
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_application_no_keys(
|
||||
cls, application: api_models.Application
|
||||
) -> "Application":
|
||||
instance = cls.from_orm(application)
|
||||
instance.vapid_key = settings.SETUP.VAPID_PUBLIC_KEY
|
||||
instance.client_id = ""
|
||||
instance.client_secret = ""
|
||||
return instance
|
||||
|
||||
|
||||
class CustomEmoji(Schema):
|
||||
|
@ -434,3 +453,53 @@ class Preferences(Schema):
|
|||
"reading:expand:spoilers": identity.config_identity.expand_content_warnings,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class PushSubscriptionKeys(Schema):
|
||||
p256dh: str
|
||||
auth: str
|
||||
|
||||
|
||||
class PushSubscriptionCreation(Schema):
|
||||
endpoint: str
|
||||
keys: PushSubscriptionKeys
|
||||
|
||||
|
||||
class PushDataAlerts(Schema):
|
||||
mention: bool = False
|
||||
status: bool = False
|
||||
reblog: bool = False
|
||||
follow: bool = False
|
||||
follow_request: bool = False
|
||||
favourite: bool = False
|
||||
poll: bool = False
|
||||
update: bool = False
|
||||
admin_sign_up: bool = Field(False, alias="admin.sign_up")
|
||||
admin_report: bool = Field(False, alias="admin.report")
|
||||
|
||||
|
||||
class PushData(Schema):
|
||||
alerts: PushDataAlerts
|
||||
policy: Literal["all", "followed", "follower", "none"] = "all"
|
||||
|
||||
|
||||
class PushSubscription(Schema):
|
||||
id: str
|
||||
endpoint: str
|
||||
alerts: PushDataAlerts
|
||||
policy: str
|
||||
server_key: str
|
||||
|
||||
@classmethod
|
||||
def from_token(
|
||||
cls,
|
||||
token: api_models.Token,
|
||||
) -> Optional["PushSubscription"]:
|
||||
value = token.push_subscription
|
||||
if value:
|
||||
value["id"] = "1"
|
||||
value["server_key"] = settings.VAPID_PUBLIC_KEY
|
||||
del value["keys"]
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
|
13
api/urls.py
13
api/urls.py
|
@ -15,6 +15,7 @@ from api.views import (
|
|||
notifications,
|
||||
polls,
|
||||
preferences,
|
||||
push,
|
||||
search,
|
||||
statuses,
|
||||
tags,
|
||||
|
@ -46,6 +47,7 @@ urlpatterns = [
|
|||
path("v1/announcements/<pk>/dismiss", announcements.announcement_dismiss),
|
||||
# Apps
|
||||
path("v1/apps", apps.add_app),
|
||||
path("v1/apps/verify_credentials", apps.verify_credentials),
|
||||
# Bookmarks
|
||||
path("v1/bookmarks", bookmarks.bookmarks),
|
||||
# Emoji
|
||||
|
@ -57,6 +59,7 @@ urlpatterns = [
|
|||
path("v1/follow_requests", follow_requests.follow_requests),
|
||||
# Instance
|
||||
path("v1/instance", instance.instance_info_v1),
|
||||
path("v1/instance/activity", instance.activity),
|
||||
path("v1/instance/peers", instance.peers),
|
||||
path("v2/instance", instance.instance_info_v2),
|
||||
# Lists
|
||||
|
@ -84,6 +87,16 @@ urlpatterns = [
|
|||
path("v1/polls/<id>/votes", polls.vote_poll),
|
||||
# Preferences
|
||||
path("v1/preferences", preferences.preferences),
|
||||
# Push
|
||||
path(
|
||||
"v1/push/subscription",
|
||||
methods(
|
||||
get=push.get_subscription,
|
||||
post=push.create_subscription,
|
||||
put=push.update_subscription,
|
||||
delete=push.delete_subscription,
|
||||
),
|
||||
),
|
||||
# Search
|
||||
path("v1/search", search.search),
|
||||
path("v2/search", search.search),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from hatchway import QueryOrBody, api_view
|
||||
|
||||
from .. import schemas
|
||||
from ..models import Application
|
||||
from api import schemas
|
||||
from api.decorators import scope_required
|
||||
from api.models import Application
|
||||
|
||||
|
||||
@api_view.post
|
||||
|
@ -18,4 +19,12 @@ def add_app(
|
|||
redirect_uris=redirect_uris,
|
||||
scopes=scopes,
|
||||
)
|
||||
return schemas.Application.from_orm(application)
|
||||
return schemas.Application.from_application(application)
|
||||
|
||||
|
||||
@scope_required("read")
|
||||
@api_view.get
|
||||
def verify_credentials(
|
||||
request,
|
||||
) -> schemas.Application:
|
||||
return schemas.Application.from_application_no_keys(request.token.application)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from hatchway import api_view
|
||||
|
||||
from activities.models import Post
|
||||
|
@ -145,3 +148,37 @@ def peers(request) -> list[str]:
|
|||
"domain", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@api_view.get
|
||||
def activity(request) -> list:
|
||||
"""
|
||||
Weekly activity endpoint
|
||||
"""
|
||||
# The stats are expensive to calculate, so don't do it very often
|
||||
stats = cache.get("instance_activity_stats")
|
||||
if stats is None:
|
||||
stats = []
|
||||
# Work out our most recent week start
|
||||
now = timezone.now()
|
||||
week_start = now.replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
) - datetime.timedelta(now.weekday())
|
||||
for i in range(12):
|
||||
week_end = week_start + datetime.timedelta(days=7)
|
||||
stats.append(
|
||||
{
|
||||
"week": int(week_start.timestamp()),
|
||||
"statuses": Post.objects.filter(
|
||||
local=True, created__gte=week_start, created__lt=week_end
|
||||
).count(),
|
||||
# TODO: Populate when we have identity activity tracking
|
||||
"logins": 0,
|
||||
"registrations": Identity.objects.filter(
|
||||
local=True, created__gte=week_start, created__lt=week_end
|
||||
).count(),
|
||||
}
|
||||
)
|
||||
week_start -= datetime.timedelta(days=7)
|
||||
cache.set("instance_activity_stats", stats, timeout=300)
|
||||
return stats
|
||||
|
|
70
api/views/push.py
Normal file
70
api/views/push.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from hatchway import ApiError, QueryOrBody, api_view
|
||||
|
||||
from api import schemas
|
||||
from api.decorators import scope_required
|
||||
|
||||
|
||||
@scope_required("push")
|
||||
@api_view.post
|
||||
def create_subscription(
|
||||
request,
|
||||
subscription: QueryOrBody[schemas.PushSubscriptionCreation],
|
||||
data: QueryOrBody[schemas.PushData],
|
||||
) -> schemas.PushSubscription:
|
||||
# First, check the server is set up to do push notifications
|
||||
if not settings.SETUP.VAPID_PRIVATE_KEY:
|
||||
raise Http404("Push not available")
|
||||
# Then, register this with our token
|
||||
request.token.set_push_subscription(
|
||||
{
|
||||
"endpoint": subscription.endpoint,
|
||||
"keys": subscription.keys,
|
||||
"alerts": data.alerts,
|
||||
"policy": data.policy,
|
||||
}
|
||||
)
|
||||
# Then return the subscription
|
||||
return schemas.PushSubscription.from_token(request.token) # type:ignore
|
||||
|
||||
|
||||
@scope_required("push")
|
||||
@api_view.get
|
||||
def get_subscription(request) -> schemas.PushSubscription:
|
||||
# First, check the server is set up to do push notifications
|
||||
if not settings.SETUP.VAPID_PRIVATE_KEY:
|
||||
raise Http404("Push not available")
|
||||
# Get the subscription if it exists
|
||||
subscription = schemas.PushSubscription.from_token(request.token)
|
||||
if not subscription:
|
||||
raise ApiError(404, "Not Found")
|
||||
return subscription
|
||||
|
||||
|
||||
@scope_required("push")
|
||||
@api_view.put
|
||||
def update_subscription(
|
||||
request, data: QueryOrBody[schemas.PushData]
|
||||
) -> schemas.PushSubscription:
|
||||
# First, check the server is set up to do push notifications
|
||||
if not settings.SETUP.VAPID_PRIVATE_KEY:
|
||||
raise Http404("Push not available")
|
||||
# Get the subscription if it exists
|
||||
subscription = schemas.PushSubscription.from_token(request.token)
|
||||
if not subscription:
|
||||
raise ApiError(404, "Not Found")
|
||||
# Update the subscription
|
||||
subscription.alerts = data.alerts
|
||||
subscription.policy = data.policy
|
||||
request.token.set_push_subscription(subscription)
|
||||
# Then return the subscription
|
||||
return schemas.PushSubscription.from_token(request.token) # type:ignore
|
||||
|
||||
|
||||
@scope_required("push")
|
||||
@api_view.delete
|
||||
def delete_subscription(request) -> dict:
|
||||
# Unset the subscription
|
||||
request.token.push_subscription = None
|
||||
return {}
|
|
@ -119,6 +119,11 @@ be provided to the containers from the first boot.
|
|||
``["andrew@aeracode.org"]`` (if you're doing this via shell, be careful
|
||||
about escaping!)
|
||||
|
||||
* If you want to support push notifications, set ``TAKAHE_VAPID_PUBLIC_KEY``
|
||||
and ``TAKAHE_VAPID_PRIVATE_KEY`` to a valid VAPID keypair (note that if you
|
||||
ever change these, push notifications will stop working). You can generate
|
||||
a keypair at `https://web-push-codelab.glitch.me/`_.
|
||||
|
||||
There are some other, optional variables you can tweak once the
|
||||
system is up and working - see :doc:`tuning` for more.
|
||||
|
||||
|
|
|
@ -25,6 +25,18 @@ or use the image name ``jointakahe/takahe:0.10``.
|
|||
Upgrade Notes
|
||||
-------------
|
||||
|
||||
VAPID keys and Push notifications
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Takahē now supports push notifications if you supply a valid VAPID keypair as
|
||||
the ``TAKAHE_VAPID_PUBLIC_KEY`` and ``TAKAHE_VAPID_PRIVATE_KEY`` environment
|
||||
variables. You can generate a keypair via `https://web-push-codelab.glitch.me/`_.
|
||||
|
||||
Note that users of apps may need to sign out and in again to their accounts for
|
||||
the app to notice that it can now do push notifications. Some apps, like Elk,
|
||||
may cache the fact your server didn't support it for a while.
|
||||
|
||||
|
||||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -146,6 +146,11 @@ class Settings(BaseSettings):
|
|||
STATOR_CONCURRENCY: int = 50
|
||||
STATOR_CONCURRENCY_PER_MODEL: int = 15
|
||||
|
||||
# Web Push keys
|
||||
# Generate via https://web-push-codelab.glitch.me/
|
||||
VAPID_PUBLIC_KEY: str | None = None
|
||||
VAPID_PRIVATE_KEY: str | None = None
|
||||
|
||||
PGHOST: str | None = None
|
||||
PGPORT: int | None = 5432
|
||||
PGNAME: str = "takahe"
|
||||
|
|
Loading…
Reference in a new issue