Accommodate gRPC in the `SpamChecker` interface
This commit is contained in:
parent
488e7c4913
commit
7ea0885474
|
@ -103,6 +103,8 @@ import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
|
|||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.spam.MessageType;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
@ -303,14 +305,27 @@ public class MessageController {
|
|||
maybeDestination = source.map(AuthenticatedDevice::getAccount);
|
||||
}
|
||||
|
||||
final SpamChecker.SpamCheckResult spamCheck = spamChecker.checkForSpam(
|
||||
context, source, maybeDestination, Optional.of(destinationIdentifier));
|
||||
final Optional<byte[]> reportSpamToken;
|
||||
switch (spamCheck) {
|
||||
case final SpamChecker.Spam spam: return spam.response();
|
||||
case final SpamChecker.NotSpam notSpam: reportSpamToken = notSpam.token();
|
||||
final MessageType messageType;
|
||||
|
||||
if (isStory) {
|
||||
messageType = MessageType.INDIVIDUAL_STORY;
|
||||
} else if (isSyncMessage) {
|
||||
messageType = MessageType.SYNC;
|
||||
} else if (source.isPresent()) {
|
||||
messageType = MessageType.INDIVIDUAL_IDENTIFIED_SENDER;
|
||||
} else {
|
||||
messageType = MessageType.INDIVIDUAL_SEALED_SENDER;
|
||||
}
|
||||
|
||||
final SpamCheckResult<Response> spamCheckResult =
|
||||
spamChecker.checkForIndividualRecipientSpamHttp(messageType, context, source, maybeDestination, Optional.of(destinationIdentifier));
|
||||
|
||||
if (spamCheckResult.response().isPresent()) {
|
||||
return spamCheckResult.response().get();
|
||||
}
|
||||
|
||||
final Optional<byte[]> reportSpamToken = spamCheckResult.token();
|
||||
|
||||
int totalContentLength = 0;
|
||||
|
||||
for (final IncomingMessage message : messages.messages()) {
|
||||
|
@ -534,9 +549,12 @@ public class MessageController {
|
|||
}
|
||||
}
|
||||
|
||||
final SpamChecker.SpamCheckResult spamCheck = spamChecker.checkForSpam(context, Optional.empty(), Optional.empty(), Optional.empty());
|
||||
if (spamCheck instanceof final SpamChecker.Spam spam) {
|
||||
return spam.response();
|
||||
final SpamCheckResult<Response> spamCheckResult = spamChecker.checkForMultiRecipientSpamHttp(
|
||||
isStory ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER,
|
||||
context);
|
||||
|
||||
if (spamCheckResult.response().isPresent()) {
|
||||
return spamCheckResult.response().get();
|
||||
}
|
||||
|
||||
if (groupSendToken == null && accessKeys == null && !isStory) {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A combination of a gRPC status and response message to communicate to callers that a message has been flagged as
|
||||
* potential spam.
|
||||
*
|
||||
* @param status The gRPC status for this response. If the status is {@link Status#OK}, then a response object will be
|
||||
* available via {@link #response}. Otherwise, callers should transmit the status as an error to clients.
|
||||
* @param response a response object to send to clients; will be present if {@link #status} is not {@link Status#OK}
|
||||
*
|
||||
* @param <R> the type of response object
|
||||
*/
|
||||
public record GrpcResponse<R>(Status status, Optional<R> response) {
|
||||
|
||||
/**
|
||||
* Constructs a new response object with the given status and no response message.
|
||||
*
|
||||
* @param status the status to send to callers
|
||||
*
|
||||
* @return a new response object with the given status and no response message
|
||||
*
|
||||
* @param <R> the type of response object
|
||||
*/
|
||||
public static <R> GrpcResponse<R> withStatus(final Status status) {
|
||||
return new GrpcResponse<>(status, Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new response object with a status of {@link Status#OK} and the given response message.
|
||||
*
|
||||
* @param response the response to send to the caller
|
||||
*
|
||||
* @return a new response object with a status of {@link Status#OK} and the given response message
|
||||
*
|
||||
* @param <R> the type of response object
|
||||
*/
|
||||
public static <R> GrpcResponse<R> withResponse(final R response) {
|
||||
return new GrpcResponse<>(Status.OK, Optional.of(response));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
public enum MessageType {
|
||||
INDIVIDUAL_IDENTIFIED_SENDER,
|
||||
SYNC,
|
||||
INDIVIDUAL_SEALED_SENDER,
|
||||
MULTI_RECIPIENT_SEALED_SENDER,
|
||||
INDIVIDUAL_STORY,
|
||||
MULTI_RECIPIENT_STORY,
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* The result of a spam check. May contain a response to relay to the caller if a message was identified as potential
|
||||
* spam or a spam reporting token to include in the delivered message.
|
||||
*
|
||||
* @param response a transport-appropriate response to return to the sender if the message was identified as potential
|
||||
* spam, or empty if processing should continue as normal
|
||||
* @param token a spam-reporting token to include in the outbound message, or empty if no token applies to the message
|
||||
*
|
||||
* @param <T> the type of response for messages identified as potential spam
|
||||
*/
|
||||
public record SpamCheckResult<T>(Optional<T> response, Optional<byte[]> token) {
|
||||
}
|
|
@ -5,35 +5,19 @@
|
|||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Optional;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.signal.chat.messages.SendMessageResponse;
|
||||
import org.signal.chat.messages.SendMultiRecipientMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAndAuthenticatedDeviceHolder;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
public interface SpamChecker {
|
||||
|
||||
/**
|
||||
* A result from the spam checker that is one of:
|
||||
* <ul>
|
||||
* <li>
|
||||
* Message is determined to be spam, and a response is returned
|
||||
* </li>
|
||||
* <li>
|
||||
* Message is not spam, and an optional spam token is returned
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
sealed interface SpamCheckResult {}
|
||||
|
||||
record Spam(Response response) implements SpamCheckResult {}
|
||||
|
||||
record NotSpam(Optional<byte[]> token) implements SpamCheckResult {
|
||||
public static final NotSpam EMPTY_TOKEN = new NotSpam(Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a message may be spam
|
||||
* Determine if a message sent to an individual recipient via HTTP may be spam.
|
||||
*
|
||||
* @param requestContext The request context for a message send attempt
|
||||
* @param maybeSource The sender of the message, could be empty if this as message sent with sealed sender
|
||||
|
@ -41,13 +25,82 @@ public interface SpamChecker {
|
|||
* not be retrieved
|
||||
* @return A {@link SpamCheckResult}
|
||||
*/
|
||||
SpamCheckResult checkForSpam(
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
SpamCheckResult<Response> checkForIndividualRecipientSpamHttp(
|
||||
final MessageType messageType,
|
||||
final ContainerRequestContext requestContext,
|
||||
final Optional<? extends AccountAndAuthenticatedDeviceHolder> maybeSource,
|
||||
final Optional<Account> maybeDestination,
|
||||
final Optional<ServiceIdentifier> maybeDestinationIdentifier);
|
||||
|
||||
/**
|
||||
* Determine if a message sent to multiple recipients via HTTP may be spam.
|
||||
*
|
||||
* @param requestContext The request context for a message send attempt
|
||||
* @return A {@link SpamCheckResult}
|
||||
*/
|
||||
SpamCheckResult<Response> checkForMultiRecipientSpamHttp(
|
||||
final MessageType messageType,
|
||||
final ContainerRequestContext requestContext);
|
||||
|
||||
/**
|
||||
* Determine if a message sent to an individual recipient via gRPC may be spam.
|
||||
*
|
||||
* @param maybeSource The sender of the message, could be empty if this as message sent with sealed sender
|
||||
* @param maybeDestination The destination of the message, could be empty if the destination does not exist or could
|
||||
* not be retrieved
|
||||
* @return A {@link SpamCheckResult}
|
||||
*/
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(
|
||||
final MessageType messageType,
|
||||
final Optional<org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice> maybeSource,
|
||||
final Optional<Account> maybeDestination,
|
||||
final Optional<ServiceIdentifier> maybeDestinationIdentifier);
|
||||
|
||||
/**
|
||||
* Determine if a message sent to multiple recipients via gRPC may be spam.
|
||||
*
|
||||
* @return A {@link SpamCheckResult}
|
||||
*/
|
||||
SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(final MessageType messageType);
|
||||
|
||||
|
||||
static SpamChecker noop() {
|
||||
return (ignoredContext, ignoredSource, ignoredDestination, ignoredDestinationIdentifier) -> NotSpam.EMPTY_TOKEN;
|
||||
return new SpamChecker() {
|
||||
|
||||
@Override
|
||||
public SpamCheckResult<Response> checkForIndividualRecipientSpamHttp(final MessageType messageType,
|
||||
final ContainerRequestContext requestContext,
|
||||
final Optional<? extends AccountAndAuthenticatedDeviceHolder> maybeSource,
|
||||
final Optional<Account> maybeDestination,
|
||||
final Optional<ServiceIdentifier> maybeDestinationIdentifier) {
|
||||
|
||||
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpamCheckResult<Response> checkForMultiRecipientSpamHttp(final MessageType messageType,
|
||||
final ContainerRequestContext requestContext) {
|
||||
|
||||
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(final MessageType messageType,
|
||||
final Optional<AuthenticatedDevice> maybeSource,
|
||||
final Optional<Account> maybeDestination,
|
||||
final Optional<ServiceIdentifier> maybeDestinationIdentifier) {
|
||||
|
||||
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(
|
||||
final MessageType messageType) {
|
||||
|
||||
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue