diff --git a/zilencer/migrations/0065_remotepushdevice.py b/zilencer/migrations/0065_remotepushdevice.py new file mode 100644 index 0000000000..9de20a5a8c --- /dev/null +++ b/zilencer/migrations/0065_remotepushdevice.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.3 on 2025-06-24 07:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0725_realmuserdefault_web_left_sidebar_unreads_count_summary_and_more"), + ("zilencer", "0001_squashed_0064_remotezulipserver_last_merge_base"), + ] + + operations = [ + migrations.CreateModel( + name="RemotePushDevice", + fields=[ + ("device_id", models.BigAutoField(primary_key=True, serialize=False)), + ("token", models.CharField(max_length=4096)), + ("token_kind", models.PositiveSmallIntegerField(choices=[(1, "APNs"), (2, "FCM")])), + ("push_account_id", models.BigIntegerField()), + ("expired_time", models.DateTimeField(null=True)), + ("ios_app_id", models.TextField(null=True)), + ( + "realm", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.realm" + ), + ), + ( + "remote_realm", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="zilencer.remoterealm", + ), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=("push_account_id", "token"), + name="unique_remote_push_device_push_account_id_token", + ) + ], + }, + ), + ] diff --git a/zilencer/models.py b/zilencer/models.py index cff38c18e0..1465617b0a 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -587,3 +587,48 @@ def has_stale_audit_log(server: RemoteZulipServer) -> bool: return True return False + + +class RemotePushDevice(models.Model): + """Each row corresponds to an account on an install of the app + registered to receive mobile push notifications. + """ + + device_id = models.BigAutoField(primary_key=True) + + # Set to null for self-hosters. `remote_realm` is set instead. + realm = models.ForeignKey(Realm, on_delete=models.CASCADE, null=True) + + # Set to null for zulip cloud. `realm` is set instead. + remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE, null=True) + + # A token that uniquely identifies the app instance. + # + # FCM and APNs don't specify a maximum token length, so we only enforce + # that they're at most the maximum FCM / APNs payload size of 4096 bytes. + token = models.CharField(max_length=4096) + + class TokenKind(models.IntegerChoices): + APNS = 1, "APNs" + FCM = 2, "FCM" + + token_kind = models.PositiveSmallIntegerField(choices=TokenKind.choices) + + # ID to identify an account within an install of the app. + push_account_id = models.BigIntegerField() + + expired_time = models.DateTimeField(null=True) + + # [optional] Contains the app id of the device if it is an iOS device + ios_app_id = models.TextField(null=True) + + class Meta: + constraints = [ + UniqueConstraint( + # Each app install (token) can have multiple accounts (push_account_id). + # The (push_account_id, token) pair needs to be unique to avoid sending + # redundant notifications to the same account on a device. + fields=["push_account_id", "token"], + name="unique_remote_push_device_push_account_id_token", + ), + ]