bump to 20261013.0
Build Unsigned iOS ipa / Build Unsigned iOS IPA (pull_request) Canceled after 0s
iOS CI / Build and Test SwiftUI App (pull_request) Canceled after 0s
Build Unsigned iOS ipa / Build Unsigned iOS IPA (push) Canceled after 0s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (pull_request) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (push) Canceled after 0s
Build Unsigned iOS ipa / Build Unsigned iOS IPA (pull_request) Canceled after 0s
iOS CI / Build and Test SwiftUI App (pull_request) Canceled after 0s
Build Unsigned iOS ipa / Build Unsigned iOS IPA (push) Canceled after 0s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (pull_request) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (push) Canceled after 0s
This commit is contained in:
1 parent
8f235b4ff9
commit
6969863d1e
3 files changed
+230
-194
No files matched your search
@@ -1527,7 +1527,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MARKETING_VERSION = 0.10.6;
|
||||
MARKETING_VERSION = 20261013.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -1606,7 +1606,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MARKETING_VERSION = 0.10.6;
|
||||
MARKETING_VERSION = 20261013.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -2,8 +2,8 @@ import Combine
|
||||
import Foundation
|
||||
@preconcurrency import Martin
|
||||
@preconcurrency import MartinOMEMO
|
||||
import os
|
||||
import WebRTC
|
||||
import os
|
||||
|
||||
private struct BufferedArchiveStanza {
|
||||
let queryID: String
|
||||
@@ -34,16 +34,16 @@ private final class ArchiveStanzaInbox: @unchecked Sendable {
|
||||
var overflowed = false
|
||||
var rejectedSource = false
|
||||
}
|
||||
|
||||
|
||||
private let lock = NSLock()
|
||||
private let maximumCount: Int
|
||||
// private var activeQueryID: String?
|
||||
// private var allowedSources: Set<String> = []
|
||||
// private var stanzas: [BufferedArchiveStanza] = []
|
||||
// private var overflowed = false
|
||||
// private var rejectedSource = false
|
||||
// private var activeQueryID: String?
|
||||
// private var allowedSources: Set<String> = []
|
||||
// private var stanzas: [BufferedArchiveStanza] = []
|
||||
// private var overflowed = false
|
||||
// private var rejectedSource = false
|
||||
private var states: [String: State] = [:]
|
||||
|
||||
|
||||
init(maximumCount: Int) {
|
||||
self.maximumCount = maximumCount
|
||||
}
|
||||
@@ -51,11 +51,11 @@ private final class ArchiveStanzaInbox: @unchecked Sendable {
|
||||
func begin(queryID: String, allowedSources: Set<String>) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
// activeQueryID = queryID
|
||||
// self.allowedSources = Set(allowedSources.map { $0.lowercased() })
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
// activeQueryID = queryID
|
||||
// self.allowedSources = Set(allowedSources.map { $0.lowercased() })
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
states[queryID] = State(
|
||||
allowedSources: Set(allowedSources.map { $0.lowercased() })
|
||||
)
|
||||
@@ -70,23 +70,23 @@ private final class ArchiveStanzaInbox: @unchecked Sendable {
|
||||
) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
// guard activeQueryID == queryID else { return }
|
||||
// guard allowedSources.contains(source.stringValue.lowercased()) else {
|
||||
// rejectedSource = true
|
||||
// guard activeQueryID == queryID else { return }
|
||||
// guard allowedSources.contains(source.stringValue.lowercased()) else {
|
||||
// rejectedSource = true
|
||||
guard var state = states[queryID] else { return }
|
||||
guard state.allowedSources.contains(source.stringValue.lowercased()) else {
|
||||
state.rejectedSource = true
|
||||
states[queryID] = state
|
||||
return
|
||||
}
|
||||
// guard stanzas.count < maximumCount else {
|
||||
// overflowed = true
|
||||
// guard stanzas.count < maximumCount else {
|
||||
// overflowed = true
|
||||
guard state.stanzas.count < maximumCount else {
|
||||
state.overflowed = true
|
||||
states[queryID] = state
|
||||
return
|
||||
}
|
||||
// stanzas.append(
|
||||
// stanzas.append(
|
||||
state.stanzas.append(
|
||||
BufferedArchiveStanza(
|
||||
queryID: queryID,
|
||||
@@ -94,28 +94,28 @@ private final class ArchiveStanzaInbox: @unchecked Sendable {
|
||||
timestamp: timestamp,
|
||||
archiveID: archiveID
|
||||
))
|
||||
// print("MAM source: \(source), allowed: \(allowedSources)")
|
||||
// print("MAM source: \(source), allowed: \(allowedSources)")
|
||||
states[queryID] = state
|
||||
}
|
||||
|
||||
func take(queryID: String) -> BufferedArchivePage {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
// guard activeQueryID == queryID else {
|
||||
// guard activeQueryID == queryID else {
|
||||
guard let state = states.removeValue(forKey: queryID) else {
|
||||
return BufferedArchivePage(stanzas: [], overflowed: false, rejectedSource: false)
|
||||
}
|
||||
// let page = BufferedArchivePage(
|
||||
// stanzas: stanzas,
|
||||
// overflowed: overflowed,
|
||||
// rejectedSource: rejectedSource
|
||||
// )
|
||||
// activeQueryID = nil
|
||||
// allowedSources.removeAll(keepingCapacity: true)
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
// return page
|
||||
// let page = BufferedArchivePage(
|
||||
// stanzas: stanzas,
|
||||
// overflowed: overflowed,
|
||||
// rejectedSource: rejectedSource
|
||||
// )
|
||||
// activeQueryID = nil
|
||||
// allowedSources.removeAll(keepingCapacity: true)
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
// return page
|
||||
return BufferedArchivePage(
|
||||
stanzas: state.stanzas,
|
||||
overflowed: state.overflowed,
|
||||
@@ -126,12 +126,12 @@ private final class ArchiveStanzaInbox: @unchecked Sendable {
|
||||
func cancel(queryID: String? = nil) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
// if let queryID, activeQueryID != queryID { return }
|
||||
// activeQueryID = nil
|
||||
// allowedSources.removeAll(keepingCapacity: true)
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
// if let queryID, activeQueryID != queryID { return }
|
||||
// activeQueryID = nil
|
||||
// allowedSources.removeAll(keepingCapacity: true)
|
||||
// stanzas.removeAll(keepingCapacity: true)
|
||||
// overflowed = false
|
||||
// rejectedSource = false
|
||||
if let queryID {
|
||||
states.removeValue(forKey: queryID)
|
||||
} else {
|
||||
@@ -714,7 +714,7 @@ final class XMPPService {
|
||||
}
|
||||
observe(room: room)
|
||||
enqueueMUCCatchup(roomJID: room.jid)
|
||||
|
||||
|
||||
eventHandler?(
|
||||
.roomState(
|
||||
RoomStateEnvelope(
|
||||
@@ -815,7 +815,9 @@ final class XMPPService {
|
||||
message.addChild(CallHistorySync.payloadElement(entry: entry))
|
||||
client.context.writer.write(message, writeCompleted: nil)
|
||||
Logger(subsystem: "Luma", category: "call-sync")
|
||||
.info("sent call-history id=\(entry.id) peer=\(entry.peerJID) status=\(entry.outcome.rawValue)")
|
||||
.info(
|
||||
"sent call-history id=\(entry.id) peer=\(entry.peerJID) status=\(entry.outcome.rawValue)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends a `<read-marker/>` service message to the user's own bare JID
|
||||
@@ -837,7 +839,9 @@ final class XMPPService {
|
||||
message.addChild(ReadStateSync.payloadElement(marker: marker))
|
||||
client.context.writer.write(message, writeCompleted: nil)
|
||||
Logger(subsystem: "Luma", category: "read-sync")
|
||||
.info("sent read-marker id=\(marker.id) message=\(marker.messageID) conversation=\(marker.conversationJID)")
|
||||
.info(
|
||||
"sent read-marker id=\(marker.id) message=\(marker.messageID) conversation=\(marker.conversationJID)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends an XEP-0333 `<displayed/>` chat marker so the peer's client can
|
||||
@@ -874,7 +878,6 @@ final class XMPPService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func sendText(
|
||||
_ text: String,
|
||||
to recipient: String,
|
||||
@@ -930,7 +933,8 @@ final class XMPPService {
|
||||
// not cached yet.
|
||||
if let omemo2 = client.moduleOrNil(.omemo2),
|
||||
omemo2.isReady,
|
||||
await Self.allRecipientsPublishOMEMO2(recipients, module: omemo2) {
|
||||
await Self.allRecipientsPublishOMEMO2(recipients, module: omemo2)
|
||||
{
|
||||
let encryptedMessage = try await encrypt(
|
||||
message,
|
||||
forGroupRecipients: recipients,
|
||||
@@ -988,7 +992,8 @@ final class XMPPService {
|
||||
// legacy.
|
||||
if let omemo2 = client.moduleOrNil(.omemo2),
|
||||
omemo2.isReady,
|
||||
await !(omemo2.deviceIDs(for: peer, fetching: true) ?? []).isEmpty {
|
||||
await !(omemo2.deviceIDs(for: peer, fetching: true) ?? []).isEmpty
|
||||
{
|
||||
let encryptedMessage = try await encrypt(message, using: omemo2)
|
||||
try await chat.send(message: encryptedMessage.message)
|
||||
return encryptedMessage.fingerprint
|
||||
@@ -1004,7 +1009,7 @@ final class XMPPService {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// OpenSSL version strings to their user-facing form ("TLSv1.3" →
|
||||
/// "TLS 1.3").
|
||||
private static func displayTLSVersion(_ version: String) -> String {
|
||||
@@ -1056,7 +1061,7 @@ final class XMPPService {
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func performOlderHistoryRequest(
|
||||
conversationJID: String,
|
||||
isGroup: Bool,
|
||||
@@ -1072,9 +1077,10 @@ final class XMPPService {
|
||||
return
|
||||
}
|
||||
|
||||
let archive = isGroup
|
||||
? MAMArchiveKey.muc(conversationJID)
|
||||
: MAMArchiveKey.account(client.userBareJid.stringValue)
|
||||
let archive =
|
||||
isGroup
|
||||
? MAMArchiveKey.muc(conversationJID)
|
||||
: MAMArchiveKey.account(client.userBareJid.stringValue)
|
||||
// Only one interactive backward-history query is allowed at a time.
|
||||
// This is independent from the normal account/MUC catch-up query.
|
||||
if let previous = olderHistoryCompletion {
|
||||
@@ -1106,9 +1112,10 @@ final class XMPPService {
|
||||
form.with = JID(conversationJID.lowercased())
|
||||
}
|
||||
let componentJID: JID? = isGroup ? JID(conversationJID.lowercased()) : nil
|
||||
let rsm: RSM.Query = before.map {
|
||||
RSM.Query(before: $0, max: archivePageSize)
|
||||
} ?? RSM.Query(lastItems: archivePageSize)
|
||||
let rsm: RSM.Query =
|
||||
before.map {
|
||||
RSM.Query(before: $0, max: archivePageSize)
|
||||
} ?? RSM.Query(lastItems: archivePageSize)
|
||||
|
||||
client.module(.mam).queryItems(
|
||||
version: version,
|
||||
@@ -1153,7 +1160,8 @@ final class XMPPService {
|
||||
self.interactiveHistoryMutations = []
|
||||
for stanza in page.stanzas {
|
||||
if stanza.message.type == .groupchat,
|
||||
let roomJID = stanza.message.from?.bareJid {
|
||||
let roomJID = stanza.message.from?.bareJid
|
||||
{
|
||||
await self.handle(
|
||||
groupMessage: stanza.message,
|
||||
archivedRoomJID: roomJID,
|
||||
@@ -1167,7 +1175,8 @@ final class XMPPService {
|
||||
timestamp: stanza.timestamp,
|
||||
archiveID: stanza.archiveID,
|
||||
isArchived: true,
|
||||
archivedPeerJID: isGroup ? nil : BareJID(conversationJID.lowercased())
|
||||
archivedPeerJID: isGroup
|
||||
? nil : BareJID(conversationJID.lowercased())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1207,12 +1216,13 @@ final class XMPPService {
|
||||
) {
|
||||
olderHistoryTimeoutTask?.cancel()
|
||||
olderHistoryTimeoutTask = Task { @MainActor [weak self, weak client] in
|
||||
try? await Task.sleep(nanoseconds: self?.olderHistoryQueryTimeoutNanoseconds ?? 15_000_000_000)
|
||||
try? await Task.sleep(
|
||||
nanoseconds: self?.olderHistoryQueryTimeoutNanoseconds ?? 15_000_000_000)
|
||||
guard !Task.isCancelled,
|
||||
let self,
|
||||
let client,
|
||||
self.client === client,
|
||||
self.olderHistoryQueryID == queryID
|
||||
let self,
|
||||
let client,
|
||||
self.client === client,
|
||||
self.olderHistoryQueryID == queryID
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -1683,7 +1693,8 @@ final class XMPPService {
|
||||
// advertises them in separate stream-feature elements.
|
||||
saslMechanisms = advertisedSASLMechanisms
|
||||
} else {
|
||||
saslMechanisms = streamFeaturesElement?
|
||||
saslMechanisms =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "mechanisms", xmlns: "urn:ietf:params:xml:ns:xmpp-sasl")?
|
||||
.children
|
||||
.filter { $0.name == "mechanism" }
|
||||
@@ -1698,7 +1709,8 @@ final class XMPPService {
|
||||
if !advertisedSASL2Mechanisms.isEmpty {
|
||||
sasl2ForDisplay = advertisedSASL2Mechanisms
|
||||
} else {
|
||||
sasl2ForDisplay = streamFeaturesElement?
|
||||
sasl2ForDisplay =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "authentication", xmlns: "urn:xmpp:sasl:2")?
|
||||
.children
|
||||
.filter { $0.name == "mechanism" }
|
||||
@@ -1710,7 +1722,8 @@ final class XMPPService {
|
||||
channelBindingStore.advertisedChannelBindingTypes
|
||||
).sorted()
|
||||
} else {
|
||||
channelBindingTypesForDisplay = streamFeaturesElement?
|
||||
channelBindingTypesForDisplay =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "sasl-channel-binding", xmlns: "urn:xmpp:sasl-cb:0")?
|
||||
.children
|
||||
.filter { $0.name == "channel-binding" }
|
||||
@@ -1721,18 +1734,23 @@ final class XMPPService {
|
||||
// server-first message.
|
||||
let downgradeProtectionForDisplay = channelBindingStore.isDowngradeProtectionDetected
|
||||
|
||||
let supportsCSI = streamFeaturesElement?
|
||||
let supportsCSI =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "csi", xmlns: "urn:xmpp:csi:0") != nil
|
||||
let supportsRosterVersioning = streamFeaturesElement?
|
||||
let supportsRosterVersioning =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "ver", xmlns: "urn:xmpp:features:rosterver") != nil
|
||||
let supportsRosterPreApproval = streamFeaturesElement?
|
||||
let supportsRosterPreApproval =
|
||||
streamFeaturesElement?
|
||||
.findChild(name: "sub", xmlns: "urn:xmpp:features:pre-approval") != nil
|
||||
|
||||
let uploadOnServer = server.features.contains("urn:xmpp:http:upload:0")
|
||||
let uploadOnServer =
|
||||
server.features.contains("urn:xmpp:http:upload:0")
|
||||
|| server.features.contains("http://jabber.org/protocol/httpupload")
|
||||
let supportsHTTPUpload = uploadOnServer || uploadOnComponent
|
||||
|
||||
let stats = connectionStatsModule?.snapshot
|
||||
let stats =
|
||||
connectionStatsModule?.snapshot
|
||||
?? LumaConnectionStatsModule.Snapshot(sent: 0, acknowledged: 0, received: 0)
|
||||
|
||||
let tlsVersion = await currentTLSVersion()
|
||||
@@ -1780,8 +1798,7 @@ final class XMPPService {
|
||||
saslMethods: saslMechanisms,
|
||||
sasl2Methods: sasl2ForDisplay,
|
||||
channelBindingTypes: channelBindingTypesForDisplay,
|
||||
usedChannelBindingType:
|
||||
(negotiatedSASLMechanism?.hasSuffix("-PLUS") == true)
|
||||
usedChannelBindingType: (negotiatedSASLMechanism?.hasSuffix("-PLUS") == true)
|
||||
? channelBindingStore.preferredChannelBindingType
|
||||
: nil,
|
||||
supportsSCRAMDowngradeProtection: downgradeProtectionForDisplay,
|
||||
@@ -1928,7 +1945,7 @@ final class XMPPService {
|
||||
)
|
||||
_ = client.modulesManager.register(
|
||||
SoftwareVersionModule(
|
||||
version: .init(name: "Luma", version: "0.10.6", os: Self.platformName))
|
||||
version: .init(name: "Luma", version: "20261013.0", os: Self.platformName))
|
||||
)
|
||||
_ = client.modulesManager.register(PingModule())
|
||||
_ = client.modulesManager.register(ClientStateIndicationModule())
|
||||
@@ -2026,7 +2043,7 @@ final class XMPPService {
|
||||
// (TLS 1.3 + STARTTLS + channel binding). Certificate trust is
|
||||
// still evaluated through the custom validator above.
|
||||
options.networkProcessorProviders = [
|
||||
LumaTLSNetworkProcessorProvider(channelBindingStore: channelBindingStore),
|
||||
LumaTLSNetworkProcessorProvider(channelBindingStore: channelBindingStore)
|
||||
]
|
||||
if let host = account.manualHost {
|
||||
let defaultPort = account.usesDirectTLS ? 5223 : 5222
|
||||
@@ -2214,7 +2231,7 @@ final class XMPPService {
|
||||
}
|
||||
await handle(message: message, timestamp: timestamp, archiveID: nil)
|
||||
}
|
||||
|
||||
|
||||
private func deliverOrDelayGroup(_ message: Message, room: RoomProtocol) async {
|
||||
let archive = MAMArchiveKey.muc(room.jid.stringValue)
|
||||
if activeMUCCatchup == archive {
|
||||
@@ -2225,7 +2242,7 @@ final class XMPPService {
|
||||
}
|
||||
await handle(groupMessage: message, room: room)
|
||||
}
|
||||
|
||||
|
||||
private func replayDelayedLive(for archive: MAMArchiveKey) async {
|
||||
let delayed = delayedLiveByArchive.removeValue(forKey: archive) ?? []
|
||||
for item in delayed {
|
||||
@@ -2331,7 +2348,9 @@ final class XMPPService {
|
||||
// devices; they never become ordinary chat messages.
|
||||
if let sync = CallHistorySync.envelope(from: message) {
|
||||
Logger(subsystem: "Luma", category: "call-sync")
|
||||
.info("received call-history id=\(sync.id) peer=\(sync.peerJID) status=\(sync.outcome.rawValue)")
|
||||
.info(
|
||||
"received call-history id=\(sync.id) peer=\(sync.peerJID) status=\(sync.outcome.rawValue)"
|
||||
)
|
||||
eventHandler?(.callHistorySync(sync))
|
||||
return
|
||||
}
|
||||
@@ -2340,7 +2359,9 @@ final class XMPPService {
|
||||
// after reinstalls. They never become ordinary chat messages.
|
||||
if let marker = ReadStateSync.envelope(from: message) {
|
||||
Logger(subsystem: "Luma", category: "read-sync")
|
||||
.info("received read-marker id=\(marker.id) message=\(marker.messageID) conversation=\(marker.conversationJID)")
|
||||
.info(
|
||||
"received read-marker id=\(marker.id) message=\(marker.messageID) conversation=\(marker.conversationJID)"
|
||||
)
|
||||
eventHandler?(.readState(marker))
|
||||
return
|
||||
}
|
||||
@@ -2353,8 +2374,8 @@ final class XMPPService {
|
||||
let ownJID = client.userBareJid
|
||||
let outgoing = sender == ownJID
|
||||
// guard let peer = outgoing ? message.to?.bareJid : message.from?.bareJid else { return }
|
||||
// let peer: BareJID?
|
||||
// if outgoing {
|
||||
// let peer: BareJID?
|
||||
// if outgoing {
|
||||
let peer: BareJID?
|
||||
if isArchived, let archivedPeerJID {
|
||||
peer = archivedPeerJID
|
||||
@@ -2368,21 +2389,21 @@ final class XMPPService {
|
||||
let security: ChatMessage.Security
|
||||
let fingerprint: String?
|
||||
let contentMessage: Message?
|
||||
let decryptionResult = await (
|
||||
Self.isOMEMO2Payload(message)
|
||||
? decodeOmemo2OffMain(
|
||||
message,
|
||||
from: sender,
|
||||
serverMsgId: archiveID,
|
||||
module: client.module(.omemo2)
|
||||
)
|
||||
: decodeOmemoOffMain(
|
||||
message,
|
||||
from: sender,
|
||||
serverMsgId: archiveID,
|
||||
module: client.module(.omemo)
|
||||
)
|
||||
)
|
||||
let decryptionResult =
|
||||
await
|
||||
(Self.isOMEMO2Payload(message)
|
||||
? decodeOmemo2OffMain(
|
||||
message,
|
||||
from: sender,
|
||||
serverMsgId: archiveID,
|
||||
module: client.module(.omemo2)
|
||||
)
|
||||
: decodeOmemoOffMain(
|
||||
message,
|
||||
from: sender,
|
||||
serverMsgId: archiveID,
|
||||
module: client.module(.omemo)
|
||||
))
|
||||
switch decryptionResult {
|
||||
case .successMessage(let decodedMessage, let value):
|
||||
security = .omemo
|
||||
@@ -2606,24 +2627,27 @@ final class XMPPService {
|
||||
else { return }
|
||||
|
||||
let nickname = from.resource ?? "Участник"
|
||||
// let outgoing = nickname == room.nickname
|
||||
// let roomJID = room.jid.stringValue.lowercased()
|
||||
let disclosedSender = Self.originalSenderJID(in: message)
|
||||
?? XMucUserElement.extract(from: message)?.jid?.bareJid
|
||||
let outgoing = room.map { nickname == $0.nickname }
|
||||
?? (disclosedSender == client.userBareJid)
|
||||
// let outgoing = nickname == room.nickname
|
||||
// let roomJID = room.jid.stringValue.lowercased()
|
||||
let disclosedSender =
|
||||
Self.originalSenderJID(in: message)
|
||||
?? XMucUserElement.extract(from: message)?.jid?.bareJid
|
||||
let outgoing =
|
||||
room.map { nickname == $0.nickname }
|
||||
?? (disclosedSender == client.userBareJid)
|
||||
let roomJID = roomBareJID.stringValue.lowercased()
|
||||
// In a MUC the author address used by XEP-0461 is the full occupant
|
||||
// JID (room@service/nickname), not the participant's disclosed real JID.
|
||||
let senderJID = from.stringValue
|
||||
// let stanzaID = Self.roomStanzaID(in: message, roomJID: room.jid)
|
||||
// let stanzaID = Self.roomStanzaID(in: message, roomJID: room.jid)
|
||||
let stanzaID = Self.roomStanzaID(in: message, roomJID: roomBareJID) ?? archiveID
|
||||
let id =
|
||||
outgoing
|
||||
? (message.originId ?? message.id ?? stanzaID ?? UUID().uuidString)
|
||||
: (stanzaID ?? message.originId ?? message.id ?? UUID().uuidString)
|
||||
|
||||
let encrypted = message.firstChild(name: "encrypted", xmlns: OMEMOModule.XMLNS) != nil
|
||||
let encrypted =
|
||||
message.firstChild(name: "encrypted", xmlns: OMEMOModule.XMLNS) != nil
|
||||
|| message.firstChild(name: "encrypted", xmlns: LumaOMEMO2Module.XMLNS) != nil
|
||||
let realSender: BareJID?
|
||||
if outgoing {
|
||||
@@ -2646,21 +2670,21 @@ final class XMPPService {
|
||||
let fingerprint: String?
|
||||
let contentMessage: Message?
|
||||
if let realSender {
|
||||
let decryptionResult = await (
|
||||
Self.isOMEMO2Payload(message)
|
||||
? decodeOmemo2OffMain(
|
||||
message,
|
||||
from: realSender,
|
||||
serverMsgId: stanzaID,
|
||||
module: client.module(.omemo2)
|
||||
)
|
||||
: decodeOmemoOffMain(
|
||||
message,
|
||||
from: realSender,
|
||||
serverMsgId: stanzaID,
|
||||
module: client.module(.omemo)
|
||||
)
|
||||
)
|
||||
let decryptionResult =
|
||||
await
|
||||
(Self.isOMEMO2Payload(message)
|
||||
? decodeOmemo2OffMain(
|
||||
message,
|
||||
from: realSender,
|
||||
serverMsgId: stanzaID,
|
||||
module: client.module(.omemo2)
|
||||
)
|
||||
: decodeOmemoOffMain(
|
||||
message,
|
||||
from: realSender,
|
||||
serverMsgId: stanzaID,
|
||||
module: client.module(.omemo)
|
||||
))
|
||||
switch decryptionResult {
|
||||
case .successMessage(let decodedMessage, let value):
|
||||
security = .omemo
|
||||
@@ -2718,7 +2742,7 @@ final class XMPPService {
|
||||
}
|
||||
|
||||
let payload = contentMessage ?? message
|
||||
// if let state = Self.chatTypingState(payload.chatState ?? message.chatState) {
|
||||
// if let state = Self.chatTypingState(payload.chatState ?? message.chatState) {
|
||||
if !isArchived, let state = Self.chatTypingState(payload.chatState ?? message.chatState) {
|
||||
knownChatStatePeers.insert(roomJID)
|
||||
if !outgoing {
|
||||
@@ -2752,12 +2776,12 @@ final class XMPPService {
|
||||
guard child.name == "reaction" else { return nil }
|
||||
return child.value
|
||||
},
|
||||
// timestamp: message.delay?.stamp ?? Date(),
|
||||
// timestamp: message.delay?.stamp ?? Date(),
|
||||
timestamp: archivedTimestamp ?? message.delay?.stamp ?? Date(),
|
||||
isOutgoing: outgoing,
|
||||
isGroupMessage: true
|
||||
),
|
||||
// isArchived: false
|
||||
// isArchived: false
|
||||
isArchived: isArchived
|
||||
)
|
||||
return
|
||||
@@ -2810,39 +2834,39 @@ final class XMPPService {
|
||||
attachmentURL == nil
|
||||
? visibleBody
|
||||
: Self.displayBody(kind: kind, filename: transportFilename ?? "Вложение")
|
||||
// eventHandler?(
|
||||
// eventHandler?(
|
||||
emitMessage(
|
||||
// .message(
|
||||
MessageEnvelope(
|
||||
id: id,
|
||||
// peerJID: room.jid.stringValue,
|
||||
peerJID: roomBareJID.stringValue,
|
||||
senderJID: senderJID,
|
||||
body: displayBody,
|
||||
// timestamp: message.delay?.stamp ?? Date(),
|
||||
timestamp: archivedTimestamp ?? message.delay?.stamp ?? Date(),
|
||||
isOutgoing: outgoing,
|
||||
security: security,
|
||||
kind: kind,
|
||||
remoteAttachmentURL: attachmentURL,
|
||||
localFilename: transportFilename,
|
||||
mimeType: inferred?.mimeType,
|
||||
duration: inferred?.duration,
|
||||
byteCount: nil,
|
||||
fingerprint: fingerprint,
|
||||
correctionTargetID: payload.lastMessageCorrectionId
|
||||
?? message.lastMessageCorrectionId,
|
||||
replyToID: reply?.getAttribute("id"),
|
||||
replyToJID: reply?.getAttribute("to"),
|
||||
replyPreview: parsedReply.preview,
|
||||
originID: message.originId,
|
||||
stanzaID: stanzaID,
|
||||
senderDisplayName: nickname,
|
||||
isGroupMessage: true,
|
||||
// isArchived: false
|
||||
// )))
|
||||
isArchived: isArchived
|
||||
), isArchived: isArchived)
|
||||
// .message(
|
||||
MessageEnvelope(
|
||||
id: id,
|
||||
// peerJID: room.jid.stringValue,
|
||||
peerJID: roomBareJID.stringValue,
|
||||
senderJID: senderJID,
|
||||
body: displayBody,
|
||||
// timestamp: message.delay?.stamp ?? Date(),
|
||||
timestamp: archivedTimestamp ?? message.delay?.stamp ?? Date(),
|
||||
isOutgoing: outgoing,
|
||||
security: security,
|
||||
kind: kind,
|
||||
remoteAttachmentURL: attachmentURL,
|
||||
localFilename: transportFilename,
|
||||
mimeType: inferred?.mimeType,
|
||||
duration: inferred?.duration,
|
||||
byteCount: nil,
|
||||
fingerprint: fingerprint,
|
||||
correctionTargetID: payload.lastMessageCorrectionId
|
||||
?? message.lastMessageCorrectionId,
|
||||
replyToID: reply?.getAttribute("id"),
|
||||
replyToJID: reply?.getAttribute("to"),
|
||||
replyPreview: parsedReply.preview,
|
||||
originID: message.originId,
|
||||
stanzaID: stanzaID,
|
||||
senderDisplayName: nickname,
|
||||
isGroupMessage: true,
|
||||
// isArchived: false
|
||||
// )))
|
||||
isArchived: isArchived
|
||||
), isArchived: isArchived)
|
||||
}
|
||||
|
||||
private func observe(room: RoomProtocol) {
|
||||
@@ -3074,7 +3098,7 @@ final class XMPPService {
|
||||
fallback.addChild(bodyRange)
|
||||
message.addChild(fallback)
|
||||
}
|
||||
|
||||
|
||||
private func addOriginID(_ id: String, to message: Message) {
|
||||
guard message.originId == nil else { return }
|
||||
let origin = Element(name: "origin-id", xmlns: Self.stanzaIDNamespace)
|
||||
@@ -3456,7 +3480,7 @@ final class XMPPService {
|
||||
}
|
||||
return encryptedMessage
|
||||
}
|
||||
|
||||
|
||||
/// Single Martin-specific MAM entry point. Martin 3.2.4 supports
|
||||
/// `componentJid:` and writes it to IQ `to`, which is required for MUC MAM.
|
||||
private func queryMAM(
|
||||
@@ -3465,7 +3489,8 @@ final class XMPPService {
|
||||
start: Date?,
|
||||
queryID: String,
|
||||
rsm: RSM.Query?,
|
||||
completion: @escaping (Result<MessageArchiveManagementModule.QueryResult, XMPPError>) -> Void
|
||||
completion:
|
||||
@escaping (Result<MessageArchiveManagementModule.QueryResult, XMPPError>) -> Void
|
||||
) {
|
||||
let componentJID: JID?
|
||||
switch archive.kind {
|
||||
@@ -3474,7 +3499,7 @@ final class XMPPService {
|
||||
case .muc:
|
||||
componentJID = JID(archive.jid)
|
||||
}
|
||||
|
||||
|
||||
client.module(.mam).queryItems(
|
||||
componentJid: componentJID,
|
||||
start: start,
|
||||
@@ -3483,7 +3508,7 @@ final class XMPPService {
|
||||
completionHandler: completion
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func allowedMAMSources(
|
||||
for archive: MAMArchiveKey,
|
||||
client: XMPPClient
|
||||
@@ -3560,26 +3585,28 @@ final class XMPPService {
|
||||
retry: 0
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func enqueueMUCCatchup(roomJID: BareJID) {
|
||||
let archive = MAMArchiveKey.muc(roomJID.stringValue)
|
||||
guard activeMUCCatchup != archive,
|
||||
!pendingMUCCatchups.contains(archive) else { return }
|
||||
!pendingMUCCatchups.contains(archive)
|
||||
else { return }
|
||||
pendingMUCCatchups.append(archive)
|
||||
startNextMUCCatchupIfPossible()
|
||||
}
|
||||
|
||||
|
||||
/// Keep this first version serialized with the account pass. It prevents
|
||||
/// archive mutations from different namespaces sharing the legacy batch.
|
||||
/// Patch 0007 makes live delivery archive-aware while catch-up runs.
|
||||
private func startNextMUCCatchupIfPossible() {
|
||||
guard activeMUCCatchup == nil,
|
||||
!archiveSyncStarted,
|
||||
let client,
|
||||
client.state == .connected(),
|
||||
!client.module(.mam).availableVersions.isEmpty,
|
||||
!pendingMUCCatchups.isEmpty else { return }
|
||||
|
||||
!archiveSyncStarted,
|
||||
let client,
|
||||
client.state == .connected(),
|
||||
!client.module(.mam).availableVersions.isEmpty,
|
||||
!pendingMUCCatchups.isEmpty
|
||||
else { return }
|
||||
|
||||
let archive = pendingMUCCatchups.removeFirst()
|
||||
activeMUCCatchup = archive
|
||||
mucCatchupHighWatermark = nil
|
||||
@@ -3590,7 +3617,7 @@ final class XMPPService {
|
||||
client: client
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func queryMUCArchivePage(
|
||||
archive: MAMArchiveKey,
|
||||
after: String?,
|
||||
@@ -3602,15 +3629,18 @@ final class XMPPService {
|
||||
queryID: queryID,
|
||||
allowedSources: allowedMAMSources(for: archive, client: client)
|
||||
)
|
||||
|
||||
|
||||
let checkpoint = mamCheckpoints[archive]
|
||||
let start = after == nil
|
||||
? checkpoint?.timestamp.addingTimeInterval(-ArchiveSyncRecoveryPolicy.incrementalOverlap)
|
||||
: nil
|
||||
let rsm = checkpoint == nil
|
||||
? RSM.Query(lastItems: archiveBootstrapMessageLimit)
|
||||
: RSM.Query(after: after, max: archivePageSize)
|
||||
|
||||
let start =
|
||||
after == nil
|
||||
? checkpoint?.timestamp.addingTimeInterval(
|
||||
-ArchiveSyncRecoveryPolicy.incrementalOverlap)
|
||||
: nil
|
||||
let rsm =
|
||||
checkpoint == nil
|
||||
? RSM.Query(lastItems: archiveBootstrapMessageLimit)
|
||||
: RSM.Query(after: after, max: archivePageSize)
|
||||
|
||||
queryMAM(
|
||||
client: client,
|
||||
archive: archive,
|
||||
@@ -3620,7 +3650,8 @@ final class XMPPService {
|
||||
) { [weak self, weak client] result in
|
||||
DispatchQueue.main.async {
|
||||
guard let self, let client,
|
||||
self.activeMUCCatchup == archive else { return }
|
||||
self.activeMUCCatchup == archive
|
||||
else { return }
|
||||
let page = self.archiveStanzaInbox.take(queryID: queryID)
|
||||
guard !page.overflowed, !page.rejectedSource else {
|
||||
self.finishMUCCatchup(archive: archive, succeeded: false)
|
||||
@@ -3628,14 +3659,16 @@ final class XMPPService {
|
||||
}
|
||||
Task { @MainActor [weak self, weak client] in
|
||||
guard let self, let client,
|
||||
self.activeMUCCatchup == archive else { return }
|
||||
self.activeMUCCatchup == archive
|
||||
else { return }
|
||||
for stanza in page.stanzas {
|
||||
self.mucCatchupHighWatermark = max(
|
||||
self.mucCatchupHighWatermark ?? .distantPast,
|
||||
stanza.timestamp
|
||||
)
|
||||
guard stanza.message.type == .groupchat,
|
||||
let roomJID = stanza.message.from?.bareJid else { continue }
|
||||
let roomJID = stanza.message.from?.bareJid
|
||||
else { continue }
|
||||
await self.handle(
|
||||
groupMessage: stanza.message,
|
||||
archivedRoomJID: roomJID,
|
||||
@@ -3652,8 +3685,9 @@ final class XMPPService {
|
||||
// from the durable timestamp overlap, never loop
|
||||
// cursor<->time.
|
||||
if self.mamCheckpoints[archive]?.cursor != nil,
|
||||
self.mucCursorFallbackArchives.insert(archive).inserted,
|
||||
let old = self.mamCheckpoints[archive] {
|
||||
self.mucCursorFallbackArchives.insert(archive).inserted,
|
||||
let old = self.mamCheckpoints[archive]
|
||||
{
|
||||
self.mamCheckpoints[archive] = MAMArchiveCheckpoint(
|
||||
timestamp: old.timestamp,
|
||||
cursor: nil
|
||||
@@ -3679,7 +3713,7 @@ final class XMPPService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func finishMUCCatchup(archive: MAMArchiveKey, succeeded: Bool) {
|
||||
defer {
|
||||
activeMUCCatchup = nil
|
||||
@@ -3730,10 +3764,10 @@ final class XMPPService {
|
||||
let accountArchive = MAMArchiveKey.account(client.userBareJid.stringValue)
|
||||
archiveStanzaInbox.begin(
|
||||
queryID: queryID,
|
||||
// allowedSources: [
|
||||
// client.userBareJid.stringValue,
|
||||
// client.userBareJid.domain,
|
||||
// ]
|
||||
// allowedSources: [
|
||||
// client.userBareJid.stringValue,
|
||||
// client.userBareJid.domain,
|
||||
// ]
|
||||
allowedSources: allowedMAMSources(for: accountArchive, client: client)
|
||||
)
|
||||
scheduleArchiveQueryTimeout(client: client, queryID: queryID)
|
||||
@@ -3748,7 +3782,7 @@ final class XMPPService {
|
||||
archiveIsBootstrapQuery
|
||||
? RSM.Query(lastItems: archiveBootstrapMessageLimit)
|
||||
: RSM.Query(after: after, max: archivePageSize)
|
||||
// client.module(.mam).queryItems(
|
||||
// client.module(.mam).queryItems(
|
||||
queryMAM(
|
||||
client: client,
|
||||
archive: accountArchive,
|
||||
@@ -3836,14 +3870,14 @@ final class XMPPService {
|
||||
// isArchived: true
|
||||
// )
|
||||
if stanza.message.type == .groupchat,
|
||||
// let roomJID = stanza.message.from?.bareJid,
|
||||
// let room = client?.module(.muc).roomManager.room(for: client!, with: roomJID)
|
||||
// let roomJID = stanza.message.from?.bareJid,
|
||||
// let room = client?.module(.muc).roomManager.room(for: client!, with: roomJID)
|
||||
let roomJID = stanza.message.from?.bareJid
|
||||
{
|
||||
// Some servers include MUC messages in the account MAM.
|
||||
// Route them through the same group handler instead of
|
||||
// dropping them in the direct-message handler.
|
||||
// handle(groupMessage: stanza.message, room: room)
|
||||
// handle(groupMessage: stanza.message, room: room)
|
||||
await handle(
|
||||
groupMessage: stanza.message,
|
||||
archivedRoomJID: roomJID,
|
||||
@@ -4249,7 +4283,7 @@ final class XMPPService {
|
||||
archiveStanzaInbox.cancel()
|
||||
guard let client else {
|
||||
archiveSyncStarted = false
|
||||
// archiveStanzaInbox.cancel()
|
||||
// archiveStanzaInbox.cancel()
|
||||
archiveActiveQueryID = nil
|
||||
archiveStanzaBuffer.removeAll(keepingCapacity: false)
|
||||
archiveBufferOverflowed = false
|
||||
@@ -4392,7 +4426,8 @@ final class XMPPService {
|
||||
// refreshing the captured state from them would wipe the screen to
|
||||
// "no methods / no channel binding". Only pre-auth features (those
|
||||
// carrying any SASL-related element) update the state.
|
||||
let hasSASLElements = !mechanisms.isEmpty
|
||||
let hasSASLElements =
|
||||
!mechanisms.isEmpty
|
||||
|| !sasl2Mechanisms.isEmpty
|
||||
|| !channelBindingTypes.isEmpty
|
||||
if hasSASLElements {
|
||||
@@ -4417,7 +4452,8 @@ final class XMPPService {
|
||||
}
|
||||
guard let activePassword, !activePassword.isEmpty else { return }
|
||||
guard mechanisms.contains(where: { $0.hasPrefix("SCRAM-") }) else { return }
|
||||
guard case .password(let currentPassword, _, _) = client.connectionConfiguration.credentials,
|
||||
guard
|
||||
case .password(let currentPassword, _, _) = client.connectionConfiguration.credentials,
|
||||
let prepared = try? SASLprep.prepare(activePassword),
|
||||
prepared != currentPassword
|
||||
else { return }
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ settings:
|
||||
base:
|
||||
SWIFT_VERSION: 5.9
|
||||
CURRENT_PROJECT_VERSION: 31
|
||||
MARKETING_VERSION: 0.10.6
|
||||
MARKETING_VERSION: 20261013.0
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: true
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true
|
||||
COMPANION_BUNDLE_IDENTIFIER: app.luma.chat
|
||||
@@ -59,7 +59,7 @@ targets:
|
||||
- package: MartinOMEMO
|
||||
- package: WebRTC
|
||||
- package: OpenSSL
|
||||
# - package: SwiftChat
|
||||
# - package: SwiftChat
|
||||
info:
|
||||
path: Config/Luma-Info.plist
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user