Clarify roles/responsibilities of components in the message-handling pathway

This commit is contained in:
Jon Chambers
2025-01-31 10:24:50 -05:00
committed by GitHub
parent 282bcf6f34
commit 48ada8e8ca
33 changed files with 1338 additions and 1199 deletions

View File

@@ -9,9 +9,14 @@ import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Util;
/**
* A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages,
@@ -42,26 +47,82 @@ public class MessageSender {
this.pushNotificationManager = pushNotificationManager;
}
public void sendMessage(final Account account, final Device device, final Envelope message, final boolean online) {
final boolean destinationPresent = messagesManager.insert(account.getUuid(),
device.getId(),
online ? message.toBuilder().setEphemeral(true).build() : message);
/**
* Sends messages to devices associated with the given destination account. If a destination device has a valid push
* notification token and does not have an active connection to a Signal server, then this method will also send a
* push notification to that device to announce the availability of new messages.
*
* @param account the account to which to send messages
* @param messagesByDeviceId a map of device IDs to message payloads
*/
public void sendMessages(final Account account, final Map<Byte, Envelope> messagesByDeviceId) {
messagesManager.insert(account.getIdentifier(IdentityType.ACI), messagesByDeviceId)
.forEach((deviceId, destinationPresent) -> {
final Envelope message = messagesByDeviceId.get(deviceId);
if (!destinationPresent && !online) {
try {
pushNotificationManager.sendNewMessageNotification(account, device.getId(), message.getUrgent());
} catch (final NotPushRegisteredException ignored) {
}
}
if (!destinationPresent && !message.getEphemeral()) {
try {
pushNotificationManager.sendNewMessageNotification(account, deviceId, message.getUrgent());
} catch (final NotPushRegisteredException ignored) {
}
}
Metrics.counter(SEND_COUNTER_NAME,
CHANNEL_TAG_NAME, getDeliveryChannelName(device),
EPHEMERAL_TAG_NAME, String.valueOf(online),
CLIENT_ONLINE_TAG_NAME, String.valueOf(destinationPresent),
URGENT_TAG_NAME, String.valueOf(message.getUrgent()),
STORY_TAG_NAME, String.valueOf(message.getStory()),
SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId()))
.increment();
Metrics.counter(SEND_COUNTER_NAME,
CHANNEL_TAG_NAME, account.getDevice(deviceId).map(MessageSender::getDeliveryChannelName).orElse("unknown"),
EPHEMERAL_TAG_NAME, String.valueOf(message.getEphemeral()),
CLIENT_ONLINE_TAG_NAME, String.valueOf(destinationPresent),
URGENT_TAG_NAME, String.valueOf(message.getUrgent()),
STORY_TAG_NAME, String.valueOf(message.getStory()),
SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId()))
.increment();
});
}
/**
* Sends messages to a group of recipients. If a destination device has a valid push notification token and does not
* have an active connection to a Signal server, then this method will also send a push notification to that device to
* announce the availability of new messages.
*
* @param multiRecipientMessage the multi-recipient message to send to the given recipients
* @param resolvedRecipients a map of recipients to resolved Signal accounts
* @param clientTimestamp the time at which the sender reports the message was sent
* @param isStory {@code true} if the message is a story or {@code false otherwise}
* @param isEphemeral {@code true} if the message should only be delivered to devices with active connections or
* {@code false otherwise}
* @param isUrgent {@code true} if the message is urgent or {@code false otherwise}
*
* @return a future that completes when all messages have been inserted into delivery queues
*/
public CompletableFuture<Void> sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,
final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients,
final long clientTimestamp,
final boolean isStory,
final boolean isEphemeral,
final boolean isUrgent) {
return messagesManager.insertMultiRecipientMessage(multiRecipientMessage, resolvedRecipients, clientTimestamp,
isStory, isEphemeral, isUrgent)
.thenAccept(clientPresenceByAccountAndDevice ->
clientPresenceByAccountAndDevice.forEach((account, clientPresenceByDeviceId) ->
clientPresenceByDeviceId.forEach((deviceId, clientPresent) -> {
if (!clientPresent && !isEphemeral) {
try {
pushNotificationManager.sendNewMessageNotification(account, deviceId, isUrgent);
} catch (final NotPushRegisteredException ignored) {
}
}
Metrics.counter(SEND_COUNTER_NAME,
CHANNEL_TAG_NAME,
account.getDevice(deviceId).map(MessageSender::getDeliveryChannelName).orElse("unknown"),
EPHEMERAL_TAG_NAME, String.valueOf(isEphemeral),
CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent),
URGENT_TAG_NAME, String.valueOf(isUrgent),
STORY_TAG_NAME, String.valueOf(isStory),
SEALED_SENDER_TAG_NAME, String.valueOf(true))
.increment();
})))
.thenRun(Util.NOOP);
}
@VisibleForTesting

View File

@@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.push;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
@@ -43,21 +44,21 @@ public class ReceiptSender {
try {
accountManager.getByAccountIdentifier(destinationIdentifier.uuid()).ifPresentOrElse(
destinationAccount -> {
final Envelope.Builder message = Envelope.newBuilder()
final Envelope message = Envelope.newBuilder()
.setServerTimestamp(System.currentTimeMillis())
.setSourceServiceId(sourceIdentifier.toServiceIdentifierString())
.setSourceDevice(sourceDeviceId)
.setDestinationServiceId(destinationIdentifier.toServiceIdentifierString())
.setClientTimestamp(messageId)
.setType(Envelope.Type.SERVER_DELIVERY_RECEIPT)
.setUrgent(false);
.setUrgent(false)
.build();
for (final Device destinationDevice : destinationAccount.getDevices()) {
try {
messageSender.sendMessage(destinationAccount, destinationDevice, message.build(), false);
} catch (final Exception e) {
logger.warn("Could not send delivery receipt", e);
}
try {
messageSender.sendMessages(destinationAccount, destinationAccount.getDevices().stream()
.collect(Collectors.toMap(Device::getId, ignored -> message)));
} catch (final Exception e) {
logger.warn("Could not send delivery receipt", e);
}
},
() -> logger.info("No longer registered: {}", destinationIdentifier)