Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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];
}
Original file line number Diff line number Diff line change
@@ -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<Unit> {
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<Unit> {
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<Boolean> {
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<BlocklistPage> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))

return repository.getBlocklist(owner, queryOptions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Model.BlockedUser, BlockedUser> {
override fun map(from: Model.BlockedUser): BlockedUser {
return BlockedUser(
userId = from.userId.toId(),
blockedAt = Instant.fromEpochSeconds(from.blockedAt.seconds, from.blockedAt.nanos),
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading
Loading