diff --git a/Sources/MatrixClient/API/AccountData.swift b/Sources/MatrixClient/API/AccountData.swift index ff5504a..98735df 100644 --- a/Sources/MatrixClient/API/AccountData.swift +++ b/Sources/MatrixClient/API/AccountData.swift @@ -30,7 +30,7 @@ public struct MatrixGetAccountData: MatrixResponse, Matrix with parameters: RequestParameter) throws -> URLComponents { guard let fqmxid = parameters.userID.FQMXID else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } if let roomID = parameters.roomID { @@ -66,7 +66,7 @@ public struct MatrixSetAccountData: MatrixRequest { with parameters: MatrixGetAccountData.RequestParameter) throws -> URLComponents { guard let fqmxid = parameters.userID.FQMXID else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } if let roomID = parameters.roomID { @@ -90,7 +90,7 @@ public extension MatrixAccountData { func components(for homeserver: MatrixHomeserver, with userID: MatrixUserIdentifier) throws -> URLComponents { guard let fqmxid = userID.FQMXID else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } return homeserver.path("/_matrix/client/v3/user/\(fqmxid)/account_data/\(Self.type)") } diff --git a/Sources/MatrixClient/API/Auth/Interactive.swift b/Sources/MatrixClient/API/Auth/Interactive.swift index b11a943..7b352fe 100644 --- a/Sources/MatrixClient/API/Auth/Interactive.swift +++ b/Sources/MatrixClient/API/Auth/Interactive.swift @@ -13,16 +13,12 @@ public struct MatrixInteractiveAuth: MatrixResponse { flows: [MatrixInteractiveAuth.Flow], params: [String: AnyCodable], session: String? = nil, - completed: [MatrixLoginFlow]? = nil, - error: String? = nil, - errcode: MatrixError? = nil + completed: [MatrixLoginFlowType]? = nil ) { self.flows = flows self.params = params self.session = session self.completed = completed - self.error = error - self.errcode = errcode } public var flows: [Flow] @@ -36,17 +32,14 @@ public struct MatrixInteractiveAuth: MatrixResponse { /// in subsequent attempts to authenticate in the same API call. public var session: String? - public var completed: [MatrixLoginFlow]? - - public var error: String? - public var errcode: MatrixError? + public var completed: [MatrixLoginFlowType]? // MARK: Dynamic vars public var notCompletedStages: [Flow] { var ret: [Flow] = [] for flow in flows { - var stages: [MatrixLoginFlow] = [] + var stages: [MatrixLoginFlowType] = [] for stage in flow.stages { if !(completed?.contains(stage) ?? false) { stages.append(stage) @@ -58,7 +51,7 @@ public struct MatrixInteractiveAuth: MatrixResponse { } /// Return the next stage, which did not yet complete, from the first login flow - public var nextStage: MatrixLoginFlow? { + public var nextStage: MatrixLoginFlowType? { notCompletedStages[0].stages[0] } @@ -76,20 +69,20 @@ public struct MatrixInteractiveAuth: MatrixResponse { /// Test if the given login flow is supported by the home server. /// This returns true, the flow is contained in one or more stages. This means the flow could be required. - public func isOptional(_ flow: MatrixLoginFlow) -> Bool { + public func isOptional(_ flow: MatrixLoginFlowType) -> Bool { flows.first { $0.stages.contains(flow) } != nil } - public func isOptional(notCompletedFlow flow: MatrixLoginFlow) -> Bool { + public func isOptional(notCompletedFlow flow: MatrixLoginFlowType) -> Bool { notCompletedStages.first { $0.stages.contains(flow) } != nil } /// Test if th given flow is required by every flow supported by the homeserver. - public func isRequierd(_ flow: MatrixLoginFlow) -> Bool { + public func isRequierd(_ flow: MatrixLoginFlowType) -> Bool { flows.allSatisfy { $0.stages.contains(flow) } } - public func isRequierd(notCompletedFlow flow: MatrixLoginFlow) -> Bool { + public func isRequierd(notCompletedFlow flow: MatrixLoginFlowType) -> Bool { notCompletedStages.allSatisfy { $0.stages.contains(flow) } } @@ -100,19 +93,17 @@ public struct MatrixInteractiveAuth: MatrixResponse { case flows case params case completed - case error - case errcode } public struct LoginFlowWithParams { - public let flow: MatrixLoginFlow + public let flow: MatrixLoginFlowType public let params: AnyCodable? } } public extension MatrixInteractiveAuth { struct Flow: Codable { - public var stages: [MatrixLoginFlow] = [] + public var stages: [MatrixLoginFlowType] = [] } } @@ -120,24 +111,24 @@ public extension MatrixInteractiveAuth { public struct MatrixInteractiveAuthResponse: Codable { public var session: String? - public var type: MatrixLoginFlow? + public var type: MatrixLoginFlowType? public var extraInfo: [String: AnyCodable] - public init(session: String? = nil, type: MatrixLoginFlow?, extraInfo: [String: AnyCodable] = [:]) { + public init(session: String? = nil, type: MatrixLoginFlowType?, extraInfo: [String: AnyCodable] = [:]) { self.session = session self.type = type self.extraInfo = extraInfo } public init(recaptchaResponse: String, session: String? = nil) { - type = MatrixLoginFlow.recaptcha + type = MatrixLoginFlowType.recaptcha self.session = session extraInfo = ["response": AnyCodable(stringLiteral: recaptchaResponse)] } public init(emailClientSecret clientSecret: String, emailSID sid: String, session: String? = nil) { - type = MatrixLoginFlow.email + type = MatrixLoginFlowType.email self.session = session extraInfo = [ "threepid_creds": [ @@ -149,40 +140,25 @@ public struct MatrixInteractiveAuthResponse: Codable { } public extension MatrixInteractiveAuthResponse { - private enum KnownCodingKeys: String, CodingKey, CaseIterable { + private enum KnownCodingKeys: String, MatrixKnownCodingKeys { case session case type - static func doesNotContain(_ key: DynamicCodingKeys) -> Bool { - !Self.allCases.map(\.stringValue).contains(key.stringValue) - } } - internal struct DynamicCodingKeys: CodingKey { - var stringValue: String - init?(stringValue: String) { - self.stringValue = stringValue - } - - // not used here, but a protocol requirement - var intValue: Int? - init?(intValue _: Int) { - nil - } - } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: KnownCodingKeys.self) session = try container.decodeIfPresent(String.self, forKey: .session) - type = try container.decode(MatrixLoginFlow.self, forKey: .type) + type = try container.decode(MatrixLoginFlowType.self, forKey: .type) extraInfo = [:] - let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self) + let extraContainer = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) { let decoded = try extraContainer.decode( AnyCodable.self, - forKey: DynamicCodingKeys(stringValue: key.stringValue)! + forKey: MatrixDynamicCodingKeys(stringValue: key.stringValue)! ) self.extraInfo[key.stringValue] = decoded } @@ -193,7 +169,7 @@ public extension MatrixInteractiveAuthResponse { try container.encodeIfPresent(session, forKey: .session) try container.encodeIfPresent(type?.rawValue, forKey: .type) - var extraContainer = encoder.container(keyedBy: DynamicCodingKeys.self) + var extraContainer = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) for (name, value) in extraInfo { try extraContainer.encode(value, forKey: .init(stringValue: name)!) } diff --git a/Sources/MatrixClient/API/Auth/Login.swift b/Sources/MatrixClient/API/Auth/Login.swift index 053f116..38a090b 100644 --- a/Sources/MatrixClient/API/Auth/Login.swift +++ b/Sources/MatrixClient/API/Auth/Login.swift @@ -6,14 +6,11 @@ // import Foundation +import AnyCodable public struct MatrixLoginFlowRequest { public struct ResponseStruct: MatrixResponse { - var flows: [FlowType] - - struct FlowType: Codable { - var type: MatrixLoginFlow - } + var flows: [MatrixLoginFlow] } } @@ -35,7 +32,7 @@ extension MatrixLoginFlowRequest: MatrixRequest { @frozen /// A login type supported by the homeserver. -public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable { +public struct MatrixLoginFlowType: RawRepresentable, Codable, Equatable, Hashable { public typealias RawValue = String public var rawValue: String @@ -68,7 +65,7 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable { /// } /// ``` /// In the case that the homeserver does not know about the supplied 3PID, the homeserver must respond with 403 Forbidden. - public static let password: MatrixLoginFlow = "m.login.password" + public static let password: MatrixLoginFlowType = "m.login.password" /// The user completes a Google ReCaptcha 2.0 challenge /// @@ -80,8 +77,8 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable { /// "session": "" /// } /// ``` - public static let recaptcha: MatrixLoginFlow = "m.login.recaptcha" - public static let oauth2: MatrixLoginFlow = "m.login.oauth2" + public static let recaptcha: MatrixLoginFlowType = "m.login.recaptcha" + public static let oauth2: MatrixLoginFlowType = "m.login.oauth2" /// Authentication is supported by authorising with an external single sign-on provider. /// @@ -95,12 +92,12 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable { /// The homeserver then validates the response from the single sign-on provider and updates the user-interactive authentication session to mark the single sign-on stage has been completed. The browser is shown the fallback authentication completion page. /// /// Once the flow has completed, the client retries the request with the session only, as above. - public static let sso: MatrixLoginFlow = "m.login.sso" - public static let email: MatrixLoginFlow = "m.login.email.identity" - public static let msisdn: MatrixLoginFlow = "m.login.msisdn" - public static let token: MatrixLoginFlow = "m.login.token" - public static let dummy: MatrixLoginFlow = "m.login.dummy" - public static let terms: MatrixLoginFlow = "m.login.terms" + public static let sso: MatrixLoginFlowType = "m.login.sso" + public static let email: MatrixLoginFlowType = "m.login.email.identity" + public static let msisdn: MatrixLoginFlowType = "m.login.msisdn" + public static let token: MatrixLoginFlowType = "m.login.token" + public static let dummy: MatrixLoginFlowType = "m.login.dummy" + public static let terms: MatrixLoginFlowType = "m.login.terms" public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -121,12 +118,145 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable { } } -extension MatrixLoginFlow: ExpressibleByStringLiteral { +extension MatrixLoginFlowType: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { rawValue = value } } +public struct MatrixLoginFlow { + public init(type: MatrixLoginFlowType, identiyProviders: [MatrixLoginFlow.IdentityProvider]? = nil, extraInfo: [String : AnyCodable] = [:]) { + self.type = type + self.identiyProviders = identiyProviders + self.extraInfo = extraInfo + } + + public var type: MatrixLoginFlowType + + public var identiyProviders: [IdentityProvider]? + + public var extraInfo: [String: AnyCodable] + + public struct IdentityProvider: Codable, Identifiable { + public init(brand: Brand? = nil, icon: MatrixContentURL? = nil, id: String, name: String) { + self.brand = brand + self.icon = icon + self.id = id + self.name = name + } + + /// Optional UI hint for what kind of common SSO provider is being described in this ``IdentityProvider``. + /// + /// Matrix maintains a registry of identifiers in the + /// [matrix-spec repo](https://github.com/matrix-org/matrix-spec/blob/main/informal/idp-brands.md) to ensure clients and servers are aligned on major/common brands. + /// + /// Clients should prefer the brand over the icon, when both are provided. + /// Clients are not required to support any particular brand, including those in the registry, though are expected to be able to present any IdP based off the name/icon to the user regardless. + /// + /// Unregistered brands are permitted using the Common Namespaced Identifier Grammar, though excluding the namespace requirements. For example, examplesso is a valid brand which is not in the registry but still permitted. Servers should be mindful that clients might not support their unregistered brand usage as intended by the server. + public var brand: Brand? + + /// Optional MXC URI to provide an image/icon representing the ``IdentityProvider``. Intended to be shown alongside the name if provided. + public var icon: MatrixContentURL? + + /// Opaque string chosen by the homeserver, uniquely identifying the ``IdentityProvider`` from other ``IdentityProvider``s the homeserver might support. + /// + /// Should be between 1 and 255 characters in length, containing unreserved characters under RFC 3986 (ALPHA DIGIT "-" / "." / "_" / "~"). Clients are not intended to parse or infer meaning from opaque strings. + public var id: String + + /// Human readable description for the ``IdentityProvider``, intended to be shown to the user. + public var name: String + + @frozen + public struct Brand: RawRepresentable, Codable, Identifiable, ExpressibleByStringLiteral, CustomStringConvertible, Equatable, Hashable { + public init?(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + public var rawValue: String + + public var id: String { + rawValue + } + + public var description: String { + rawValue + } + + // MARK: Brand Registry + /// Apple + /// + /// Suitable for "Sign in with Apple": see + /// [https://appleid.apple.com/signinwithapple/button](https://appleid.apple.com/signinwithapple/button). + public static let apple: Self = "apple" + + /// Facebok + /// + /// "Continue with Facebook": see https://developers.facebook.com/docs/facebook-login/web/login-button/. + public static let facebook: Self = "facebook" + + /// GitHub + /// + /// Logos available at https://github.com/logos. + public static let github: Self = "github" + + /// GitLab + /// + /// Logos available at https://about.gitlab.com/press/press-kit/. + public static let gitlab: Self = "gitlab" + + /// Google + /// + /// Suitable for "Google Sign-In": see https://developers.google.com/identity/branding-guidelines. + public static let google: Self = "google" + + /// Twitter + /// + /// Suitable for "Log in with Twitter": see https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter#tab1. + public static let twitter: Self = "twitter" + } + } +} + +extension MatrixLoginFlow: Codable { + private enum KnownCodingKeys: String, MatrixKnownCodingKeys { + case type + case identiyProviders = "identity_providers" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: KnownCodingKeys.self) + type = try container.decode(MatrixLoginFlowType.self, forKey: .type) + identiyProviders = try container.decodeIfPresent([IdentityProvider].self, forKey: .identiyProviders) + + extraInfo = [:] + let extraContainer = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) + + for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) { + let decoded = try extraContainer.decode( + AnyCodable.self, + forKey: MatrixDynamicCodingKeys(stringValue: key.stringValue)! + ) + self.extraInfo[key.stringValue] = decoded + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: KnownCodingKeys.self) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(identiyProviders, forKey: .identiyProviders) + + var extraContainer = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) + for (name, value) in extraInfo { + try extraContainer.encode(value, forKey: .init(stringValue: name)!) + } + } +} + public enum MatrixLoginUserIdentifier: Codable { /// 5.4.6.1 Matrix User id /// @@ -309,7 +439,7 @@ extension MatrixLoginRequest: MatrixRequest { public struct MatrixLogin: MatrixResponse { public init( - userId: MatrixUserIdentifier? = nil, + userId: MatrixFullUserIdentifier? = nil, accessToken: String? = nil, homeServer: String? = nil, deviceId: String? = nil, @@ -323,7 +453,7 @@ public struct MatrixLogin: MatrixResponse { } /// The fully-qualified Matrix ID that has been registered. - public var userId: MatrixUserIdentifier? + public var userId: MatrixFullUserIdentifier? /// An access token for the account. This access token can then be used to authorise other requests. public var accessToken: String? diff --git a/Sources/MatrixClient/API/Auth/Register.swift b/Sources/MatrixClient/API/Auth/Register.swift index 9bd5d5c..3770a2f 100644 --- a/Sources/MatrixClient/API/Auth/Register.swift +++ b/Sources/MatrixClient/API/Auth/Register.swift @@ -64,22 +64,10 @@ extension MatrixRegisterRequest: MatrixRequest { false } - public typealias Response = MatrixRegisterContainer + public typealias Response = MatrixRegister /// The kind of account to register. Defaults to user. public typealias URLParameters = MatrixRegisterRequest.RegisterKind - - public func parse(data: Data, response: HTTPURLResponse) throws -> Response { - guard response.statusCode != 401 else { - return try MatrixRegisterContainer.interactive(.init(fromMatrixRequestData: data)) - } - - guard response.statusCode == 200 else { - throw try MatrixServerError(json: data, code: response.statusCode) - } - - return try MatrixRegisterContainer.success(.init(fromMatrixRequestData: data)) - } } public struct MatrixRegister: MatrixResponse { @@ -121,39 +109,6 @@ public struct MatrixRegister: MatrixResponse { } } -/// Container to either hold a successfully register answer, or an answer to do it interactivly. -public enum MatrixRegisterContainer: MatrixResponse { - case success(MatrixRegister) - case interactive(MatrixInteractiveAuth) - - public var isSuccess: Bool { - switch self { - case .success: - return true - case .interactive: - return false - } - } - - public var successData: MatrixRegister? { - switch self { - case let .success(register): - return register - case .interactive: - return nil - } - } - - public var interactiveData: MatrixInteractiveAuth? { - switch self { - case .success: - return nil - case let .interactive(interactive): - return interactive - } - } -} - public struct MatrixRegisterRequestEmailTokenRequest: MatrixRequest { public init(clientSecret: String, email: String, sendAttempt: Int = 0) { self.clientSecret = clientSecret diff --git a/Sources/MatrixClient/API/Capability.swift b/Sources/MatrixClient/API/Capability.swift index 2fa4160..a38c40e 100644 --- a/Sources/MatrixClient/API/Capability.swift +++ b/Sources/MatrixClient/API/Capability.swift @@ -307,29 +307,12 @@ public extension MatrixCapabilities { // MARK: - Codable extension MatrixCapabilities.Capabilities { - enum KnownCodingKeys: String, CodingKey, CaseIterable { + enum KnownCodingKeys: String, MatrixKnownCodingKeys { case changePassword = "m.change_password" case roomVersions = "m.room_versions" case setDisplayName = "m.set_displayname" case setAvatarUrl = "m.set_avatar_url" case change3Pid = "m.3pid_changes" - - static func doesNotContain(_ key: DynamicCodingKeys) -> Bool { - !Self.allCases.map(\.stringValue).contains(key.stringValue) - } - } - - struct DynamicCodingKeys: CodingKey { - var stringValue: String - init?(stringValue: String) { - self.stringValue = stringValue - } - - // not used here, but a protocol requirement - var intValue: Int? - init?(intValue _: Int) { - nil - } } public init(from decoder: Decoder) throws { @@ -338,12 +321,12 @@ extension MatrixCapabilities.Capabilities { roomVersions = try container.decodeIfPresent(RoomVersionsCapability.self, forKey: .roomVersions) extraInfo = [:] - let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self) + let extraContainer = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) { let decoded = try extraContainer.decode( AnyCodable.self, - forKey: DynamicCodingKeys(stringValue: key.stringValue)! + forKey: .init(stringValue: key.stringValue)! ) self.extraInfo[key.stringValue] = decoded } @@ -354,7 +337,7 @@ extension MatrixCapabilities.Capabilities { try container.encodeIfPresent(changePassword, forKey: .changePassword) try container.encodeIfPresent(roomVersions, forKey: .roomVersions) - var extraContainer = encoder.container(keyedBy: DynamicCodingKeys.self) + var extraContainer = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) for (name, value) in extraInfo { try extraContainer.encode(value, forKey: .init(stringValue: name)!) } diff --git a/Sources/MatrixClient/API/Events/Messages/Messages.Codable.swift b/Sources/MatrixClient/API/Events/Messages/Messages.Codable.swift index 53862eb..d3f74da 100644 --- a/Sources/MatrixClient/API/Events/Messages/Messages.Codable.swift +++ b/Sources/MatrixClient/API/Events/Messages/Messages.Codable.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - Messag types -public protocol MatrixMessageType: Codable { +public protocol MatrixMessageType: MatrixCodableContent { static var type: String { get } } @@ -18,14 +18,14 @@ enum MatrixMessageTypeCodingKeys: String, CodingKey { case type = "msgtype" } -extension CodingUserInfoKey { +public extension CodingUserInfoKey { static var matrixMessageTypes: CodingUserInfoKey { CodingUserInfoKey(rawValue: "MatrixClient.MessageTypes")! } } @propertyWrapper -public struct MatrixCodableMessageType: Codable { +public struct MatrixCodableMessageType: MatrixCodableContent { public var wrappedValue: MatrixMessageType /// An initializer that allows initialization with a wrapped value of `nil` diff --git a/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift index 23cdc9d..a2ee437 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift @@ -7,12 +7,12 @@ public struct MatrixMessageEvent: MatrixEvent { @MatrixCodableMessageType public var content: MatrixMessageType public var eventID: String? - public var sender: String? + public var sender: MatrixFullUserIdentifier? public var date: Date? public var unsigned: AnyCodable? enum CodingKeys: String, CodingKey { - case content + case content = "content" case eventID = "event_id" case sender case date = "origin_server_ts" diff --git a/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift index f5c2b92..4a51389 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift @@ -6,7 +6,7 @@ public struct MatrixReactionEvent: MatrixEvent { public var content: Content public var eventID: String? - public var sender: String? + public var sender: MatrixFullUserIdentifier? public var date: Date? public var unsigned: AnyCodable? diff --git a/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift index 961ef63..2a01c5b 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift @@ -6,7 +6,7 @@ public struct MatrixRedactionEvent: MatrixEvent { public var content: Content public var eventID: String? - public var sender: String? + public var sender: MatrixFullUserIdentifier? public var date: Date? public var unsigned: AnyCodable? @@ -21,7 +21,7 @@ public struct MatrixRedactionEvent: MatrixEvent { case redacts } - public struct Content: Codable { + public struct Content: MatrixCodableContent { public let reason: String? } } diff --git a/Sources/MatrixClient/API/Events/Room/RoomStateEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomStateEvent.swift new file mode 100644 index 0000000..24a5224 --- /dev/null +++ b/Sources/MatrixClient/API/Events/Room/RoomStateEvent.swift @@ -0,0 +1,84 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 24.04.22. +// + +import AnyCodable +import Foundation + +public struct MatrixStateEvent: MatrixEvent { + public init( + eventID: String? = nil, + stateKey: String = "", + sender: MatrixFullUserIdentifier? = nil, + date: Date? = nil, + unsigned: AnyCodable? = nil, + content: MatrixStateEventType + ) { + self.stateKey = stateKey + self.eventID = eventID + self.sender = sender + self.date = date + self.unsigned = unsigned + self.content = content + } + + public static let type: String = "" + + /// The content of the event. + @MatrixCodableStateEventType + public var content: MatrixStateEventType + + public var stateKey: String = "" + + public var eventID: String? + + public var sender: MatrixFullUserIdentifier? + + public var date: Date? + + public var unsigned: AnyCodable? + + enum CodingKeys: String, CodingKey { + case content + case stateKey = "state_key" + case eventID = "event_id" + case sender + case date = "origin_server_ts" + case unsigned + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + stateKey = try container.decode(String.self, forKey: .stateKey) + eventID = try container.decode(String.self, forKey: .eventID) + sender = try container.decode(MatrixFullUserIdentifier.self, forKey: .sender) + date = try container.decodeIfPresent(Date.self, forKey: .date) + unsigned = try container.decodeIfPresent(AnyCodable.self, forKey: .unsigned) + + let typeContainer = try decoder.container(keyedBy: MatrixStateEventTypeCodingKeys.self) + let typeId = try typeContainer.decode(String.self, forKey: .type) + + let superEncoder = try container.superDecoder(forKey: .content) + _content = try MatrixCodableStateEventType(from: superEncoder, typeID: typeId) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(stateKey, forKey: .stateKey) + try container.encode(eventID, forKey: .eventID) + try container.encode(sender, forKey: .sender) + try container.encodeIfPresent(date, forKey: .date) + try container.encodeIfPresent(unsigned, forKey: .unsigned) + + let contentType = Swift.type(of: content) + + var typeContainer = encoder.container(keyedBy: MatrixStateEventTypeCodingKeys.self) + try typeContainer.encode(contentType.type, forKey: .type) + + let superEncoder = container.superEncoder(forKey: .content) + try _content.encode(to: superEncoder) + } +} diff --git a/Sources/MatrixClient/API/Events/RoomEvent.swift b/Sources/MatrixClient/API/Events/RoomEvent.swift index 9914b81..a357c58 100644 --- a/Sources/MatrixClient/API/Events/RoomEvent.swift +++ b/Sources/MatrixClient/API/Events/RoomEvent.swift @@ -8,14 +8,25 @@ public protocol MatrixEvent: Codable { static var type: String { get } var eventID: String? { get } - var sender: String? { get } + var sender: MatrixFullUserIdentifier? { get } var date: Date? { get } var unsigned: AnyCodable? { get } + + //var content: MatrixCodableContent { get } + + //var stateKey: String? { get } +} + +public extension MatrixEvent { + var stateKey: String? { + nil + } } /// The coding keys needed to determine an event's type before decoding. -enum MatrixEventTypeKeys: CodingKey { +enum MatrixEventTypeKeys: String, CodingKey { case type + case stateKey = "state_key" } enum MatrixEventCodableError: Error { @@ -31,13 +42,41 @@ extension KeyedDecodingContainer { } } -extension CodingUserInfoKey { +public extension CodingUserInfoKey { /// The key used to determine the types of `MatrixEvent` that can be decoded. static var matrixEventTypes: CodingUserInfoKey { CodingUserInfoKey(rawValue: "MatrixClient.EventTypes")! } } +public struct MatrixInvalidEvent: MatrixEvent { + public static let type: String = "" + + public var type: String? + + public var eventID: String? + + public var sender: MatrixFullUserIdentifier? + + public var date: Date? { + nil + } + + public var unsigned: AnyCodable? { + nil + } + + // FIXME: implement + public var content: Bool { + false + } + + enum CodingKeys: String, CodingKey { + case eventID = "event_id" + case sender + } +} + // TODO: encodable @propertyWrapper public struct MatrixCodableEvents: Codable where Value.Element == MatrixEvent { @@ -51,28 +90,51 @@ public struct MatrixCodableEvents: Codable where Value.Elemen // these can throw as something has gone seriously wrong if the type key is missing let container = try decoder.container(keyedBy: MatrixEventTypeKeys.self) let typeID = try container.decode(String.self, forKey: .type) - - guard let types = decoder.userInfo[.matrixEventTypes] as? [MatrixEvent.Type] else { - // the decoder must be supplied with some event types to decode - throw MatrixEventCodableError.missingTypes - } - - guard let matchingType = types.first(where: { $0.type == typeID }) else { - // simply ignore events with no matching type as throwing would prevent access to other events + let stateKey = try container.decodeIfPresent(String.self, forKey: .stateKey) + + do { + if stateKey != nil { + let stateEvent = try MatrixStateEvent(from: decoder) + guard let decoded = stateEvent as? T + else { + throw MatrixEventCodableError.unableToCast(decoded: stateEvent, into: "state_event") + } + wrappedEvent = decoded + return + } + + guard let types = decoder.userInfo[.matrixEventTypes] as? [MatrixEvent.Type] else { + // the decoder must be supplied with some event types to decode + throw MatrixEventCodableError.missingTypes + } + + guard let matchingType = types.first(where: { $0.type == typeID }) else { + // simply ignore events with no matching type as throwing would prevent access to other events + return + } + + guard let decoded = try? matchingType.init(from: decoder) else { + assertionFailure("Failed to decode MatrixEvent as \(String(describing: T.self))") + return + } + + guard let decoded = decoded as? T else { + // something has probably gone very wrong at this stage + throw MatrixEventCodableError.unableToCast(decoded: decoded, into: String(describing: T.self)) + } + wrappedEvent = decoded return - } + } catch { + print(error.localizedDescription) + var event = try! MatrixInvalidEvent(from: decoder) + event.type = typeID - guard let decoded = try? matchingType.init(from: decoder) else { - assertionFailure("Failed to decode MatrixEvent as \(String(describing: T.self))") - return - } + guard let decoded = event as? T else { + throw MatrixEventCodableError.missingTypes + } - guard let decoded = decoded as? T else { - // something has probably gone very wrong at this stage - throw MatrixEventCodableError.unableToCast(decoded: decoded, into: String(describing: T.self)) + wrappedEvent = decoded } - - wrappedEvent = decoded } func encode(to encoder: Encoder) throws { @@ -97,11 +159,15 @@ public struct MatrixCodableEvents: Codable where Value.Elemen } public init(from decoder: Decoder) { - guard let container = try? decoder.singleValueContainer(), - let wrappers = try? container.decode([EventWrapper].self) - else { return } - - wrappedValue = wrappers.compactMap(\.wrappedEvent) as? Value + do { + let container = try decoder.singleValueContainer() + let wrappers = try container.decode([EventWrapper].self) + wrappedValue = wrappers.compactMap(\.wrappedEvent) as? Value + } catch { + if #available(iOS 14.0, macOS 11.0, *) { + MatrixClient.logger.warning("Failed to parse sync: \(error.localizedDescription)") + } else {} + } } public func encode(to encoder: Encoder) throws { diff --git a/Sources/MatrixClient/API/Events/State/RoomCreateEvent.swift b/Sources/MatrixClient/API/Events/State/RoomCreateEvent.swift deleted file mode 100644 index 671ec08..0000000 --- a/Sources/MatrixClient/API/Events/State/RoomCreateEvent.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// File.swift -// -// -// Created by Finn Behrens on 13.03.22. -// - -import AnyCodable -import Foundation - -/// This is the first event in a room and cannot be changed. It acts as the root of all other events. -/// -/// # Example -/// ```json -/// { -/// "content": { -/// "creator": "@example:example.org", -/// "m.federate": true, -/// "predecessor": { -/// "event_id": "$something:example.org", -/// "room_id": "!oldroom:example.org" -/// }, -/// "room_version": "1" -/// }, -/// "event_id": "$143273582443PhrSn:example.org", -/// "origin_server_ts": 1432735824653, -/// "room_id": "!jEsUZKDJdhlrceRyVU:example.org", -/// "sender": "@example:example.org", -/// "state_key": "", -/// "type": "m.room.create", -/// "unsigned": { -/// "age": 1234 -/// } -/// } -/// ``` -public struct MatrixRoomCreateEvent: MatrixEvent { - public static let type = "m.room.create" - - public var content: Content - - public var eventID: String? - public var sender: String? - public var date: Date? - public var unsigned: AnyCodable? -} - -public extension MatrixRoomCreateEvent { - struct Content: Codable { - /// The `user_id` of the room creator. This is set by the homeserver. - public var creator: String - - /// Whether users on other servers can join this room. Defaults to true if key does not exist. - public var federate: Bool? - - /// A reference to the room this room replaces, if the previous room was upgraded. - public var predecessor: PreviousRoom? - - /// The version of the room. Defaults to "1" if the key does not exist. - public var roomVersion: String = "1" - - // TODO: add links - /// Optional room [type] to denote a room’s intended function outside of traditional conversation. - /// - /// Unspecified room types are possible using [Namespaced Identifiers]. - public var roomType: String? - - enum CodingKeys: String, CodingKey { - case creator - case federate = "m.federate" - case predecessor - case roomVersion = "room_version" - case roomType = "type" - } - } -} - -/// A reference to an old room. -public extension MatrixRoomCreateEvent.Content { - struct PreviousRoom: Codable { - /// The event ID of the last known event in the old room. - public var eventID: String - - /// The ID of the old room. - public var roomID: String - - enum CodingKeys: String, CodingKey { - case eventID = "event_id" - case roomID = "room_id" - } - } -} diff --git a/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift b/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift deleted file mode 100644 index a9f1d90..0000000 --- a/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift +++ /dev/null @@ -1,25 +0,0 @@ -import AnyCodable -import Foundation - -public struct MatrixEncryptionEvent: MatrixEvent { - public static let type = "m.room.encryption" - - public var content: Content - public var eventID: String? - public var sender: String? - public var date: Date? - public var unsigned: AnyCodable? - - public var stateKey: String? - - enum CodingKeys: String, CodingKey { - case content - case eventID = "event_id" - case sender - case date = "origin_server_ts" - case unsigned - case stateKey = "state_key" - } - - public struct Content: Codable {} -} diff --git a/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift b/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift deleted file mode 100644 index 60c0675..0000000 --- a/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift +++ /dev/null @@ -1,37 +0,0 @@ -import AnyCodable -import Foundation - -public struct MatrixMemberEvent: MatrixEvent { - public static let type = "m.room.member" - - public var content: Content - public var eventID: String? - public var sender: String? - public var date: Date? - public var unsigned: AnyCodable? - - public let stateKey: String? - - enum CodingKeys: String, CodingKey { - case content - case eventID = "event_id" - case sender - case date = "origin_server_ts" - case unsigned - case stateKey = "state_key" - } - - public struct Content: Codable { - public let avatarURL: String? - public let displayName: String? - public let membership: MatrixMembership? - public let isDirect: Bool? - - enum CodingKeys: String, CodingKey { - case avatarURL = "avatar_url" - case displayName = "displayname" - case membership - case isDirect = "is_direct" - } - } -} diff --git a/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift b/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift deleted file mode 100644 index 333522f..0000000 --- a/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift +++ /dev/null @@ -1,27 +0,0 @@ -import AnyCodable -import Foundation - -public struct MatrixNameEvent: MatrixEvent { - public static let type = "m.room.name" - - public var content: Content - public var eventID: String? - public var sender: String? - public var date: Date? - public var unsigned: AnyCodable? - - public let stateKey: String? - - enum CodingKeys: String, CodingKey { - case content - case eventID = "event_id" - case sender - case date = "origin_server_ts" - case unsigned - case stateKey = "state_key" - } - - public struct Content: Codable { - public let name: String? - } -} diff --git a/Sources/MatrixClient/API/Events/State/StateEvent+Codable.swift b/Sources/MatrixClient/API/Events/State/StateEvent+Codable.swift new file mode 100644 index 0000000..3c9ba07 --- /dev/null +++ b/Sources/MatrixClient/API/Events/State/StateEvent+Codable.swift @@ -0,0 +1,71 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 13.03.22. +// + +import Foundation + +public protocol MatrixStateEventType: MatrixCodableContent { + static var type: String { get } + + static var unstableType: String? { get } +} + +public extension MatrixStateEventType { + static var unstableType: String? { nil } +} + +enum MatrixStateEventTypeCodingKeys: String, CodingKey { + case type + case content +} + +public extension CodingUserInfoKey { + static var matrixStateEventTypes: CodingUserInfoKey { + CodingUserInfoKey(rawValue: "MatrixClient.StateTypes")! + } +} + +@propertyWrapper +public struct MatrixCodableStateEventType: Encodable { + public var wrappedValue: MatrixStateEventType + + public init(wrappedValue: MatrixStateEventType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder, typeID: String) throws { + guard let types = decoder.userInfo[.matrixStateEventTypes] as? [MatrixStateEventType.Type] else { + throw StateTypeError.missingTypes + } + + guard let matchingType = types.first(where: { $0.type == typeID || $0.unstableType == typeID }) else { + throw StateTypeError.unableToFindType(typeID) + } + + let decoded = try matchingType.init(from: decoder) + + wrappedValue = decoded + } + + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(to: encoder) + } + + public enum StateTypeError: LocalizedError { + case missingTypes + case unableToFindType(String) + // case unableToCast(decoded: MatrixStateEventType?, into: MatrixStateEventType.Type) + + public var errorDescription: String? { + switch self { + case .missingTypes: + return NSLocalizedString("Types are missing", comment: "StateTypeError") + case let .unableToFindType(type): + return NSLocalizedString("Type \(type) could not be found", comment: "") + } + } + } +} diff --git a/Sources/MatrixClient/API/Events/State/StateEvent+Space.swift b/Sources/MatrixClient/API/Events/State/StateEvent+Space.swift new file mode 100644 index 0000000..c8a6b81 --- /dev/null +++ b/Sources/MatrixClient/API/Events/State/StateEvent+Space.swift @@ -0,0 +1,60 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 29.04.22. +// + +import Foundation + +public extension MatrixRoomCreateEvent.RoomType { + static let space: Self = "m.space" +} + +/// Defines the relationship of a child room to a space-room. Has no effect in rooms which are not . +public struct MatrixRoomSpaceChildEvent: MatrixStateEventType { + public init(order: String? = nil, suggested: Bool? = false, via: [String]? = nil) { + self.order = order + self.suggested = suggested + self.via = via + } + + public static let type = "m.space.child" + + /// Optional string to define ordering among space children. These are lexicographically compared against other children’s order, if present. + /// + /// Must consist of ASCII characters within the range \x20 (space) and \x7E (~), inclusive. Must not exceed 50 characters. + /// + /// order values with the wrong type, or otherwise invalid contents, are to be treated as though the order key was not provided. + // TODO: ordering type? + public var order: String? + + /// Optional (default false) flag to denote whether the child is “suggested” or of interest to members of the space. + /// + /// This is primarily intended as a rendering hint for clients to display the room differently, such as eagerly rendering them in the room list. + public var suggested: Bool? = false + + /// A list of servers to try and join through. + /// + /// When not present or invalid, the child room is not considered to be part of the space. + public var via: [String]? +} + +public struct MatrixRoomSpaceParentEvent: MatrixStateEventType { + public init(canonical: Bool? = false, via: [String]?) { + self.canonical = canonical + self.via = via + } + + public static let type: String = " m.space.parent" + + /// Optional (default false) flag to denote this parent is the primary parent for the room. + /// + /// When multiple canonical parents are found, the lowest parent when ordering by room ID lexicographically by Unicode code-points should be used. + public var canonical: Bool? = false + + /// A list of servers to try and join through. + /// + /// When not present or invalid, the child room is not considered to be part of the space. + public var via: [String]? +} diff --git a/Sources/MatrixClient/API/Events/State/StateEvent.swift b/Sources/MatrixClient/API/Events/State/StateEvent.swift index dc05997..72fbca1 100644 --- a/Sources/MatrixClient/API/Events/State/StateEvent.swift +++ b/Sources/MatrixClient/API/Events/State/StateEvent.swift @@ -2,23 +2,466 @@ // File.swift // // -// Created by Finn Behrens on 13.03.22. +// Created by Finn Behrens on 24.04.22. // import Foundation -// TODO: make somehow generic -/* - public struct MatrixStateEvent: MatrixEvent { - public static var type = "m." +/// This event is used to inform the room about which alias should be considered the canonical one, and which other aliases point to the room. +/// +/// This could be for display purposes or as suggestion to users which alias to use to advertise and access the room. +public struct MatrixRoomCanonicalAliasEvent: MatrixStateEventType { + public static let type = "m.room.canonical_alias" - public var eventID: String + /// The canonical alias for the room. + /// + /// If not present, null, or empty the room should be considered to have no canonical alias. + public var alias: String? - public var sender: String + /// Alternative aliases the room advertises. + /// + /// This list can have aliases despite the alias field being null, empty, or otherwise not present. + public var altAliases: [String]? - public var date: Date + enum CodingKeys: String, CodingKey { + case alias + case altAliases = "alt_aliases" + } +} - public var unsigned: AnyCodable? +/// This is the first event in a room and cannot be changed. It acts as the root of all other events. +public struct MatrixRoomCreateEvent: MatrixStateEventType { + public init( + creator: MatrixFullUserIdentifier, + federate: Bool? = nil, + predecessor: MatrixRoomCreateEvent.PreviousRoom? = nil, + roomVersion: String? = nil, + roomType: RoomType? = nil + ) { + self.creator = creator + self.federate = federate + self.predecessor = predecessor + self.roomVersion = roomVersion + self.roomType = roomType + } - } - */ + public static let type = "m.room.create" + + /// The ``MatrixFullUserIdentifier`` of the room creator. + /// + /// This is set by the homeserver + public var creator: MatrixFullUserIdentifier + + /// Whether users on other servers can join this room. + /// + /// Defaults to true if key does not exist. + public var federate: Bool? + + /// A reference to the room this room replaces, if the previous room was upgraded. + public var predecessor: PreviousRoom? + + /// The version of the room. Defaults to "1" if the key does not exist. + public var roomVersion: String? + + /// Optional room type to denote a room’s intended function outside of traditional conversation. + /// + /// Unspecified room types are possible using Namespaced Identifiers. + public var roomType: RoomType? + + public struct PreviousRoom: Codable, Equatable, Hashable { + /// The event ID of the last known event in the old room. + public var eventID: String + + /// The ID of the old room. + public var roomID: String + + enum CodingKeys: String, CodingKey { + case eventID = "event_id" + case roomID = "room_id" + } + } + + enum CodingKeys: String, CodingKey { + case creator + case federate = "m.federate" + case predecessor + case roomVersion = "room_version" + case roomType = "type" + } + + public struct RoomType: RawRepresentable, Codable, ExpressibleByStringLiteral { + public var rawValue: String + + public init?(rawValue: String){ + self.rawValue = rawValue + } + + public init(stringLiteral value: StringLiteralType) { + self.init(rawValue: value)! + } + } +} + +public struct MatrixRoomJoinRulesEvent: MatrixStateEventType { + public static let type = "m.room.join_rules" + + /// For restricted rooms, the conditions the user will be tested against. + /// + /// The user needs only to satisfy one of the conditions to join the restricted room. + /// If the user fails to meet any condition, or the condition is unable to be confirmed as satisfied, + /// then the user requires an invite to join the room. + /// Improper or no allow conditions on a restricted join rule imply the room is effectively invite-only (no conditions can be satisfied). + public var allow: [AllowCondition]? + + /// The type of rules used for users wishing to join this room. + public var joinRule: JoinRule + + public struct AllowCondition: Codable { + /// The room ID to check the user’s membership against. + /// + /// If the user is joined to this room, they satisfy the condition and thus are permitted to join the restricted room. + /// Required if type is ``ConditionType.roomMembership``. + public var roomId: String? + + /// The type of condition + public var type: ConditionType + + public enum ConditionType: String, Codable { + case roomMembership = "m.room_membership" + } + } + + public enum JoinRule: String, Codable { + /// Anyone can join the room without any prior action. + case `public` + /// A user must first receive an invite from someone already in the room in order to join. + case invite + /// A user can request an invite to the room. + /// + /// They can be allowed (invited) or denied (kicked/banned) access. Otherwise, users need to be invited in. + /// Only available in rooms which support knocking. + case knock + /// Reserved without implementation. No significant meaning. + case `private` + /// Anyone able to satisfy at least one of the allow conditions is able to join the room without prior action. + /// + /// Otherwise, an invite is required. Only available in rooms which support the join rule. + case restricted + } + + enum CodingKeys: String, CodingKey { + case allow + case joinRule = "join_rule" + } +} + +/// Adjusts the membership state for a user in a room. +/// +/// It is preferable to use the membership APIs (`/rooms//invite` etc) when performing membership actions +/// rather than adjusting the state directly as there are a restricted set of valid transformations. +/// For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. +/// +/// The following membership states are specified: +/// +/// `invite` - The user has been invited to join a room, but has not yet joined it. They may not participate in the room until they join. +/// +/// `join` - The user has joined the room (possibly after accepting an invite), and may participate in it. +/// +/// `leave` - The user was once joined to the room, but has since left (possibly by choice, or possibly by being kicked). +/// +/// `ban` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than ban). +//// +/// `knock` - The user has knocked on the room, requesting permission to participate. They may not participate in the room until they join. +/// +/// The third_party_invite property will be set if this invite is an invite event and is the successor of an m.room.third_party_invite event, and absent otherwise. +/// +/// +/// This event may also include an `invite_room_state` key inside the event’s unsigned data. If present, +/// this contains an array of stripped state events to assist the receiver in identifying the room. +/// +/// The user for which a membership applies is represented by the `state_key`. +/// Under some conditions, the `sender` and `state_key` may not match - this may be interpreted as the sender +/// affecting the membership state of the `state_key` user. +/// +/// The membership for a given user can change over time. +/// Previous membership can be retrieved from the `prev_content` object on an event. +/// If not present, the user’s previous membership must be assumed as leave. +public struct MatrixRoomMemberEvent: MatrixStateEventType { + public static let type = "m.room.member" + + /// The avatar URL for this user, if any. + public var avatarUrl: String? + + /// The display name for this user, if any. + public var displayname: String? + + /// Flag indicating if the room containing this event was created with the intention of being a direct chat. + public var isDirect: Bool? + + /// Usually found on join events, this field is used to denote which homeserver (through representation of a user with + /// sufficient power level) authorised the user’s join. More information about this field can be found in the Restricted Rooms + /// Specification. + /// + /// Client and server implementations should be aware of the signing implications of including this field in further events: + /// in particular, the event must be signed by the server which owns the user ID in the field. + /// When copying the membership event’s content (for profile updates and similar) it is therefore encouraged to + /// exclude this field in the copy, as otherwise the event might fail event authorization. + public var joinAuthorizedViaUsersServer: String? + + /// The membership state of the user. + public var membership: Membership + + /// Optional user-supplied text for why their membership has changed. + /// + /// For kicks and bans, this is typically the reason for the kick or ban. + /// For other membership changes, this is a way for the user to communicate their intent without having to send a + /// message to the room, such as in a case where Bob rejects an invite from Alice about an upcoming concert, + /// but can’t make it that day. + /// + /// Clients are not recommended to show this reason to users when receiving an invite due to the potential for spam and abuse. + /// Hiding the reason behind a button or other component is recommended. + public var reason: String? + + public var thirdPartyInvite: Invite? + + public enum Membership: String, Codable { + case invite, join, knock, leave, ban + } + + public struct Invite: Codable { + /// A name which can be displayed to represent the user instead of their third party identifier + public var displayName: String + + // /// A block of content which has been signed, which servers can use to verify the event. Clients should ignore this. + // public var signed: Signed + + enum CodingKeys: String, CodingKey { + case displayName = "display_name" + } + } + + enum CodingKeys: String, CodingKey { + case avatarUrl = "avatar_url" + case displayname + case isDirect = "is_direct" + case joinAuthorizedViaUsersServer = "join_authorised_via_users_server" + case membership + case reason + case thirdPartyInvite = "third_party_invite" + } +} + +/// This event specifies the minimum level a user must have in order to perform a certain action. +/// It also specifies the levels of each user in the room. +/// +/// If a `user_id` is in the users list, then that `user_id` has the associated power level. +/// Otherwise they have the default level `users_default`. If `users_default` is not supplied, it is assumed to be 0. +/// If the room contains no ``MatrixRoomPowerLevelsEvent`` event, the room’s creator has a power level of 100, +/// and all other users have a power level of 0. +/// +/// The level required to send a certain event is governed by events, `state_default` and `events_default`. +/// If an event type is specified in events, then the user must have at least the level specified in order to send that event. +/// If the event type is not supplied, it defaults to events_default for Message Events and state_default for State Events. +/// +/// If there is no `state_default` in the ``MatrixRoomPowerLevelsEvent`` event, the `state_default` is 50. +/// If there is no `events_default` in the ``MatrixRoomPowerLevelsEvent`` event, the `events_default` is 0. If the room contains no ``MatrixRoomPowerLevelsEvent`` event, both the `state_default` and `events_default` are 0. +/// +/// The power level required to invite a user to the room, kick a user from the room, ban a user from the room, or redact an event sent +/// by another user, is defined by invite, kick, ban, and redact, respectively. Each of these levels defaults to 50 if they are not specified +/// in the ``MatrixRoomPowerLevelsEvent`` event, or if the room contains no ``MatrixRoomPowerLevelsEvent`` event. +/// +/// +/// ### Note +/// +/// As noted above, in the absence of an ``MatrixRoomPowerLevelsEvent`` event, the `state_default` is 0, and all users +/// are considered to have power level 0. That means that any member of the room can send an +/// ``MatrixRoomPowerLevelsEvent`` event, changing the permissions in the room. +/// +/// Server implementations should therefore ensure that each room has an ``MatrixRoomPowerLevelsEvent`` event as soon as +/// it is created. See also the documentation of the /createRoom API. +public struct MatrixRoomPowerLevelsEvent: MatrixStateEventType { + public static let type = "m.room.power_levels" + + /// The level required to ban a user. Defaults to 50 if unspecified. + public var ban: Int? = 50 + + /// The level required to send specific event types. This is a mapping from event type to power level required. + public var events: [String: Int]? + + /// The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified. + public var eventsDefault: Int? = 0 + + /// The level required to invite a user. Defaults to 50 if unspecified. + public var invite: Int? = 50 + + /// The level required to kick a user. Defaults to 50 if unspecified. + public var kick: Int? = 50 + + /// The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key. + public var notifications: Notifications? + + /// The level required to redact an event sent by another user. Defaults to 50 if unspecified. + public var redact: Int? = 50 + + /// The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified. + public var stateDefault: Int? = 50 + + /// The power levels for specific users. This is a mapping from user_id to power level for that user. + public var users: [String: Int]? + + /// The default power level for every user in the room, unless their user_id is mentioned in the users key. + /// Defaults to 0 if unspecified. + public var usersDefault: Int? = 0 + + public struct Notifications: Codable { + /// The level required to trigger an @room notification. Defaults to 50 if unspecified. + public var room: Int? = 50 + } + + enum CodingKeys: String, CodingKey { + case ban, events + case eventsDefault = "events_default" + case invite, kick + case notifications + case redact + case stateDefault = "state_default" + case users + case usersDefault = "user_default" + } +} + +/// A room has an opaque room ID which is not human-friendly to read. +/// A room alias is human-friendly, but not all rooms have room aliases. +/// The room name is a human-friendly string designed to be displayed to the end-user. +/// The room name is not unique, as multiple rooms can have the same room name set. +/// +/// A room with an ``MatrixRoomNameEvent`` event with an absent, null, or empty ``name`` field should be treated the same as +/// a room with no ``MatrixRoomNameEvent`` event. +/// +/// An event of this type is automatically created when creating a room using /createRoom with the name key. +public struct MatrixRoomNameEvent: MatrixStateEventType { + public static let type = "m.room.name" + + /// The name of the room. This MUST NOT exceed 255 bytes. + public var name: String +} + +/// A topic is a short message detailing what is currently being discussed in the room. +/// It can also be used as a way to display extra information about the room, which may not be suitable for the room name. +/// The room topic can also be set when creating a room using /createRoom with the topic key. +public struct MatrixRoomTopicEvent: MatrixStateEventType { + public static let type = "m.room.topic" + + /// The topic text. + public var topic: String +} + +/// A picture that is associated with the room. This can be displayed alongside the room information. +public struct MatrixRoomAvatarEvent: MatrixStateEventType { + public static let type = "m.room.avatar" + + /// Metadata about the image referred to in ``url``. + public var info: MatrixMessageImage.ImageInfo + + /// The URL to the image. + public var url: String +} + +/// This event is used to “pin” particular events in a room for other participants to review later. +/// +/// The order of the pinned events is guaranteed and based upon the order supplied in the event. +/// Clients should be aware that the current user may not be able to see some of the events pinned due to visibility settings in the room. +/// Clients are responsible for determining if a particular event in the pinned list is displayable, and have the option to not display it if it +/// cannot be pinned in the client. +public struct MatrixRoomPinnedEvents: MatrixStateEventType { + public static let type: String = "m.room.pinned_events" + + /// An ordered list of event IDs to pin. + public var pinned: [String] +} + +/// Defines how messages sent in this room should be encrypted. +public struct MatrixRoomEncryptionEvent: MatrixStateEventType { + public static let type: String = "m.room.encryption" + + /// The encryption algorithm to be used to encrypt messages sent in this room. + public var algorithm: Algorithm + + /// How long the session should be used before changing it. + /// 604800000 (a week) is the recommended default. + public var rotationPeriodMS: Int? + + /// How many messages should be sent before changing the session. 100 is the recommended default. + public var RotationPeriodMsgs: Int? + + public enum Algorithm: String, Codable { + case megolmV1AESSHA1 = "m.megolm.v1.aes-sha2" + } + + enum CodingKeys: String, CodingKey { + case algorithm + case rotationPeriodMS = "rotation_period_ms" + case RotationPeriodMsgs = "rotation_period_msgs" + } +} + +/// +/// +/// # Removing +/// +/// When removing a bridge, you simply need to send a new state event with the same `state_key` with a `content` of `{}`. +/// This is because matrix does not yet have a mechanism to remove a state event in it's entireity. +public struct MatrixRoomBridgeEvent: MatrixStateEventType { + public static let type: String = "m.bridge" + public static let unstableType: String = "uk.half-shot.bridge" + + /// Should be the MXID of the bridge bot. + /// + /// It is important to note that `sender` should not be presumed to be the bridge bot. + /// This is because room upgrades, other bridges or admins could also set the state in the room on behalf of the bridge bot. + public var bridgebot: MatrixFullUserIdentifier? + + /// The name of the user which provisioned the bridge. + /// + /// In the case of alias based bridges, where the creator is not known it should be omitted. + public var creator: MatrixFullUserIdentifier? + + /// Describes the protocol that is being bridged. + /// + /// For example, it may be `"IRC"`, `"Slack"`, or `"Discord"`. + /// This field does not describe the low level protocol the bridge is using to access the network, + /// but a common user recongnisable name. + public var `protocol`: External? + + /// Should be information about the specific network the bridge is connected to. + /// + /// It's important to make the distinction here that this does NOT describe the protocol name, but the specific network the user is on. + /// For protocols that do not have the concept of a network, this field may be omitted. + public var network: External? + + /// Should be information about the specific channel the room is connected to. + public var channel: External? + + public struct External: Codable { + /// Case-insensitive and should be lowercase. + /// + /// Uppercase characters should be escaped (e.g. using QP encoding or similar).The purpose of the id field is not to be human + /// readable but just for comparing within the same bridge type, hence no encoding standard will be enforced in this proposal. + public var id: String + public var displayname: String? + public var avatarUrl: String? + public var externalUrl: String? + + enum CodingKeys: String, CodingKey { + case id + case displayname + case avatarUrl = "avatar_url" + case externalUrl = "external_url" + } + } + + /// Test if this state event is a redaction of an old bridge information. + public var isEmpty: Bool { + bridgebot == nil && `protocol` == nil && channel == nil + } +} diff --git a/Sources/MatrixClient/API/Filter.swift b/Sources/MatrixClient/API/Filter.swift index 5df2678..a62fd0d 100644 --- a/Sources/MatrixClient/API/Filter.swift +++ b/Sources/MatrixClient/API/Filter.swift @@ -34,7 +34,7 @@ extension MatrixFilterRequest: MatrixRequest { let userId = parameters.user, let filterId = parameters.filter else { - throw MatrixError.Unrecognized + throw MatrixErrorCode.Unrecognized } var components = homeserver.url diff --git a/Sources/MatrixClient/API/HomeServer.swift b/Sources/MatrixClient/API/HomeServer.swift index 2a9aebe..d60a616 100644 --- a/Sources/MatrixClient/API/HomeServer.swift +++ b/Sources/MatrixClient/API/HomeServer.swift @@ -23,8 +23,15 @@ public struct MatrixHomeserver: Codable { } public init?(string: String) { + let urlString: String + if string.starts(with: "http") { + urlString = string + } else { + urlString = "https://" + string + } + guard - let components = URLComponents(string: string), + let components = URLComponents(string: urlString), components.host != nil else { return nil @@ -37,7 +44,7 @@ public struct MatrixHomeserver: Codable { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) public init(resolve string: String, withUrlSession urlSession: URLSession = URLSession.shared) async throws { guard let self = MatrixHomeserver(string: string) else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } var res: MatrixWellKnown @@ -54,12 +61,22 @@ public struct MatrixHomeserver: Codable { url = self.url } } + + @available(swift, introduced: 5.5) + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + public init(resolve mxID: MatrixFullUserIdentifier, withUrlSession urlSession: URLSession = URLSession.shared) async throws { + try await self.init(resolve: mxID.domain, withUrlSession: urlSession) + } public func path(_ path: String) -> URLComponents { var components = url components.path = path return components } + + public var string: String? { + self.url.string + } } public struct MatrixServerInfoRequest {} @@ -163,26 +180,10 @@ public struct MatrixWellKnown: MatrixResponse { } extension MatrixWellKnown: Codable { - private enum KnownCodingKeys: String, CodingKey, CaseIterable { + private enum KnownCodingKeys: String, MatrixKnownCodingKeys { case homeserver = "m.homeserver" case identityServer = "m.identity_server" - static func doesNotContain(_ key: DynamicCodingKeys) -> Bool { - !Self.allCases.map(\.stringValue).contains(key.stringValue) - } - } - - struct DynamicCodingKeys: CodingKey { - var stringValue: String - init?(stringValue: String) { - self.stringValue = stringValue - } - - // not used here, but a protocol requirement - var intValue: Int? - init?(intValue _: Int) { - nil - } } public init(from decoder: Decoder) throws { @@ -191,12 +192,12 @@ extension MatrixWellKnown: Codable { identityServer = try container.decodeIfPresent(ServerInformation.self, forKey: .identityServer) extraInfo = [:] - let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self) + let extraContainer = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) { let decoded = try extraContainer.decode( AnyCodable.self, - forKey: DynamicCodingKeys(stringValue: key.stringValue)! + forKey: MatrixDynamicCodingKeys(stringValue: key.stringValue)! ) self.extraInfo[key.stringValue] = decoded } @@ -207,7 +208,7 @@ extension MatrixWellKnown: Codable { try container.encodeIfPresent(homeserver, forKey: .homeserver) try container.encodeIfPresent(identityServer, forKey: .identityServer) - var extraContainer = encoder.container(keyedBy: DynamicCodingKeys.self) + var extraContainer = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) for (name, value) in extraInfo { try extraContainer.encode(value, forKey: .init(stringValue: name)!) } diff --git a/Sources/MatrixClient/API/Request.swift b/Sources/MatrixClient/API/Request.swift index 3934d2c..4b2d9e4 100644 --- a/Sources/MatrixClient/API/Request.swift +++ b/Sources/MatrixClient/API/Request.swift @@ -6,6 +6,7 @@ // import Foundation +import AnyCodable /// HTTP method to be used by ``MatrixClient/MatrixRequest``. public enum HttpMethod: String, CaseIterable { @@ -52,7 +53,7 @@ public extension MatrixRequest { if Self.requiresAuth { guard let token = token else { - throw MatrixError.Forbidden + throw MatrixErrorCode.Forbidden } urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } @@ -101,7 +102,7 @@ public extension MatrixRequest { let (data, urlResponse) = try await urlSession.data(for: request) guard let response = urlResponse as? HTTPURLResponse else { - throw MatrixError.Unknown + throw MatrixErrorCode.Unknown } return (data, response) @@ -140,7 +141,7 @@ public extension MatrixRequest { guard let response = response as? HTTPURLResponse, let data = data else { - callback(.failure(MatrixError.Unknown)) + callback(.failure(MatrixErrorCode.Unknown)) return } @@ -203,6 +204,8 @@ public extension MatrixResponse { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .millisecondsSince1970 decoder.userInfo[.matrixEventTypes] = MatrixClient.eventTypes + decoder.userInfo[.matrixMessageTypes] = MatrixClient.messageTypes + decoder.userInfo[.matrixStateEventTypes] = MatrixClient.stateTypes self = try decoder.decode(Self.self, from: data) } } @@ -217,3 +220,30 @@ public extension MatrixClient { } public struct MatrixEmptyResponse: MatrixResponse {} + + + + +// MARK: - Codable + +public struct MatrixDynamicCodingKeys: CodingKey { + public var stringValue: String + public init?(stringValue: String) { + self.stringValue = stringValue + } + + public var intValue: Int? + public init?(intValue: Int) { + nil + } +} + +public protocol MatrixKnownCodingKeys: CodingKey, CaseIterable { + +} + +public extension MatrixKnownCodingKeys { + static func doesNotContain(_ key: MatrixDynamicCodingKeys) -> Bool { + !Self.allCases.map(\.stringValue).contains(key.stringValue) + } +} diff --git a/Sources/MatrixClient/API/Room/CreateRoom.swift b/Sources/MatrixClient/API/Room/CreateRoom.swift deleted file mode 100644 index ec99c5f..0000000 --- a/Sources/MatrixClient/API/Room/CreateRoom.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// File.swift -// -// -// Created by Finn Behrens on 13.03.22. -// - -import AnyCodable -import Foundation - -// TODO: Links -public struct MatrixCreateRoomRequest: Codable { - /// Extra keys, such as `m.federate`, to be added to the content of the - /// ``MatrixClient/ event. The server will overwrite the following keys: creator, room_version. Future versions of the specification may allow the server to overwrite other keys. - public var creationContent: MatrixRoomCreateEvent.Content? - - /// A list of state events to set in the new room. - /// - /// This allows the user to override the default state events set in the new room. - /// The expected format of the state events are an object with type, state_key and - /// content keys set. - /// - /// Takes precedence over events set by preset, but gets overridden by name and topic keys. - public var initialState: [StateEvent]? - - /// A list of user IDs to invite to the room. - /// - /// This will tell the server to invite everyone in the list to the newly created room. - public var invite: [String]? - - /// A list of objects representing third party IDs to invite into the room. - public var invite3PID: [Invite3PID]? - - /// This flag makes the server set the is_direct flag on the m.room.member - /// events sent to the users in invite and invite_3pid. - /// - /// See [Direct Messaging] for more information. - public var isDirect: Bool? - - /// If this is included, an `m.room.name` event will be sent into the room to indicate - /// the name of the room. - /// - /// See [Room Events] for more information on m.room.name. - public var name: String? - - // TODO: - // The power level content to override in the default power level event. - // - // This object is applied on top of the generated - // `m.room.power_levels` event content prior to it being sent to the room. - // Defaults to overriding nothing. - // public var powerLevelContentOverride: - - /// Convenience parameter for setting various default state events based on a preset. - /// - /// If unspecified, the server should use the visibility to determine which preset to use. - /// A visibility of public equates to a preset of public_chat and private visibility equates - /// to a preset of private_chat. - public var preset: Preset? - - /// The desired room alias **local part**. - /// - /// If this is included, a room alias will be created and mapped to the newly created room. - /// The alias will belong on the same homeserver which created the room. - /// For example, if this was set to “foo” and sent to the homeserver “example.com” - /// the complete room alias would be #foo:example.com. - /// - /// The complete room alias will become the canonical alias - /// for the room and an `m.room.canonical_alias` event will be sent into the room. - public var roomAliasName: String? - - /// The room version to set for the room. - /// - /// If not provided, the homeserver is to use its configured default. - /// If provided, the homeserver will return a 400 error with the - /// errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version. - public var roomVersion: String? - - /// If this is included, an `m.room.topic` event will be sent into the room to indicate the topic for the room. - /// See Room Events for more information on `m.room.topic`. - public var topic: String? - - /// Visibility of the room. - public var visibility: Visibility? = .private - - enum CodingKeys: String, CodingKey { - case creationContent = "creation_content" - case initialState = "initial_state" - case invite - case invite3PID = "invite_3pid" - case isDirect = "is_direct" - case name - // case powerLevelContentOverride = "power_level_content_override" - case preset - case roomAliasName = "room_alias_name" - case roomVersion = "room_version" - case topic - } -} - -public extension MatrixCreateRoomRequest { - enum Preset: String, RawRepresentable, Codable { - case privateChat = "private_chat" - case publicChat = "public_chat" - case trustedPrivateChat = "trusted_private_chat" - } - - struct Invite3PID: Codable { - /// The invitee’s third party identifier. - public var address: String - - /// An access token previously registered with the identity server. - /// Servers can treat this as optional to distinguish between r0.5-compatible - /// clients and this specification version. - public var identityAccessToken: String - - /// The hostname+port of the identity server which should be used for third party - /// identifier lookups. - public var identityServer: String - - /// The kind of address being passed in the address field, for example email. - public var medium: String - - enum CodingKeys: String, CodingKey { - case address - case identityAccessToken = "id_access_token" - case identityServer = "id_server" - case medium - } - } - - struct StateEvent: Codable { - /// The content of the event. - public var content: [String: AnyCodable] - - /// The state_key of the state event. Defaults to an empty string. - public var stateKey: String? - - /// The type of event to send. - public var type: String - - enum CodingKeys: String, CodingKey { - case content - case stateKey = "state_key" - case type - } - } - - enum Visibility: String, RawRepresentable, Codable { - /// A public visibility indicates that the room will be shown in the published room list. - case `public` - /// A private visibility will hide the room from the published room list. - case `private` - } -} - -extension MatrixCreateRoomRequest: MatrixRequest { - public typealias Response = MatrixCreateRoom - - public typealias URLParameters = () - - public func components(for homeserver: MatrixHomeserver, with _: ()) throws -> URLComponents { - var components = homeserver.url - components.path = "/_matrix/client/v3/createRoom" - - return components - } - - public static var httpMethod: HttpMethod { - .POST - } - - public static var requiresAuth: Bool { - true - } -} - -public struct MatrixCreateRoom: MatrixResponse { - /// The created room’s ID. - public var roomID: String - - enum CodingKeys: String, CodingKey { - case roomID = "room_id" - } -} diff --git a/Sources/MatrixClient/API/Sync.swift b/Sources/MatrixClient/API/Sync.swift index 8a48812..5fb30f0 100644 --- a/Sources/MatrixClient/API/Sync.swift +++ b/Sources/MatrixClient/API/Sync.swift @@ -16,7 +16,9 @@ public struct MatrixSyncRequest: MatrixRequest { } // TODO: fullState - // TODO: presence + if let presence = parameters.presence { + queryItems.append(URLQueryItem(name: "set_presence", value: presence.rawValue)) + } if let timeout = parameters.timeout { queryItems.append(URLQueryItem(name: "timeout", value: String(timeout))) @@ -36,11 +38,11 @@ public struct MatrixSyncRequest: MatrixRequest { public static var requiresAuth = true public struct Parameters { - public let filter: String? - public let since: String? - public let fullState: Bool? - public let presence: Presence? - public let timeout: Int? + public var filter: String? + public var since: String? + public var fullState: Bool? + public var presence: Presence? + public var timeout: Int? public enum Presence: String { case online diff --git a/Sources/MatrixClient/API/UserData/Direct.swift b/Sources/MatrixClient/API/UserData/Direct.swift index ddfec95..197db4e 100644 --- a/Sources/MatrixClient/API/UserData/Direct.swift +++ b/Sources/MatrixClient/API/UserData/Direct.swift @@ -14,20 +14,8 @@ public struct MatrixDirectAccountData: MatrixAccountData { } extension MatrixDirectAccountData: Codable { - private struct DynamicCodingKeys: CodingKey { - var stringValue: String - init?(stringValue: String) { - self.stringValue = stringValue - } - - var intValue: Int? - init?(intValue _: Int) { - nil - } - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + let container = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) for key in container.allKeys { let decoded = try container.decode([String].self, forKey: key) @@ -36,7 +24,7 @@ extension MatrixDirectAccountData: Codable { } public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: DynamicCodingKeys.self) + var container = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) for (name, value) in users { try container.encode(value, forKey: .init(stringValue: name)!) } diff --git a/Sources/MatrixClient/API/WhoAmI.swift b/Sources/MatrixClient/API/WhoAmI.swift new file mode 100644 index 0000000..ac0bab8 --- /dev/null +++ b/Sources/MatrixClient/API/WhoAmI.swift @@ -0,0 +1,47 @@ +// +// WhoAmI.swift +// +// +// Created by Finn Behrens on 23.04.22. +// + +import Foundation + +public struct MatrixWhoAmIRequest: MatrixRequest { + public func components(for homeserver: MatrixHomeserver, with _: ()) throws -> URLComponents { + homeserver.path("/_matrix/client/v3/account/whoami") + } + + public static var httpMethod: HttpMethod { + .GET + } + + public static var requiresAuth: Bool { + true + } + + public typealias Response = MatrixWhoAmI + + public typealias URLParameters = () +} + +public struct MatrixWhoAmI: MatrixResponse { + /// Device ID associated with the access token. + /// + /// If no device is associated with the access token (such as in the case of application services) then this field can be omitted. Otherwise this is required. + public var deviceID: String? + + /// When true, the user is a Guest User. + /// + /// When not present or false, the user is presumed to be a non-guest user. + public var isGuest: Bool? = false + + /// The user ID that owns the access token. + public var userID: MatrixFullUserIdentifier + + enum CodingKeys: String, CodingKey { + case deviceID = "device_id" + case isGuest = "is_guest" + case userID = "user_id" + } +} diff --git a/Sources/MatrixClient/ContentURI.swift b/Sources/MatrixClient/ContentURI.swift new file mode 100644 index 0000000..fdc0251 --- /dev/null +++ b/Sources/MatrixClient/ContentURI.swift @@ -0,0 +1,83 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 22.06.22. +// + +import Foundation + +@frozen +public struct MatrixContentURL: RawRepresentable, Equatable, Identifiable, Hashable, Codable { + public init?(string: String) { + guard let url = URL(string: string) else { + return nil + } + self.rawValue = url + } + + public init?(rawValue: URL) { + self.rawValue = rawValue + } + + public var rawValue: URL + + public var absoluteString: String { + rawValue.absoluteString + } + + public var host: String? { + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + return rawValue.host() + } else { + return rawValue.host + } + } + + public var path: String? { + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + return rawValue.path() + } else { + return rawValue.path + } + } + + public var mediaId: String? { + path + } + + public func downloadURL(allowRemote: Bool? = nil) -> URL? { + guard let host, + let path + else { + return nil + } + var components = URLComponents() + components.host = host + components.scheme = "https" + components.path = "/_matrix/media/v3/download/" + host + path + if let allowRemote { + components.queryItems = [.init(name: "allow_remote", value: allowRemote.description)] + } + + return components.url + } + + public var id: URL { + rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.rawValue = try container.decode(URL.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + public enum CodingKeys: CodingKey { + case rawValue + } +} diff --git a/Sources/MatrixClient/Error.swift b/Sources/MatrixClient/Error.swift index 45ad25b..4bb2dee 100644 --- a/Sources/MatrixClient/Error.swift +++ b/Sources/MatrixClient/Error.swift @@ -5,46 +5,264 @@ // Created by Finn Behrens on 07.08.21. // +import AnyCodable import Foundation -public enum MatrixError: String, Error, Codable { - case Forbidden = "M_FORBIDDEN" - case Unknown = "M_UNKNOWN" - case UnknownToken = "M_UNKNOWN_TOKEN" - case BadJSON = "M_BAD_JSON" - case NotFound = "M_NOT_FOUND" - case LimitExceeded = "M_LIMIT_EXCEEDED" - case UserInUse = "M_USER_IN_USE" - case RoomInUse = "M_ROOM_IN_USE" - case BadPagination = "M_BAD_PAGINATON" - case Unauthorized = "M_UNAUTHORIZED" - case OldVersion = "M_OLD_VERSION" - case Unrecognized = "M_UNRECOGNIZED" - case LoginEmailURLNotYet = "M_LOGIN_EMAIL_URL_NOT_YET" - case ThreePIDAuthFailed = "M_THREEPID_AUTH_FAILED" - case ThreePIDInUse = "M_THREEPID_IN_USE" - case ThreePIDNotFound = "M_THREEPID_NOT_FOUND" - case ServerNotTrusted = "M_SERVER_NOT_TRUSTED" - case GuestAccessForbidden = "M_GUEST_ACCESS_FORBIDDEN" - case ConsentNotGiven = "M_CONSENT_NOT_GIVEN" - case ResourceLimitExceeded = "M_RESOURCE_LIMIT_EXCEEDED" - case BackupWrongKeysVersion = "M_WRONG_ROOM_KEYS_VERSION" - case PasswordTooShort = "M_PASSWORD_TOO_SHORT" - case PasswordNoDigit = "M_PASSWORD_NO_DIGIT" - case PasswordNoUppercase = "M_PASSWORD_NO_UPPERCASE" - case PasswordNoLowercase = "M_PASSWORD_NO_LOWERCASE" - case PasswordNoSymbol = "M_PASSWORD_NO_SYMBOL" - case PasswordInDictionary = "M_PASSWORD_IN_DICTIONARY" - case PasswordWeak = "M_WEAK_PASSWORD" - case TermsNotSigned = "M_TERMS_NOT_SIGNED" - case InvalidPepper = "M_INVALID_PEPPER" - case Exclusive = "M_EXCLUSIVE" - case InvalidParam = "M_INVALID_PARAM" +public enum MatrixCommonErrorCode: String, Error, Codable { + // MARK: Common error codes + + /// Forbidden access, e.g. joining a room without permission, failed login. + case forbidden = "M_FORBIDDEN" + /// The access token specified was not recognised. + /// + /// An additional response parameter, soft_logout, might be present on the response for 401 HTTP status codes. + case unknownToken = "M_UNKNOWN_TOKEN" + /// No access token was specified for the request. + case missingToken = "M_MISSING_TOKEN" + /// Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. + case badJSON = "M_BAD_JSON" + /// Request did not contain valid JSON. + case notJSON = "M_NOT_JSON" + /// No resource was found for this request. + case notFound = "M_NOT_FOUND" + /// Too many requests have been sent in a short period of time. Wait a while then try again. + case limitExceeded = "M_LIMIT_EXCEEDED" + /// An unknown error has occurred. + case unknown = "M_UNKNOWN" + + // MARK: Other error codes + + /// The server did not understand the request. + case unrecognized = "M_UNRECOGNIZED" + /// The request was not correctly authorized. Usually due to login failures. + case unauthorized = "M_UNAUTHORIZED" + /// Encountered when trying to register a user ID which has been taken. + case userInUse = "M_USER_IN_USE" + /// Encountered when trying to register a user ID which is not valid. + case invalidUserName = "M_INVALID_USERNAME" + /// Sent when the room alias given to the createRoom API is already in use. + case roomInUse = "M_ROOM_IN_USE" + /// Sent when the initial state given to the createRoom API is invalid. + case invalidRoomState = "M_INVALID_ROOM_STATE" + /// Sent when a threepid given to an API cannot be used because the same threepid is already in use. + case threePIDInUse = "M_THREEPID_IN_USE" + /// Sent when a threepid given to an API cannot be used because no record matching the threepid was found. + case threePIDNotFound = "M_THREEPID_NOT_FOUND" + /// Authentication could not be performed on the third party identifier. + case threePIDAuthFailed = "M_THREEPID_AUTH_FAILED" + /// The server does not permit this third party identifier. + /// + /// This may happen if the server only permits, for example, email addresses from a particular domain. + case threePIDDenied = "M_THREEPID_DENIED" + /// The client’s request used a third party server, e.g. identity server, that this server does not trust. + case serverNotTrusted = "M_SERVER_NOT_TRUSTED" + /// The client’s request to create a room used a room version that the server does not support. + case unsupportedRoomVersion = "M_UNSUPPORTED_ROOM_VERSION" + /// The client attempted to join a room that has a version the server does not support. + /// + /// Inspect the `room_version` property of the error response for the room’s version. + case incompatibleRoomVersion = "M_INCOMPATIBLE_ROOM_VERSION" + /// The state change requested cannot be performed, such as attempting to unban a user who is not banned. + case badState = "M_BAD_STATE" + /// The room or resource does not permit guests to access it. + case guestAccessForbidden = "M_GUEST_ACCESS_FORBIDDEN" + /// A Captcha is required to complete the request. + case captchaNeeded = "M_CAPTCHA_NEEDED" + /// The Captcha provided did not match what was expected. + case captchaInvalid = "M_CAPTCHA_INVALID" + /// A required parameter was missing from the request. + case missingParam = "M_MISSING_PARAM" + /// A parameter that was specified has the wrong value. + /// + /// For example, the server expected an integer and instead received a string. + case invalidParam = "M_INVALID_PARAM" + /// The request or entity was too large. + case tooLarge = "M_TOO_LARGE" + /// The resource being requested is reserved by an application service, + /// or the application service making the request has not created the resource. + case exclusive = "M_EXCLUSIVE" + /// The request cannot be completed because the homeserver has reached a resource limit imposed on it. + /// + /// For example, a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much + /// memory or disk space. The error MUST have an `admin_contact` field to provide the user receiving the error a + /// place to reach out to. Typically, this error will appear on routes which attempt to modify + /// state (e.g.: sending messages, account data, etc) and not routes which only read + /// state (e.g.: /sync, get account data, etc). + case resourceLimitExceeded = "M_RESOURCE_LIMIT_EXCEEDED" + /// The user is unable to reject an invite to join the server notices room. + case cannotLeaveServerNoticeRoom = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" + + case badPagination = "M_BAD_PAGINATON" + case oldVersion = "M_OLD_VERSION" + case loginEmailURLNotYet = "M_LOGIN_EMAIL_URL_NOT_YET" + case consentNotGiven = "M_CONSENT_NOT_GIVEN" + case backupWrongKeysVersion = "M_WRONG_ROOM_KEYS_VERSION" + case passwordTooShort = "M_PASSWORD_TOO_SHORT" + case passwordNoDigit = "M_PASSWORD_NO_DIGIT" + case passwordNoUppercase = "M_PASSWORD_NO_UPPERCASE" + case passwordNoLowercase = "M_PASSWORD_NO_LOWERCASE" + case passwordNoSymbol = "M_PASSWORD_NO_SYMBOL" + case passwordInDictionary = "M_PASSWORD_IN_DICTIONARY" + case passwordWeak = "M_WEAK_PASSWORD" + case termsNotSigned = "M_TERMS_NOT_SIGNED" + case invalidPepper = "M_INVALID_PEPPER" + + // MSC 3575 + case unknownPos = "M_UNKNOWN_POS" +} + +public struct MatrixErrorCode: RawRepresentable, Error, Codable { + public private(set) var common: MatrixCommonErrorCode? + var string: String? + + public var rawValue: String { + if let common = common { + return common.rawValue + } + return string! + } + + public init?(rawValue: String) { + common = MatrixCommonErrorCode(rawValue: rawValue) + if common == nil { + string = rawValue + } + } + + public init(_ common: MatrixCommonErrorCode) { + self.common = common + } +} + +public extension MatrixErrorCode { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + self.init(rawValue: rawValue)! + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} + +extension MatrixErrorCode: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self.init(rawValue: value)! + } +} + +public extension MatrixErrorCode { + // MARK: Common error codes + + /// Forbidden access, e.g. joining a room without permission, failed login. + static let Forbidden = MatrixErrorCode(.forbidden) + /// The access token specified was not recognised. + /// + /// An additional response parameter, soft_logout, might be present on the response for 401 HTTP status codes. + static let UnknownToken = MatrixErrorCode(.unknownToken) + /// No access token was specified for the request. + static let MissingToken = MatrixErrorCode(.missingToken) + /// Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. + static let BadJSON = MatrixErrorCode(.badJSON) + /// Request did not contain valid JSON. + static let NotJSON = MatrixErrorCode(.notJSON) + /// No resource was found for this request. + static let NotFound = MatrixErrorCode(.notFound) + /// Too many requests have been sent in a short period of time. Wait a while then try again. + static let LimitExceeded = MatrixErrorCode(.limitExceeded) + /// An unknown error has occurred. + static let Unknown = MatrixErrorCode(.unknown) + + // MARK: Other error codes + + /// The server did not understand the request. + static let Unrecognized = MatrixErrorCode(.unrecognized) + /// The request was not correctly authorized. Usually due to login failures. + static let Unauthorized = MatrixErrorCode(.unauthorized) + /// Encountered when trying to register a user ID which has been taken. + static let UserInUse = MatrixErrorCode(.userInUse) + /// Encountered when trying to register a user ID which is not valid. + static let InvalidUserName = MatrixErrorCode(.invalidUserName) + /// Sent when the room alias given to the createRoom API is already in use. + static let RoomInUse = MatrixErrorCode(.roomInUse) + /// Sent when the initial state given to the createRoom API is invalid. + static let InvalidRoomState = MatrixErrorCode(.invalidRoomState) + /// Sent when a threepid given to an API cannot be used because the same threepid is already in use. + static let ThreePIDInUse = MatrixErrorCode(.threePIDInUse) + /// Sent when a threepid given to an API cannot be used because no record matching the threepid was found. + static let ThreePIDNotFound = MatrixErrorCode(.threePIDNotFound) + /// Authentication could not be performed on the third party identifier. + static let ThreePIDAuthFailed = MatrixErrorCode(.threePIDAuthFailed) + /// The server does not permit this third party identifier. + /// + /// This may happen if the server only permits, for example, email addresses from a particular domain. + static let ThreePIDDenied = MatrixErrorCode(.threePIDDenied) + /// The client’s request used a third party server, e.g. identity server, that this server does not trust. + static let ServerNotTrusted = MatrixErrorCode(.serverNotTrusted) + /// The client’s request to create a room used a room version that the server does not support. + static let UnsupportedRoomVersion = MatrixErrorCode(.unsupportedRoomVersion) + /// The client attempted to join a room that has a version the server does not support. + /// + /// Inspect the `room_version` property of the error response for the room’s version. + static let IncompatibleRoomVersion = MatrixErrorCode(.incompatibleRoomVersion) + /// The state change requested cannot be performed, such as attempting to unban a user who is not banned. + static let BadState = MatrixErrorCode(.badState) + /// The room or resource does not permit guests to access it. + static let GuestAccessForbidden = MatrixErrorCode(.guestAccessForbidden) + /// A Captcha is required to complete the request. + static let CaptchaNeeded = MatrixErrorCode(.captchaNeeded) + /// The Captcha provided did not match what was expected. + static let CaptchaInvalid = MatrixErrorCode(.captchaInvalid) + /// A required parameter was missing from the request. + static let MissingParam = MatrixErrorCode(.missingParam) + /// A parameter that was specified has the wrong value. + /// + /// For example, the server expected an integer and instead received a string. + static let InvalidParam = MatrixErrorCode(.invalidParam) + /// The request or entity was too large. + static let TooLarge = MatrixErrorCode(.tooLarge) + /// The resource being requested is reserved by an application service, + /// or the application service making the request has not created the resource. + static let Exclusive = MatrixErrorCode(.exclusive) + /// The request cannot be completed because the homeserver has reached a resource limit imposed on it. + /// + /// For example, a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much + /// memory or disk space. The error MUST have an `admin_contact` field to provide the user receiving the error a + /// place to reach out to. Typically, this error will appear on routes which attempt to modify + /// state (e.g.: sending messages, account data, etc) and not routes which only read + /// state (e.g.: /sync, get account data, etc). + static let ResourceLimitExceeded = MatrixErrorCode(.resourceLimitExceeded) + /// The user is unable to reject an invite to join the server notices room. + static let CannotLeaveServerNoticeRoom = MatrixErrorCode(.cannotLeaveServerNoticeRoom) + + static let BadPagination = MatrixErrorCode(.badPagination) + static let OldVersion = MatrixErrorCode(.oldVersion) + static let LoginEmailURLNotYet = MatrixErrorCode(.loginEmailURLNotYet) + static let ConsentNotGiven = MatrixErrorCode(.consentNotGiven) + static let BackupWrongKeysVersion = MatrixErrorCode(.backupWrongKeysVersion) + static let PasswordTooShort = MatrixErrorCode(.passwordTooShort) + static let PasswordNoDigit = MatrixErrorCode(.passwordNoDigit) + static let PasswordNoUppercase = MatrixErrorCode(.passwordNoUppercase) + static let PasswordNoLowercase = MatrixErrorCode(.passwordNoLowercase) + static let PasswordNoSymbol = MatrixErrorCode(.passwordNoSymbol) + static let PasswordInDictionary = MatrixErrorCode(.passwordInDictionary) + static let PasswordWeak = MatrixErrorCode(.passwordWeak) + static let TermsNotSigned = MatrixErrorCode(.termsNotSigned) + static let InvalidPepper = MatrixErrorCode(.invalidPepper) } public struct MatrixServerError: Error, Codable { + public init(errcode: MatrixErrorCode, error: String, code: Int? = nil, extraInfo: [String: AnyCodable] = [:]) { + self.errcode = errcode + self.error = error + self.code = code + self.extraInfo = extraInfo + } + /// Error code - public var errcode: MatrixError + public var errcode: MatrixErrorCode /// Error message reported by the server public var error: String @@ -52,12 +270,115 @@ public struct MatrixServerError: Error, Codable { /// HTTP status code public var code: Int? + public var interactiveAuth: MatrixInteractiveAuth? + + public var extraInfo: [String: AnyCodable] + // TODO: extra data public init(json: Data, code: Int? = nil) throws { let decoder = JSONDecoder() + decoder.userInfo[.matrixErrorHttpCode] = code - self = try decoder.decode(Self.self, from: json) + do { + self = try decoder.decode(Self.self, from: json) + } catch { + throw MatrixServerError( + errcode: .Unknown, + error: error.localizedDescription, + code: code, + extraInfo: ["json": .init(json)] + ) + } self.code = code } } + +public extension MatrixServerError { + internal enum KnownCodingKeys: String, MatrixKnownCodingKeys { + case errcode + case error + + static let extraIgnoreValues = [ + "session", + "flows", + "params", + "completed", + ] + + static func doesNotContain(_ key: MatrixDynamicCodingKeys) -> Bool { + !Self.allCases.map(\.stringValue).contains(key.stringValue) && !Self.extraIgnoreValues + .contains(key.stringValue) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: KnownCodingKeys.self) + errcode = try container.decodeIfPresent(MatrixErrorCode.self, forKey: .errcode) ?? .Unknown + error = try container.decodeIfPresent(String.self, forKey: .error) ?? "" + + extraInfo = [:] + let extraContainer = try decoder.container(keyedBy: MatrixDynamicCodingKeys.self) + + for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) { + let decoded = try extraContainer.decode( + AnyCodable.self, + forKey: .init(stringValue: key.stringValue)! + ) + self.extraInfo[key.stringValue] = decoded + } + + guard let code = decoder.userInfo[.matrixErrorHttpCode] as? Int, + code == 401 + else { + return + } + + do { + interactiveAuth = try MatrixInteractiveAuth(from: decoder) + } catch { + // don't care if it fails + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: KnownCodingKeys.self) + try container.encode(errcode, forKey: .errcode) + try container.encode(error, forKey: .error) + + var extraContainer = encoder.container(keyedBy: MatrixDynamicCodingKeys.self) + for (name, value) in extraInfo { + try extraContainer.encode(value, forKey: .init(stringValue: name)!) + } + } +} + +public extension MatrixServerError { + var is401: Bool { + errcode == .Unauthorized && code == 401 + } + + var is404: Bool { + errcode == .NotFound && code == 404 + } + + var isTokenError: Bool { + errcode == .UnknownToken || errcode == .MissingToken + } + + var isLimitexceededError: Bool { + code == 429 && errcode == .LimitExceeded + } + + var shouldbeRetried: Bool { + // Investigate network error codes + isLimitexceededError + } +} + +extension CodingUserInfoKey { + /// The key used to determine the types of `MatrixEvent` that can be decoded. + static var matrixErrorHttpCode: CodingUserInfoKey { + CodingUserInfoKey(rawValue: "MatrixClient.ErrorHttpCode")! + } +} diff --git a/Sources/MatrixClient/MatrixClient.swift b/Sources/MatrixClient/MatrixClient.swift index f304d15..26a914e 100644 --- a/Sources/MatrixClient/MatrixClient.swift +++ b/Sources/MatrixClient/MatrixClient.swift @@ -19,13 +19,10 @@ public struct MatrixClient { /// /// Add any custom events you would like decoded to this array. public static var eventTypes: [MatrixEvent.Type] = [ - MatrixEncryptionEvent.self, - MatrixMemberEvent.self, + MatrixStateEvent.self, MatrixMessageEvent.self, - MatrixNameEvent.self, MatrixReactionEvent.self, MatrixRedactionEvent.self, - MatrixRoomCreateEvent.self, ] public static var messageTypes: [MatrixMessageType.Type] = [ @@ -39,6 +36,20 @@ public struct MatrixClient { MatrixMessageVideo.self, ] + public static var stateTypes: [MatrixStateEventType.Type] = [ + MatrixRoomCanonicalAliasEvent.self, + MatrixRoomCreateEvent.self, + MatrixRoomJoinRulesEvent.self, + MatrixRoomMemberEvent.self, + MatrixRoomPowerLevelsEvent.self, + MatrixRoomNameEvent.self, + MatrixRoomTopicEvent.self, + MatrixRoomAvatarEvent.self, + MatrixRoomPinnedEvents.self, + MatrixRoomEncryptionEvent.self, + MatrixRoomBridgeEvent.self, + ] + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) internal static var logger = Logger() @@ -145,6 +156,13 @@ public struct MatrixClient { .response(on: homeserver, withToken: accessToken, with: (), withUrlSession: urlSession, callback: callback) } + @available(swift, introduced: 5.5) + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + public func whoami() async throws -> MatrixWhoAmI { + try await MatrixWhoAmIRequest() + .response(on: homeserver, withToken: accessToken, with: (), withUrlSession: urlSession) + } + @available(swift, introduced: 5.5) @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) public func sync(parameters: MatrixSyncRequest.Parameters) async throws -> MatrixSync { @@ -172,7 +190,11 @@ public struct MatrixClient { public func isReady() async throws { let versions = try await getVersions() if !versions.versions.contains("v1.2") { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } } } + +//public typealias MatrixCodableContent = Codable +public protocol MatrixCodableContent: Codable {} + diff --git a/Sources/MatrixClient/MatrixClient/MatrixClient+AccountData.swift b/Sources/MatrixClient/MatrixClient/MatrixClient+AccountData.swift index 62e0be0..2573cb7 100644 --- a/Sources/MatrixClient/MatrixClient/MatrixClient+AccountData.swift +++ b/Sources/MatrixClient/MatrixClient/MatrixClient+AccountData.swift @@ -12,7 +12,7 @@ public extension MatrixClient { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func getDisplayName(_ userID: MatrixUserIdentifier) async throws -> String { guard let userID = userID.FQMXID else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } return try await getDisplayName(userID: userID) } @@ -29,7 +29,7 @@ public extension MatrixClient { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func setDisplayName(_ displayName: String, _ userID: MatrixUserIdentifier) async throws { guard let userID = userID.FQMXID else { - throw MatrixError.NotFound + throw MatrixErrorCode.NotFound } try await setDisplayName(displayName, userID: userID) diff --git a/Sources/MatrixClient/MatrixClient/MatrixClient+Auth.swift b/Sources/MatrixClient/MatrixClient/MatrixClient+Auth.swift index 087ee9b..ae587c0 100644 --- a/Sources/MatrixClient/MatrixClient/MatrixClient+Auth.swift +++ b/Sources/MatrixClient/MatrixClient/MatrixClient+Auth.swift @@ -21,7 +21,7 @@ public extension MatrixClient { func getLoginFlows() async throws -> [MatrixLoginFlow] { try await MatrixLoginFlowRequest() .response(on: homeserver, withToken: accessToken, with: (), withUrlSession: urlSession) - .flows.map(\.type) + .flows } /// Test if the server supports password authentication. @@ -29,22 +29,23 @@ public extension MatrixClient { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func supportsPasswordAuth() async throws -> Bool { let flows = try await getLoginFlows() - return flows.contains(where: { $0 == MatrixLoginFlow.password }) + return flows.contains(where: { $0.type == MatrixLoginFlowType.password }) } // MARK: - Register @available(swift, introduced: 5.5) @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func getRegisterFlows(kind: MatrixRegisterRequest.RegisterKind = .user) async throws -> MatrixInteractiveAuth { - let resp = try await MatrixRegisterRequest(password: "") - .response(on: homeserver, with: kind, withUrlSession: urlSession) - - switch resp { - case let .interactive(flows): - return flows - default: - throw MatrixError.NotFound + func getRegisterFlows(kind: MatrixRegisterRequest.RegisterKind = .user) async throws -> MatrixInteractiveAuth? { + do { + _ = try await MatrixRegisterRequest(password: "") + .response(on: homeserver, with: kind, withUrlSession: urlSession) + return nil + } catch let error as MatrixServerError { + guard let interactive = error.interactiveAuth else { + throw error + } + return interactive } } @@ -72,7 +73,7 @@ public extension MatrixClient { auth: MatrixInteractiveAuthResponse? = nil, bind_email: Bool? = nil, kind: MatrixRegisterRequest.RegisterKind = .user - ) async throws -> MatrixRegisterContainer { + ) async throws -> MatrixRegister { try await MatrixRegisterRequest( username: username, bindEmail: bind_email, @@ -114,20 +115,27 @@ public extension MatrixClient { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func login( token: Bool = false, - username: String, + username: String? = nil, password: String, displayName: String? = nil, deviceId: String? = nil ) async throws -> MatrixLogin { - let flow: MatrixLoginFlow + let flow: MatrixLoginFlowType + let identifier: MatrixLoginUserIdentifier? if token { flow = .token + identifier = nil } else { flow = .password + guard let username else { + throw MatrixCommonErrorCode.missingParam + } + identifier = .user(id: username) } + var request = MatrixLoginRequest( type: flow.rawValue, - identifier: MatrixLoginUserIdentifier.user(id: username), + identifier: identifier, deviceId: deviceId, initialDeviceDisplayName: displayName ) diff --git a/Sources/MatrixClient/MatrixClient/MatrixClient+Device.swift b/Sources/MatrixClient/MatrixClient/MatrixClient+Device.swift index 61445c5..0726099 100644 --- a/Sources/MatrixClient/MatrixClient/MatrixClient+Device.swift +++ b/Sources/MatrixClient/MatrixClient/MatrixClient+Device.swift @@ -23,6 +23,12 @@ public extension MatrixClient { .response(on: homeserver, withToken: accessToken, with: deviceID, withUrlSession: urlSession) } + @available(swift, introduced: 5.5) + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func setDeviceDisplayName(_ displayName: String, device: MatrixDevice) async throws { + try await setDeviceDisplayName(displayName, deviceID: device.deviceID) + } + @available(swift, introduced: 5.5) @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func setDeviceDisplayName(_ displayName: String, deviceID: String) async throws { diff --git a/Sources/MatrixClient/UserIdentifier.swift b/Sources/MatrixClient/UserIdentifier.swift index 3445fe9..53cadcd 100644 --- a/Sources/MatrixClient/UserIdentifier.swift +++ b/Sources/MatrixClient/UserIdentifier.swift @@ -7,6 +7,12 @@ import Foundation +public protocol MatrixUserIdentifierProtocol: Equatable, Comparable, Hashable, CustomStringConvertible { + var localpart: String { get set } + + init?(string: String) +} + /// Users within Matrix are uniquely identified by their Matrix user ID. /// /// The user ID is namespaced to the homeserver which allocated the account and has the form: @@ -55,7 +61,11 @@ import Foundation /// The length restriction is derived from the limit on the length of the `sender` key on events; since the user ID /// appears in every event sent by the user, it is limited to ensure that the user ID does not dominate over the actual /// content of the events. -public struct MatrixUserIdentifier: RawRepresentable, Equatable { +public struct MatrixUserIdentifier: RawRepresentable, MatrixUserIdentifierProtocol { + public static func < (lhs: MatrixUserIdentifier, rhs: MatrixUserIdentifier) -> Bool { + lhs.rawValue < rhs.rawValue + } + public var localpart: String public var domain: String? @@ -138,6 +148,10 @@ public struct MatrixUserIdentifier: RawRepresentable, Equatable { return nil } + public var description: String { + rawValue + } + // MARK: static variables static let allowedLocalCharacters = CharacterSet(charactersIn: "1234567890abcdefghijklmnopqrstuvwxyz-.=_/") @@ -152,7 +166,77 @@ extension MatrixUserIdentifier: Codable { if let id = id { self = id } else { - throw MatrixError.BadJSON + throw MatrixErrorCode.BadJSON + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} + +public struct MatrixFullUserIdentifier: RawRepresentable, MatrixUserIdentifierProtocol, Identifiable { + public init(localpart: String, domain: String) { + self.localpart = localpart + self.domain = domain + } + + public var localpart: String + public var domain: String + + public init?(rawValue: MatrixUserIdentifier) { + guard let domain = rawValue.domain else { + return nil + } + self.domain = domain + localpart = rawValue.localpart + } + + public init?(string: String) { + guard let rawValue = MatrixUserIdentifier(string: string) + else { + return nil + } + guard let domain = rawValue.domain else { + return nil + } + self.domain = domain + localpart = rawValue.localpart + } + + public var rawValue: MatrixUserIdentifier { + .init(locapart: localpart, domain: domain) + } + + public var FQMXID: String { + "@\(localpart):\(domain)" + } + + public var description: String { + FQMXID + } + + public var id: String { + FQMXID + } + + public typealias RawValue = MatrixUserIdentifier + + public static func < (lhs: MatrixFullUserIdentifier, rhs: MatrixFullUserIdentifier) -> Bool { + lhs.FQMXID < rhs.FQMXID + } +} + +extension MatrixFullUserIdentifier: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + let id = MatrixFullUserIdentifier(string: rawValue) + if let id = id { + self = id + } else { + throw MatrixErrorCode.BadJSON } } diff --git a/Sources/MatrixCore/MatrixCore+Sync.swift b/Sources/MatrixCore/MatrixCore+Sync.swift new file mode 100644 index 0000000..c6399b1 --- /dev/null +++ b/Sources/MatrixCore/MatrixCore+Sync.swift @@ -0,0 +1,97 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 26.04.22. +// + +import Foundation +import MatrixClient +/* +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +public extension MatrixCore { + /// Start a task to sync + /// + /// - Throws: ``MatrixCoreError`` if sync is already running + func startSync() throws { + guard syncTask == nil else { + throw MatrixCoreError.syncAlreadyStarted + } + + syncTask = buildSyncTask() + } + + private nonisolated func buildSyncTask() -> Task { + Task(priority: .background) { [self] in + var parameters = MatrixSyncRequest.Parameters(timeout: 45 * 1000) + while true { + do { + parameters = try await self.runSync(parameters: parameters) + + parameters.presence = await self.presence + + try Task.checkCancellation() + } catch is CancellationError { + return + } catch { + MatrixCoreLogger.logger.fault("Sync task: \(error.localizedDescription)") + } + } + } + } + + nonisolated func runSync(parameters: MatrixSyncRequest.Parameters) async throws -> MatrixSyncRequest.Parameters { + var parameters = parameters + let client = await self.client + let sync = try await client.sync(parameters: parameters) + + try await parseSync(sync) + + parameters.since = sync.nextBatch + return parameters + } + + nonisolated func parseSync(_ sync: MatrixSync) async throws { + guard let rooms = sync.rooms else { + return + } + + var acountRooms: [String] = [] + + guard let joinedRooms = rooms.joined else { + return + } + + for (roomId, room) in joinedRooms { + try await parseRoom(roomId: roomId, room: room) + acountRooms.append(roomId) + } + + // TODO: save account <-> roomId mapping + } + + nonisolated func parseRoom(roomId: String, room: MatrixSync.JoinedRoom) async throws { + MatrixCoreLogger.logger.trace("Parsing room \(roomId)") + + if let timeline = room.timeline { + for event in timeline.events ?? [] { + if let event = event as? MatrixStateEvent { + try await store.addRoomState(state: .init(roomId: roomId, event: event)) + } + } + } + } + + /// Stop the sync task + /// + /// - Throws: ``MatrixCoreError`` if the sync task is not running + func stopSync() throws { + guard let syncTask = self.syncTask, + !syncTask.isCancelled + else { + throw MatrixCoreError.syncNotRunning + } + syncTask.cancel() + self.syncTask = nil + } +}*/ diff --git a/Sources/MatrixCore/MatrixCore.swift b/Sources/MatrixCore/MatrixCore.swift index 6686d85..46014b9 100644 --- a/Sources/MatrixCore/MatrixCore.swift +++ b/Sources/MatrixCore/MatrixCore.swift @@ -1,26 +1,94 @@ -import CoreData import Foundation import MatrixClient import OSLog +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +internal struct MatrixCoreLogger { + internal static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MatrixCore") +} + +@available(swift, introduced: 5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +@MainActor +public class MatrixCore { + public let store: T + public var info: T.AccountInfo + + public var client: MatrixClient + + // MARK: sync + + internal var syncTask: Task? + public var presence: MatrixSyncRequest.Parameters.Presence = .offline + + deinit { + // cancel unconditionally if the task is not nil + self.syncTask?.cancel() + } + + // MARK: - computed variables + + public var id: T.AccountInfo.AccountIdentifier { + info.id + } + + public var accessToken: String? { + get { + info.accessToken + } + set { + info.accessToken = newValue + client.accessToken = newValue + } + } + + public var mxID: MatrixFullUserIdentifier { + info.mxID + } + + public var FQMXID: String { + info.FQMXID + } + + public convenience init(store: T, accountID: T.AccountInfo.AccountIdentifier) async throws { + let info = try await store.getAccountInfo(accountID: accountID) + self.init(store: store, account: info) + } + + public init(store: T, account: T.AccountInfo) { + self.store = store + info = account + client = MatrixClient( + homeserver: account.homeServer, + urlSession: URLSession(configuration: .default), + accessToken: account.accessToken + ) + } + + // MARK: auth management + + /// Issue loggout request to Homeserver and remove account info from store. + public func logout() async throws { + do { + try await client.logout() + } catch let error as MatrixServerError { + if error.errcode == .UnknownToken { + MatrixCoreLogger.logger.warning("Token already unknown at homeserver. deleting account info.") + } else { + throw error + } + } + + try await store.deleteAccountInfo(account: info) + } +} + /* @available(swift, introduced: 5.5) @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) @MainActor public class MatrixCore { - public let context: NSManagedObjectContext - - // TODO: internal? - public var coreDataMatrixAccount: MatrixAccount - - public internal(set) var client: MatrixClient - public internal(set) var userID: MatrixUserIdentifier - - internal static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MatrixCore") - - // MARK: - Dynamic variables - public var displayName: String? { coreDataMatrixAccount.displayName } diff --git a/Sources/MatrixCore/MatrixCoreError.swift b/Sources/MatrixCore/MatrixCoreError.swift index f22668b..2d440d4 100644 --- a/Sources/MatrixCore/MatrixCoreError.swift +++ b/Sources/MatrixCore/MatrixCoreError.swift @@ -8,8 +8,11 @@ import Foundation public enum MatrixCoreError: Error { - case actorMissing case missingData + case syncAlreadyStarted + case syncNotRunning + + case actorMissing case creationError case batchInsertError case batchDeleteError @@ -21,13 +24,18 @@ public enum MatrixCoreError: Error { extension MatrixCoreError: LocalizedError { public var errorDescription: String? { switch self { - case .actorMissing: - return NSLocalizedString("Did not found MatrixCore instance to use for request", comment: "") case .missingData: return NSLocalizedString( - "Found and will discard a quake missing a valid code, magnitude, place, or time.", + "Data missing", comment: "" ) + case .syncAlreadyStarted: + return NSLocalizedString("Sync Task already started", comment: "MatrixCore.startSync()") + case .syncNotRunning: + return NSLocalizedString("Sync Task not running", comment: "MatrixCore.stopSync()") + + case .actorMissing: + return NSLocalizedString("Did not found MatrixCore instance to use for request", comment: "") case .creationError: return NSLocalizedString("Failed to create a new Quake object.", comment: "") case .batchInsertError: diff --git a/Sources/MatrixCore/MatrixStore.swift b/Sources/MatrixCore/MatrixStore.swift index b3e7e6e..b32d03b 100644 --- a/Sources/MatrixCore/MatrixStore.swift +++ b/Sources/MatrixCore/MatrixStore.swift @@ -9,105 +9,47 @@ import Foundation import MatrixClient import OSLog +@available(swift, introduced: 5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) public protocol MatrixStore { // MARK: - Account Info + static var extraKeychainArguments: [String: Any] { get } + /// Type for Account Informations. associatedtype AccountInfo: MatrixStoreAccountInfo + //associatedtype RoomState: MatrixStoreRoomState + //associatedtype AccountMapping: MatrixStoreAccountRoom - @available(swift, introduced: 5.5) - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func saveAccountInfo(account: AccountInfo) async throws + func saveAccountInfo(_ mxID: MatrixFullUserIdentifier, name: String, homeServer: MatrixHomeserver, deviceId: String, accessToken: String?, saveToKeychain: Bool, extraKeychainArguments: [String: Any]) async throws -> AccountInfo - @available(swift, introduced: 5.5) - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func getAccountInfo(accountID: MatrixUserIdentifier) async throws -> AccountInfo + func getAccountInfo(accountID: AccountInfo.AccountIdentifier) async throws -> AccountInfo - @available(swift, introduced: 5.5) - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func getAccountInfos() async throws -> [AccountInfo] -} - -public protocol MatrixStoreAccountInfo { - var name: String { get } - var displayName: String? { get } - var mxID: MatrixUserIdentifier { get } - var homeServer: MatrixHomeserver { get } - var accessToken: String? { get } -} - -// TODO: only on Darwin platforms -public extension MatrixStoreAccountInfo { - /// Load the ``accessToken`` from Keychain. - static func getFromKeychain(account: MatrixUserIdentifier, - extraKeychainArguments: [String: Any] = [:]) throws -> String - { - guard let userID = account.FQMXID, - let domain = account.domain - else { - throw MatrixError.NotFound - } - - var keychainQuery = extraKeychainArguments - keychainQuery[kSecClass as String] = kSecClassInternetPassword - keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne - keychainQuery[kSecAttrAccount as String] = userID - keychainQuery[kSecAttrServer as String] = domain - keychainQuery[kSecReturnAttributes as String] = true - keychainQuery[kSecReturnData as String] = true - var item: CFTypeRef? - let status = SecItemCopyMatching(keychainQuery as CFDictionary, &item) - guard status == errSecSuccess else { - throw MatrixCoreError.keychainError(status) - } + func deleteAccountInfo(account: AccountInfo) async throws - guard let existingItem = item as? [String: Any], - let tokenData = existingItem[kSecValueData as String] as? Data, - let token = String(data: tokenData, encoding: .utf8) - else { - throw MatrixCoreError.keychainError(errSecInvalidData) - } + // MARK: - Room - return token - } + // MARK: Account Room Mapping + /*func addAccountMapping(accountId: MatrixFullUserIdentifier, roomId: String) async throws + func addAccountMapping(_ mapping: AccountMapping) async throws + func getAccountMapping(accountId: MatrixFullUserIdentifier, roomId: String) async throws -> AccountMapping + func getRoomsForAccount(accountI: MatrixFullUserIdentifier) async throws -> [AccountMapping] + func getAccountsForRoom(roomId: MatrixFullUserIdentifier) async throws -> [AccountMapping]*/ - /// Save the ``accessToken`` to keychain, using the accountID data as identifier. - func saveToKeychain(extraKeychainArguments: [String: Any] = [:]) throws { - guard let userID = mxID.FQMXID, - let domain = mxID.domain, - let accessToken = self.accessToken?.data(using: .utf8) - else { - throw MatrixError.NotFound - } - var keychainInsertQuery = extraKeychainArguments - keychainInsertQuery[kSecClass as String] = kSecClassInternetPassword - keychainInsertQuery[kSecAttrAccount as String] = userID - keychainInsertQuery[kSecAttrServer as String] = domain - keychainInsertQuery[kSecValueData as String] = accessToken - let status = SecItemAdd(keychainInsertQuery as CFDictionary, nil) - guard status == errSecSuccess else { - throw MatrixCoreError.keychainError(status) - } - } - func deleteFromKeychain(extraKeychainArguments: [String: Any] = [:]) throws { - guard let userID = mxID.FQMXID, - let domain = mxID.domain - else { - throw MatrixError.NotFound - } + // MARK: Room State - var keychainQuery = extraKeychainArguments - keychainQuery[kSecClass as String] = kSecClassInternetPassword - keychainQuery[kSecAttrAccount as String] = userID - keychainQuery[kSecAttrServer as String] = domain + /*func addRoomState(state: RoomState) async throws - let status = SecItemDelete(keychainQuery as CFDictionary) - guard status == errSecSuccess else { - throw MatrixCoreError.keychainError(status) - } - } + func getRoomState(roomId: String) async throws -> [RoomState] + func getRoomState(eventId: String) async throws -> RoomState? + func getRoomState(roomId: String, stateType: String) async throws -> [RoomState] + func getRoomState(roomId: String, stateKey: String) async throws -> [RoomState] + func getRoomState(roomId: String, stateType: String, stateKey: String) async throws -> [RoomState] + */ } diff --git a/Sources/MatrixCore/MatrixStoreAccountInfo.swift b/Sources/MatrixCore/MatrixStoreAccountInfo.swift new file mode 100644 index 0000000..5c065d9 --- /dev/null +++ b/Sources/MatrixCore/MatrixStoreAccountInfo.swift @@ -0,0 +1,170 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 16.04.22. +// + +import Foundation +import MatrixClient + +let MXkSecAttrLabel: String = "dev.matrixcore.access_token" + +public protocol MatrixStoreAccountInfo { + associatedtype AccountIdentifier + + var id: AccountIdentifier { get } + + var name: String { get } + var displayName: String? { get set } + var mxID: MatrixFullUserIdentifier { get } + var homeServer: MatrixHomeserver { get } + var accessToken: String? { get set } + + var FQMXID: String { get } + + // Keychain functions + func saveToKeychain(extraKeychainArguments: [String: Any]) throws + static func getFromKeychain(account: MatrixFullUserIdentifier, + extraKeychainArguments: [String: Any]) throws -> String + func deleteFromKeychain(extraKeychainArguments: [String: Any]) throws + + mutating func loadAccessToken(extraKeychainArguments: [String: Any]) throws +} + +// TODO: only on Darwin platforms +public extension MatrixStoreAccountInfo { + static func addDefaultInfo(_ dict: [String: Any], mxID: MatrixFullUserIdentifier) -> [String: Any] { + var dict = dict + dict[kSecClass as String] = kSecClassGenericPassword + dict[kSecAttrAccount as String] = mxID.FQMXID + dict[kSecUseDataProtectionKeychain as String] = true + if dict[kSecAttrLabel as String] == nil { + dict[kSecAttrLabel as String] = MXkSecAttrLabel + } + + return dict + } + + /// Load the ``accessToken`` from Keychain. + static func getFromKeychain(account: MatrixFullUserIdentifier, + extraKeychainArguments: [String: Any] = [:]) throws -> String + { + var keychainQuery = Self.addDefaultInfo(extraKeychainArguments, mxID: account) + keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne + keychainQuery[kSecReturnAttributes as String] = true + keychainQuery[kSecReturnData as String] = true + + var item: CFTypeRef? + let status = SecItemCopyMatching(keychainQuery as CFDictionary, &item) + guard status == errSecSuccess else { + throw MatrixCoreError.keychainError(status) + } + + guard let existingItem = item as? [String: Any], + let tokenData = existingItem[kSecValueData as String] as? Data, + let token = String(data: tokenData, encoding: .utf8) + else { + throw MatrixCoreError.keychainError(errSecInvalidData) + } + + return token + } + + /// Save the ``accessToken`` to keychain, using the accountID data as identifier. + func saveToKeychain(extraKeychainArguments: [String: Any] = [:]) throws { + do { + try self.deleteFromKeychain(extraKeychainArguments: extraKeychainArguments) + } + + guard let accessToken = self.accessToken?.data(using: .utf8) + else { + throw MatrixErrorCode.NotFound + } + + var keychainInsertQuery = Self.addDefaultInfo(extraKeychainArguments, mxID: mxID) + keychainInsertQuery[kSecValueData as String] = accessToken + + let status = SecItemAdd(keychainInsertQuery as CFDictionary, nil) + guard status == errSecSuccess || status == errSecDuplicateItem else { + throw MatrixCoreError.keychainError(status) + } + } + + func deleteFromKeychain(extraKeychainArguments: [String: Any] = [:]) throws { + var keychainQuery = Self.addDefaultInfo(extraKeychainArguments, mxID: mxID) + keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne + keychainQuery[kSecReturnRef as String] = true + keychainQuery[kSecReturnAttributes as String] = true + + + /*var item: CFTypeRef? + var status = SecItemCopyMatching(keychainQuery as CFDictionary, &item) + guard status == errSecSuccess else { + throw MatrixCoreError.keychainError(status) + } + */ + + + let status = SecItemDelete(keychainQuery as CFDictionary) + guard status == errSecSuccess else { + throw MatrixCoreError.keychainError(status) + } + } + + internal var accessTokenTag: String { + Self.accessTokenTag(forId: mxID) + } + + internal static func accessTokenTag(forId: MatrixFullUserIdentifier) -> String { + "dev.matrixcore.keychain.\(forId.FQMXID.replacingOccurrences(of: "@", with: ""))" + } + + func getFromKeychain(extraKeychainArguments: [String: Any] = [:]) throws -> String { + try Self.getFromKeychain(account: self.mxID, extraKeychainArguments: extraKeychainArguments) + } + + mutating func loadAccessToken(extraKeychainArguments: [String: Any] = [:]) throws { + try self.accessToken = self.getFromKeychain(extraKeychainArguments: extraKeychainArguments) + } +} + +@available(swift, introduced: 5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +public extension MatrixStore { + /// Create ``MatrixCore`` instances from the account data saved in the store. + @MainActor + func getAccounts() async throws -> [MatrixCore] { + let accounts = try await getAccountInfos() + + var cores: [MatrixCore] = [] + + for account in accounts { + var account = account + try account.loadAccessToken(extraKeychainArguments: Self.extraKeychainArguments) + cores.append(MatrixCore(store: self, account: account)) + } + + return cores + } + + /// Create ``MatrixCore`` instance for the given MatrixUser ID with data from the store. + @MainActor + func getAccount(_ mxID: AccountInfo.AccountIdentifier) async throws -> MatrixCore { + let account = try await getAccountInfo(accountID: mxID) + + return MatrixCore(store: self, account: account) + } +} + +public extension MatrixStoreAccountInfo { + var FQMXID: String { + mxID.FQMXID + } +} + +public extension MatrixStoreAccountInfo where Self.AccountIdentifier == MatrixFullUserIdentifier { + var id: MatrixFullUserIdentifier { + mxID + } +} diff --git a/Sources/MatrixCore/MatrixStoreRoomState.swift b/Sources/MatrixCore/MatrixStoreRoomState.swift new file mode 100644 index 0000000..5674fba --- /dev/null +++ b/Sources/MatrixCore/MatrixStoreRoomState.swift @@ -0,0 +1,42 @@ +// +// File.swift +// +// +// Created by Finn Behrens on 24.04.22. +// + +import Foundation +import MatrixClient + +public protocol MatrixStoreRoomState { + var eventId: String { get } + var roomId: String { get } + var stateKey: String { get } + var sender: MatrixFullUserIdentifier? { get } + var content: MatrixStateEventType { get } + + init(roomId: String, event: MatrixStateEvent) throws +} + +public protocol MatrixStoreAccountRoom { + var accountId: MatrixFullUserIdentifier { get } + var roomId: MatrixFullUserIdentifier { get } + var localMuted: Bool { get } + + init(accountId: MatrixFullUserIdentifier, roomId: String) +} + +public extension MatrixStoreAccountRoom { + var localMuted: Bool { true } +} + +/* +@available(swift, introduced: 5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +public extension MatrixStore { + func addAccountMapping(accountId: MatrixFullUserIdentifier, roomId: String) async throws { + let mapping = AccountMapping(accountId: accountId, roomId: roomId) + try await self.addAccountMapping(mapping) + } +} +*/ diff --git a/Tests/MatrixClientTests/ApiAuthInteractiveTests.swift b/Tests/MatrixClientTests/ApiAuthInteractiveTests.swift index d6ad383..39fcf7e 100644 --- a/Tests/MatrixClientTests/ApiAuthInteractiveTests.swift +++ b/Tests/MatrixClientTests/ApiAuthInteractiveTests.swift @@ -11,11 +11,11 @@ import XCTest final class ApiAuthInteractiveTests: XCTestCase { var flow = MatrixInteractiveAuth(flows: [ - MatrixInteractiveAuth.Flow(stages: [MatrixLoginFlow.recaptcha, MatrixLoginFlow.terms, MatrixLoginFlow.email]), + MatrixInteractiveAuth.Flow(stages: [MatrixLoginFlowType.recaptcha, MatrixLoginFlowType.terms, MatrixLoginFlowType.email]), MatrixInteractiveAuth - .Flow(stages: [MatrixLoginFlow.recaptcha, MatrixLoginFlow.token, MatrixLoginFlow.oauth2, - MatrixLoginFlow.email]), - ], params: [:], session: nil, completed: [MatrixLoginFlow.email, MatrixLoginFlow.terms], error: nil, errcode: nil) + .Flow(stages: [MatrixLoginFlowType.recaptcha, MatrixLoginFlowType.token, MatrixLoginFlowType.oauth2, + MatrixLoginFlowType.email]), + ], params: [:], session: nil, completed: [MatrixLoginFlowType.email, MatrixLoginFlowType.terms]) func testIsOptional() throws { XCTAssertTrue(flow.isOptional(.recaptcha)) diff --git a/Tests/MatrixClientTests/ApiCapabilitiesTests.swift b/Tests/MatrixClientTests/ApiCapabilitiesTests.swift index 2902523..734801a 100644 --- a/Tests/MatrixClientTests/ApiCapabilitiesTests.swift +++ b/Tests/MatrixClientTests/ApiCapabilitiesTests.swift @@ -52,7 +52,7 @@ final class ApiCapabilityTests: XCTestCase { .value as? [String: Any], let ratelimit = ratelimitContainer["max_requests_per_hour"] as? Int else { - throw MatrixError.BadJSON + throw MatrixErrorCode.BadJSON } XCTAssertEqual(ratelimit, 600) diff --git a/Tests/MatrixClientTests/ContentTests.swift b/Tests/MatrixClientTests/ContentTests.swift new file mode 100644 index 0000000..ac82d8c --- /dev/null +++ b/Tests/MatrixClientTests/ContentTests.swift @@ -0,0 +1,16 @@ +// +// ContentTests.swift +// +// +// Created by Finn Behrens on 22.06.22. +// + +import XCTest +@testable import MatrixClient + +final class ContentURITests: XCTestCase { + + func testGetComponents() throws { + let uri = MatrixContentURL(string: "mxc://example.com/id") + } +} diff --git a/Tests/MatrixClientTests/ErrorTests.swift b/Tests/MatrixClientTests/ErrorTests.swift new file mode 100644 index 0000000..2b6963e --- /dev/null +++ b/Tests/MatrixClientTests/ErrorTests.swift @@ -0,0 +1,55 @@ +// +// ErrorTests.swift +// +// +// Created by Finn Behrens on 24.04.22. +// + +@testable import MatrixClient +import XCTest + +class ErrorTests: XCTestCase { + func testDoesNotContain() throws { + XCTAssertFalse(MatrixServerError.KnownCodingKeys.doesNotContain(.init(stringValue: "error")!)) + XCTAssertFalse(MatrixServerError.KnownCodingKeys.doesNotContain(.init(stringValue: "flows")!)) + } + + func testExample() throws { + let data = Data(""" + { + "session": "session_id", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "recaptha_public_key" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https://example.com/_matrix/consent?v=1.0" + } + } + } + } + } + } + """.utf8) + + let error = try MatrixServerError(json: data, code: 401) + + XCTAssertNotNil(error.interactiveAuth) + XCTAssertEqual(error.interactiveAuth?.session, "session_id") + XCTAssertEqual(error.interactiveAuth?.flows.count, 1) + } +} diff --git a/Tests/MatrixClientTests/UserIdentifierTests.swift b/Tests/MatrixClientTests/UserIdentifierTests.swift index 936fcf1..31369ad 100644 --- a/Tests/MatrixClientTests/UserIdentifierTests.swift +++ b/Tests/MatrixClientTests/UserIdentifierTests.swift @@ -31,4 +31,12 @@ final class UserIdentifierTests: XCTestCase { XCTAssertEqual(userIdentifier.rawValue, "localpart") XCTAssertNil(userIdentifier.FQMXID) } + + func testFullId() { + let userIdentifier = MatrixFullUserIdentifier(string: "@localpart:example.com")! + + XCTAssertEqual(userIdentifier.localpart, "localpart") + XCTAssertEqual(userIdentifier.domain, "example.com") + XCTAssertEqual(userIdentifier.FQMXID, "@localpart:example.com") + } }