Skip to content

Commit 170950b

Browse files
committed
concrete publish session written and verified
1 parent 5f261c7 commit 170950b

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
import GRDB
3+
@preconcurrency import XMTPiOS
4+
5+
/// Concrete `ProfilePublishSession` backed by the XMTP client and upload API.
6+
/// Mirrors the encrypt/upload/send path in `MyProfileWriter`, including the
7+
/// best-effort second channel (writing the profile into group app-data) so
8+
/// clients that read `ConversationProfile` rather than the `ProfileUpdate`
9+
/// message still see the identity.
10+
///
11+
/// This is boundary code (it uses XMTP types directly, like the writers). It is
12+
/// exercised only once the publisher is fed at the cutover; there is no
13+
/// meaningful unit test - it is verified via integration / manual runs.
14+
struct MessagingProfilePublishSession: ProfilePublishSession {
15+
private let sessionStateManager: any SessionStateManagerProtocol
16+
private let databaseReader: any DatabaseReader
17+
18+
init(sessionStateManager: any SessionStateManagerProtocol, databaseReader: any DatabaseReader) {
19+
self.sessionStateManager = sessionStateManager
20+
self.databaseReader = databaseReader
21+
}
22+
23+
func conversationIds() async throws -> [String] {
24+
try await databaseReader.read { db in
25+
try String.fetchAll(db, sql: "SELECT id FROM conversation")
26+
}
27+
}
28+
29+
func imageKey(conversationId: String) async throws -> Data? {
30+
let inboxReady = try await sessionStateManager.waitForInboxReadyResult()
31+
guard let conversation = try await inboxReady.client.conversation(with: conversationId),
32+
case .group(let group) = conversation else {
33+
return nil
34+
}
35+
return try await group.ensureImageEncryptionKey()
36+
}
37+
38+
func encrypt(_ plaintext: Data, groupKey: Data) throws -> EncryptedAvatarPayload {
39+
let payload = try ImageEncryption.encrypt(imageData: plaintext, groupKey: groupKey)
40+
return EncryptedAvatarPayload(ciphertext: payload.ciphertext, salt: payload.salt, nonce: payload.nonce)
41+
}
42+
43+
func upload(_ ciphertext: Data, filename: String) async throws -> String {
44+
let inboxReady = try await sessionStateManager.waitForInboxReadyResult()
45+
return try await inboxReady.apiClient.uploadAttachment(
46+
data: ciphertext,
47+
filename: filename,
48+
contentType: "application/octet-stream",
49+
acl: "public-read"
50+
)
51+
}
52+
53+
func sendProfileUpdate(name: String?, avatar: PublishedAvatar?, conversationId: String) async throws {
54+
let inboxReady = try await sessionStateManager.waitForInboxReadyResult()
55+
guard let conversation = try await inboxReady.client.conversation(with: conversationId),
56+
case .group(let group) = conversation else {
57+
throw ProfilePublishSessionError.conversationNotFound(conversationId: conversationId)
58+
}
59+
60+
var update = ProfileUpdate()
61+
if let name {
62+
update.name = name
63+
}
64+
if let avatar {
65+
var ref = EncryptedProfileImageRef()
66+
ref.url = avatar.url
67+
ref.salt = avatar.salt
68+
ref.nonce = avatar.nonce
69+
update.encryptedImage = ref
70+
}
71+
let encoded = try ProfileUpdateCodec().encode(content: update)
72+
_ = try await group.send(encodedContent: encoded)
73+
74+
// Second channel: mirror into group app-data (best-effort). `updateProfile`
75+
// merges, so a nil avatar preserves the existing app-data image rather
76+
// than clearing it.
77+
let memberProfile = DBMemberProfile(
78+
conversationId: conversationId,
79+
inboxId: inboxReady.client.inboxId,
80+
name: name,
81+
avatar: avatar?.url,
82+
avatarSalt: avatar?.salt,
83+
avatarNonce: avatar?.nonce,
84+
avatarKey: avatar?.key
85+
)
86+
do {
87+
try await group.updateProfile(memberProfile)
88+
} catch {
89+
Log.warning("ProfilePublishSession app-data updateProfile failed (best-effort): \(error.localizedDescription)")
90+
}
91+
}
92+
}
93+
94+
enum ProfilePublishSessionError: Error {
95+
case conversationNotFound(conversationId: String)
96+
}

ConvosCore/Sources/ConvosCore/Profiles/Repository/ProfilePublishSession.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ struct PublishedAvatar: Sendable {
2121
/// `ProfilePublisher` (and ConvosCore) free of XMTPiOS. The messaging layer
2222
/// provides the concrete implementation when the publisher is wired up.
2323
protocol ProfilePublishSession: Sendable {
24-
var inboxId: String { get }
25-
2624
/// All conversations the current user can publish their profile to.
2725
func conversationIds() async throws -> [String]
2826

0 commit comments

Comments
 (0)