mirror of
https://github.com/vector-im/element-ios.git
synced 2024-10-02 09:12:40 +00:00
338 lines
13 KiB
Swift
338 lines
13 KiB
Swift
/*
|
|
Copyright 2019 New Vector Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import Foundation
|
|
|
|
#if canImport(JitsiMeetSDK)
|
|
import JitsiMeetSDK
|
|
|
|
enum JitsiServiceError: Error {
|
|
case widgetContentCreationFailed
|
|
case emptyResponse
|
|
case noWellKnown
|
|
case unknown
|
|
}
|
|
|
|
private enum HTTPStatusCodes {
|
|
static let notFound: Int = 404
|
|
}
|
|
|
|
/// JitsiService enables to abstract and configure Jitsi Meet SDK
|
|
@objcMembers
|
|
final class JitsiService: NSObject {
|
|
|
|
static let shared = JitsiService()
|
|
|
|
private enum Constants {
|
|
static let widgetIdLength = 7
|
|
}
|
|
|
|
private struct Route {
|
|
static let wellKnown = "/.well-known/element/jitsi"
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
var enableCallKit: Bool = true {
|
|
didSet {
|
|
JMCallKitProxy.enabled = enableCallKit
|
|
}
|
|
}
|
|
|
|
var serverURL: URL? {
|
|
return self.jitsiMeet.defaultConferenceOptions?.serverURL
|
|
}
|
|
|
|
private let jitsiMeet = JitsiMeet.sharedInstance()
|
|
private var httpClient: MXHTTPClient?
|
|
private let serializationService: SerializationServiceType = SerializationService()
|
|
|
|
private lazy var jwtTokenBuilder: JitsiJWTTokenBuilder = {
|
|
return JitsiJWTTokenBuilder()
|
|
}()
|
|
|
|
private var httpClients: [String: MXHTTPClient] = [:]
|
|
|
|
/// Holds widgetIds for declined group calls. Made a map to speed up lookups.
|
|
/// Values are useless, not used with false values.
|
|
private var declinedJitsiWidgets: [String: Bool] = [:]
|
|
|
|
// MARK: - Setup
|
|
|
|
private override init() {
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
func declineWidget(withId widgetId: String) {
|
|
declinedJitsiWidgets[widgetId] = true
|
|
}
|
|
|
|
func resetDeclineForWidget(withId widgetId: String) {
|
|
declinedJitsiWidgets.removeValue(forKey: widgetId)
|
|
}
|
|
|
|
func isWidgetDeclined(withId widgetId: String) -> Bool {
|
|
return declinedJitsiWidgets[widgetId] == true
|
|
}
|
|
|
|
// MARK: Configuration
|
|
|
|
func configureDefaultConferenceOptions(with serverURL: URL) {
|
|
self.jitsiMeet.defaultConferenceOptions = JitsiMeetConferenceOptions.fromBuilder({ (builder) in
|
|
builder.serverURL = serverURL
|
|
})
|
|
}
|
|
|
|
func configureCallKitProvider(localizedName: String, ringtoneName: String?, iconTemplateImageData: Data?) {
|
|
JMCallKitProxy.configureProvider(localizedName: localizedName, ringtoneSound: ringtoneName, iconTemplateImageData: iconTemplateImageData)
|
|
}
|
|
|
|
// MARK: WellKnown
|
|
|
|
/// Get Jitsi server Well-Known
|
|
@discardableResult
|
|
func getWellKnown(for jitsiServerURL: URL, completion: @escaping (Result<JitsiWellKnown, Error>) -> Void) -> MXHTTPOperation? {
|
|
guard let httpClient = self.httpClient(for: jitsiServerURL) else {
|
|
completion(.failure(JitsiServiceError.unknown))
|
|
return nil
|
|
}
|
|
|
|
return httpClient.request(withMethod: "GET", path: Route.wellKnown, parameters: nil, success: { response in
|
|
guard let response = response else {
|
|
completion(.failure(JitsiServiceError.emptyResponse))
|
|
return
|
|
}
|
|
|
|
do {
|
|
let jitsiWellKnown: JitsiWellKnown = try self.serializationService.deserialize(response)
|
|
completion(.success(jitsiWellKnown))
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}, failure: { (error) in
|
|
if let urlResponse = MXHTTPOperation.urlResponse(fromError: error),
|
|
urlResponse.statusCode == HTTPStatusCodes.notFound {
|
|
completion(.failure(JitsiServiceError.noWellKnown))
|
|
return
|
|
}
|
|
completion(.failure(error ?? JitsiServiceError.unknown))
|
|
})
|
|
}
|
|
|
|
/// Create Jitsi widget content
|
|
@discardableResult
|
|
func createJitsiWidgetContent(jitsiServerURL: URL, roomID: String, isAudioOnly: Bool, success: @escaping ([String: Any]) -> Void, failure: @escaping ((Error) -> Void)) -> MXHTTPOperation? {
|
|
guard let serverDomain = jitsiServerURL.host else {
|
|
failure(JitsiServiceError.widgetContentCreationFailed)
|
|
return nil
|
|
}
|
|
|
|
return self.getWellKnown(for: jitsiServerURL) { (result) in
|
|
var continueOperation: Bool = false
|
|
var authType: JitsiAuthenticationType?
|
|
|
|
switch result {
|
|
case .success(let jitsiWellKnown):
|
|
authType = jitsiWellKnown.authenticationType
|
|
continueOperation = true
|
|
case .failure(let error):
|
|
NSLog("[JitsiService] Fail to get Jitsi Well Known with error: \(error)")
|
|
if let error = error as? JitsiServiceError, error == .noWellKnown {
|
|
// no well-known, continue with no auth
|
|
continueOperation = true
|
|
} else {
|
|
failure(error)
|
|
}
|
|
}
|
|
|
|
if continueOperation,
|
|
let widgetContent = self.createJitsiWidgetContent(serverDomain: serverDomain,
|
|
authenticationType: authType,
|
|
roomID: roomID,
|
|
isAudioOnly: isAudioOnly) {
|
|
success(widgetContent)
|
|
} else {
|
|
failure(JitsiServiceError.widgetContentCreationFailed)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if Jitsi widget requires "openidtoken-jwt" authentication
|
|
func isOpenIdJWTAuthenticationRequired(for widgetData: JitsiWidgetData) -> Bool {
|
|
return widgetData.authenticationType == JitsiAuthenticationType.openIDTokenJWT.identifier
|
|
}
|
|
|
|
/// Get Jitsi JWT token using user OpenID token
|
|
@discardableResult
|
|
func getOpenIdJWTToken(jitsiServerDomain: String,
|
|
roomId: String,
|
|
matrixSession: MXSession,
|
|
success: @escaping (String) -> Void,
|
|
failure: @escaping (Error) -> Void) -> MXHTTPOperation? {
|
|
|
|
let myUser: MXUser = matrixSession.myUser
|
|
let userDisplayName: String = myUser.displayname ?? myUser.userId
|
|
let avatarStringURL: String = myUser.avatarUrl ?? ""
|
|
|
|
return matrixSession.matrixRestClient.openIdToken({ (openIdToken) in
|
|
guard let openIdToken = openIdToken, let openIdAccessToken = openIdToken.accessToken else {
|
|
failure(JitsiServiceError.unknown)
|
|
return
|
|
}
|
|
|
|
do {
|
|
let jwtToken = try self.jwtTokenBuilder.build(jitsiServerDomain: jitsiServerDomain,
|
|
openIdAccessToken: openIdAccessToken,
|
|
roomId: roomId,
|
|
userAvatarUrl: avatarStringURL,
|
|
userDisplayName: userDisplayName)
|
|
|
|
success(jwtToken)
|
|
} catch {
|
|
failure(error)
|
|
}
|
|
}, failure: { error in
|
|
failure(error ?? JitsiServiceError.unknown)
|
|
})
|
|
}
|
|
|
|
// MARK: AppDelegate methods
|
|
|
|
@discardableResult
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
return self.jitsiMeet.application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
|
|
}
|
|
|
|
func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
return self.jitsiMeet.application(application, open: url, options: options)
|
|
}
|
|
|
|
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
|
return self.jitsiMeet.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func httpClient(for jitsiServerURL: URL) -> MXHTTPClient? {
|
|
let httpClient: MXHTTPClient?
|
|
|
|
let baseStringURL = jitsiServerURL.absoluteString
|
|
|
|
if let existingHttpClient = self.httpClients[baseStringURL] {
|
|
httpClient = existingHttpClient
|
|
} else if let createdHttpClient = MXHTTPClient(baseURL: baseStringURL, andOnUnrecognizedCertificateBlock: nil) {
|
|
|
|
httpClient = createdHttpClient
|
|
self.httpClients[baseStringURL] = httpClient
|
|
} else {
|
|
httpClient = nil
|
|
}
|
|
|
|
return httpClient
|
|
}
|
|
|
|
private func createJitsiWidgetContent(serverDomain: String,
|
|
authenticationType: JitsiAuthenticationType?,
|
|
roomID: String,
|
|
isAudioOnly: Bool) -> [String: Any]? {
|
|
guard MXTools.isMatrixRoomIdentifier(roomID) else {
|
|
NSLog("[JitsiService] createJitsiWidgetContent the roomID is not valid")
|
|
return nil
|
|
}
|
|
|
|
// Create a random enough jitsi conference id
|
|
// Note: the jitsi server automatically creates conference when the conference
|
|
// id does not exist yet
|
|
let widgetSessionId = (ProcessInfo.processInfo.globallyUniqueString as NSString).substring(to: Constants.widgetIdLength).lowercased()
|
|
|
|
let conferenceID: String
|
|
|
|
let authenticationTypeString: String?
|
|
|
|
if let authenticationType = authenticationType, authenticationType == .openIDTokenJWT {
|
|
|
|
// For compatibility with Jitsi, use base32 without padding.
|
|
// More details here:
|
|
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
|
conferenceID = Base32Coder.encodedString(roomID, padding: false)
|
|
authenticationTypeString = authenticationType.identifier
|
|
} else {
|
|
let roomIdComponents = RoomIdComponents(matrixID: roomID)
|
|
let localRoomId = roomIdComponents?.localRoomId ?? ""
|
|
conferenceID = localRoomId + widgetSessionId
|
|
authenticationTypeString = nil
|
|
}
|
|
|
|
// Build widget url
|
|
// Riot-iOS does not directly use it but extracts params from it (see `[JitsiViewController openWidget:withVideo:]`)
|
|
// This url can be used as is inside a web container (like iframe for Riot-web)
|
|
|
|
// Build it from the riot-web app
|
|
let appUrlString = BuildSettings.applicationWebAppUrlString
|
|
|
|
// We mix v1 and v2 param for backward compability
|
|
let v1queryStringParts = [
|
|
"confId=\(conferenceID)",
|
|
"isAudioConf=\(isAudioOnly ? "true" : "false")",
|
|
"displayName=$matrix_display_name",
|
|
"avatarUrl=$matrix_avatar_url",
|
|
"email=$matrix_user_id"
|
|
]
|
|
|
|
let v1Params = v1queryStringParts.joined(separator: "&")
|
|
|
|
var v2queryStringParts = [
|
|
"conferenceDomain=$domain",
|
|
"conferenceId=$conferenceId",
|
|
"isAudioOnly=$isAudioOnly",
|
|
"displayName=$matrix_display_name",
|
|
"avatarUrl=$matrix_avatar_url",
|
|
"userId=$matrix_user_id"
|
|
]
|
|
|
|
if let authenticationTypeString = authenticationTypeString {
|
|
v2queryStringParts.append("auth=\(authenticationTypeString)")
|
|
}
|
|
|
|
let v2Params = v2queryStringParts.joined(separator: "&")
|
|
|
|
let widgetStringURL = "\(appUrlString)/widgets/jitsi.html?\(v1Params)#\(v2Params)"
|
|
|
|
// Build widget data
|
|
// We mix v1 and v2 widget data for backward compability
|
|
let jitsiWidgetData = JitsiWidgetData()
|
|
jitsiWidgetData.domain = serverDomain
|
|
jitsiWidgetData.conferenceId = conferenceID
|
|
jitsiWidgetData.isAudioOnly = isAudioOnly
|
|
jitsiWidgetData.authenticationType = authenticationType?.identifier
|
|
|
|
let v2WidgetData: [AnyHashable: Any] = jitsiWidgetData.jsonDictionary()
|
|
|
|
var v1AndV2WidgetData = v2WidgetData
|
|
v1AndV2WidgetData["widgetSessionId"] = widgetSessionId
|
|
|
|
let widgetContent: [String: Any] = [
|
|
"url": widgetStringURL,
|
|
"type": kWidgetTypeJitsiV1,
|
|
"data": v1AndV2WidgetData
|
|
]
|
|
|
|
return widgetContent
|
|
}
|
|
}
|
|
#endif
|