← Back to index

PhoneExtractor

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import FoundationFramework importsImports Foundation.
enum PhoneExtractor { private static let apiKey = Secrets.anthropicAPIKey private static let apiURL = URL(string: "https://api.anthropic.com/v1/messages")! private static let model = "claude-haiku-4-5-20251001" // MARK: - Public entry point /// Extract a phone number and website URL for an entry that has a confirmationLink. /// Calls `completion` on the main thread with (phone, website); empty string = not found. static func extract(for entry: SpotEntry, completion: @escaping (String, String) -> Void) { let link = entry.confirmationLink guard !link.isEmpty else { completion("", ""); return } if isLocalFile(link) { extractFromFile(link, completion: completion) } else if link.hasPrefix("http") { extractFromURL(link, completion: completion) } else { completion("", "") } } // MARK: - File path private static func isLocalFile(_ link: String) -> Bool { (link.hasSuffix(".eml") || link.hasSuffix(".pdf")) && !link.hasPrefix("http") } private static func extractFromFile(_ filename: String, completion: @escaping (String, String) -> Void) { DispatchQueue.global(qos: .utility).async { guard let resolvedPath = EmlParser.resolveActualPath(for: filename) else { DispatchQueue.main.async { completion("", "") } return } if filename.hasSuffix(".pdf") { guard let data = try? Data(contentsOf: URL(fileURLWithPath: resolvedPath)) else { DispatchQueue.main.async { completion("", "") } return } callWithPDF(data, completion: completion) } else { // .eml — extract HTML body then strip tags to get plain text let html = EmlParser.extractHTMLBody(from: resolvedPath) ?? "" var plain = html.replacingOccurrences(of: "<[^>]+>", with: " ", options: .regularExpression) plain = plain.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) callWithText(String(plain.prefix(5000)), completion: completion) } } } // MARK: - URL private static func extractFromURL(_ urlString: String, completion: @escaping (String, String) -> Void) { guard let url = URL(string: urlString) else { completion("", ""); return } var request = URLRequest(url: url) request.setValue("Mozilla/5.0", forHTTPHeaderField: "User-Agent") request.timeoutInterval = 15 URLSession.shared.dataTask(with: request) { data, _, _ in guard let data, let html = String(data: data, encoding: .utf8) else { DispatchQueue.main.async { completion("", "") } return } // Strip HTML tags to plain text var plain = html.replacingOccurrences(of: "<[^>]+>", with: " ", options: .regularExpression) plain = plain.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) let trimmed = String(plain.prefix(5000)) callWithText(trimmed, completion: completion) }.resume() } // MARK: - Claude calls private static func callWithText(_ text: String, completion: @escaping (String, String) -> Void) { let prompt = """ Extract the phone number and official website URL of the venue, theater, or \ restaurant from the following text. Return ONLY a JSON object with two keys: \ "phone" (phone number string or empty string) and "website" (official website \ URL string or empty string). No other text. \(text) """ call(body: textBody(prompt), completion: completion) } private static func callWithPDF(_ data: Data, completion: @escaping (String, String) -> Void) { let b64 = data.base64EncodedString() let prompt = """ Extract the phone number and official website URL of the venue, theater, or \ restaurant from this document. Return ONLY a JSON object with two keys: \ "phone" (phone number string or empty string) and "website" (official website \ URL string or empty string). No other text. """ let body: [String: Any] = [ "model": model, "max_tokens": 60, "messages": [[ "role": "user", "content": [ ["type": "document", "source": ["type": "base64", "media_type": "application/pdf", "data": b64]], ["type": "text", "text": prompt] ] ]] ] call(body: body, completion: completion) } private static func textBody(_ prompt: String) -> [String: Any] { ["model": model, "max_tokens": 120, "messages": [["role": "user", "content": prompt]]] } private static func call(body: [String: Any], completion: @escaping (String, String) -> Void) { guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else { DispatchQueue.main.async { completion("", "") } return } var request = URLRequest(url: apiURL) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(apiKey, forHTTPHeaderField: "x-api-key") request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version") request.httpBody = jsonData request.timeoutInterval = 30 URLSession(configuration: .ephemeral).dataTask(with: request) { data, _, _ in var phone = "" var website = "" if let data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let content = json["content"] as? [[String: Any]], let text = content.first?["text"] as? String { let cleaned = text .replacingOccurrences(of: "```json", with: "") .replacingOccurrences(of: "```", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) if let parsed = try? JSONSerialization.jsonObject(with: Data(cleaned.utf8)) as? [String: Any] { phone = parsed["phone"] as? String ?? "" website = parsed["website"] as? String ?? "" } } DispatchQueue.main.async { completion(phone, website) } }.resume() } }`PhoneExtractor` enumDefines the `PhoneExtractor` enum.