Accommodate gRPC in the SpamChecker interface

This commit is contained in:
Jon Chambers
2025-04-02 13:16:55 -04:00
committed by GitHub
parent 488e7c4913
commit 7ea0885474
5 changed files with 187 additions and 32 deletions

View File

@@ -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));
}
}

View File

@@ -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,
}

View File

@@ -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) {
}

View File

@@ -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());
}
};
}
}