mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	This commit adds support to send encrypted push notifications to devices registered to receive encrypted notifications. URL: `POST /api/v1/remotes/push/e2ee/notify` payload: `realm_uuid` and `device_id_to_encrypted_data` The POST request needs to be authenticated with the server’s API key. Note: For Zulip Cloud, a background fact about the push bouncer is that it runs on the same server and database as the main application; it’s not a separate service. So, as an optimization we directly call 'send_e2ee_push_notifications' function and skip the HTTP request.
		
			
				
	
	
		
			144 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			144 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from typing import Literal
 | 
						|
 | 
						|
from django.db import models
 | 
						|
from django.db.models import CASCADE, F, Q, UniqueConstraint
 | 
						|
from django.db.models.functions import Lower
 | 
						|
 | 
						|
from zerver.lib.exceptions import (
 | 
						|
    InvalidBouncerPublicKeyError,
 | 
						|
    InvalidEncryptedPushRegistrationError,
 | 
						|
    RequestExpiredError,
 | 
						|
)
 | 
						|
from zerver.models.users import UserProfile
 | 
						|
 | 
						|
 | 
						|
class AbstractPushDeviceToken(models.Model):
 | 
						|
    APNS = 1
 | 
						|
    FCM = 2
 | 
						|
 | 
						|
    KINDS = (
 | 
						|
        (APNS, "apns"),
 | 
						|
        # The string value in the database is "gcm" for legacy reasons.
 | 
						|
        # TODO: We should migrate it.
 | 
						|
        (FCM, "gcm"),
 | 
						|
    )
 | 
						|
 | 
						|
    kind = models.PositiveSmallIntegerField(choices=KINDS)
 | 
						|
 | 
						|
    # The token is a unique device-specific token that is
 | 
						|
    # sent to us from each device:
 | 
						|
    #   - APNS token if kind == APNS
 | 
						|
    #   - FCM registration id if kind == FCM
 | 
						|
    token = models.CharField(max_length=4096, db_index=True)
 | 
						|
 | 
						|
    # TODO: last_updated should be renamed date_created, since it is
 | 
						|
    # no longer maintained as a last_updated value.
 | 
						|
    last_updated = models.DateTimeField(auto_now=True)
 | 
						|
 | 
						|
    # [optional] Contains the app id of the device if it is an iOS device
 | 
						|
    ios_app_id = models.TextField(null=True)
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        abstract = True
 | 
						|
 | 
						|
 | 
						|
class PushDeviceToken(AbstractPushDeviceToken):
 | 
						|
    # The user whose device this is
 | 
						|
    user = models.ForeignKey(UserProfile, db_index=True, on_delete=CASCADE)
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        constraints = [
 | 
						|
            models.UniqueConstraint(
 | 
						|
                "user_id",
 | 
						|
                "kind",
 | 
						|
                Lower(F("token")),
 | 
						|
                name="zerver_pushdevicetoken_apns_user_kind_token",
 | 
						|
                condition=Q(kind=AbstractPushDeviceToken.APNS),
 | 
						|
            ),
 | 
						|
            models.UniqueConstraint(
 | 
						|
                "user_id",
 | 
						|
                "kind",
 | 
						|
                "token",
 | 
						|
                name="zerver_pushdevicetoken_fcm_user_kind_token",
 | 
						|
                condition=Q(kind=AbstractPushDeviceToken.FCM),
 | 
						|
            ),
 | 
						|
        ]
 | 
						|
 | 
						|
 | 
						|
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.
 | 
						|
                #
 | 
						|
                # Also, the unique index created is used by queries in `get_push_accounts`,
 | 
						|
                # `register_push_device`, and `handle_register_push_device_to_bouncer`.
 | 
						|
                fields=["user", "push_account_id"],
 | 
						|
                name="unique_push_device_user_push_account_id",
 | 
						|
            ),
 | 
						|
        ]
 | 
						|
        indexes = [
 | 
						|
            models.Index(
 | 
						|
                # Used in 'send_push_notifications' function,
 | 
						|
                # in 'zerver/lib/push_notifications'.
 | 
						|
                fields=["user", "bouncer_device_id"],
 | 
						|
                condition=Q(bouncer_device_id__isnull=False),
 | 
						|
                name="zerver_pushdevice_user_bouncer_device_id_idx",
 | 
						|
            ),
 | 
						|
        ]
 | 
						|
 | 
						|
    @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"
 |