diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index a3b850bfa..6e1f893c5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -239,7 +239,7 @@ public class MessageController { if (destinationDevice.isPresent()) { Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment(); - sendMessage(source, destination.get(), destinationDevice.get(), destinationUuid, messages.timestamp(), messages.online(), incomingMessage, userAgent); + sendMessage(source, destination.get(), destinationDevice.get(), destinationUuid, messages.timestamp(), messages.online(), messages.urgent(), incomingMessage, userAgent); } } @@ -523,6 +523,7 @@ public class MessageController { UUID destinationUuid, long timestamp, boolean online, + boolean urgent, IncomingMessage incomingMessage, String userAgentString) throws NoSuchUserException { @@ -533,7 +534,8 @@ public class MessageController { envelope = incomingMessage.toEnvelope(destinationUuid, source.map(AuthenticatedAccount::getAccount).orElse(null), source.map(authenticatedAccount -> authenticatedAccount.getAuthenticatedDevice().getId()).orElse(null), - timestamp == 0 ? System.currentTimeMillis() : timestamp); + timestamp == 0 ? System.currentTimeMillis() : timestamp, + urgent); } catch (final IllegalArgumentException e) { logger.warn("Received bad envelope type {} from {}", incomingMessage.type(), userAgentString); throw new BadRequestException(e); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java index 58ec5165c..fe6176e5f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java @@ -13,7 +13,12 @@ import org.whispersystems.textsecuregcm.storage.Account; public record IncomingMessage(int type, long destinationDeviceId, int destinationRegistrationId, String content) { - public MessageProtos.Envelope toEnvelope(final UUID destinationUuid, @Nullable Account sourceAccount, @Nullable Long sourceDeviceId, final long timestamp) { + public MessageProtos.Envelope toEnvelope(final UUID destinationUuid, + @Nullable Account sourceAccount, + @Nullable Long sourceDeviceId, + final long timestamp, + final boolean urgent) { + final MessageProtos.Envelope.Type envelopeType = MessageProtos.Envelope.Type.forNumber(type()); if (envelopeType == null) { @@ -25,7 +30,8 @@ public record IncomingMessage(int type, long destinationDeviceId, int destinatio envelopeBuilder.setType(envelopeType) .setTimestamp(timestamp) .setServerTimestamp(System.currentTimeMillis()) - .setDestinationUuid(destinationUuid.toString()); + .setDestinationUuid(destinationUuid.toString()) + .setUrgent(urgent); if (sourceAccount != null && sourceDeviceId != null) { envelopeBuilder.setSourceUuid(sourceAccount.getUuid().toString()) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java index e6ebe88cc..d5bb2af4e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java @@ -4,9 +4,21 @@ */ package org.whispersystems.textsecuregcm.entities; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import javax.validation.Valid; import javax.validation.constraints.NotNull; -public record IncomingMessageList(@NotNull @Valid List<@NotNull IncomingMessage> messages, boolean online, long timestamp) { +public record IncomingMessageList(@NotNull @Valid List<@NotNull IncomingMessage> messages, + boolean online, boolean urgent, long timestamp) { + + @JsonCreator + public IncomingMessageList(@JsonProperty("messages") @NotNull @Valid List<@NotNull IncomingMessage> messages, + @JsonProperty("online") boolean online, + @JsonProperty("urgent") Boolean urgent, + @JsonProperty("timestamp") long timestamp) { + + this(messages, online, urgent == null || urgent, timestamp); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java index 0436602cf..63afaf1ef 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java @@ -13,7 +13,7 @@ import javax.annotation.Nullable; public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullable UUID sourceUuid, int sourceDevice, UUID destinationUuid, @Nullable UUID updatedPni, byte[] content, - long serverTimestamp) { + long serverTimestamp, boolean urgent) { public MessageProtos.Envelope toEnvelope() { final MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder() @@ -21,7 +21,8 @@ public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullab .setTimestamp(timestamp()) .setServerTimestamp(serverTimestamp()) .setDestinationUuid(destinationUuid().toString()) - .setServerGuid(guid().toString()); + .setServerGuid(guid().toString()) + .setUrgent(urgent); if (sourceUuid() != null) { builder.setSourceUuid(sourceUuid().toString()); @@ -49,7 +50,8 @@ public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullab envelope.hasDestinationUuid() ? UUID.fromString(envelope.getDestinationUuid()) : null, envelope.hasUpdatedPni() ? UUID.fromString(envelope.getUpdatedPni()) : null, envelope.getContent().toByteArray(), - envelope.getServerTimestamp()); + envelope.getServerTimestamp(), + envelope.getUrgent()); } @Override @@ -64,13 +66,13 @@ public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullab return type == that.type && timestamp == that.timestamp && sourceDevice == that.sourceDevice && serverTimestamp == that.serverTimestamp && guid.equals(that.guid) && Objects.equals(sourceUuid, that.sourceUuid) && destinationUuid.equals(that.destinationUuid) - && Objects.equals(updatedPni, that.updatedPni) && Arrays.equals(content, that.content); + && Objects.equals(updatedPni, that.updatedPni) && Arrays.equals(content, that.content) && urgent == that.urgent; } @Override public int hashCode() { int result = Objects.hash(guid, type, timestamp, sourceUuid, sourceDevice, destinationUuid, updatedPni, - serverTimestamp); + serverTimestamp, urgent); result = 31 * result + Arrays.hashCode(content); return result; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java index afa71ffb8..ffd5704db 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java @@ -278,7 +278,7 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore { final UUID updatedPni = AttributeValues.getUUID(item, KEY_UPDATED_PNI, null); envelope = new OutgoingMessageEntity(messageUuid, type, timestamp, sourceUuid, sourceDevice, destinationUuid, - updatedPni, content, sortKey.getServerTimestamp()).toEnvelope(); + updatedPni, content, sortKey.getServerTimestamp(), true).toEnvelope(); GET_MESSAGE_WITH_ATTRIBUTES_COUNTER.increment(); } diff --git a/service/src/main/proto/TextSecure.proto b/service/src/main/proto/TextSecure.proto index 0d3e77210..29c05e376 100644 --- a/service/src/main/proto/TextSecure.proto +++ b/service/src/main/proto/TextSecure.proto @@ -40,6 +40,7 @@ message Envelope { optional uint64 server_timestamp = 10; optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline optional string destination_uuid = 13; + optional bool urgent = 14 [default=true]; optional string updated_pni = 15; // next: 16 } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java index 14c94d647..9da3e4028 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -207,6 +207,28 @@ class MessageControllerTest { assertTrue(captor.getValue().hasSourceUuid()); assertTrue(captor.getValue().hasSourceDevice()); + assertTrue(captor.getValue().getUrgent()); + } + + @Test + void testSingleDeviceCurrentNotUrgent() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.getMapper().readValue(jsonFixture("fixtures/current_message_single_device_not_urgent.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + + assertTrue(captor.getValue().hasSourceUuid()); + assertTrue(captor.getValue().hasSourceDevice()); + assertFalse(captor.getValue().getUrgent()); } @Test @@ -328,7 +350,31 @@ class MessageControllerTest { assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); - verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class), eq(false)); + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(Envelope.class); + + verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), envelopeCaptor.capture(), eq(false)); + + envelopeCaptor.getAllValues().forEach(envelope -> assertTrue(envelope.getUrgent())); + } + + @Test + void testMultiDeviceNotUrgent() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.getMapper().readValue(jsonFixture("fixtures/current_message_multi_device_not_urgent.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(Envelope.class); + + verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), envelopeCaptor.capture(), eq(false)); + + envelopeCaptor.getAllValues().forEach(envelope -> assertFalse(envelope.getUrgent())); } @Test @@ -595,7 +641,7 @@ class MessageControllerTest { .request() .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString("1234".getBytes())) .put(Entity.entity(new IncomingMessageList( - List.of(new IncomingMessage(1, 1L, 1, new String(contentBytes))), false, + List.of(new IncomingMessage(1, 1L, 1, new String(contentBytes))), false, true, System.currentTimeMillis()), MediaType.APPLICATION_JSON_TYPE)); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java new file mode 100644 index 000000000..28ca9abf6 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +import static org.junit.jupiter.api.Assertions.*; + +class IncomingMessageListTest { + + @Test + void fromJson() throws JsonProcessingException { + { + final String incomingMessageListJson = """ + { + "messages": [], + "timestamp": 123456789, + "online": true, + "urgent": false + } + """; + + final IncomingMessageList incomingMessageList = + SystemMapper.getMapper().readValue(incomingMessageListJson, IncomingMessageList.class); + + assertTrue(incomingMessageList.online()); + assertFalse(incomingMessageList.urgent()); + } + + { + final String incomingMessageListJson = """ + { + "messages": [], + "timestamp": 123456789, + "online": true + } + """; + + final IncomingMessageList incomingMessageList = + SystemMapper.getMapper().readValue(incomingMessageListJson, IncomingMessageList.class); + + assertTrue(incomingMessageList.online()); + assertTrue(incomingMessageList.urgent()); + } + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java index bd9ce8fa6..fd65f5f31 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java @@ -35,7 +35,8 @@ class OutgoingMessageEntityTest { UUID.randomUUID(), updatedPni, messageContent, - serverTimestamp); + serverTimestamp, + true); assertEquals(outgoingMessageEntity, OutgoingMessageEntity.fromEnvelope(outgoingMessageEntity.toEnvelope())); } diff --git a/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json b/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json new file mode 100644 index 000000000..c07ca93a2 --- /dev/null +++ b/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json @@ -0,0 +1,19 @@ +{ + "urgent": false, + "messages": [ + { + "type": 1, + "destinationDeviceId": 1, + "destinationRegistrationId": 222, + "content": "Zm9vYmFyego", + "timestamp": 1234 + }, + { + "type": 1, + "destinationDeviceId": 2, + "destinationRegistrationId": 333, + "content": "Zm9vYmFyego", + "timestamp": 1234 + } + ] +} diff --git a/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json b/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json new file mode 100644 index 000000000..78aa66af3 --- /dev/null +++ b/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json @@ -0,0 +1,11 @@ +{ + "urgent": false, + "messages": [ + { + "type": 1, + "destinationDeviceId": 1, + "content": "Zm9vYmFyego", + "timestamp": 1234 + } + ] +}