import CocoaLumberjackSwift
import Foundation
import ThreemaProtocols
import WebRTC
struct SFUHTTPConnection {
let dependencies: Dependencies
let groupCallDescription: GroupCallBaseState
}
extension SFUHTTPConnection {
enum PeekResponse {
case notRunning
case notDetermined
case needsTokenRefresh
case invalidRequest
case failed
case running(GroupCall_SfuHttpResponse.Peek)
}
enum JoinResponse {
case running(GroupCall_SfuHttpResponse.Join)
case notDetermined
case full
case notRunning
case timeout
}
}
extension SFUHTTPConnection {
func peek() async throws -> PeekResponse {
guard let (data, status) = try await send(.peek) else {
return .notDetermined
}
guard let httpStatus = status as? HTTPURLResponse else {
return .failed
}
/// **Protocol Step: Group Call Peek Steps** 5.2.: Only a 404 means the call is not running.
if httpStatus.statusCode == 404 {
DDLogWarn(
"[GroupCall] [PeriodicCleanup] Peek for group call \(groupCallDescription.callID) returned 404. Call is not running"
)
return .notRunning
}
/// **Protocol Step: Group Call Peek Steps** 5.3.: Any other unexpected status code (e.g. 5xx) means the peek
/// failed, but the call might still be running.
guard httpStatus.statusCode == 200 || httpStatus.statusCode == 400 || httpStatus.statusCode == 401 else {
DDLogWarn(
"[GroupCall] [PeriodicCleanup] Peek for group call \(groupCallDescription.callID) returned \(httpStatus.statusCode) instead of 200, 400, 401 or 404. Keeping call"
)
return .failed
}
if httpStatus.statusCode == 401 {
DDLogWarn(
"[GroupCall] [PeriodicCleanup] Our credentials have expired. Try again in some time."
)
return .needsTokenRefresh
}
if httpStatus.statusCode == 400 {
DDLogWarn(
"[GroupCall] [PeriodicCleanup] Peek for group call \(groupCallDescription.callID) failed with 400, because the provided data invalid or call ids don't match"
)
return .invalidRequest
}
guard let peekResponse = try? GroupCall_SfuHttpResponse.Peek(serializedBytes: data) else {
return .notDetermined
}
return .running(peekResponse)
}
func join(with certificate: RTCCertificate) async throws -> JoinResponse {
let task = Task {
try await send(.join(certificate))
}
let intermediateResult = try await Task.timeout(task, 10)
switch intermediateResult {
case let .error(error):
if let error {
throw error
}
else {
DDLogError("[GroupCall] [Join Steps] An unknown error occurred")
return .notDetermined
}
case .timeout:
DDLogError("[GroupCall] [Join Steps] Join timed out")
return .timeout
case let .result(result):
guard let (data, status) = result else {
DDLogError("[GroupCall] [Join Steps] Undetermined result")
return .notDetermined
}
guard let httpStatus = status as? HTTPURLResponse else {
DDLogError("[GroupCall] [Join Steps] Group call not running")
return .notRunning
}
/// **Protocol Step: Group Call Join Steps**
/// 3. If the received status code is `503`, notify the user that the group call
/// is full and abort these steps.
if httpStatus.statusCode == 503 {
DDLogWarn("[GroupCall] [Join Steps] Group call is full")
return .full
}
if httpStatus.statusCode == 401 {
DDLogWarn("[GroupCall] [Join Steps] Our credentials have expired. Try again in some time.")
return .notDetermined
}
/// **Protocol Step: Group Call Join Steps**
/// 4. If the server could not be reached or the received status code is not
/// `200` or if the _Join_ response could not be decoded, abort these steps
/// and notify the user.
guard httpStatus.statusCode == 200 else {
DDLogWarn(
"[GroupCall] [Join Steps] Peek for group call \(groupCallDescription.callID) returned \(httpStatus.statusCode) instead of 200. Removing call"
)
return .notRunning
}
guard let joinResponse = try? GroupCall_SfuHttpResponse.Join(serializedBytes: data) else {
DDLogError("[GroupCall] [Join Steps] Could not create join from received data")
return .notDetermined
}
return .running(joinResponse)
}
}
}
extension SFUHTTPConnection {
fileprivate enum SFUHTTPRequest {
case join(RTCCertificate)
case peek
var param: String {
switch self {
case .peek:
"peek"
case .join:
"join"
}
}
}
private func send(_ requestType: SFUHTTPRequest) async throws -> (Data, URLResponse)? {
/// **Protocol Step: SfuHttpRequest (Sending 1. - 3.)**
/// 1. Use `POST` as method.
/// 2. Set the `Authorization` header to `ThreemaSfuToken <sfu-token>`.
/// 3. Set the encoded `SfuHttpRequest.Peek`/`SfuHttpRequest.Join` message as body.
let param: String = requestType.param
let requestBody: Data =
switch requestType {
case .peek:
try create(.peek)
case let .join(certificate):
try create(.join(certificate))
}
guard let authorizationToken = try? await dependencies.httpHelper.sfuCredentials() else {
throw GroupCallError.invalidToken
}
guard !Task.isCancelled else {
return nil
}
let groupCallURL = groupCallDescription.sfuBaseURL
.appendingPathComponent("v1")
.appendingPathComponent(param)
.appendingPathComponent(groupCallDescription.callID.bytes.hexEncodedString())
DDLogNotice("[GroupCall] Checking at URL \(groupCallURL)")
return try await dependencies.groupCallsHTTPClientAdapter.sendPeek(
authorization: "ThreemaSfuToken \(authorizationToken.sfuToken)",
url: groupCallURL,
body: requestBody
)
}
}
extension SFUHTTPConnection {
private func create(_ request: SFUHTTPRequest) throws -> Data {
switch request {
case .peek:
guard let data = try createPeekRequest() else {
throw GroupCallError.serializationFailure
}
return data
case let .join(certificate):
guard let fingerprint = certificate.groupCallFingerprint else {
throw GroupCallError.serializationFailure
}
guard let data = try createJoinRequest(fingerprint: fingerprint) else {
throw GroupCallError.serializationFailure
}
return data
}
}
private func createPeekRequest() throws -> Data? {
var body = GroupCall_SfuHttpRequest.Peek()
body.callID = groupCallDescription.callID.bytes
return try? body.ownSerializedData()
}
private func createJoinRequest(fingerprint: Data) throws -> Data? {
var body = GroupCall_SfuHttpRequest.Join()
body.callID = groupCallDescription.callID.bytes
body.protocolVersion = groupCallDescription.protocolVersion
body.dtlsFingerprint = fingerprint
return try? body.ownSerializedData()
}
}
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland