From 2a30b1a23981cb35b9cff3a1b74d24aa275dea82 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Tue, 30 Dec 2025 20:52:11 +0700 Subject: [PATCH 01/10] chore: resubscribe to chat list changes on user switch Added logic to detect user ID changes and force resubscription to chat list updates when the account changes. This ensures chat list updates are correctly handled after switching users. --- .../Sources/Node/ChatListNode.swift | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index f87208ecac1..9fbca2aaa1b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -2253,12 +2253,21 @@ public final class ChatListNode: ListView { contacts = .single([]) } + let accountPeerId = context.account.peerId //CloudVeil start - self.cloudVeilSubscribeToChatListChanges() + var hasBeenChangedUserId = false + TGUserController.withLock({ tg in + let tgId = tg.getUserID() + + let currentPeerId = accountPeerId.id._internalGetInt64Value() + if tgId != currentPeerId { + print("user changed from \(tgId) to \(currentPeerId), unsubscribing from chat list changes") + hasBeenChangedUserId = true + } + }) + self.cloudVeilSubscribeToChatListChanges(forceResubscribe: hasBeenChangedUserId) //CloudVeil end - let accountPeerId = context.account.peerId - let chatListFilters: Signal<[ChatListFilter]?, NoError> if case .chatList = mode { chatListFilters = combineLatest(queue: .mainQueue(), @@ -3294,9 +3303,14 @@ public final class ChatListNode: ListView { static let timeoutCallInSec = 0.1 var lastCallTime = Date().timeIntervalSince1970 - 10*ChatListNode.timeoutCallInSec static var subscriptionDisposable: Disposable? = nil - func cloudVeilSubscribeToChatListChanges() { + func cloudVeilSubscribeToChatListChanges(forceResubscribe: Bool = false) { if ChatListNode.subscriptionDisposable != nil { - return + if !forceResubscribe { + return + } else { + ChatListNode.subscriptionDisposable?.dispose() + ChatListNode.subscriptionDisposable = nil + } } self.blockNotifications() ChatListNode.subscriptionDisposable = (context.engine.messages.chatList(group: .root, count: Int(Int16.max)) |> distinctUntilChanged{ l, r in From 3c5c82c5ad66b827daa7b06dc12489cba0c6d0c0 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Tue, 30 Dec 2025 20:52:30 +0700 Subject: [PATCH 02/10] chore: track user ID changes in TGUserController Introduced a static property `didChangedUserID` to indicate when the user ID has changed. Updated the `set(userID:)` method to set this flag when the user ID changes, and added `setCacheHasBeenUpdated()` to reset the flag after cache updates. --- .../Classes/Controllers/TGUserController.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift index 6fec52d1d3a..d37a75e586c 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift @@ -11,6 +11,7 @@ import Foundation @objc open class TGUserController: NSObject { private static let lock = NSLock() private static let shared = TGUserController() + private(set) static var didChangedUserID = true // MARK: - Singleton @@ -24,7 +25,12 @@ import Foundation // MARK: - Actions + // This set func should call from a Self.withLock completion block @objc open func set(userID id: NSInteger) { + // Check if id has changed + if TGUserModel1.id != id { + TGUserController.didChangedUserID = true + } TGUserModel1.set(userID: id) } @@ -40,6 +46,11 @@ import Foundation TGUserModel1.set(userNames: names) } + //acknowledge that cache has been updated in CloudVeilSecurityController + @objc open func setCacheHasBeenUpdated() { + TGUserController.didChangedUserID = false + } + @objc open func getUserID() -> NSInteger { return TGUserModel1.id } From b5d621cf83ff334fe860144998a374abe179dc1e Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Tue, 30 Dec 2025 20:54:19 +0700 Subject: [PATCH 03/10] chore: improve thread safety for settings cache access Replaced the concurrent access queue with a serial queue to protect settingsCache. Updated the settings property to ensure user ID consistency and cache validity by accessing TGUserController within a lock and reading from disk inside the lock. --- .../CloudVeilSecurityController.swift | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index 13c99b49893..d5f1326d34d 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -67,18 +67,34 @@ open class CloudVeilSecurityController: NSObject { set { UserDefaults.standard.set(newValue, forKey: kWasFirstLoaded) } } - private let accessQueue = DispatchQueue(label: "TGSettingsResponseAccess", attributes: .concurrent) + // Serial queue to protect access to settingsCache + private let accessQueue = DispatchQueue(label: "TGSettingsResponseAccess") private var settingsCache: TGSettingsResponse? private var settings: TGSettingsResponse? { var resp: TGSettingsResponse? - if settingsCache != nil { - resp = settingsCache - } else { - settingsCache = DataSource.value(mapper: mapper) - resp = settingsCache + var userId = 0 + var isCacheValid = true + + // Should access TGUserController with lock + TGUserController.withLock { tg in + userId = tg.getUserID() + isCacheValid = !TGUserController.didChangedUserID + + if settingsCache != nil && isCacheValid { + resp = settingsCache + } else { + // Reading from disk inside the lock ensures the User ID cannot change + // while we are loading the file. + settingsCache = DataSource.value(forKey: "\(userId)", mapper: mapper) + resp = settingsCache + + // Update the flag directly on the instance (no nested lock) + tg.setCacheHasBeenUpdated() + } } - return resp + + return resp } public var needOrganizationChange: Bool { From 1f74b32c2535c62e15340bad054dba1ddfdf1607 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Tue, 30 Dec 2025 21:48:55 +0700 Subject: [PATCH 04/10] refactor: saveSettings to support per-user/org cache Updated indentation and whitespace throughout CloudVeilSecurityController.swift to improve code readability and maintain consistent formatting. No functional changes were made. feat: upgrade cache key to include user and organization ID Enhanced the caching system to use a composite key of user ID and organization ID for improved cache isolation and correctness when users switch organizations. Added orgId support to TGUserModel, updated TGUserController to manage orgId and its change flag, and modified CloudVeilSecurityController to use the new composite cache key. All changes maintain thread safety and do not break existing code. feat: add orgID support to user and settings controllers Introduces orgID tracking in TGUserController, including change detection and setter. Updates CloudVeilSecurityController to use orgID in cache key and cache validation, ensuring settings are correctly scoped per user and organization. refactor: saveSettings to support per-user/org cache Updated saveSettings to accept a user ID and use a cache key based on both user and organization IDs. The method now loads and merges settings from the correct disk cache, and only updates the in-memory cache if the user and org IDs match the current user. This improves multi-user and multi-organization support for settings caching. --- .../CloudVeilSecurityController.swift | 308 ++++++++++-------- .../Controllers/TGUserController.swift | 19 ++ .../Sources/Classes/Models/TGUserModel.swift | 11 + 3 files changed, 199 insertions(+), 139 deletions(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index d5f1326d34d..81cadbd2d34 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -21,21 +21,21 @@ public class UserBlacklist { fileprivate init() { cache = UserDefaults.standard.array(forKey: key)?.compactMap { $0 as? Int64 } ?? [] } - + fileprivate func clear() { cache = [] UserDefaults.standard.set(cache, forKey: key) } - + fileprivate func contains(_ id: Int64) -> Bool { return cache.contains(id) } - + fileprivate func remove(_ id: Int64) { cache.removeAll(where: { $0 == id }) UserDefaults.standard.set(cache, forKey: key) } - + fileprivate func blacklist(_ id: Int64) { cache.append(id) UserDefaults.standard.set(cache, forKey: key) @@ -46,47 +46,50 @@ open class CloudVeilSecurityController: NSObject { private let SUPPORT_BOT_ID = 689684671 private let TAG = "CloudVeilSecurityController" - public struct SecurityStaticSettings { - public static let disableGlobalSearch = true - public static let disableYoutubeVideoEmbedding = true - public static let disableInAppBrowser = true - public static let disableAutoPlayGifs = true - public static let disablePayments = true - public static let disableBots = false - public static let disableInlineBots = true + public struct SecurityStaticSettings { + public static let disableGlobalSearch = true + public static let disableYoutubeVideoEmbedding = true + public static let disableInAppBrowser = true + public static let disableAutoPlayGifs = true + public static let disablePayments = true + public static let disableBots = false + public static let disableInlineBots = true public static let disableGifs = true - } - - public static let shared = CloudVeilSecurityController() - - private let mapper = Mapper() - - private let kWasFirstLoaded = "wasFirstLoaded" - private var wasFirstLoaded: Bool { - get { return UserDefaults.standard.bool(forKey: kWasFirstLoaded) } - set { UserDefaults.standard.set(newValue, forKey: kWasFirstLoaded) } - } + } + + public static let shared = CloudVeilSecurityController() + + private let mapper = Mapper() + + private let kWasFirstLoaded = "wasFirstLoaded" + private var wasFirstLoaded: Bool { + get { return UserDefaults.standard.bool(forKey: kWasFirstLoaded) } + set { UserDefaults.standard.set(newValue, forKey: kWasFirstLoaded) } + } // Serial queue to protect access to settingsCache private let accessQueue = DispatchQueue(label: "TGSettingsResponseAccess") - private var settingsCache: TGSettingsResponse? + private var settingsCache: TGSettingsResponse? - private var settings: TGSettingsResponse? { + private var settings: TGSettingsResponse? { var resp: TGSettingsResponse? var userId = 0 + var orgId = 0 var isCacheValid = true // Should access TGUserController with lock TGUserController.withLock { tg in userId = tg.getUserID() - isCacheValid = !TGUserController.didChangedUserID + orgId = tg.getOrgID() + isCacheValid = !TGUserController.didChangedUserID && !TGUserController.didChangedOrgID if settingsCache != nil && isCacheValid { resp = settingsCache } else { - // Reading from disk inside the lock ensures the User ID cannot change + // Reading from disk inside the lock ensures the User ID and Org ID cannot change // while we are loading the file. - settingsCache = DataSource.value(forKey: "\(userId)", mapper: mapper) + let cacheKey = "\(userId)_\(orgId)" + settingsCache = DataSource.value(forKey: cacheKey, mapper: mapper) resp = settingsCache // Update the flag directly on the instance (no nested lock) @@ -95,7 +98,7 @@ open class CloudVeilSecurityController: NSObject { } return resp - } + } public var needOrganizationChange: Bool { var res = false @@ -112,73 +115,73 @@ open class CloudVeilSecurityController: NSObject { return res } - public var disableStickers: Bool { + public var disableStickers: Bool { var res = false self.accessQueue.sync { res = settings?.disableSticker ?? false } return res - } - public var disableBio: Bool { + } + public var disableBio: Bool { var res = false self.accessQueue.sync { res = settings?.disableBio ?? false } return res - } - public var disableBioChange: Bool { + } + public var disableBioChange: Bool { var res = false self.accessQueue.sync { res = settings?.disableBioChange ?? false } return res - } - public var disableProfilePhoto: Bool { + } + public var disableProfilePhoto: Bool { var res = false self.accessQueue.sync { res = settings?.disableProfilePhoto ?? false } return res - } - public var disableProfilePhotoChange: Bool { + } + public var disableProfilePhotoChange: Bool { var res = false self.accessQueue.sync { res = settings?.disableProfilePhotoChange ?? false } return res - } + } - public var isSecretChatAvailable: Bool { + public var isSecretChatAvailable: Bool { var res = false self.accessQueue.sync { res = settings?.secretChat ?? false } return res - } - - public var disableProfileVideo: Bool { + } + + public var disableProfileVideo: Bool { var res = false self.accessQueue.sync { res = settings?.disableProfileVideo ?? false } return res - } - public var disableProfileVideoChange: Bool { + } + public var disableProfileVideoChange: Bool { var res = false self.accessQueue.sync { res = settings?.disableProfileVideoChange ?? false } return res - } - - public var isInChatVideoRecordingEnabled: Bool { + } + + public var isInChatVideoRecordingEnabled: Bool { var res = false self.accessQueue.sync { res = settings?.inputToggleVoiceVideo ?? false } return res - } - + } + public var disableEmojiStatus: Bool { var res = false self.accessQueue.sync { @@ -188,7 +191,7 @@ open class CloudVeilSecurityController: NSObject { } - public var profilePhotoLimit: Int { + public var profilePhotoLimit: Int { var v = 1 self.accessQueue.sync { v = Int(settings?.profilePhotoLimit ?? "-1")! @@ -198,9 +201,9 @@ open class CloudVeilSecurityController: NSObject { v = 1 } } - return v - } - + return v + } + public var organizationId: Int? { var res: Int? self.accessQueue.sync { @@ -208,41 +211,41 @@ open class CloudVeilSecurityController: NSObject { } return res } - - public var secretChatMinimumLength: NSInteger { + + public var secretChatMinimumLength: NSInteger { var res = -1 self.accessQueue.sync { if let lenghtStr = settings?.secretChatMinimumLength { - res = Int(lenghtStr) ?? -1 + res = Int(lenghtStr) ?? -1 } } - - return res - } - - + + return res + } + + // MARK - Networking private let netQueue = DispatchQueue(label: "CloudVeilNetwork") - private var nextRequest: TGSettingsRequest? = nil - private var lastRequestTime: TimeInterval = 0.0 - private let UPDATE_INTERVAL = 10*60.0 //10min - + private var nextRequest: TGSettingsRequest? = nil + private var lastRequestTime: TimeInterval = 0.0 + private let UPDATE_INTERVAL = 10*60.0 //10min + // Blacklist of Telegram users who we shouldn't sent settings requests for. private let userBlacklist = UserBlacklist() - + public func clearUserBlacklist() { self.netQueue.async { self.userBlacklist.clear() } } - + // temporary: for use by web ui account delete only public func blacklistUser(_ userId: Int64) { self.netQueue.sync { self.userBlacklist.blacklist(userId) } } - + // temporary: for use by web ui account delete only public func withDeleteAccountUrl(_ userId: Int64, completion: @escaping (URL) -> Void) { let req = TGSettingsRequest(userId: userId) @@ -253,9 +256,9 @@ open class CloudVeilSecurityController: NSObject { } task?.resume() } - + private var getSettingsTask: URLSessionTask? = nil - + public func deleteAccount(_ tgUserID: Int64, onSucceed: @escaping () -> Void, onFail: @escaping () -> Void) { self.netQueue.async { self.userBlacklist.blacklist(tgUserID) @@ -286,14 +289,14 @@ open class CloudVeilSecurityController: NSObject { task?.resume() } } - + // Must only be called from code running on netQueue - private func sendSettingsRequest(_ body: TGSettingsRequest) { + private func sendSettingsRequest(_ body: TGSettingsRequest) { if let state = self.getSettingsTask?.state, state != .completed && state != .canceling { return } let task = self.sendSettingsRequest(body) { response in - self.saveSettings(response) + self.saveSettings(response, forUserId: body.id ?? 0) self.netQueue.async { if let nextReq = self.nextRequest, nextReq != body { self.sendSettingsRequest(nextReq) @@ -305,8 +308,8 @@ open class CloudVeilSecurityController: NSObject { task.resume() self.getSettingsTask = task } - } - + } + private func sendSettingsRequest(_ body: TGSettingsRequest, ignoreBlacklist: Bool = false, _ callback: @escaping (TGSettingsResponse?) -> Void) -> URLSessionTask? { if let id = body.id, self.userBlacklist.contains(Int64(id)) && !ignoreBlacklist { return nil @@ -372,7 +375,7 @@ open class CloudVeilSecurityController: NSObject { } } } - + open func getSettings(groups: inout [TGRow], bots: inout [TGRow], channels: inout [TGRow], stickers: inout [TGRow]) { let request = TGSettingsRequest( sessionId: self.nextRequest?.clientSessionId, @@ -391,13 +394,13 @@ open class CloudVeilSecurityController: NSObject { self.sendSettingsRequest(request) } } - + public func replayRequestWith(group: TGRow? = nil, channel: TGRow? = nil, bot: TGRow? = nil) { self.netQueue.async { let nextReq = self.nextRequest ?? TGSettingsRequest( sessionId: self.nextRequest?.clientSessionId, groups: [], bots: [], channels: [], stickers: []) - + var send = false if let g = group, !nextReq.groups.contains(g) { nextReq.groups.append(g) @@ -411,66 +414,93 @@ open class CloudVeilSecurityController: NSObject { nextReq.bots.append(b) send = true } - + if send { self.nextRequest = nextReq self.sendSettingsRequest(nextReq) } } } - + open func replayRequestWithGroup(group: TGRow) { self.replayRequestWith(group: group) } - + open func replayRequestWithChannel(channel: TGRow) { self.replayRequestWith(channel: channel) } - + open func replayRequestWithBot(bot: TGRow) { self.replayRequestWith(bot: bot) } - private func saveSettings(_ settings: TGSettingsResponse?) { - print("Save settings called") + private func saveSettings(_ settings: TGSettingsResponse?, forUserId: Int64 = 0) { + print("Save settings called") self.accessQueue.sync { if let settings = settings { + // Update org ID if present in settings + let userId = forUserId + var targetOrgId = 0 + + // Read current user org ID + TGUserController.withLock { tg in + targetOrgId = tg.getOrgID() + } + + if let orgId = settings.organization?.id, targetOrgId != orgId { + // ORG ID has changed + TGUserController.withLock { tg in + tg.set(orgID: orgId) + } + targetOrgId = orgId + } + let cacheKey = "\(userId)_\(targetOrgId)" + // Current disk cache + let cache = DataSource.value(forKey: cacheKey, mapper: mapper) + // if last response's org is this response's org, // keep old allowed peers around even when this response doesn't have them // discard old blocked peers - if settings.organization?.id == settingsCache?.organization?.id { + if settings.organization?.id == cache?.organization?.id { settings.access = settings.access ?? AccessObject() - + settings.access?.groups = settings.access?.groups ?? [:] settings.access?.groups?.merge( - settingsCache?.access?.groups?.filter { $0.value } ?? [:], + cache?.access?.groups?.filter { $0.value } ?? [:], uniquingKeysWith: { x, _ in x }) - + settings.access?.channels = settings.access?.channels ?? [:] settings.access?.channels?.merge( - settingsCache?.access?.channels?.filter { $0.value } ?? [:], + cache?.access?.channels?.filter { $0.value } ?? [:], uniquingKeysWith: { x, _ in x }) - + settings.access?.bots = settings.access?.bots ?? [:] settings.access?.bots?.merge( - settingsCache?.access?.bots?.filter { $0.value } ?? [:], + cache?.access?.bots?.filter { $0.value } ?? [:], uniquingKeysWith: { x, _ in x }) - + settings.access?.stickers = settings.access?.stickers ?? [:] settings.access?.stickers?.merge( - settingsCache?.access?.stickers?.filter { $0.value } ?? [:], + cache?.access?.stickers?.filter { $0.value } ?? [:], uniquingKeysWith: { x, _ in x }) - + settings.access?.users = settings.access?.users ?? [:] settings.access?.users?.merge( - settingsCache?.access?.users?.filter { $0.value } ?? [:], + cache?.access?.users?.filter { $0.value } ?? [:], uniquingKeysWith: { x, _ in x }) } - DataSource.set(settings) - settingsCache = settings + + // Save with cache key + DataSource.set(settings, forKey: cacheKey) + // Force update of in-memory cache if user Id and org ID match + TGUserController.withLock { tg in + if tg.getUserID() == Int(userId) && tg.getOrgID() == targetOrgId { + settingsCache = settings + } + } } } - } + } open func isUrlWhitelisted(_ url: String) -> Bool { let parsedUrl = URL(string: url) @@ -481,27 +511,27 @@ open class CloudVeilSecurityController: NSObject { } return false } - + open func isAvailable(groupID: NSInteger) -> Bool? { var res: Bool? self.accessQueue.sync { res = settings?.access?.groups?["\(groupID)"] } - return res - } - - open func isAvailable(channelID: NSInteger) -> Bool? { + return res + } + + open func isAvailable(channelID: NSInteger) -> Bool? { var res: Bool? self.accessQueue.sync { res = settings?.access?.channels?["\(channelID)"] } - return res - } + return res + } - open func isAvailable(botID: NSInteger) -> Bool? { - if SecurityStaticSettings.disableBots { - return false - } + open func isAvailable(botID: NSInteger) -> Bool? { + if SecurityStaticSettings.disableBots { + return false + } if botID == self.SUPPORT_BOT_ID { return true } @@ -509,8 +539,8 @@ open class CloudVeilSecurityController: NSObject { self.accessQueue.sync { res = settings?.access?.bots?["\(botID)"] } - return res - } + return res + } open func isAvailable(stickerId: NSInteger) -> Bool? { if disableStickers { @@ -523,16 +553,16 @@ open class CloudVeilSecurityController: NSObject { } return res } - - open func isBotAvailable(botID: NSInteger) -> Bool { + + open func isBotAvailable(botID: NSInteger) -> Bool { return isAvailable(botID: botID) ?? false - } + } open func isStickerAvailable(stickerId: NSInteger) -> Bool { return isAvailable(stickerId: stickerId) ?? false } - - open func isConversationAvailable(conversationId: NSInteger) -> Bool? { + + open func isConversationAvailable(conversationId: NSInteger) -> Bool? { var res: Bool? if let avail = isAvailable(botID: conversationId) { res = (res ?? false) || avail @@ -543,41 +573,41 @@ open class CloudVeilSecurityController: NSObject { if let avail = isAvailable(groupID: -conversationId) { res = (res ?? false) || avail } - - return res - } - - open func isConversationCheckedOnServer(conversationId: NSInteger, channelId: NSInteger) -> Bool { - var res = false + + return res + } + + open func isConversationCheckedOnServer(conversationId: NSInteger, channelId: NSInteger) -> Bool { + var res = false self.accessQueue.sync { guard let settings = settings else { res = true return } - + guard let access = settings.access else { return } - + let haveGroup = access.groups?["\(channelId)"] != nil let haveChannel = access.channels?["\(channelId)"] != nil let haveBot = access.bots?["\(conversationId)"] != nil - + res = haveGroup || haveChannel || haveBot } - return res - } - - open func showFirstRunPopup(_ viewController: UIViewController) { - if !wasFirstLoaded { - wasFirstLoaded = true - - let alert = UIAlertController(title: "CloudVeil!", message: "CloudVeil Messenger uses a server based system to control access to Bots, Channels, and Groups and other policy rules. This is used to block unacceptable content. Your Telegram id and list of channels, bots, and groups will be sent to our system to allow this to work. We do not have access to your messages themselves.", preferredStyle: .alert) - alert.addAction(.init(title: "OK", style: .default, handler: nil)) - - viewController.present(alert, animated: false) - } - } + return res + } + + open func showFirstRunPopup(_ viewController: UIViewController) { + if !wasFirstLoaded { + wasFirstLoaded = true + + let alert = UIAlertController(title: "CloudVeil!", message: "CloudVeil Messenger uses a server based system to control access to Bots, Channels, and Groups and other policy rules. This is used to block unacceptable content. Your Telegram id and list of channels, bots, and groups will be sent to our system to allow this to work. We do not have access to your messages themselves.", preferredStyle: .alert) + alert.addAction(.init(title: "OK", style: .default, handler: nil)) + + viewController.present(alert, animated: false) + } + } open func showContentDisableWarningPopup(_ viewController: UIViewController) { let alert = UIAlertController(title: "CloudVeil!", message: "Blocked", preferredStyle: .alert) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift index d37a75e586c..517dd78b3d7 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/TGUserController.swift @@ -12,6 +12,7 @@ import Foundation private static let lock = NSLock() private static let shared = TGUserController() private(set) static var didChangedUserID = true + private(set) static var didChangedOrgID = true // MARK: - Singleton @@ -23,6 +24,10 @@ import Foundation return Self.withLock({ $0.getUserID() }) } + public static var orgID: Int? { + return Self.withLock({ $0.getOrgID() }) + } + // MARK: - Actions // This set func should call from a Self.withLock completion block @@ -34,6 +39,15 @@ import Foundation TGUserModel1.set(userID: id) } + // This set func should call from a Self.withLock completion block + @objc open func set(orgID id: NSInteger) { + // Check if id has changed + if TGUserModel1.orgId != id { + TGUserController.didChangedOrgID = true + } + TGUserModel1.set(orgID: id) + } + @objc open func set(userPhoneNumber phone: NSString) { TGUserModel1.set(userPhoneNumber: phone) } @@ -49,12 +63,17 @@ import Foundation //acknowledge that cache has been updated in CloudVeilSecurityController @objc open func setCacheHasBeenUpdated() { TGUserController.didChangedUserID = false + TGUserController.didChangedOrgID = false } @objc open func getUserID() -> NSInteger { return TGUserModel1.id } + @objc open func getOrgID() -> NSInteger { + return TGUserModel1.orgId + } + @objc open func getUserPhoneNumber() -> NSString { return TGUserModel1.phoneNumber } diff --git a/CloudVeil/SecurityManager/Sources/Classes/Models/TGUserModel.swift b/CloudVeil/SecurityManager/Sources/Classes/Models/TGUserModel.swift index fdb914e980a..5d9781efc9c 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Models/TGUserModel.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Models/TGUserModel.swift @@ -13,6 +13,7 @@ class TGUserModel1: NSObject { // MARK: - Constants static let kTGUserModelId = "TGUserModelId" + static let kTGUserModelOrgId = "TGUserModelOrgId" static let kTGUserModelPhoneNumber = "TGUserModelPhoneNumber" static let kTGUserModelUserName = "TGUserModelUserName" static let kTGUserModelUserNames = "TGUserModelUserNames" @@ -26,6 +27,12 @@ class TGUserModel1: NSObject { get { return UserDefaults.standard.object(forKey: kTGUserModelId) as? NSInteger ?? 0} } + public static private(set) var orgId: NSInteger { + + set { UserDefaults.standard.set(newValue, forKey: kTGUserModelOrgId) } + get { return UserDefaults.standard.object(forKey: kTGUserModelOrgId) as? NSInteger ?? 0} + } + public static private(set) var phoneNumber: NSString { set { UserDefaults.standard.set(newValue, forKey: kTGUserModelPhoneNumber) } @@ -51,6 +58,10 @@ class TGUserModel1: NSObject { id = userID } + public static func set(orgID: NSInteger) { + orgId = orgID + } + public static func set(userPhoneNumber phone: NSString) { phoneNumber = phone } From 04158fd642dc71d283ba8b9077680903edaf26db Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Tue, 30 Dec 2025 22:48:16 +0700 Subject: [PATCH 05/10] chore: preserve orgId in settings cache operations Added orgId property to TGSettingsRequest and updated CloudVeilSecurityController to use orgId when saving settings to ensure correct cache key per user and organization. This change helps maintain accurate settings cache for users across different organizations. --- .../CloudVeilSecurityController.swift | 16 +++++++--------- .../Classes/Models/TGSettingsRequest.swift | 6 ++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index 81cadbd2d34..7375031e439 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -296,7 +296,7 @@ open class CloudVeilSecurityController: NSObject { return } let task = self.sendSettingsRequest(body) { response in - self.saveSettings(response, forUserId: body.id ?? 0) + self.saveSettings(response, forUserId: body.id ?? 0, orgId: body.orgId ?? 0) self.netQueue.async { if let nextReq = self.nextRequest, nextReq != body { self.sendSettingsRequest(nextReq) @@ -434,18 +434,13 @@ open class CloudVeilSecurityController: NSObject { self.replayRequestWith(bot: bot) } - private func saveSettings(_ settings: TGSettingsResponse?, forUserId: Int64 = 0) { + private func saveSettings(_ settings: TGSettingsResponse?, forUserId: Int64 = 0, orgId: NSInteger = 0) { print("Save settings called") self.accessQueue.sync { if let settings = settings { // Update org ID if present in settings let userId = forUserId - var targetOrgId = 0 - - // Read current user org ID - TGUserController.withLock { tg in - targetOrgId = tg.getOrgID() - } + var targetOrgId = orgId if let orgId = settings.organization?.id, targetOrgId != orgId { // ORG ID has changed @@ -455,12 +450,15 @@ open class CloudVeilSecurityController: NSObject { targetOrgId = orgId } let cacheKey = "\(userId)_\(targetOrgId)" - // Current disk cache + // Current disk cache for this specific user+org combo let cache = DataSource.value(forKey: cacheKey, mapper: mapper) // if last response's org is this response's org, // keep old allowed peers around even when this response doesn't have them // discard old blocked peers + + // Merge with disk cache (not in-memory cache) to preserve allowed peers + // for the same org across network requests if settings.organization?.id == cache?.organization?.id { settings.access = settings.access ?? AccessObject() diff --git a/CloudVeil/SecurityManager/Sources/Classes/Models/TGSettingsRequest.swift b/CloudVeil/SecurityManager/Sources/Classes/Models/TGSettingsRequest.swift index 7fbdfda2874..4c4fcc0298b 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Models/TGSettingsRequest.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Models/TGSettingsRequest.swift @@ -14,6 +14,8 @@ public class TGSettingsRequest: Mappable, Equatable { // MARK: - Properties public private(set) var id: Int64? + // We don't send orgId to server, but we need it to save correct cache later + public private(set) var orgId: NSInteger? public var phoneNumber: String? public var userName: String? public var userNames: [String] = [] @@ -55,6 +57,10 @@ public class TGSettingsRequest: Mappable, Equatable { self.channels = channels self.bots = bots self.stickers = stickers + TGUserController.withLock({ + // Read current org ID to save correct cache later + orgId = $0.getOrgID() + }) } private static func getClientId(_ userId: Int64) -> String { From c79f60486d5086c47c9d52ec70f258e62f48a1c0 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Thu, 1 Jan 2026 16:21:39 +0700 Subject: [PATCH 06/10] chore: improve logging for settings cache operations Added detailed log statements to track settings access and cache updates, including user and org IDs and cache validity. Logging now clarifies when the settings cache is updated or not due to user/org mismatches, aiding debugging and monitoring. --- .../CloudVeilSecurityController.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index 7375031e439..b248342408f 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -71,6 +71,7 @@ open class CloudVeilSecurityController: NSObject { private let accessQueue = DispatchQueue(label: "TGSettingsResponseAccess") private var settingsCache: TGSettingsResponse? + // Make sure access this computed property only inside accessQueue.sync private var settings: TGSettingsResponse? { var resp: TGSettingsResponse? var userId = 0 @@ -82,21 +83,23 @@ open class CloudVeilSecurityController: NSObject { userId = tg.getUserID() orgId = tg.getOrgID() isCacheValid = !TGUserController.didChangedUserID && !TGUserController.didChangedOrgID + let cacheKey = "\(userId)_\(orgId)" if settingsCache != nil && isCacheValid { resp = settingsCache } else { // Reading from disk inside the lock ensures the User ID and Org ID cannot change // while we are loading the file. - let cacheKey = "\(userId)_\(orgId)" + settingsCache = DataSource.value(forKey: cacheKey, mapper: mapper) resp = settingsCache - - // Update the flag directly on the instance (no nested lock) - tg.setCacheHasBeenUpdated() + if settingsCache != nil { + // Update the flag directly on the instance (no nested lock) + tg.setCacheHasBeenUpdated() + } } } - + print("[SETTINGS] Settings accessed for user \(userId) org \(orgId), isCacheValid=\(isCacheValid ? "true" : "false")") return resp } @@ -435,7 +438,7 @@ open class CloudVeilSecurityController: NSObject { } private func saveSettings(_ settings: TGSettingsResponse?, forUserId: Int64 = 0, orgId: NSInteger = 0) { - print("Save settings called") + print("[SETTINGS] Save settings called") self.accessQueue.sync { if let settings = settings { // Update org ID if present in settings @@ -494,6 +497,10 @@ open class CloudVeilSecurityController: NSObject { TGUserController.withLock { tg in if tg.getUserID() == Int(userId) && tg.getOrgID() == targetOrgId { settingsCache = settings + print("[SETTINGS] Settings cache updated in memory for user \(Int(userId)) org \(targetOrgId)") + } else { + print("[SETTINGS] Settings cache NOT updated in memory due to user/org mismatch") + print("[SETTINGS] details: current user \(tg.getUserID()) org \(tg.getOrgID()), settings user \(Int(userId)) org \(targetOrgId)") } } } From 641c5a36fca0655c40e47e220e4ce2cf5c12c1b1 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Fri, 2 Jan 2026 10:51:32 +0700 Subject: [PATCH 07/10] refactor: settings access to use thread-safe helper Introduced a private withSettings accessor to ensure thread-safe access to settingsCache. Replaced repetitive accessQueue.sync blocks in computed properties with this new helper for improved code clarity and maintainability. --- .../CloudVeilSecurityController.swift | 97 ++++++------------- 1 file changed, 31 insertions(+), 66 deletions(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index b248342408f..d755da1295c 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -70,7 +70,15 @@ open class CloudVeilSecurityController: NSObject { // Serial queue to protect access to settingsCache private let accessQueue = DispatchQueue(label: "TGSettingsResponseAccess") private var settingsCache: TGSettingsResponse? - + + // Thread-safe accessor - use this instead of accessing settings directly + // WARNING: Do not call this from code already running inside accessQueue.sync or you'll deadlock + private func withSettings(_ block: (TGSettingsResponse?) -> T) -> T { + return self.accessQueue.sync { + return block(settings) + } + } + // Make sure access this computed property only inside accessQueue.sync private var settings: TGSettingsResponse? { var resp: TGSettingsResponse? @@ -104,96 +112,53 @@ open class CloudVeilSecurityController: NSObject { } public var needOrganizationChange: Bool { - var res = false - self.accessQueue.sync { - res = settings?.organization?.needChange ?? false - } - return res + return withSettings { $0?.organization?.needChange ?? false } } + public var disableStories: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableStories ?? false - } - return res + return withSettings { $0?.disableStories ?? false } } public var disableStickers: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableSticker ?? false - } - return res + return withSettings { $0?.disableSticker ?? false } } + public var disableBio: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableBio ?? false - } - return res + return withSettings { $0?.disableBio ?? false } } + public var disableBioChange: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableBioChange ?? false - } - return res + return withSettings { $0?.disableBioChange ?? false } } + public var disableProfilePhoto: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableProfilePhoto ?? false - } - return res + return withSettings { $0?.disableProfilePhoto ?? false } } + public var disableProfilePhotoChange: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableProfilePhotoChange ?? false - } - return res + return withSettings { $0?.disableProfilePhotoChange ?? false } } - + public var isSecretChatAvailable: Bool { - var res = false - self.accessQueue.sync { - res = settings?.secretChat ?? false - } - return res + return withSettings { $0?.secretChat ?? false } } - + public var disableProfileVideo: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableProfileVideo ?? false - } - return res + return withSettings { $0?.disableProfileVideo ?? false } } + public var disableProfileVideoChange: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableProfileVideoChange ?? false - } - return res + return withSettings { $0?.disableProfileVideoChange ?? false } } - + public var isInChatVideoRecordingEnabled: Bool { - var res = false - self.accessQueue.sync { - res = settings?.inputToggleVoiceVideo ?? false - } - return res + return withSettings { $0?.inputToggleVoiceVideo ?? false } } public var disableEmojiStatus: Bool { - var res = false - self.accessQueue.sync { - res = settings?.disableEmojiStatus ?? false - } - return res + return withSettings { $0?.disableEmojiStatus ?? false } } - - + public var profilePhotoLimit: Int { var v = 1 self.accessQueue.sync { From 6b02f90e58ad9858082f4c8ba995bdb8790048fb Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Fri, 2 Jan 2026 10:52:17 +0700 Subject: [PATCH 08/10] chore: Bump app version to 11.5.4 in versions.json --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 1afca2e80b9..7b2c87d0954 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.5.2", + "app": "11.5.4", "xcode": "16.0", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15.0" From 57d803eccd3cd895d2316762b684d9f792a22041 Mon Sep 17 00:00:00 2001 From: duyhungtnn Date: Fri, 2 Jan 2026 11:37:21 +0700 Subject: [PATCH 09/10] chore: reduce logs in the production build --- .../Classes/Controllers/CloudVeilSecurityController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift index d755da1295c..f25dad2ec9e 100644 --- a/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift +++ b/CloudVeil/SecurityManager/Sources/Classes/Controllers/CloudVeilSecurityController.swift @@ -104,10 +104,11 @@ open class CloudVeilSecurityController: NSObject { if settingsCache != nil { // Update the flag directly on the instance (no nested lock) tg.setCacheHasBeenUpdated() + print("[SETTINGS] Settings loaded from disk cache for user \(userId) org \(orgId)") } } } - print("[SETTINGS] Settings accessed for user \(userId) org \(orgId), isCacheValid=\(isCacheValid ? "true" : "false")") + CVLog.log(self.TAG, "[SETTINGS] Settings accessed for user \(userId) org \(orgId), isCacheValid=\(isCacheValid ? "true" : "false")") return resp } From c4d2c191feb6d4b65e51594a218eb4af216d18b1 Mon Sep 17 00:00:00 2001 From: kenji Date: Fri, 2 Jan 2026 11:52:45 +0700 Subject: [PATCH 10/10] Update BUILD.md --- BUILD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index cea1940112a..8ec892b1205 100644 --- a/BUILD.md +++ b/BUILD.md @@ -94,4 +94,5 @@ Telegram has some fake codesigning files that we can use. (see build-system/fake # Build number - Create a file `buildNumber.txt` at root folder -- Put the build number on this before start a new build \ No newline at end of file +- Put the build number on this before start a new build +- If build_number is not correct after build, please check `build.sh` - `BUILD_NUMBER` \ No newline at end of file