mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
zerver: Add PushDevice
model.
This commit adds a `PushDevice` model where each row corresponds to an account on an install of the app that has attempted to register with the bouncer to receive mobile push notifications. This is the core server table storing registrations that are potentially registered with the mobile push notifications bouncer service.
This commit is contained in:
committed by
Tim Abbott
parent
3c6a3b0d77
commit
6a4b06b6f4
@@ -176,6 +176,7 @@ ALL_ZULIP_TABLES = {
|
||||
"zerver_preregistrationuser_streams",
|
||||
"zerver_preregistrationuser_groups",
|
||||
"zerver_presencesequence",
|
||||
"zerver_pushdevice",
|
||||
"zerver_pushdevicetoken",
|
||||
"zerver_reaction",
|
||||
"zerver_realm",
|
||||
@@ -237,6 +238,7 @@ NON_EXPORTED_TABLES = {
|
||||
"zerver_scheduledmessagenotificationemail",
|
||||
# When switching servers, clients will need to re-log in and
|
||||
# reregister for push notifications anyway.
|
||||
"zerver_pushdevice",
|
||||
"zerver_pushdevicetoken",
|
||||
# We don't use these generated Django tables
|
||||
"zerver_userprofile_groups",
|
||||
|
58
zerver/migrations/0730_pushdevice.py
Normal file
58
zerver/migrations/0730_pushdevice.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-13 03:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0729_externalauthid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PushDevice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
(
|
||||
"token_kind",
|
||||
models.CharField(choices=[("apns", "APNs"), ("fcm", "FCM")], max_length=4),
|
||||
),
|
||||
("push_account_id", models.BigIntegerField()),
|
||||
("push_public_key", models.TextField()),
|
||||
("bouncer_device_id", models.BigIntegerField(null=True)),
|
||||
(
|
||||
"error_code",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("INVALID_BOUNCER_PUBLIC_KEY", "Invalid Bouncer Public Key"),
|
||||
("BAD_REQUEST", "Invalid Encrypted Push Registration"),
|
||||
("REQUEST_EXPIRED", "Request Expired"),
|
||||
],
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
fields=("user", "push_account_id"),
|
||||
name="unique_push_device_user_push_account_id",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
@@ -42,6 +42,7 @@ from zerver.models.prereg_users import PreregistrationUser as PreregistrationUse
|
||||
from zerver.models.prereg_users import RealmReactivationStatus as RealmReactivationStatus
|
||||
from zerver.models.presence import UserPresence as UserPresence
|
||||
from zerver.models.presence import UserStatus as UserStatus
|
||||
from zerver.models.push_notifications import AbstractPushDevice as AbstractPushDevice
|
||||
from zerver.models.push_notifications import AbstractPushDeviceToken as AbstractPushDeviceToken
|
||||
from zerver.models.push_notifications import PushDeviceToken as PushDeviceToken
|
||||
from zerver.models.realm_audit_logs import AbstractRealmAuditLog as AbstractRealmAuditLog
|
||||
|
@@ -1,6 +1,13 @@
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from typing import Literal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE, UniqueConstraint
|
||||
|
||||
from zerver.lib.exceptions import (
|
||||
InvalidBouncerPublicKeyError,
|
||||
InvalidEncryptedPushRegistrationError,
|
||||
RequestExpiredError,
|
||||
)
|
||||
from zerver.models.users import UserProfile
|
||||
|
||||
|
||||
@@ -40,3 +47,69 @@ class PushDeviceToken(AbstractPushDeviceToken):
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "kind", "token")
|
||||
|
||||
|
||||
class AbstractPushDevice(models.Model):
|
||||
class TokenKind(models.TextChoices):
|
||||
APNS = "apns", "APNs"
|
||||
FCM = "fcm", "FCM"
|
||||
|
||||
token_kind = models.CharField(max_length=4, choices=TokenKind.choices)
|
||||
|
||||
# 64-bit random integer ID generated by the client; will only be
|
||||
# guaranteed to be unique within the client's own table of accounts.
|
||||
push_account_id = models.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class PushDevice(AbstractPushDevice):
|
||||
"""Core zulip server table storing registrations that are potentially
|
||||
registered with the mobile push notifications bouncer service.
|
||||
|
||||
Each row corresponds to an account on an install of the app
|
||||
that has attempted to register with the bouncer to receive
|
||||
mobile push notifications.
|
||||
"""
|
||||
|
||||
# The user on this server to whom this PushDevice belongs.
|
||||
user = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
||||
|
||||
# Key to use to encrypt notifications for delivery to this device.
|
||||
push_public_key = models.TextField()
|
||||
|
||||
# `device_id` of the corresponding `RemotePushDevice`
|
||||
# row created after successful registration to bouncer.
|
||||
# Set to NULL while registration to bouncer is in progress or failed.
|
||||
bouncer_device_id = models.BigIntegerField(null=True)
|
||||
|
||||
class ErrorCode(models.TextChoices):
|
||||
INVALID_BOUNCER_PUBLIC_KEY = InvalidBouncerPublicKeyError.code.name
|
||||
INVALID_ENCRYPTED_PUSH_REGISTRATION = InvalidEncryptedPushRegistrationError.code.name
|
||||
REQUEST_EXPIRED = RequestExpiredError.code.name
|
||||
|
||||
# The error code returned when registration to bouncer fails.
|
||||
# Set to NULL if the registration is in progress or successful.
|
||||
error_code = models.CharField(max_length=100, choices=ErrorCode.choices, null=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
# In theory, there's possibility that a user with
|
||||
# two different devices having the same 'push_account_id'
|
||||
# generated by the client to register. But the API treats
|
||||
# that as idempotent request.
|
||||
# We treat (user, push_account_id) as a unique registration.
|
||||
fields=["user", "push_account_id"],
|
||||
name="unique_push_device_user_push_account_id",
|
||||
),
|
||||
]
|
||||
|
||||
@property
|
||||
def status(self) -> Literal["active", "pending", "failed"]:
|
||||
if self.error_code is not None:
|
||||
return "failed"
|
||||
elif self.bouncer_device_id is None:
|
||||
return "pending"
|
||||
return "active"
|
||||
|
@@ -11,7 +11,13 @@ from typing_extensions import override
|
||||
from analytics.models import BaseCount
|
||||
from zerver.lib.rate_limiter import RateLimitedObject
|
||||
from zerver.lib.rate_limiter import rules as rate_limiter_rules
|
||||
from zerver.models import AbstractPushDeviceToken, AbstractRealmAuditLog, Realm, UserProfile
|
||||
from zerver.models import (
|
||||
AbstractPushDevice,
|
||||
AbstractPushDeviceToken,
|
||||
AbstractRealmAuditLog,
|
||||
Realm,
|
||||
UserProfile,
|
||||
)
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
|
||||
|
||||
@@ -594,7 +600,7 @@ def has_stale_audit_log(server: RemoteZulipServer) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class RemotePushDevice(models.Model):
|
||||
class RemotePushDevice(AbstractPushDevice):
|
||||
"""Core bouncer server table storing registrations to receive
|
||||
mobile push notifications via the bouncer server.
|
||||
|
||||
@@ -618,16 +624,6 @@ class RemotePushDevice(models.Model):
|
||||
# that they're at most the maximum FCM / APNs payload size of 4096 bytes.
|
||||
token = models.CharField(max_length=4096)
|
||||
|
||||
class TokenKind(models.TextChoices):
|
||||
APNS = "apns", "APNs"
|
||||
FCM = "fcm", "FCM"
|
||||
|
||||
token_kind = models.CharField(max_length=4, choices=TokenKind.choices)
|
||||
|
||||
# 64-bit random integer ID generated by the client; will only be
|
||||
# guaranteed to be unique within the client's own table of accounts.
|
||||
push_account_id = models.BigIntegerField()
|
||||
|
||||
# If the token is expired, the date when the bouncer learned it
|
||||
# was expired via an error from the FCM/APNs server. Used to
|
||||
# support delayed deletion. Null if the bouncer believes this
|
||||
|
Reference in New Issue
Block a user