|
|
@@ -0,0 +1,73 @@
|
|
|
+import Foundation
|
|
|
+
|
|
|
+enum UserFacingError {
|
|
|
+ static let generic = "Something went wrong. Please try again."
|
|
|
+
|
|
|
+ static func message(for error: Error) -> String {
|
|
|
+ if shouldUseGenericMessage(for: error) {
|
|
|
+ return generic
|
|
|
+ }
|
|
|
+
|
|
|
+ let raw = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
|
|
|
+ return sanitized(raw) ?? generic
|
|
|
+ }
|
|
|
+
|
|
|
+ static func networkMessage(for error: URLError) -> String {
|
|
|
+ switch error.code {
|
|
|
+ case .notConnectedToInternet:
|
|
|
+ "You appear to be offline. Check your internet connection and try again."
|
|
|
+ case .timedOut:
|
|
|
+ "The request timed out. Please try again."
|
|
|
+ case .networkConnectionLost:
|
|
|
+ "The connection was interrupted. Please try again."
|
|
|
+ default:
|
|
|
+ generic
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static func sanitized(_ message: String) -> String? {
|
|
|
+ let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ guard !trimmed.isEmpty else { return nil }
|
|
|
+ guard !containsSensitiveContent(trimmed.lowercased()) else { return nil }
|
|
|
+ return trimmed
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func shouldUseGenericMessage(for error: Error) -> Bool {
|
|
|
+ if error is DecodingError || error is EncodingError {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ switch error {
|
|
|
+ case OpenAIServiceError.missingAPIKey,
|
|
|
+ OpenAIServiceError.invalidResponse,
|
|
|
+ DictionaryServiceError.missingAPIKey,
|
|
|
+ DictionaryServiceError.invalidResponse,
|
|
|
+ GrammarCheckServiceError.missingAPIKey,
|
|
|
+ GrammarCheckServiceError.invalidResponse,
|
|
|
+ JournalFinderServiceError.missingAPIKey,
|
|
|
+ JournalFinderServiceError.invalidResponse:
|
|
|
+ return true
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func containsSensitiveContent(_ lower: String) -> Bool {
|
|
|
+ lower.contains("api key")
|
|
|
+ || lower.contains("apikey")
|
|
|
+ || lower.contains("openai")
|
|
|
+ || lower.contains("invalid_api_key")
|
|
|
+ || lower.contains("incorrect api key")
|
|
|
+ || lower.contains("bearer ")
|
|
|
+ || lower.contains("sk-")
|
|
|
+ || lower.contains("authentication")
|
|
|
+ || lower.contains("not configured")
|
|
|
+ || lower.contains("response format")
|
|
|
+ || lower.contains("request failed")
|
|
|
+ || lower.contains("nsurlerrordomain")
|
|
|
+ || lower.contains("cocoaerrordomain")
|
|
|
+ || lower.contains("error domain")
|
|
|
+ || (lower.contains("could not read") && lower.contains("response"))
|
|
|
+ || (lower.contains("status") && lower.range(of: #"\b[1-5]\d{2}\b"#, options: .regularExpression) != nil)
|
|
|
+ }
|
|
|
+}
|