import Foundation import SwiftSignalKit import TelegramCore import Postbox import Photos import TelegramUIPreferences import AccountContext private func appSpecificAssetCollection() -> Signal { return Signal { subscriber in let fetchOption = PHFetchOptions() let albumName = "Telegram" fetchOption.predicate = NSPredicate(format: "title == '" + albumName + "'") let fetchResult = PHAssetCollection.fetchAssetCollections( with: .album, subtype: .albumRegular, options: fetchOption) if let collection = fetchResult.firstObject { subscriber.putNext(collection) subscriber.putCompletion() } else { PHPhotoLibrary.shared().performChanges({ PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName) }, completionHandler: { success, error in if let error = error { Logger.shared.log("appSpecificAssetCollection", "error: \(error)") } if success { let fetchResult = PHAssetCollection.fetchAssetCollections( with: .album, subtype: .albumRegular, options: fetchOption) if let collection = fetchResult.firstObject { subscriber.putNext(collection) subscriber.putCompletion() } } }) } return EmptyDisposable } } private final class DownloadedMediaStoreContext { private let queue: Queue private var disposable: Disposable? init(queue: Queue) { self.queue = queue } deinit { self.disposable?.dispose() } func start(postbox: Postbox, collection: Signal, peerId: EnginePeer.Id, timestamp: Int32, media: AnyMediaReference, completed: @escaping () -> Void) { var resource: TelegramMediaResource? if let image = media.media as? TelegramMediaImage { resource = largestImageRepresentation(image.representations)?.resource } else if let file = media.media as? TelegramMediaFile { resource = file.resource } if let resource = resource { self.disposable = (postbox.transaction { transaction -> (MediaAutoSaveSettings, EnginePeer?) in let peer = transaction.getPeer(peerId).flatMap(EnginePeer.init) guard let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings)?.get(MediaAutoSaveSettings.self) else { return (.default, peer) } return (entry, peer) } |> map { storeSettings, peer -> Bool in guard let peer = peer else { return false } let configuration: MediaAutoSaveConfiguration if let exception = storeSettings.exceptions.first(where: { $0.id == peerId }) { configuration = exception.configuration } else { let peerTypeValue: MediaAutoSaveSettings.PeerType switch peer { case .user: peerTypeValue = .users case .secretChat: return false case .legacyGroup: peerTypeValue = .groups case let .channel(channel): if channel.flags.contains(.copyProtectionEnabled) { return false } if case .broadcast = channel.info { peerTypeValue = .channels } else { peerTypeValue = .groups } } configuration = storeSettings.configurations[peerTypeValue] ?? .default } if let _ = media.media as? TelegramMediaImage { if configuration.photo { return true } else { return false } } else if let file = media.media as? TelegramMediaFile { if configuration.video, let fileSize = file.size, fileSize <= configuration.maximumVideoSize { return true } else { return false } } else { return false } } |> take(1) |> mapToSignal { store -> Signal<(PHAssetCollection, MediaResourceData), NoError> in if !store { return .complete() } else { return combineLatest(collection |> take(1), postbox.mediaBox.resourceData(resource)) } } |> deliverOn(queue)).startStrict(next: { collection, data in if !data.complete { return } var filename: String? if let image = media.media as? TelegramMediaImage { filename = "telegram-photo-\(image.imageId.namespace)-\(image.imageId.id).jpg" } else if let file = media.media as? TelegramMediaFile { filename = "telegram-video-\(file.fileId.namespace)-\(file.fileId.id).mov" } let creationDate = Date(timeIntervalSince1970: TimeInterval(timestamp)) let storeAsset: () -> Void = { if let _ = media.media as? TelegramMediaImage { PHPhotoLibrary.shared().performChanges({ if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { let creationRequest = PHAssetCreationRequest.forAsset() let options = PHAssetResourceCreationOptions() if let filename = filename { options.originalFilename = filename } creationRequest.addResource(with: .photo, data: fileData, options: options) creationRequest.creationDate = creationDate let request = PHAssetCollectionChangeRequest(for: collection) if let placeholderForCreatedAsset = creationRequest.placeholderForCreatedAsset { request?.addAssets([placeholderForCreatedAsset] as NSArray) } } }) } else if let file = media.media as? TelegramMediaFile, file.isVideo { let tempFile = TempBox.shared.tempFile(fileName: filename ?? "file.mov") PHPhotoLibrary.shared().performChanges({ if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempFile.path) { let creationRequest = PHAssetCreationRequest.forAsset() let options = PHAssetResourceCreationOptions() if let filename = filename { options.originalFilename = filename } creationRequest.addResource(with: .video, fileURL: URL(fileURLWithPath: tempFile.path), options: options) creationRequest.creationDate = creationDate let request = PHAssetCollectionChangeRequest(for: collection) if let placeholderForCreatedAsset = creationRequest.placeholderForCreatedAsset { request?.addAssets([placeholderForCreatedAsset] as NSArray) } } }, completionHandler: { _, error in if let error = error { print("\(error)") } TempBox.shared.dispose(tempFile) }) } } let options = PHFetchOptions() if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { options.fetchLimit = 11 } options.predicate = NSPredicate(format: "creationDate == %@", creationDate as CVarArg) var alreadyStored = false let assets = PHAsset.fetchAssets(in: collection, options: options) assets.enumerateObjects({ asset, _, done in if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if let assetResource = PHAssetResource.assetResources(for: asset).first { if assetResource.originalFilename == filename { alreadyStored = true done.pointee = true } } } }) if !alreadyStored { storeAsset() } completed() }) } else { completed() } } } private final class DownloadedMediaStoreManagerPrivateImpl { private let queue: Queue private let postbox: Postbox private var nextId: Int32 = 1 private var storeContexts: [MediaId: DownloadedMediaStoreContext] = [:] private let appSpecificAssetCollectionValue: Promise private let storeSettings = Promise() init(queue: Queue, postbox: Postbox, accountManager: AccountManager) { self.queue = queue self.postbox = postbox self.appSpecificAssetCollectionValue = Promise(initializeOnFirstAccess: appSpecificAssetCollection()) self.storeSettings.set(accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) |> map { sharedData -> MediaAutoDownloadSettings in if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { return settings } else { return .defaultSettings } }) } deinit { assert(self.queue.isCurrent()) } private func takeNextId() -> Int32 { let nextId = self.nextId self.nextId += 1 return nextId } func store(_ media: AnyMediaReference, timestamp: Int32, peerId: EnginePeer.Id) { guard let id = media.media.id else { return } if self.storeContexts[id] == nil { let context = DownloadedMediaStoreContext(queue: self.queue) self.storeContexts[id] = context let appSpecificAssetCollectionValue = self.appSpecificAssetCollectionValue context.start(postbox: self.postbox, collection: deferred { appSpecificAssetCollectionValue.get() }, peerId: peerId, timestamp: timestamp, media: media, completed: { [weak self, weak context] in guard let strongSelf = self, let context = context else { return } assert(strongSelf.queue.isCurrent()) if strongSelf.storeContexts[id] === context { strongSelf.storeContexts.removeValue(forKey: id) } }) } } } final class DownloadedMediaStoreManagerImpl: DownloadedMediaStoreManager { private let queue = Queue() private let postbox: Postbox private let impl: QueueLocalObject init(postbox: Postbox, accountManager: AccountManager) { let queue = self.queue self.postbox = postbox self.impl = QueueLocalObject(queue: queue, generate: { return DownloadedMediaStoreManagerPrivateImpl(queue: queue, postbox: postbox, accountManager: accountManager) }) } func store(_ media: AnyMediaReference, timestamp: Int32, peerId: EnginePeer.Id) { self.impl.with { impl in impl.store(media, timestamp: timestamp, peerId: peerId) } } func runTasks() { let _ = (self.postbox.transaction({ transaction -> [(index: Int32, message: Message, mediaId: MediaId)] in return _internal_getSynchronizeAutosaveItemOperations(transaction: transaction) }) |> deliverOnMainQueue).startStandalone(next: { [weak self] items in guard let self else { return } for item in items { for media in item.message.media { if let id = media.id, id == item.mediaId { self.store(.standalone(media: media), timestamp: item.message.timestamp, peerId: item.message.id.peerId) break } } } let _ = self.postbox.transaction({ transaction -> Void in return _internal_removeSyncrhonizeAutosaveItemOperations(transaction: transaction, indices: items.map(\.index)) }).startStandalone() }) } }