From 4b7d8ae53a024de0e59290f3a1a571d86b9f0910 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 24 Jul 2026 10:07:42 -0400 Subject: [PATCH] feat(blocklist): scaffold blocklist data layer (api/service/repo/controller) Adds the end-to-end data layer for the Blocklist gRPC service, mirroring the existing ActivityFeed/Moderation conventions in :services:flipcash: - BlocklistApi: gRPC stub wrapper for BlockUser/UnblockUser/IsBlocked/GetBlocklist with auth-signed requests and protovalidate - BlocklistService: folds proto Result enums into typed Result - BlocklistRepository (+ Internal impl): maps proto -> domain, assembles paged BlocklistPage (users, pagingToken, hasMore) - BlocklistController: resolves owner KeyPair from UserManager - BlockedUser/BlocklistPage domain models + BlockedUserMapper - Block/Unblock/IsBlocked/GetBlocklist error sealed classes - Hilt wiring in FlipcashModule - blocklist/v1 proto definitions --- .../blocklist/v1/blocklist_service.proto | 116 ++++++++++++++++++ .../src/main/proto/blocklist/v1/model.proto | 20 +++ .../controllers/BlocklistController.kt | 41 +++++++ .../services/inject/FlipcashModule.kt | 10 ++ .../internal/domain/BlockedUserMapper.kt | 18 +++ .../internal/network/api/BlocklistApi.kt | 99 +++++++++++++++ .../network/services/BlocklistService.kt | 95 ++++++++++++++ .../InternalBlocklistRepository.kt | 41 +++++++ .../flipcash/services/models/BlockedUser.kt | 29 +++++ .../com/flipcash/services/models/Errors.kt | 38 ++++++ .../repository/BlocklistRepository.kt | 16 +++ 11 files changed, 523 insertions(+) create mode 100644 definitions/flipcash/protos/src/main/proto/blocklist/v1/blocklist_service.proto create mode 100644 definitions/flipcash/protos/src/main/proto/blocklist/v1/model.proto create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/controllers/BlocklistController.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/BlockedUserMapper.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/BlocklistApi.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/BlocklistService.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalBlocklistRepository.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/models/BlockedUser.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/repository/BlocklistRepository.kt diff --git a/definitions/flipcash/protos/src/main/proto/blocklist/v1/blocklist_service.proto b/definitions/flipcash/protos/src/main/proto/blocklist/v1/blocklist_service.proto new file mode 100644 index 000000000..c0ed1ba46 --- /dev/null +++ b/definitions/flipcash/protos/src/main/proto/blocklist/v1/blocklist_service.proto @@ -0,0 +1,116 @@ +syntax = "proto3"; + +package flipcash.blocklist.v1; + +import "blocklist/v1/model.proto"; +import "common/v1/common.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/blocklist/v1;blocklistpb"; +option java_package = "com.codeinc.flipcash.gen.blocklist.v1"; +option objc_class_prefix = "FPBBlocklistV1"; + +// Blocklist manages the set of users a user has blocked. +service Blocklist { + // BlockUser adds a user to the caller's blocklist. Blocking a user that + // is already blocked is a no-op and returns OK. + rpc BlockUser(BlockUserRequest) returns (BlockUserResponse); + + // UnblockUser removes a user from the caller's blocklist. Unblocking a + // user that isn't blocked is a no-op and returns OK. + rpc UnblockUser(UnblockUserRequest) returns (UnblockUserResponse); + + // IsBlocked checks whether a user is on the caller's blocklist. + rpc IsBlocked(IsBlockedRequest) returns (IsBlockedResponse); + + // GetBlocklist gets the caller's blocklist using a paged API, ordered by + // most recently blocked first. + rpc GetBlocklist(GetBlocklistRequest) returns (GetBlocklistResponse); +} + +message BlockUserRequest { + // The user to block + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message BlockUserResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + // The user to block doesn't exist + USER_NOT_FOUND = 2; + // Users cannot block themselves + CANNOT_BLOCK_SELF = 3; + } +} + +message UnblockUserRequest { + // The user to unblock + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message UnblockUserResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + } +} + +message IsBlockedRequest { + // The user to check against the caller's blocklist + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message IsBlockedResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + } + + // Whether the user is on the caller's blocklist. Set when result is OK. + bool is_blocked = 2; +} + +message GetBlocklistRequest { + // QueryOptions controls page_size. Ordering is fixed to most recently + // blocked first and is not client-selectable. + // + // Leave query_options.paging_token unset on the first request. On every + // subsequent request, set query_options.paging_token to the paging_token + // from the most recent response to advance to the next page. The token is + // opaque and server-generated; do not construct it. + common.v1.QueryOptions query_options = 1; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message GetBlocklistResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + } + + repeated BlockedUser blocked_users = 2 [(validate.rules).repeated = { + min_items: 0 + max_items: 100 + }]; + + // PagingToken is the server-generated cursor for this paginated read. The + // client MUST send the most recent value back in query_options.paging_token + // on the next GetBlocklistRequest. Set when result is OK. + common.v1.PagingToken paging_token = 3; + + // HasMore indicates whether further pages remain. When true, the client + // should issue another GetBlocklistRequest with the returned paging_token. + bool has_more = 4; +} diff --git a/definitions/flipcash/protos/src/main/proto/blocklist/v1/model.proto b/definitions/flipcash/protos/src/main/proto/blocklist/v1/model.proto new file mode 100644 index 000000000..d3a76426e --- /dev/null +++ b/definitions/flipcash/protos/src/main/proto/blocklist/v1/model.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package flipcash.blocklist.v1; + +import "common/v1/common.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/blocklist/v1;blocklistpb"; +option java_package = "com.codeinc.flipcash.gen.blocklist.v1"; +option objc_class_prefix = "FPBBlocklistV1"; + +// BlockedUser is a single entry in a user's blocklist +message BlockedUser { + // The user that is blocked + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + // Timestamp when the user was blocked + google.protobuf.Timestamp blocked_at = 2 [(validate.rules).timestamp.required = true]; +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/BlocklistController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/BlocklistController.kt new file mode 100644 index 000000000..778dc040b --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/BlocklistController.kt @@ -0,0 +1,41 @@ +package com.flipcash.services.controllers + +import com.flipcash.services.models.BlocklistPage +import com.flipcash.services.models.QueryOptions +import com.flipcash.services.repository.BlocklistRepository +import com.flipcash.services.user.UserManager +import com.getcode.opencode.model.core.ID +import javax.inject.Inject + +class BlocklistController @Inject constructor( + private val repository: BlocklistRepository, + private val userManager: UserManager, +) { + suspend fun blockUser(userId: ID): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + + return repository.blockUser(userId, owner) + } + + suspend fun unblockUser(userId: ID): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + + return repository.unblockUser(userId, owner) + } + + suspend fun isBlocked(userId: ID): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + + return repository.isBlocked(userId, owner) + } + + suspend fun getBlocklist(queryOptions: QueryOptions = QueryOptions()): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + + return repository.getBlocklist(owner, queryOptions) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt index b202f2ba4..639bdfb90 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt @@ -5,6 +5,7 @@ import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.annotations.FlipcashManagedStreamingChannel import com.flipcash.services.internal.annotations.FlipcashProtocol import com.flipcash.services.internal.domain.ActivityFeedMessageMapper +import com.flipcash.services.internal.domain.BlockedUserMapper import com.flipcash.services.internal.domain.ContactMapper import com.flipcash.services.internal.domain.ImageModerationResponseMapper import com.flipcash.services.internal.domain.UserFlagsMapper @@ -17,6 +18,7 @@ import com.flipcash.services.internal.network.HttpBlobUploader import com.flipcash.services.internal.network.services.AccountService import com.flipcash.services.internal.network.services.ActivityFeedService import com.flipcash.services.internal.network.services.BlobStorageService +import com.flipcash.services.internal.network.services.BlocklistService import com.flipcash.services.internal.network.services.ChatService import com.flipcash.services.internal.network.services.EventStreamingService import com.flipcash.services.internal.network.services.ChatMessagingService @@ -33,6 +35,7 @@ import com.flipcash.services.internal.network.services.ThirdPartyService import com.flipcash.services.internal.repositories.InternalAccountRepository import com.flipcash.services.internal.repositories.InternalActivityFeedRepository import com.flipcash.services.internal.repositories.InternalBlobStorageRepository +import com.flipcash.services.internal.repositories.InternalBlocklistRepository import com.flipcash.services.internal.repositories.InternalChatRepository import com.flipcash.services.internal.repositories.InternalEventStreamingRepository import com.flipcash.services.internal.repositories.InternalChatMessagingRepository @@ -47,6 +50,7 @@ import com.flipcash.services.internal.repositories.InternalSettingsRepository import com.flipcash.services.internal.repositories.InternalThirdPartyRepository import com.flipcash.services.repository.AccountRepository import com.flipcash.services.repository.ActivityFeedRepository +import com.flipcash.services.repository.BlocklistRepository import com.flipcash.services.repository.ChatRepository import com.flipcash.services.repository.EventStreamingRepository import com.flipcash.services.repository.ChatMessagingRepository @@ -199,6 +203,12 @@ internal object FlipcashModule { mapper: ActivityFeedMessageMapper, ): ActivityFeedRepository = InternalActivityFeedRepository(service, mapper) + @Provides + internal fun providesBlocklistRepository( + service: BlocklistService, + mapper: BlockedUserMapper, + ): BlocklistRepository = InternalBlocklistRepository(service, mapper) + @Provides internal fun providesPurchaseRepository( service: PurchaseService, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/BlockedUserMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/BlockedUserMapper.kt new file mode 100644 index 000000000..516b960e8 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/BlockedUserMapper.kt @@ -0,0 +1,18 @@ +package com.flipcash.services.internal.domain + +import com.codeinc.flipcash.gen.blocklist.v1.Model +import com.flipcash.services.internal.domain.mapper.Mapper +import com.flipcash.services.internal.network.extensions.toId +import com.flipcash.services.models.BlockedUser +import kotlin.time.Instant +import javax.inject.Inject + +internal class BlockedUserMapper @Inject constructor( +) : Mapper { + override fun map(from: Model.BlockedUser): BlockedUser { + return BlockedUser( + userId = from.userId.toId(), + blockedAt = Instant.fromEpochSeconds(from.blockedAt.seconds, from.blockedAt.nanos), + ) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/BlocklistApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/BlocklistApi.kt new file mode 100644 index 000000000..829de957a --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/BlocklistApi.kt @@ -0,0 +1,99 @@ +package com.flipcash.services.internal.network.api + +import com.codeinc.flipcash.gen.blocklist.v1.BlocklistGrpcKt +import com.codeinc.flipcash.gen.blocklist.v1.BlocklistService +import com.codeinc.flipcash.gen.blocklist.v1.validate +import com.flipcash.services.internal.annotations.FlipcashManagedChannel +import com.flipcash.services.internal.network.extensions.asQueryOptions +import com.flipcash.services.internal.network.extensions.asUserId +import com.flipcash.services.internal.network.extensions.authenticate +import com.flipcash.services.models.QueryOptions +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.opencode.internal.network.core.GrpcApi +import com.getcode.opencode.model.core.ID +import dev.bmcreations.protovalidate.orThrow +import io.grpc.ManagedChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class BlocklistApi @Inject constructor( + @FlipcashManagedChannel + managedChannel: ManagedChannel, +) : GrpcApi(managedChannel) { + + private val api = BlocklistGrpcKt.BlocklistCoroutineStub(managedChannel) + .withWaitForReady() + + /** + * Adds a user to the caller's blocklist. Blocking a user that is already + * blocked is a no-op and returns OK. + */ + suspend fun blockUser(userId: ID, owner: KeyPair): BlocklistService.BlockUserResponse { + val request = BlocklistService.BlockUserRequest.newBuilder() + .setUserId(userId.asUserId()) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.blockUser(request) + } + } + + /** + * Removes a user from the caller's blocklist. Unblocking a user that isn't + * blocked is a no-op and returns OK. + */ + suspend fun unblockUser(userId: ID, owner: KeyPair): BlocklistService.UnblockUserResponse { + val request = BlocklistService.UnblockUserRequest.newBuilder() + .setUserId(userId.asUserId()) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.unblockUser(request) + } + } + + /** + * Checks whether a user is on the caller's blocklist. + */ + suspend fun isBlocked(userId: ID, owner: KeyPair): BlocklistService.IsBlockedResponse { + val request = BlocklistService.IsBlockedRequest.newBuilder() + .setUserId(userId.asUserId()) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.isBlocked(request) + } + } + + /** + * Gets the caller's blocklist using a paging API, ordered by most recently + * blocked first. + */ + suspend fun getBlocklist( + owner: KeyPair, + queryOptions: QueryOptions, + ): BlocklistService.GetBlocklistResponse { + val request = BlocklistService.GetBlocklistRequest.newBuilder() + .setQueryOptions(queryOptions.asQueryOptions()) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.getBlocklist(request) + } + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/BlocklistService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/BlocklistService.kt new file mode 100644 index 000000000..8ecf02657 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/BlocklistService.kt @@ -0,0 +1,95 @@ +package com.flipcash.services.internal.network.services + +import com.codeinc.flipcash.gen.blocklist.v1.BlocklistService +import com.flipcash.services.internal.network.api.BlocklistApi +import com.flipcash.services.models.BlockUserError +import com.flipcash.services.models.GetBlocklistError +import com.flipcash.services.models.IsBlockedError +import com.flipcash.services.models.QueryOptions +import com.flipcash.services.models.UnblockUserError +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.opencode.internal.network.extensions.foldWithSuppression +import com.getcode.opencode.model.core.ID +import com.getcode.opencode.utils.toValidationOrElse +import javax.inject.Inject + +internal class BlocklistService @Inject constructor( + private val api: BlocklistApi, +) { + suspend fun blockUser(userId: ID, owner: KeyPair): Result { + return runCatching { + api.blockUser(userId, owner) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + BlocklistService.BlockUserResponse.Result.OK -> Result.success(Unit) + BlocklistService.BlockUserResponse.Result.DENIED -> Result.failure(BlockUserError.Denied()) + BlocklistService.BlockUserResponse.Result.USER_NOT_FOUND -> Result.failure(BlockUserError.UserNotFound()) + BlocklistService.BlockUserResponse.Result.CANNOT_BLOCK_SELF -> Result.failure(BlockUserError.CannotBlockSelf()) + BlocklistService.BlockUserResponse.Result.UNRECOGNIZED -> Result.failure(BlockUserError.Unrecognized()) + else -> Result.failure(BlockUserError.Other()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { BlockUserError.Other(cause = it) }) + } + ) + } + + suspend fun unblockUser(userId: ID, owner: KeyPair): Result { + return runCatching { + api.unblockUser(userId, owner) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + BlocklistService.UnblockUserResponse.Result.OK -> Result.success(Unit) + BlocklistService.UnblockUserResponse.Result.DENIED -> Result.failure(UnblockUserError.Denied()) + BlocklistService.UnblockUserResponse.Result.UNRECOGNIZED -> Result.failure(UnblockUserError.Unrecognized()) + else -> Result.failure(UnblockUserError.Other()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { UnblockUserError.Other(cause = it) }) + } + ) + } + + suspend fun isBlocked(userId: ID, owner: KeyPair): Result { + return runCatching { + api.isBlocked(userId, owner) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + BlocklistService.IsBlockedResponse.Result.OK -> Result.success(response.isBlocked) + BlocklistService.IsBlockedResponse.Result.DENIED -> Result.failure(IsBlockedError.Denied()) + BlocklistService.IsBlockedResponse.Result.UNRECOGNIZED -> Result.failure(IsBlockedError.Unrecognized()) + else -> Result.failure(IsBlockedError.Other()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { IsBlockedError.Other(cause = it) }) + } + ) + } + + suspend fun getBlocklist( + owner: KeyPair, + queryOptions: QueryOptions, + ): Result { + return runCatching { + api.getBlocklist(owner, queryOptions) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + BlocklistService.GetBlocklistResponse.Result.OK -> Result.success(response) + BlocklistService.GetBlocklistResponse.Result.DENIED -> Result.failure(GetBlocklistError.Denied()) + BlocklistService.GetBlocklistResponse.Result.UNRECOGNIZED -> Result.failure(GetBlocklistError.Unrecognized()) + else -> Result.failure(GetBlocklistError.Other()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { GetBlocklistError.Other(cause = it) }) + } + ) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalBlocklistRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalBlocklistRepository.kt new file mode 100644 index 000000000..ef4a5cac0 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalBlocklistRepository.kt @@ -0,0 +1,41 @@ +package com.flipcash.services.internal.repositories + +import com.flipcash.services.internal.domain.BlockedUserMapper +import com.flipcash.services.internal.network.extensions.toPagingToken +import com.flipcash.services.internal.network.services.BlocklistService +import com.flipcash.services.models.BlocklistPage +import com.flipcash.services.models.QueryOptions +import com.flipcash.services.repository.BlocklistRepository +import com.getcode.ed25519.Ed25519 +import com.getcode.opencode.model.core.ID +import com.getcode.utils.ErrorUtils + +internal class InternalBlocklistRepository( + private val service: BlocklistService, + private val mapper: BlockedUserMapper, +) : BlocklistRepository { + override suspend fun blockUser(userId: ID, owner: Ed25519.KeyPair): Result = + service.blockUser(userId, owner) + .onFailure { ErrorUtils.handleError(it) } + + override suspend fun unblockUser(userId: ID, owner: Ed25519.KeyPair): Result = + service.unblockUser(userId, owner) + .onFailure { ErrorUtils.handleError(it) } + + override suspend fun isBlocked(userId: ID, owner: Ed25519.KeyPair): Result = + service.isBlocked(userId, owner) + .onFailure { ErrorUtils.handleError(it) } + + override suspend fun getBlocklist( + owner: Ed25519.KeyPair, + queryOptions: QueryOptions, + ): Result = service.getBlocklist(owner, queryOptions) + .onFailure { ErrorUtils.handleError(it) } + .map { response -> + BlocklistPage( + users = response.blockedUsersList.map { mapper.map(it) }, + pagingToken = if (response.hasPagingToken()) response.pagingToken.toPagingToken() else null, + hasMore = response.hasMore, + ) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/BlockedUser.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/BlockedUser.kt new file mode 100644 index 000000000..2a3ea0e62 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/BlockedUser.kt @@ -0,0 +1,29 @@ +package com.flipcash.services.models + +import com.getcode.opencode.model.core.ID +import kotlin.time.Instant + +/** + * A single entry in a user's blocklist. + * + * @param userId The user that is blocked + * @param blockedAt When the user was blocked + */ +data class BlockedUser( + val userId: ID, + val blockedAt: Instant, +) + +/** + * A single page of a user's blocklist, ordered most recently blocked first. + * + * @param users The blocked users on this page + * @param pagingToken Opaque, server-generated cursor to pass back in the next + * [QueryOptions.token] to advance to the next page. Null when there is no cursor. + * @param hasMore Whether further pages remain + */ +data class BlocklistPage( + val users: List, + val pagingToken: PagingToken?, + val hasMore: Boolean, +) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt index a420093e3..a047d2edb 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt @@ -536,6 +536,44 @@ sealed class GetUploadPolicyError( data class Other(override val cause: Throwable? = null) : GetUploadPolicyError(message = cause?.message, cause = cause), NotifiableError } +sealed class BlockUserError( + override val message: String? = null, + override val cause: Throwable? = null +) : CodeServerError(message, cause) { + class Denied : BlockUserError("Denied") + class UserNotFound : BlockUserError("User not found") + class CannotBlockSelf : BlockUserError("Cannot block self") + class Unrecognized : BlockUserError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : BlockUserError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class UnblockUserError( + override val message: String? = null, + override val cause: Throwable? = null +) : CodeServerError(message, cause) { + class Denied : UnblockUserError("Denied") + class Unrecognized : UnblockUserError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : UnblockUserError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class IsBlockedError( + override val message: String? = null, + override val cause: Throwable? = null +) : CodeServerError(message, cause) { + class Denied : IsBlockedError("Denied") + class Unrecognized : IsBlockedError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : IsBlockedError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class GetBlocklistError( + override val message: String? = null, + override val cause: Throwable? = null +) : CodeServerError(message, cause) { + class Denied : GetBlocklistError("Denied") + class Unrecognized : GetBlocklistError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : GetBlocklistError(message = cause?.message, cause = cause), NotifiableError +} + // Thrown when a reserved blob failed server-side finalization (moderation / decode / size). // Terminal: the client must reserve a fresh upload to retry. class BlobRejectedException(val rejection: BlobRejection) : diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/BlocklistRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/BlocklistRepository.kt new file mode 100644 index 000000000..74d3cb391 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/BlocklistRepository.kt @@ -0,0 +1,16 @@ +package com.flipcash.services.repository + +import com.flipcash.services.models.BlocklistPage +import com.flipcash.services.models.QueryOptions +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.opencode.model.core.ID + +interface BlocklistRepository { + suspend fun blockUser(userId: ID, owner: KeyPair): Result + + suspend fun unblockUser(userId: ID, owner: KeyPair): Result + + suspend fun isBlocked(userId: ID, owner: KeyPair): Result + + suspend fun getBlocklist(owner: KeyPair, queryOptions: QueryOptions): Result +}