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
Expand Up @@ -8,6 +8,7 @@ import com.flipcash.services.controllers.ChatController
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.ChatMember
import com.flipcash.services.models.chat.ChatMetadata
import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.models.chat.PointerType
import com.flipcash.shared.chat.ChatSummary
import com.flipcash.shared.chat.FeedOperations
Expand Down Expand Up @@ -156,7 +157,7 @@ class FeedSyncDelegate @Inject constructor(

private suspend fun performFeedSync() {
stateHolder.update { it.copy(feedSyncState = FeedSyncState.Syncing) }
chatController.getDmChatFeed()
chatController.getDmChatFeed(ChatType.CONTACT_DM)
.onSuccess { page ->
metadataDataSource.upsert(page.chats)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class ChatCoordinatorEagerBalanceTest {
every { eventStreamingController.isStreamActive } returns true

val chatController = mockk<ChatController>(relaxed = true)
coEvery { chatController.getDmChatFeed() } returns Result.failure(RuntimeException("not needed"))
coEvery { chatController.getDmChatFeed(any(), any()) } returns Result.failure(RuntimeException("not needed"))

testDispatchers = TestDispatchers(TestCoroutineScheduler())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ChatCoordinatorEventsTest {
every { eventStreamingController.isStreamActive } returns true

val chatController = mockk<ChatController>(relaxed = true)
coEvery { chatController.getDmChatFeed() } returns Result.failure(RuntimeException("not needed"))
coEvery { chatController.getDmChatFeed(any(), any()) } returns Result.failure(RuntimeException("not needed"))

metadataDataSource = mockk(relaxed = true)
messageDataSource = mockk(relaxed = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class ContactCoordinator @Inject constructor(
}

suspend fun resolve(e164: String): Result<PublicKey> {
return resolverController.resolve(ContactMethod.Phone(e164))
return resolverController.resolve(phone = ContactMethod.Phone(e164))
}

fun refreshContact(e164: String): DeviceContact? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class ChatTypeConverters {
?: compat.verifiedEmailAddress?.let { address ->
VerifiableContactMethod(address, verified = true)
},
profilePicture = compat.profilePicture,
)
}.getOrNull()
}
Expand Down Expand Up @@ -163,6 +164,7 @@ data class UserProfileSerialized(
val socialAccounts: List<SocialAccountSerialized>,
val phoneNumber: VerifiableContactMethod? = null,
val email: VerifiableContactMethod? = null,
val profilePicture: MediaItem? = null,
)

/**
Expand All @@ -179,6 +181,7 @@ private data class UserProfileSerializedCompat(
val email: VerifiableContactMethod? = null,
val verifiedPhoneNumber: String? = null,
val verifiedEmailAddress: String? = null,
val profilePicture: MediaItem? = null,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,13 +275,15 @@ private fun UserProfile.toSerialized(): UserProfileSerialized = UserProfileSeria
socialAccounts = socialAccounts.map { it.toSerialized() },
phoneNumber = phoneNumber,
email = email,
profilePicture = profilePicture,
)

private fun UserProfileSerialized.toDomain(): UserProfile = UserProfile(
displayName = displayName,
socialAccounts = socialAccounts.map { it.toDomain() },
phoneNumber = phoneNumber,
email = email,
profilePicture = profilePicture,
)

private fun SocialAccount.toSerialized(): SocialAccountSerialized = when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.flipcash.services.models.QueryOptions
import com.flipcash.services.models.chat.ChatFeedPage
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.ChatMetadata
import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.repository.ChatRepository
import com.flipcash.services.user.UserManager
import javax.inject.Inject
Expand All @@ -21,10 +22,13 @@ class ChatController @Inject constructor(
return repository.getChat(owner, chatId)
}

suspend fun getDmChatFeed(queryOptions: QueryOptions = QueryOptions()): Result<ChatFeedPage> {
suspend fun getDmChatFeed(
chatType: ChatType,
queryOptions: QueryOptions = QueryOptions(),
): Result<ChatFeedPage> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))

return repository.getDmChatFeed(owner, queryOptions)
return repository.getDmChatFeed(owner, queryOptions, chatType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.flipcash.services.models.SocialAccount
import com.flipcash.services.models.SocialAccountLinkRequest
import com.flipcash.services.models.SocialAccountUnlinkRequest
import com.flipcash.services.models.UserProfile
import com.flipcash.services.models.chat.BlobId
import com.flipcash.services.models.chat.MediaItem
import com.flipcash.services.repository.ProfileRepository
import com.flipcash.services.user.UserManager
import com.getcode.opencode.model.core.ID
Expand Down Expand Up @@ -84,6 +86,19 @@ class ProfileController @Inject constructor(
return repository.setDisplayName(displayName, owner)
}

/**
* Sets the caller's profile picture to a blob already uploaded via BlobStorage.
* Returns the full set of renditions the server derived from it.
*/
suspend fun setProfilePicture(
blobId: BlobId,
): Result<MediaItem> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))

return repository.setProfilePicture(blobId, owner)
}

suspend fun linkTwitterXAccount(
token: LinkingToken
): Result<SocialAccount> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package com.flipcash.services.controllers

import com.flipcash.services.models.ContactMethod
import com.flipcash.services.models.ResolveIdentifier
import com.flipcash.services.repository.ResolverRepository
import com.flipcash.services.user.UserManager
import com.getcode.opencode.model.core.ID
import com.getcode.solana.keys.PublicKey
import javax.inject.Inject

class ResolverController @Inject constructor(
private val repository: ResolverRepository,
private val userManager: UserManager,
) {
suspend fun resolve(phone: ContactMethod.Phone): Result<PublicKey> {
/** Resolves a phone number to its on-chain address. */
suspend fun resolve(phone: ContactMethod.Phone): Result<PublicKey> =
resolve(ResolveIdentifier.Phone(phone))

/** Resolves a user ID to their on-chain address (e.g. for a tip DM counterparty). */
suspend fun resolve(userId: ID): Result<PublicKey> =
resolve(ResolveIdentifier.UserId(userId))

private suspend fun resolve(identifier: ResolveIdentifier): Result<PublicKey> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.resolve(owner, phone)
return repository.resolve(owner, identifier)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipcash.services.internal.domain
import com.codeinc.flipcash.gen.profile.v1.Model
import com.codeinc.flipcash.gen.profile.v1.emailAddressOrNull
import com.codeinc.flipcash.gen.profile.v1.phoneNumberOrNull
import com.flipcash.services.internal.network.extensions.toMediaItem
import com.flipcash.services.models.UserProfile
import com.flipcash.services.models.VerifiableContactMethod
import com.getcode.opencode.mapper.Mapper
Expand All @@ -18,6 +19,7 @@ class UserProfileMapper @Inject constructor(
// A value is only present on the server profile once verified.
phoneNumber = from.phoneNumberOrNull?.value?.let { VerifiableContactMethod(it, verified = true) },
email = from.emailAddressOrNull?.value?.let { VerifiableContactMethod(it, verified = true) },
profilePicture = if (from.hasProfilePicture()) from.profilePicture.toMediaItem() else null,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import com.codeinc.flipcash.gen.chat.v1.ChatService as RpcChatService
import com.codeinc.flipcash.gen.chat.v1.validate
import com.flipcash.services.internal.annotations.FlipcashManagedChannel
import com.flipcash.services.internal.network.extensions.asChatId
import com.flipcash.services.internal.network.extensions.asProtoChatType
import com.flipcash.services.internal.network.extensions.asQueryOptions
import com.flipcash.services.internal.network.extensions.authenticate
import com.flipcash.services.models.QueryOptions
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.ChatType
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.opencode.internal.network.core.GrpcApi
import dev.bmcreations.protovalidate.orThrow
Expand Down Expand Up @@ -46,9 +48,11 @@ internal class ChatApi @Inject constructor(
suspend fun getDmChatFeed(
owner: KeyPair,
queryOptions: QueryOptions,
chatType: ChatType,
): RpcChatService.GetDmChatFeedResponse {
val request = RpcChatService.GetDmChatFeedRequest.newBuilder()
.setQueryOptions(queryOptions.asQueryOptions())
.setDmChatType(chatType.asProtoChatType())
.apply { setAuth(authenticate(owner)) }
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import com.flipcash.services.internal.network.extensions.authenticate
import com.flipcash.services.internal.network.extensions.linkingToken
import com.flipcash.services.models.SocialAccountLinkRequest
import com.flipcash.services.models.SocialAccountUnlinkRequest
import com.flipcash.services.models.chat.BlobId
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.internal.network.core.GrpcApi
import com.getcode.opencode.model.core.ID
import com.getcode.utils.toByteString
import com.codeinc.flipcash.gen.profile.v1.validate
import dev.bmcreations.protovalidate.orThrow
import io.grpc.ManagedChannel
Expand Down Expand Up @@ -61,6 +63,30 @@ internal class ProfileApi @Inject constructor(
}
}

/**
* Sets the caller's profile picture to a blob they have already uploaded via
* BlobStorage. The server derives the DISPLAY/THUMBNAIL renditions and returns
* the full set.
*/
suspend fun setProfilePicture(
blobId: BlobId,
owner: Ed25519.KeyPair,
): ProfileService.SetProfilePictureResponse {
val request = ProfileService.SetProfilePictureRequest.newBuilder()
.setBlobId(
com.codeinc.flipcash.gen.blob.v1.Model.BlobId.newBuilder()
.setValue(blobId.bytes.toByteString())
)
.apply { setAuth(authenticate(owner)) }
.build()

request.validate().orThrow()

return withContext(Dispatchers.IO) {
api.setProfilePicture(request)
}
}

/**
* links a social account to a user
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package com.flipcash.services.internal.network.api
import com.codeinc.flipcash.gen.phone.v1.Model
import com.codeinc.flipcash.gen.resolver.v1.ResolverGrpcKt
import com.codeinc.flipcash.gen.resolver.v1.validate
import com.flipcash.services.models.ContactMethod
import com.flipcash.services.models.ResolveIdentifier
import com.flipcash.services.internal.annotations.FlipcashManagedChannel
import com.flipcash.services.internal.network.extensions.asUserId
import com.flipcash.services.internal.network.extensions.authenticate
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.opencode.internal.network.core.GrpcApi
Expand All @@ -28,13 +29,10 @@ internal class ResolverApi @Inject constructor(

suspend fun resolve(
owner: KeyPair,
phone: ContactMethod.Phone,
identifier: ResolveIdentifier,
): RpcResolverService.ResolveResponse {
val request = RpcResolverService.ResolveRequest.newBuilder()
.setIdentifier(
ResolverModel.Identifier.newBuilder()
.setPhone(Model.PhoneNumber.newBuilder().setValue(phone.phoneNumber))
)
.setIdentifier(identifier.asProtoIdentifier())
.apply { setAuth(authenticate(owner)) }
.build()

Expand All @@ -44,4 +42,14 @@ internal class ResolverApi @Inject constructor(
api.resolve(request)
}
}

private fun ResolveIdentifier.asProtoIdentifier(): ResolverModel.Identifier {
val builder = ResolverModel.Identifier.newBuilder()
return when (this) {
is ResolveIdentifier.Phone ->
builder.setPhone(Model.PhoneNumber.newBuilder().setValue(phone.phoneNumber))
is ResolveIdentifier.UserId ->
builder.setUserId(userId.asUserId())
}.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.flipcash.services.models.PagingToken
import com.flipcash.services.models.QueryOptions
import com.flipcash.services.models.SocialAccountLinkRequest
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.models.chat.ClientMessageId
import com.flipcash.services.models.chat.MessageContent
import com.flipcash.services.models.chat.PointerType
Expand All @@ -20,6 +21,7 @@ import com.getcode.solana.keys.PublicKey
import com.getcode.utils.toByteString
import com.google.protobuf.Timestamp
import kotlin.time.Instant
import com.codeinc.flipcash.gen.chat.v1.Model as ChatModel
import com.codeinc.flipcash.gen.messaging.v1.Model as MessagingModel

internal fun Checksum.asHash(): Common.Hash {
Expand Down Expand Up @@ -178,6 +180,14 @@ internal fun com.flipcash.services.models.chat.Emoji.asEmoji(): MessagingModel.E
return MessagingModel.Emoji.newBuilder().setValue(value).build()
}

internal fun ChatType.asProtoChatType(): ChatModel.ChatType {
return when (this) {
ChatType.UNKNOWN -> ChatModel.ChatType.UNKNOWN
ChatType.CONTACT_DM -> ChatModel.ChatType.CONTACT_DM
ChatType.TIP_DM -> ChatModel.ChatType.TIP_DM
}
}

internal fun Long.asMessageId(): MessagingModel.MessageId {
return MessagingModel.MessageId.newBuilder().setValue(this).build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ internal fun ChatModel.Metadata.toChatMetadata(): ChatMetadata {
socialAccounts = emptyList(),
phoneNumber = phoneNumber.value.takeIf { it.isNotEmpty() }?.let { VerifiableContactMethod(it, verified = true) },
email = emailAddress.value.takeIf { it.isNotEmpty() }?.let { VerifiableContactMethod(it, verified = true) },
profilePicture = if (hasProfilePicture()) profilePicture.toMediaItem() else null,
)
},
pointers = member.pointersList.map { it.toPointer() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.flipcash.services.models.GetChatError
import com.flipcash.services.models.GetDmChatFeedError
import com.flipcash.services.models.QueryOptions
import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.ChatType
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.opencode.internal.network.extensions.foldWithSuppression
import com.getcode.opencode.utils.toValidationOrElse
Expand Down Expand Up @@ -40,9 +41,10 @@ internal class ChatService @Inject constructor(
suspend fun getDmChatFeed(
owner: KeyPair,
queryOptions: QueryOptions,
chatType: ChatType,
): Result<RpcChatService.GetDmChatFeedResponse> {
return runCatching {
api.getDmChatFeed(owner, queryOptions)
api.getDmChatFeed(owner, queryOptions, chatType)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import com.getcode.opencode.utils.toValidationOrElse
import com.flipcash.services.models.GetUserProfileError
import com.flipcash.services.models.LinkSocialAccountError
import com.flipcash.services.models.SetDisplayNameError
import com.flipcash.services.models.SetProfilePictureError
import com.flipcash.services.models.SocialAccountLinkRequest
import com.flipcash.services.models.SocialAccountUnlinkRequest
import com.flipcash.services.models.UnlinkSocialAccountError
import com.flipcash.services.models.chat.BlobId
import com.flipcash.services.models.chat.MediaItem
import com.flipcash.services.internal.network.extensions.toMediaItem
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.internal.network.extensions.foldWithSuppression
import com.getcode.opencode.model.core.ID
Expand Down Expand Up @@ -58,6 +62,28 @@ internal class ProfileService @Inject constructor(
)
}

suspend fun setProfilePicture(
blobId: BlobId,
owner: Ed25519.KeyPair,
): Result<MediaItem> {
return runCatching {
api.setProfilePicture(blobId, owner)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
ProfileService.SetProfilePictureResponse.Result.OK -> Result.success(response.profilePicture.toMediaItem())
ProfileService.SetProfilePictureResponse.Result.DENIED -> Result.failure(SetProfilePictureError.Denied())
ProfileService.SetProfilePictureResponse.Result.BLOB_NOT_FOUND -> Result.failure(SetProfilePictureError.BlobNotFound())
ProfileService.SetProfilePictureResponse.Result.BLOB_NOT_READY -> Result.failure(SetProfilePictureError.BlobNotReady())
ProfileService.SetProfilePictureResponse.Result.BLOB_REJECTED -> Result.failure(SetProfilePictureError.BlobRejected())
ProfileService.SetProfilePictureResponse.Result.INVALID_BLOB -> Result.failure(SetProfilePictureError.InvalidBlob())
ProfileService.SetProfilePictureResponse.Result.UNRECOGNIZED -> Result.failure(SetProfilePictureError.Unrecognized())
}
},
onFailure = { Result.failure(it.toValidationOrElse { cause -> SetProfilePictureError.Other(cause) }) }
)
}

suspend fun linkSocialAccount(
request: SocialAccountLinkRequest,
owner: Ed25519.KeyPair,
Expand Down
Loading
Loading