08 · Groups
Sources/SophaxChatCore/Group/ChatManager+MLS.swift
647 lines · Swift
1// ChatManager+MLS.swift
2// SophaxChatCore
3//
4// Phase 4: MLS wire-message handlers and MLS send paths for ChatManager.
5//
6// Threading contract: same as ChatManager — @unchecked Sendable, all access
7// serialised by the caller (AppState / MeshManager callbacks on main thread).
8// MLS operations are synchronous (mls-rs is pure-Rust, no I/O).
9
10import Foundation
11
12// MARK: - MLS inbound handlers
13
14extension ChatManager {
15
16 // MARK: Welcome
17
18 /// Process an incoming MLS Welcome — join the group and notify the delegate.
19 func handleMLSWelcome(_ msg: MLSWelcomeMessage, fromPeer senderID: String) {
20 // Guard: creator must be the sender
21 guard msg.creatorID == senderID else { return }
22
23 Task { [weak self] in
24 guard let self else { return }
25 do {
26 let mls = try self.requireMLSManager()
27 try await mls.processWelcome(msg.welcomeBytes, ratchetTree: msg.ratchetTreeBytes)
28 try await mls.commitWelcomeState(groupID: msg.groupID)
29
30 let group = GroupInfo(
31 id: msg.groupID,
32 name: msg.groupName,
33 memberIDs: msg.memberIDs,
34 creatorID: msg.creatorID,
35 cryptoVersion: .mls
36 )
37 self.joinedGroups[msg.groupID] = Set(msg.memberIDs)
38 self.groupCreators[msg.groupID] = msg.creatorID
39 self.groupCoordinators[msg.groupID] = msg.creatorID
40
41 // Proactively exchange Hellos with group members we haven't met directly,
42 // so avatar and MLS KeyPackage data propagates for group-only contacts.
43 let myID = self.identity.publicIdentity.peerID
44 for memberID in msg.memberIDs where memberID != myID
45 && self.peerBundles[memberID] == nil {
46 self.sendHelloToPeer(memberID)
47 }
48
49 DispatchQueue.main.async {
50 self.delegate?.chatManager(self, didJoinGroup: group)
51 }
52 } catch {
53 DispatchQueue.main.async {
54 self.delegate?.chatManager(self, didEncounterError: error)
55 }
56 }
57 }
58 }
59
60 // MARK: Commit
61
62 /// Process an MLS Commit broadcast from the coordinator.
63 /// Drops silently if the sender is not the group coordinator.
64 func handleMLSCommit(_ msg: MLSCommitMessage, fromPeer senderID: String) {
65 // Security: only the coordinator may issue commits
66 guard let members = joinedGroups[msg.groupID],
67 msg.coordinatorID == senderID else { return }
68
69 Task { [weak self] in
70 guard let self else { return }
71 do {
72 let mls = try self.requireMLSManager()
73 let result = try await mls.processCommit(groupID: msg.groupID, commitBytes: msg.commitBytes)
74
75 // Update in-memory membership
76 var updated = members
77 result.addedPeerIds.forEach { updated.insert($0) }
78 result.removedPeerIds.forEach { updated.remove($0) }
79 self.joinedGroups[msg.groupID] = updated
80
81 // If we were removed, clean up local state
82 let myID = self.identity.publicIdentity.peerID
83 if result.removedPeerIds.contains(myID) {
84 self.joinedGroups.removeValue(forKey: msg.groupID)
85 try? await mls.deleteGroupState(groupID: msg.groupID)
86 }
87 } catch {
88 DispatchQueue.main.async {
89 self.delegate?.chatManager(self, didEncounterError: error)
90 }
91 }
92 }
93 }
94
95 // MARK: Application message
96
97 /// Decrypt and store an MLS application message.
98 func handleMLSApplicationMessage(_ msg: MLSApplicationMessage) {
99 guard let members = joinedGroups[msg.groupID],
100 members.contains(msg.senderPeerID) else { return }
101 // Basic field-length guards (mirrors Sender Keys v2 handler)
102 guard msg.senderPeerID.count <= 64,
103 msg.senderUsername.count <= 64,
104 msg.ciphertext.count <= 512_000 else { return }
105
106 let convID = "group.\(msg.groupID)"
107 let messageID = msg.messageID
108
109 Task { [weak self] in
110 guard let self else { return }
111 do {
112 let mls = try self.requireMLSManager()
113 let captionPlain = try await mls.decrypt(groupID: msg.groupID, ciphertext: msg.ciphertext)
114 let body = String(data: captionPlain, encoding: .utf8) ?? ""
115
116 // Decrypt attachment if present and save to AttachmentStore
117 var attachmentID: String? = nil
118 if let ac = msg.attachmentCiphertext, msg.mimeType != nil {
119 let attachData = try await mls.decrypt(groupID: msg.groupID, ciphertext: ac)
120 let aid = UUID().uuidString
121 try? self.attachmentStore.save(attachData, id: aid)
122 attachmentID = aid
123 }
124
125 let stored = StoredMessage(
126 id: messageID,
127 peerID: convID,
128 direction: .received,
129 body: body,
130 status: .delivered,
131 replyToID: msg.replyToID,
132 expiresAt: msg.expiresAt,
133 attachmentID: attachmentID,
134 attachmentMimeType: msg.mimeType,
135 audioDuration: msg.audioDuration,
136 senderID: msg.senderPeerID
137 )
138 try self.messageStore.append(message: stored)
139
140 // Send delivery receipt back to the original sender (mirrors v2 group handler).
141 // Best-effort: silently dropped if no path to sender yet.
142 let receiptPayload = GroupReadReceiptMessage(groupID: msg.groupID, targetMessageID: msg.messageID, isRead: false)
143 if let receipt = try? self.wireBuilder.build(.groupReadReceipt, payload: receiptPayload) {
144 try? self.sendOrQueue(receipt, toPeerID: msg.senderPeerID, messageID: UUID().uuidString)
145 }
146
147 // Cache sender avatar for group-only contacts
148 if let avatarData = msg.senderAvatarData,
149 !avatarData.isEmpty, avatarData.count <= 8_192,
150 self.peerBundles[msg.senderPeerID]?.avatarData == nil {
151 let senderID = msg.senderPeerID
152 DispatchQueue.main.async {
153 self.delegate?.chatManager(self, didReceiveAvatarData: avatarData, fromPeerID: senderID)
154 }
155 }
156
157 DispatchQueue.main.async {
158 self.delegate?.chatManager(self, didReceiveGroupMessage: stored, inGroup: msg.groupID)
159 }
160 } catch {
161 DispatchQueue.main.async {
162 self.delegate?.chatManager(self, didEncounterError: error)
163 }
164 }
165 }
166 }
167
168 // MARK: Group edit (MLS)
169
170 /// Send an edit for a previously sent MLS group message.
171 /// Encrypts the replacement body with the current MLS epoch key before sending.
172 func sendMLSGroupEditMessage(messageID: String, newBody: String, group: GroupInfo) {
173 guard !newBody.isEmpty, newBody.utf8.count <= Self.maxMessageBytes else { return }
174 let convID = "group.\(group.id)"
175 let editedAt = Date()
176 guard let msgs = try? messageStore.messages(forPeer: convID),
177 let existing = msgs.first(where: { $0.id == messageID }),
178 existing.direction == .sent,
179 existing.attachmentID == nil,
180 existing.timestamp.timeIntervalSinceNow > -300 else { return }
181
182 Task { [weak self] in
183 guard let self else { return }
184 do {
185 let mls = try self.requireMLSManager()
186 guard let plaintext = newBody.data(using: .utf8) else { return }
187 let ciphertext = try await mls.encrypt(groupID: group.id, plaintext: plaintext)
188 let payload = MLSGroupEditMessage(
189 groupID: group.id,
190 messageID: messageID,
191 ciphertext: ciphertext,
192 editedAt: editedAt
193 )
194 guard let wire = try? self.wireBuilder.build(.mlsGroupEditMessage, payload: payload) else { return }
195 let myID = self.identity.publicIdentity.peerID
196 for peerID in group.memberIDs where peerID != myID {
197 try? self.sendOrQueue(wire, toPeerID: peerID, messageID: UUID().uuidString)
198 }
199 try? self.messageStore.updateMessage(
200 id: messageID, peerID: convID, newBody: newBody, editedAt: editedAt
201 )
202 let body = newBody
203 let editTime = editedAt
204 let groupID = group.id
205 DispatchQueue.main.async {
206 self.delegate?.chatManager(self, didReceiveEditedGroupMessage: messageID,
207 newBody: body, editedAt: editTime, groupID: groupID)
208 }
209 } catch { /* silently drop failed encrypt */ }
210 }
211 }
212
213 /// Handle an incoming MLS group edit — decrypts and updates the message store.
214 func handleMLSGroupEditMessage(_ msg: MLSGroupEditMessage) {
215 guard joinedGroups[msg.groupID] != nil else { return }
216 let convID = "group.\(msg.groupID)"
217
218 Task { [weak self] in
219 guard let self else { return }
220 do {
221 let mls = try self.requireMLSManager()
222 let plaintext = try await mls.decrypt(groupID: msg.groupID, ciphertext: msg.ciphertext)
223 guard let newBody = String(data: plaintext, encoding: .utf8),
224 !newBody.isEmpty,
225 newBody.utf8.count <= Self.maxMessageBytes else { return }
226
227 // Validate: text-only, within 5-minute window (+10 s clock slack)
228 guard let msgs = try? self.messageStore.messages(forPeer: convID),
229 let existing = msgs.first(where: { $0.id == msg.messageID }),
230 existing.attachmentID == nil,
231 existing.timestamp.timeIntervalSinceNow > -310 else { return }
232
233 try? self.messageStore.updateMessage(
234 id: msg.messageID, peerID: convID, newBody: newBody, editedAt: msg.editedAt
235 )
236 let messageID = msg.messageID
237 let body = newBody
238 let editedAt = msg.editedAt
239 let groupID = msg.groupID
240 DispatchQueue.main.async {
241 self.delegate?.chatManager(self, didReceiveEditedGroupMessage: messageID,
242 newBody: body, editedAt: editedAt, groupID: groupID)
243 }
244 } catch { }
245 }
246 }
247
248 // MARK: Commit request (coordinator path)
249
250 /// Handle a CommitRequest from a non-coordinator member (add or remove).
251 /// Only the group creator processes this — others drop it.
252 func handleMLSCommitRequest(_ msg: MLSCommitRequestMessage, fromPeer senderID: String) {
253 let myID = identity.publicIdentity.peerID
254 guard joinedGroups[msg.groupID] != nil else { return }
255 // Only the coordinator (creator) issues commits
256 // We verify by checking if we created this group — stored in GroupInfo at the delegate layer.
257 // ChatManager has no direct access to persisted GroupInfo, so we rely on the delegate
258 // to guard coordinator access. Here we just execute if called.
259 Task { [weak self] in
260 guard let self else { return }
261 do {
262 let mls = try self.requireMLSManager()
263 let commitOut: CommitOutput
264 switch msg.action {
265 case .add:
266 guard let kp = msg.keyPackage else { return }
267 commitOut = try await mls.addMember(groupID: msg.groupID, keyPackage: kp)
268 case .remove:
269 guard let target = msg.targetPeerID, target != myID else { return }
270 commitOut = try await mls.removeMember(groupID: msg.groupID, peerID: target)
271 }
272 // Broadcast commit to all current members
273 let commitMsg = MLSCommitMessage(
274 groupID: msg.groupID,
275 epoch: commitOut.newEpoch,
276 commitBytes: commitOut.commitBytes,
277 coordinatorID: myID
278 )
279 if let wire = try? wireBuilder.build(.mlsCommit, payload: commitMsg) {
280 let members = joinedGroups[msg.groupID] ?? []
281 for peerID in members where peerID != myID {
282 try? sendOrQueue(wire, toPeerID: peerID, messageID: UUID().uuidString)
283 }
284 }
285 // Unicast Welcome to the new member (add only)
286 if msg.action == .add,
287 let welcomeBytes = commitOut.welcomeBytes,
288 let targetPeerID = msg.targetPeerID ?? (msg.keyPackage != nil ? senderID : nil) {
289 guard let members = joinedGroups[msg.groupID] else { return }
290 let welcomeMsg = MLSWelcomeMessage(
291 groupID: msg.groupID,
292 groupName: "", // delegate fills this in via GroupInfo
293 memberIDs: Array(members),
294 creatorID: myID,
295 welcomeBytes: welcomeBytes,
296 ratchetTreeBytes: Data()
297 )
298 if let wire = try? wireBuilder.build(.mlsWelcome, payload: welcomeMsg) {
299 try? sendOrQueue(wire, toPeerID: targetPeerID, messageID: UUID().uuidString)
300 }
301 }
302 // Update local membership
303 if msg.action == .remove, let target = msg.targetPeerID {
304 self.joinedGroups[msg.groupID]?.remove(target)
305 }
306 } catch {
307 DispatchQueue.main.async {
308 self.delegate?.chatManager(self, didEncounterError: error)
309 }
310 }
311 }
312 }
313
314 func handleMLSReaction(_ msg: MLSReactionMessage) {
315 guard joinedGroups[msg.groupID] != nil else { return }
316 let convID = "group.\(msg.groupID)"
317 let senderPeerID = msg.senderPeerID
318 let targetID = msg.targetMessageID
319 let groupID = msg.groupID
320
321 Task { [weak self] in
322 guard let self else { return }
323 do {
324 let mls = try self.requireMLSManager()
325 let plain = try await mls.decrypt(groupID: groupID, ciphertext: msg.ciphertext)
326 struct ReactionPayload: Codable { let emoji: String? }
327 guard let payload = try? JSONDecoder().decode(ReactionPayload.self, from: plain) else { return }
328
329 if let reactions = self.applyReaction(emoji: payload.emoji, senderID: senderPeerID,
330 messageID: targetID, convID: convID) {
331 DispatchQueue.main.async { [weak self] in
332 guard let self else { return }
333 self.delegate?.chatManager(self, didUpdateGroupReactions: reactions,
334 onMessageID: targetID, groupID: groupID)
335 }
336 }
337 } catch {
338 #if DEBUG
339 print("[ChatManager][MLS] handleMLSReaction error: \(error)")
340 #endif
341 }
342 }
343 }
344}
345
346// MARK: - MLS outbound
347
348extension ChatManager {
349
350 /// Create a new MLS group and distribute Welcomes to all members.
351 /// Requires all `memberPeerIDs` to have an `mlsKeyPackage` in their stored PreKeyBundle.
352 /// Returns nil if any member is missing a KeyPackage or MLS init fails.
353 @discardableResult
354 public func createMLSGroup(name: String, memberPeerIDs: [String]) -> GroupInfo? {
355 let myID = identity.publicIdentity.peerID
356 var seen = Set<String>()
357 let allMembers = ([myID] + memberPeerIDs).filter { seen.insert($0).inserted }
358
359 // Collect KeyPackages — fail if any member is missing one
360 var keyPackages: [(peerID: String, kp: Data)] = []
361 for peerID in memberPeerIDs {
362 guard let bundle = peerBundles[peerID],
363 let kp = bundle.mlsKeyPackage else {
364 delegate?.chatManager(self, didEncounterError:
365 SophaxError.invalidMessageFormat("Peer \(peerID) has no MLS KeyPackage — cannot create MLS group"))
366 return nil
367 }
368 keyPackages.append((peerID, kp))
369 }
370
371 let groupID = UUID().uuidString
372 let group = GroupInfo(id: groupID, name: name, memberIDs: allMembers, creatorID: myID, cryptoVersion: .mls)
373
374 Task { [weak self] in
375 guard let self else { return }
376 do {
377 let mls = try self.requireMLSManager()
378 let createOut = try await mls.createGroup(
379 groupID: groupID,
380 memberKeyPackages: keyPackages.map(\.kp)
381 )
382 self.joinedGroups[groupID] = Set(allMembers)
383 self.groupCreators[groupID] = myID
384 self.groupCoordinators[groupID] = myID
385
386 // Send each member their Welcome via DR unicast
387 for welcome in createOut.welcomes {
388 let welcomeMsg = MLSWelcomeMessage(
389 groupID: groupID,
390 groupName: name,
391 memberIDs: allMembers,
392 creatorID: myID,
393 welcomeBytes: welcome.welcomeBytes,
394 ratchetTreeBytes: welcome.ratchetTreeBytes
395 )
396 if let wire = try? self.wireBuilder.build(.mlsWelcome, payload: welcomeMsg) {
397 try? self.sendOrQueue(wire, toPeerID: welcome.peerId, messageID: UUID().uuidString)
398 }
399 }
400
401 DispatchQueue.main.async {
402 self.delegate?.chatManager(self, didJoinGroup: group)
403 }
404 } catch {
405 DispatchQueue.main.async {
406 self.delegate?.chatManager(self, didEncounterError: error)
407 }
408 }
409 }
410 return group
411 }
412
413 // MARK: - Shared send-path helper
414
415 /// Mark a group message as failed and surface the error to the delegate.
416 private func failGroupMessage(_ error: Error, messageID: String, convID: String) {
417 try? messageStore.updateStatus(.failed, forMessageID: messageID, peerID: convID)
418 DispatchQueue.main.async { [weak self] in
419 guard let self else { return }
420 self.delegate?.chatManager(self, didEncounterError: error)
421 }
422 }
423
424 /// Send a text message to an MLS group.
425 public func sendMLSGroupMessage(
426 _ body: String,
427 group: GroupInfo,
428 expiresAt: Date? = nil,
429 replyToID: String? = nil
430 ) {
431 guard !body.isEmpty else { return }
432 let myID = identity.publicIdentity.peerID
433 let messageID = UUID().uuidString
434 let convID = "group.\(group.id)"
435
436 // Store locally as .sending so the bubble appears immediately
437 let stored = StoredMessage(
438 id: messageID, peerID: convID,
439 direction: .sent, body: body, status: .sending,
440 replyToID: replyToID, expiresAt: expiresAt, senderID: myID
441 )
442 do { try messageStore.append(message: stored) } catch { failGroupMessage(error, messageID: messageID, convID: convID); return }
443 DispatchQueue.main.async { [weak self] in
444 guard let self else { return }
445 self.delegate?.chatManager(self, didReceiveGroupMessage: stored, inGroup: group.id)
446 }
447
448 Task { [weak self] in
449 guard let self else { return }
450 do {
451 let mls = try self.requireMLSManager()
452 guard let plaintext = body.data(using: .utf8) else { throw SophaxError.encryptionFailed("Body is not UTF-8") }
453 let ciphertext = try await mls.encrypt(groupID: group.id, plaintext: plaintext)
454
455 let appMsg = MLSApplicationMessage(
456 groupID: group.id,
457 messageID: messageID,
458 senderPeerID: myID,
459 senderUsername: self.identity.publicIdentity.username,
460 timestamp: Date(),
461 ciphertext: ciphertext,
462 expiresAt: expiresAt,
463 replyToID: replyToID,
464 attachmentCiphertext: nil,
465 mimeType: nil,
466 audioDuration: nil,
467 senderAvatarData: self.identity.loadAvatar()
468 )
469 guard let wire = try? self.wireBuilder.build(.mlsMessage, payload: appMsg) else {
470 throw SophaxError.encryptionFailed("Failed to build MLS wire message")
471 }
472
473 try? self.messageStore.updateStatus(.delivered, forMessageID: messageID, peerID: convID)
474
475 let members = group.memberIDs
476 for peerID in members where peerID != myID {
477 try? self.sendOrQueue(wire, toPeerID: peerID, messageID: messageID)
478 }
479 } catch {
480 failGroupMessage(error, messageID: messageID, convID: convID)
481 }
482 }
483 }
484
485 /// Send a binary attachment (image or audio) to an MLS group.
486 public func sendMLSGroupAttachment(
487 _ data: Data,
488 mimeType: String,
489 caption: String = "",
490 audioDuration: Double? = nil,
491 group: GroupInfo,
492 expiresAt: Date? = nil,
493 replyToID: String? = nil
494 ) {
495 guard data.count <= 512_000 else {
496 delegate?.chatManager(self, didEncounterError:
497 SophaxError.invalidMessageFormat("Attachment exceeds 512 KB limit"))
498 return
499 }
500
501 let myID = identity.publicIdentity.peerID
502 let messageID = UUID().uuidString
503 let attachmentID = UUID().uuidString
504 let convID = "group.\(group.id)"
505 let msgType = mimeType.hasPrefix("image/") ? "image" : "audio"
506 let displayBody = caption.isEmpty
507 ? (msgType == "image" ? "📷 Photo" : "🎤 Voice message")
508 : caption
509
510 // Save attachment locally for sender's own bubble
511 try? attachmentStore.save(data, id: attachmentID)
512
513 let stored = StoredMessage(
514 id: messageID, peerID: convID,
515 direction: .sent, body: displayBody, status: .sending,
516 replyToID: replyToID, expiresAt: expiresAt,
517 attachmentID: attachmentID, attachmentMimeType: mimeType,
518 audioDuration: audioDuration, senderID: myID
519 )
520 do { try messageStore.append(message: stored) } catch { failGroupMessage(error, messageID: messageID, convID: convID); return }
521 DispatchQueue.main.async { [weak self] in
522 guard let self else { return }
523 self.delegate?.chatManager(self, didReceiveGroupMessage: stored, inGroup: group.id)
524 }
525
526 Task { [weak self] in
527 guard let self else { return }
528 do {
529 let mls = try self.requireMLSManager()
530
531 // Encrypt caption (may be empty)
532 let captionPlain = caption.data(using: .utf8) ?? Data()
533 let bodyCipher = try await mls.encrypt(groupID: group.id, plaintext: captionPlain)
534 // Encrypt attachment bytes
535 let attachCipher = try await mls.encrypt(groupID: group.id, plaintext: data)
536
537 let appMsg = MLSApplicationMessage(
538 groupID: group.id,
539 messageID: messageID,
540 senderPeerID: myID,
541 senderUsername: self.identity.publicIdentity.username,
542 timestamp: Date(),
543 ciphertext: bodyCipher,
544 expiresAt: expiresAt,
545 replyToID: replyToID,
546 attachmentCiphertext: attachCipher,
547 mimeType: mimeType,
548 audioDuration: audioDuration,
549 senderAvatarData: self.identity.loadAvatar()
550 )
551 guard let wire = try? self.wireBuilder.build(.mlsMessage, payload: appMsg) else {
552 throw SophaxError.encryptionFailed("Failed to build MLS attachment wire message")
553 }
554
555 try? self.messageStore.updateStatus(.delivered, forMessageID: messageID, peerID: convID)
556
557 for peerID in group.memberIDs where peerID != myID {
558 try? self.sendOrQueue(wire, toPeerID: peerID, messageID: messageID)
559 }
560 } catch {
561 failGroupMessage(error, messageID: messageID, convID: convID)
562 }
563 }
564 }
565
566 /// Send an emoji reaction (or remove one) on a message in an MLS group.
567 public func sendMLSGroupReaction(emoji: String?, toMessageID targetID: String, group: GroupInfo) {
568 let myID = identity.publicIdentity.peerID
569 let messageID = UUID().uuidString
570 let convID = "group.\(group.id)"
571
572 // Encode reaction payload and apply locally immediately
573 struct ReactionPayload: Codable { let emoji: String? }
574 guard let payloadData = try? JSONEncoder().encode(ReactionPayload(emoji: emoji)) else { return }
575
576 // Apply locally
577 if let reactions = applyReaction(emoji: emoji, senderID: myID, messageID: targetID, convID: convID) {
578 DispatchQueue.main.async { [weak self] in
579 guard let self else { return }
580 self.delegate?.chatManager(self, didUpdateGroupReactions: reactions,
581 onMessageID: targetID, groupID: group.id)
582 }
583 }
584
585 Task { [weak self] in
586 guard let self else { return }
587 do {
588 let mls = try self.requireMLSManager()
589 let ciphertext = try await mls.encrypt(groupID: group.id, plaintext: payloadData)
590 let wireMsg = MLSReactionMessage(
591 groupID: group.id, messageID: messageID,
592 senderPeerID: myID, targetMessageID: targetID,
593 ciphertext: ciphertext, timestamp: Date()
594 )
595 guard let wire = try? self.wireBuilder.build(.mlsReaction, payload: wireMsg) else { return }
596 for peerID in group.memberIDs where peerID != myID {
597 try? self.sendOrQueue(wire, toPeerID: peerID, messageID: messageID)
598 }
599 } catch {
600 #if DEBUG
601 print("[ChatManager][MLS] sendMLSGroupReaction error: \(error)")
602 #endif
603 }
604 }
605 }
606}
607
608// MARK: - Wire dispatch (replaces Phase 3 stubs)
609
610extension ChatManager {
611
612 /// Called from the two main dispatch switches to route MLS wire messages.
613 func dispatchMLSMessage(_ message: WireMessage) {
614 do {
615 switch message.type {
616 case .mlsWelcome:
617 let payload = try wireBuilder.decodePayload(MLSWelcomeMessage.self, from: message)
618 handleMLSWelcome(payload, fromPeer: message.senderID)
619 case .mlsCommit:
620 let payload = try wireBuilder.decodePayload(MLSCommitMessage.self, from: message)
621 handleMLSCommit(payload, fromPeer: message.senderID)
622 case .mlsMessage:
623 let payload = try wireBuilder.decodePayload(MLSApplicationMessage.self, from: message)
624 handleMLSApplicationMessage(payload)
625 case .mlsCommitRequest:
626 let payload = try wireBuilder.decodePayload(MLSCommitRequestMessage.self, from: message)
627 handleMLSCommitRequest(payload, fromPeer: message.senderID)
628 case .mlsReaction:
629 let payload = try wireBuilder.decodePayload(MLSReactionMessage.self, from: message)
630 handleMLSReaction(payload)
631 case .mlsCoordinatorHandoff:
632 let payload = try wireBuilder.decodePayload(MLSCoordinatorHandoffMessage.self, from: message)
633 handleCoordinatorHandoff(payload, senderID: message.senderID)
634 case .mlsGroupEditMessage:
635 if let payload = try? wireBuilder.decodePayload(MLSGroupEditMessage.self, from: message) {
636 handleMLSGroupEditMessage(payload)
637 }
638 default:
639 break
640 }
641 } catch {
642 #if DEBUG
643 print("[ChatManager][MLS] dispatch error: \(error)")
644 #endif
645 }
646 }
647}