import Foundation import Postbox import SwiftSignalKit import TelegramCore #if canImport(SGLogging) import SGLogging #endif // MARK: - Constants private let messagesPerPage = 1000 // MARK: - Export Progress public enum SGChatExportProgress { case preparing case exporting(current: Int, total: Int) case copyingMedia(current: Int, total: Int) case done(URL) case error(String) } // MARK: - Peer Display Name Helper private func peerDisplayName(_ peer: Peer?) -> String { guard let peer = peer else { return "Unknown" } if let user = peer as? TelegramUser { let first = user.firstName ?? "" let last = user.lastName ?? "" let name = [first, last].filter { !$0.isEmpty }.joined(separator: " ") return name.isEmpty ? (user.username ?? "User") : name } else if let channel = peer as? TelegramChannel { return channel.title } else if let group = peer as? TelegramGroup { return group.title } return "Chat" } private func peerInitial(_ peer: Peer?) -> String { let name = peerDisplayName(peer) return String(name.prefix(1)) } private func userpicColorIndex(_ peer: Peer?) -> Int { guard let peer = peer else { return 1 } let id = peer.id.id._internalGetInt64Value() return Int(abs(id) % 8) + 1 } // MARK: - Chat Title Helper private func chatTitle(peerId: PeerId, transaction: Transaction) -> String { if let peer = transaction.getPeer(peerId) { return peerDisplayName(peer) } return "Chat" } // MARK: - HTML Escaping private func htmlEscape(_ text: String) -> String { return text .replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") .replacingOccurrences(of: "\"", with: """) } // MARK: - Date Formatting private let dateFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "dd.MM.yyyy HH:mm:ss" f.locale = Locale(identifier: "en_US_POSIX") return f }() private let timeFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "HH:mm" f.locale = Locale(identifier: "en_US_POSIX") return f }() private let dateSeparatorFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "d MMMM yyyy" f.locale = Locale(identifier: "en_US") return f }() private let fileDateFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "dd-MM-yyyy_HH-mm-ss" f.locale = Locale(identifier: "en_US_POSIX") return f }() private func timeZoneSuffix() -> String { let tz = TimeZone.current let seconds = tz.secondsFromGMT() let hours = seconds / 3600 let minutes = abs(seconds % 3600) / 60 return String(format: "UTC%+03d:%02d", hours, minutes) } // MARK: - Duration Formatting private func formatDuration(_ seconds: Int) -> String { let m = seconds / 60 let s = seconds % 60 return String(format: "%02d:%02d", m, s) } // MARK: - Media Info private struct MediaFileInfo { let sourceResourcePath: String? let exportSubdir: String let exportFileName: String let htmlBlock: String } private func mediaInfoForMessage( _ message: Message, mediaBox: MediaBox, messageDate: Date ) -> MediaFileInfo? { for media in message.media { if let image = media as? TelegramMediaImage { guard let largest = image.representations.last else { continue } let sourcePath = mediaBox.completedResourcePath(largest.resource, pathExtension: "jpg") let dateStr = fileDateFormatter.string(from: messageDate) let fileName = "photo_\(message.id.id)@\(dateStr).jpg" let thumbFileName = "photo_\(message.id.id)@\(dateStr)_thumb.jpg" let dims = largest.dimensions let w = min(Int(dims.width), 260) let h = Int(Double(dims.height) * Double(w) / max(Double(dims.width), 1)) let html = """
""" return MediaFileInfo( sourceResourcePath: sourcePath, exportSubdir: "photos", exportFileName: fileName, htmlBlock: html ) } if let file = media as? TelegramMediaFile { let sourcePath = mediaBox.completedResourcePath(file.resource) let dateStr = fileDateFormatter.string(from: messageDate) if file.isVoice { let duration = Int(file.duration ?? 0) let fileName = "audio_\(message.id.id)@\(dateStr).ogg" let html = """