mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 23:13:25 +00:00
push_notification: Update the payload data that gets encrypted.
This commit updates the data that gets encrypted to be the same on both android and iOS. The data and its format is almost the same as what we send as FCM payload to android clients with no E2EE support, changes are: For send push notification payload: * 'realm_id`, 'server', 'sender_email', and 'realm_uri' fields don't exist in the new payload. * 'event' field renamed to 'type' * 'stream' and 'stream_id' fields renamed to 'channel_name' and 'channel_id' respectively. * The value of 'recipient_type' will be 'channel' & 'direct' instead of 'stream' & 'private' respectively. * 'zulip_message_id' field renamed to 'message_id' For remove push notification payload: * 'realm_id`, 'server', and 'realm_uri' fields don't exist in the new payload. * 'event' field renamed to 'type' * 'zulip_message_ids' field renamed to 'message_ids' and it's value will be a JSON array instead of a string. In the existing iOS client, we have no code of our own involved in constructing the notifications in the UI, and instead we leave it to the iOS SDK to do so. Since, for clients with E2EE support the data is going to be interpreted by our own code, not by the iOS SDK - we are free to keep the same data and format. Co-authored-by: Tim Abbott <tabbott@zulip.com>
This commit is contained in:
committed by
Tim Abbott
parent
dd134ef325
commit
787d73f018
@@ -962,14 +962,15 @@ def truncate_content(content: str) -> tuple[str, bool]:
|
||||
return content[:200] + "…", True
|
||||
|
||||
|
||||
def get_base_payload(user_profile: UserProfile) -> dict[str, Any]:
|
||||
def get_base_payload(user_profile: UserProfile, for_legacy_clients: bool = True) -> dict[str, Any]:
|
||||
"""Common fields for all notification payloads."""
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
# These will let the app support logging into multiple realms and servers.
|
||||
data["server"] = settings.EXTERNAL_HOST
|
||||
data["realm_id"] = user_profile.realm.id
|
||||
data["realm_uri"] = user_profile.realm.url
|
||||
if for_legacy_clients:
|
||||
data["server"] = settings.EXTERNAL_HOST
|
||||
data["realm_id"] = user_profile.realm.id
|
||||
data["realm_uri"] = user_profile.realm.url
|
||||
data["realm_url"] = user_profile.realm.url
|
||||
data["realm_name"] = user_profile.realm.name
|
||||
data["user_id"] = user_profile.id
|
||||
@@ -991,22 +992,25 @@ def get_message_payload(
|
||||
mentioned_user_group_id: int | None = None,
|
||||
mentioned_user_group_name: str | None = None,
|
||||
can_access_sender: bool = True,
|
||||
for_legacy_clients: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Common fields for `message` payloads, for all platforms."""
|
||||
data = get_base_payload(user_profile)
|
||||
data = get_base_payload(user_profile, for_legacy_clients)
|
||||
|
||||
# `sender_id` is preferred, but some existing versions use `sender_email`.
|
||||
data["sender_id"] = message.sender.id
|
||||
if not can_access_sender:
|
||||
# A guest user can only receive a stream message from an
|
||||
# inaccessible user as we allow unsubscribed users to send
|
||||
# messages to streams. For direct messages, the guest gains
|
||||
# access to the user if they where previously inaccessible.
|
||||
data["sender_email"] = Address(
|
||||
username=f"user{message.sender.id}", domain=get_fake_email_domain(message.realm.host)
|
||||
).addr_spec
|
||||
else:
|
||||
data["sender_email"] = message.sender.email
|
||||
if for_legacy_clients:
|
||||
if not can_access_sender:
|
||||
# A guest user can only receive a stream message from an
|
||||
# inaccessible user as we allow unsubscribed users to send
|
||||
# messages to streams. For direct messages, the guest gains
|
||||
# access to the user if they where previously inaccessible.
|
||||
data["sender_email"] = Address(
|
||||
username=f"user{message.sender.id}",
|
||||
domain=get_fake_email_domain(message.realm.host),
|
||||
).addr_spec
|
||||
else:
|
||||
data["sender_email"] = message.sender.email
|
||||
|
||||
if mentioned_user_group_id is not None:
|
||||
assert mentioned_user_group_name is not None
|
||||
@@ -1014,12 +1018,21 @@ def get_message_payload(
|
||||
data["mentioned_user_group_name"] = mentioned_user_group_name
|
||||
|
||||
if message.recipient.type == Recipient.STREAM:
|
||||
data["recipient_type"] = "stream"
|
||||
data["stream"] = get_message_stream_name_from_database(message)
|
||||
data["stream_id"] = message.recipient.type_id
|
||||
channel_id = message.recipient.type_id
|
||||
channel_name = get_message_stream_name_from_database(message)
|
||||
|
||||
if for_legacy_clients:
|
||||
data["recipient_type"] = "stream"
|
||||
data["stream"] = channel_name
|
||||
data["stream_id"] = channel_id
|
||||
else:
|
||||
data["recipient_type"] = "channel"
|
||||
data["channel_name"] = channel_name
|
||||
data["channel_id"] = channel_id
|
||||
|
||||
data["topic"] = get_topic_display_name(message.topic_name(), user_profile.default_language)
|
||||
elif message.recipient.type == Recipient.DIRECT_MESSAGE_GROUP:
|
||||
data["recipient_type"] = "private"
|
||||
data["recipient_type"] = "private" if for_legacy_clients else "direct"
|
||||
# For group DMs, we need to fetch the users for the pm_users field.
|
||||
# Note that this doesn't do a separate database query, because both
|
||||
# functions use the get_display_recipient_by_id cache.
|
||||
@@ -1027,7 +1040,7 @@ def get_message_payload(
|
||||
if len(recipients) > 2:
|
||||
data["pm_users"] = direct_message_group_users(message.recipient.id)
|
||||
else: # Recipient.PERSONAL
|
||||
data["recipient_type"] = "private"
|
||||
data["recipient_type"] = "private" if for_legacy_clients else "direct"
|
||||
|
||||
return data
|
||||
|
||||
@@ -1161,10 +1174,16 @@ def get_message_payload_gcm(
|
||||
mentioned_user_group_id: int | None = None,
|
||||
mentioned_user_group_name: str | None = None,
|
||||
can_access_sender: bool = True,
|
||||
for_legacy_clients: bool = True,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""A `message` payload + options, for Android via FCM."""
|
||||
data = get_message_payload(
|
||||
user_profile, message, mentioned_user_group_id, mentioned_user_group_name, can_access_sender
|
||||
user_profile,
|
||||
message,
|
||||
mentioned_user_group_id,
|
||||
mentioned_user_group_name,
|
||||
can_access_sender,
|
||||
for_legacy_clients,
|
||||
)
|
||||
|
||||
if not can_access_sender:
|
||||
@@ -1178,12 +1197,17 @@ def get_message_payload_gcm(
|
||||
sender_avatar_url = absolute_avatar_url(message.sender)
|
||||
sender_name = message.sender.full_name
|
||||
|
||||
if for_legacy_clients:
|
||||
data["event"] = "message"
|
||||
data["zulip_message_id"] = message.id # message_id is reserved for CCS
|
||||
else:
|
||||
data["type"] = "message"
|
||||
data["message_id"] = message.id
|
||||
|
||||
assert message.rendered_content is not None
|
||||
with override_language(user_profile.default_language):
|
||||
content, unused = truncate_content(get_mobile_push_content(message.rendered_content))
|
||||
data.update(
|
||||
event="message",
|
||||
zulip_message_id=message.id, # message_id is reserved for CCS
|
||||
time=datetime_to_timestamp(message.date_sent),
|
||||
content=content,
|
||||
sender_full_name=sender_name,
|
||||
@@ -1193,16 +1217,39 @@ def get_message_payload_gcm(
|
||||
return data, gcm_options
|
||||
|
||||
|
||||
def get_payload_data_to_encrypt(
|
||||
user_profile: UserProfile,
|
||||
message: Message,
|
||||
mentioned_user_group_id: int | None = None,
|
||||
mentioned_user_group_name: str | None = None,
|
||||
can_access_sender: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
payload_data_to_encrypt, unused = get_message_payload_gcm(
|
||||
user_profile,
|
||||
message,
|
||||
mentioned_user_group_id,
|
||||
mentioned_user_group_name,
|
||||
can_access_sender,
|
||||
for_legacy_clients=False,
|
||||
)
|
||||
return payload_data_to_encrypt
|
||||
|
||||
|
||||
def get_remove_payload_gcm(
|
||||
user_profile: UserProfile,
|
||||
message_ids: list[int],
|
||||
for_legacy_clients: bool = True,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""A `remove` payload + options, for Android via FCM."""
|
||||
gcm_payload = get_base_payload(user_profile)
|
||||
gcm_payload.update(
|
||||
event="remove",
|
||||
zulip_message_ids=",".join(str(id) for id in message_ids),
|
||||
)
|
||||
gcm_payload = get_base_payload(user_profile, for_legacy_clients)
|
||||
|
||||
if for_legacy_clients:
|
||||
gcm_payload["event"] = "remove"
|
||||
gcm_payload["zulip_message_ids"] = ",".join(str(id) for id in message_ids)
|
||||
else:
|
||||
gcm_payload["type"] = "remove"
|
||||
gcm_payload["message_ids"] = message_ids
|
||||
|
||||
gcm_options = {"priority": "normal"}
|
||||
return gcm_payload, gcm_options
|
||||
|
||||
@@ -1221,6 +1268,16 @@ def get_remove_payload_apns(user_profile: UserProfile, message_ids: list[int]) -
|
||||
return apns_data
|
||||
|
||||
|
||||
def get_remove_payload_data_to_encrypt(
|
||||
user_profile: UserProfile,
|
||||
message_ids: list[int],
|
||||
) -> dict[str, Any]:
|
||||
payload_data_to_encrypt, unused = get_remove_payload_gcm(
|
||||
user_profile, message_ids, for_legacy_clients=False
|
||||
)
|
||||
return payload_data_to_encrypt
|
||||
|
||||
|
||||
def handle_remove_push_notification(user_profile_id: int, message_ids: list[int]) -> None:
|
||||
"""This should be called when a message that previously had a
|
||||
mobile push notification executed is read. This triggers a push to the
|
||||
@@ -1263,12 +1320,15 @@ def handle_remove_push_notification(user_profile_id: int, message_ids: list[int]
|
||||
truncated_message_ids = sorted(message_ids)[-MAX_APNS_MESSAGE_IDS:]
|
||||
gcm_payload, gcm_options = get_remove_payload_gcm(user_profile, truncated_message_ids)
|
||||
apns_payload = get_remove_payload_apns(user_profile, truncated_message_ids)
|
||||
payload_data_to_encrypt = get_remove_payload_data_to_encrypt(
|
||||
user_profile, truncated_message_ids
|
||||
)
|
||||
|
||||
# We need to call both the legacy/non-E2EE and E2EE functions
|
||||
# for sending mobile notifications, since we don't at this time
|
||||
# know which mobile app version the user may be using.
|
||||
send_push_notifications_legacy(user_profile, apns_payload, gcm_payload, gcm_options)
|
||||
send_push_notifications(user_profile, apns_payload, gcm_payload, is_removal=True)
|
||||
send_push_notifications(user_profile, payload_data_to_encrypt, is_removal=True)
|
||||
|
||||
# We intentionally use the non-truncated message_ids here. We are
|
||||
# assuming in this very rare case that the user has manually
|
||||
@@ -1402,8 +1462,7 @@ def get_encrypted_data(payload_data_to_encrypt: dict[str, Any], public_key_str:
|
||||
|
||||
def send_push_notifications(
|
||||
user_profile: UserProfile,
|
||||
apns_payload_data_to_encrypt: dict[str, Any],
|
||||
fcm_payload_data_to_encrypt: dict[str, Any],
|
||||
payload_data_to_encrypt: dict[str, Any],
|
||||
is_removal: bool = False,
|
||||
) -> None:
|
||||
# Uses 'zerver_pushdevice_user_bouncer_device_id_idx' index.
|
||||
@@ -1426,15 +1485,12 @@ def send_push_notifications(
|
||||
push_requests: list[FCMPushRequest | APNsPushRequest] = []
|
||||
for push_device in push_devices:
|
||||
assert push_device.bouncer_device_id is not None # for mypy
|
||||
encrypted_data = get_encrypted_data(payload_data_to_encrypt, push_device.push_public_key)
|
||||
if push_device.token_kind == PushDevice.TokenKind.APNS:
|
||||
apns_http_headers = APNsHTTPHeaders(
|
||||
apns_priority=apns_priority,
|
||||
apns_push_type=apns_push_type,
|
||||
)
|
||||
encrypted_data = get_encrypted_data(
|
||||
apns_payload_data_to_encrypt,
|
||||
push_device.push_public_key,
|
||||
)
|
||||
apns_payload = APNsPayload(
|
||||
push_account_id=push_device.push_account_id,
|
||||
encrypted_data=encrypted_data,
|
||||
@@ -1446,10 +1502,6 @@ def send_push_notifications(
|
||||
)
|
||||
push_requests.append(apns_push_request)
|
||||
else:
|
||||
encrypted_data = get_encrypted_data(
|
||||
fcm_payload_data_to_encrypt,
|
||||
push_device.push_public_key,
|
||||
)
|
||||
fcm_payload = PushRequestBasePayload(
|
||||
push_account_id=push_device.push_account_id,
|
||||
encrypted_data=encrypted_data,
|
||||
@@ -1667,13 +1719,16 @@ def handle_push_notification(user_profile_id: int, missed_message: dict[str, Any
|
||||
gcm_payload, gcm_options = get_message_payload_gcm(
|
||||
user_profile, message, mentioned_user_group_id, mentioned_user_group_name, can_access_sender
|
||||
)
|
||||
payload_data_to_encrypt = get_payload_data_to_encrypt(
|
||||
user_profile, message, mentioned_user_group_id, mentioned_user_group_name, can_access_sender
|
||||
)
|
||||
logger.info("Sending push notifications to mobile clients for user %s", user_profile_id)
|
||||
|
||||
# We need to call both the legacy/non-E2EE and E2EE functions
|
||||
# for sending mobile notifications, since we don't at this time
|
||||
# know which mobile app version the user may be using.
|
||||
send_push_notifications_legacy(user_profile, apns_payload, gcm_payload, gcm_options)
|
||||
send_push_notifications(user_profile, apns_payload, gcm_payload)
|
||||
send_push_notifications(user_profile, payload_data_to_encrypt)
|
||||
|
||||
|
||||
def send_test_push_notification_directly_to_devices(
|
||||
|
||||
Reference in New Issue
Block a user