forked from eatvio/eatvio_app
150 lines
5.4 KiB
Swift
150 lines
5.4 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
enum VideoCallDeclineClient {
|
|
private static let uuidPattern =
|
|
#"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"#
|
|
|
|
/// `POST /video-call/decline` from the CallKit callback. The process is
|
|
/// awake here even when the user never opens the app afterwards.
|
|
static func send(extra: [String: Any], handle: String) {
|
|
let fields = declineFields(extra: extra, handle: handle)
|
|
guard let request = makeRequest(
|
|
baseUrl: prefsString("apiBaseUrl") ?? prefsString("customBaseUrl"),
|
|
token: prefsString("token"),
|
|
hwId: UIDevice.current.identifierForVendor?.uuidString,
|
|
fields: fields
|
|
) else {
|
|
NSLog(
|
|
"[EatvioVoIP] decline POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
|
)
|
|
return
|
|
}
|
|
|
|
let task = EatvioBackgroundTask()
|
|
task.begin(name: "video-call-decline")
|
|
let urlTask = URLSession.shared.dataTask(with: request) { _, response, error in
|
|
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
|
|
NSLog(
|
|
"[EatvioVoIP] decline POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
|
)
|
|
task.end()
|
|
}
|
|
urlTask.resume()
|
|
}
|
|
|
|
static func makeRequest(
|
|
baseUrl: String?,
|
|
token: String?,
|
|
hwId: String?,
|
|
fields: VideoCallDeclineFields
|
|
) -> URLRequest? {
|
|
if fields.endedPush { return nil }
|
|
let conversation = fields.conversationUuid.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let authToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
let deviceId = hwId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
var root = baseUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
while root.hasSuffix("/") {
|
|
root.removeLast()
|
|
}
|
|
guard !conversation.isEmpty, !authToken.isEmpty else { return nil }
|
|
guard deviceId.count >= 8, deviceId.count <= 64 else { return nil }
|
|
guard root.hasPrefix("http://") || root.hasPrefix("https://") else { return nil }
|
|
guard let encoded = conversation.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
|
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/decline")
|
|
else { return nil }
|
|
|
|
var body: [String: String] = ["hw_id": deviceId]
|
|
if let callId = fields.callId {
|
|
body["call_id"] = callId
|
|
}
|
|
guard let data = try? JSONSerialization.data(withJSONObject: body) else { return nil }
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.timeoutInterval = 10
|
|
request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = data
|
|
return request
|
|
}
|
|
|
|
private static func declineFields(extra: [String: Any], handle: String) -> VideoCallDeclineFields {
|
|
let conversation = firstString(extra, keys: ["conversationUuid", "conversation_uuid"])
|
|
?? handle.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let rawCallId = firstString(extra, keys: ["call_id", "callId"])
|
|
return VideoCallDeclineFields(
|
|
conversationUuid: conversation,
|
|
callId: backendCallId(rawCallId, conversation: conversation),
|
|
endedPush: marksEndedPush(extra["endedPush"])
|
|
)
|
|
}
|
|
|
|
private static func backendCallId(_ callId: String?, conversation: String) -> String? {
|
|
let id = callId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
guard let regex = try? NSRegularExpression(pattern: uuidPattern),
|
|
regex.firstMatch(in: id, range: NSRange(id.startIndex..., in: id)) != nil
|
|
else { return nil }
|
|
if id.compare(conversation.trimmingCharacters(in: .whitespacesAndNewlines), options: .caseInsensitive) == .orderedSame {
|
|
return nil
|
|
}
|
|
return id
|
|
}
|
|
|
|
private static func firstString(_ extra: [String: Any], keys: [String]) -> String? {
|
|
for key in keys {
|
|
guard let value = extra[key] as? String else { continue }
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !trimmed.isEmpty { return trimmed }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func marksEndedPush(_ value: Any?) -> Bool {
|
|
if let bool = value as? Bool { return bool }
|
|
if let number = value as? NSNumber { return number.intValue != 0 }
|
|
if let text = value as? String {
|
|
let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return normalized == "true" || normalized == "1"
|
|
}
|
|
return false
|
|
}
|
|
|
|
private static func prefsString(_ key: String) -> String? {
|
|
let value = UserDefaults.standard.string(forKey: "flutter.\(key)")?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let value, !value.isEmpty { return value }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
struct VideoCallDeclineFields {
|
|
var conversationUuid: String
|
|
var callId: String?
|
|
var endedPush: Bool
|
|
}
|
|
|
|
private final class EatvioBackgroundTask {
|
|
private let lock = NSLock()
|
|
private var id: UIBackgroundTaskIdentifier = .invalid
|
|
|
|
func begin(name: String) {
|
|
let taskId = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
|
|
self?.end()
|
|
}
|
|
lock.lock()
|
|
id = taskId
|
|
lock.unlock()
|
|
}
|
|
|
|
func end() {
|
|
lock.lock()
|
|
let current = id
|
|
id = .invalid
|
|
lock.unlock()
|
|
guard current != .invalid else { return }
|
|
UIApplication.shared.endBackgroundTask(current)
|
|
}
|
|
}
|