diff --git a/Sources/MatrixClient/API/Events/Membership.swift b/Sources/MatrixClient/API/Events/Membership.swift index 1547af5..c08b9ff 100644 --- a/Sources/MatrixClient/API/Events/Membership.swift +++ b/Sources/MatrixClient/API/Events/Membership.swift @@ -1,12 +1,12 @@ import Foundation -public enum Membership: String, Decodable { +public enum MatrixMembership: String, Codable { case invite, join, knock, leave, ban, unknown // implement a custom decoder that will decode as unknown if // the string received can't be decoded as one of the cases public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - self = Membership(rawValue: (try? container.decode(String.self)) ?? "") ?? .unknown + self = MatrixMembership(rawValue: (try? container.decode(String.self)) ?? "") ?? .unknown } } diff --git a/Sources/MatrixClient/API/Events/Relationship.swift b/Sources/MatrixClient/API/Events/Relationship.swift index b116229..e12e16b 100644 --- a/Sources/MatrixClient/API/Events/Relationship.swift +++ b/Sources/MatrixClient/API/Events/Relationship.swift @@ -1,6 +1,6 @@ import Foundation -public struct Relationship: Codable { +public struct MatrixRelationship: Codable { public let type: RelationshipType? public let eventID: String? public let key: String? diff --git a/Sources/MatrixClient/API/Events/Room/MessageContent.swift b/Sources/MatrixClient/API/Events/Room/MessageContent.swift index a85e1d5..dee81d8 100644 --- a/Sources/MatrixClient/API/Events/Room/MessageContent.swift +++ b/Sources/MatrixClient/API/Events/Room/MessageContent.swift @@ -1,12 +1,12 @@ import Foundation -public struct MessageContent: Decodable { +public struct MatrixMessageContent: Codable { // required public let body: String? public let type: MessageType? // optional - public let relationship: Relationship? + public let relationship: MatrixRelationship? public let mediaURL: URL? public let mediaInfo: MediaInfo? @@ -22,7 +22,7 @@ public struct MessageContent: Decodable { case newContent = "m.new_content" } - public struct MediaInfo: Decodable { + public struct MediaInfo: Codable { public let width: Int? public let height: Int? public let mimetype: String? @@ -36,11 +36,11 @@ public struct MessageContent: Decodable { } } - public struct NewContent: Decodable { + public struct NewContent: Codable { public let body: String? } - public enum MessageType: String, Decodable { + public enum MessageType: String, Codable { case text = "m.text" case emote = "m.emote" case notice = "m.notice" diff --git a/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift index 9725d02..c7c7370 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomMessageEvent.swift @@ -1,10 +1,10 @@ import Foundation import AnyCodable -public struct RoomMessageEvent: RoomEvent { +public struct MatrixMessageEvent: MatrixEvent { public static var type = "m.room.message" - public let content: MessageContent + public let content: MatrixMessageContent public let type: String public let eventID: String public let sender: String diff --git a/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift index d0e44cd..73e4b11 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomReactionEvent.swift @@ -1,7 +1,7 @@ import Foundation import AnyCodable -public struct RoomReactionEvent: RoomEvent { +public struct MatrixReactionEvent: MatrixEvent { public static var type = "m.reaction" public let content: Content @@ -20,8 +20,8 @@ public struct RoomReactionEvent: RoomEvent { case unsigned } - public struct Content: Decodable { - public let relationship: Relationship? + public struct Content: Codable { + public let relationship: MatrixRelationship? enum CodingKeys: String, CodingKey { case relationship = "m.relates_to" diff --git a/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift b/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift index b0b0829..1132245 100644 --- a/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift +++ b/Sources/MatrixClient/API/Events/Room/RoomRedactionEvent.swift @@ -1,7 +1,7 @@ import Foundation import AnyCodable -public struct RoomRedactionEvent: RoomEvent { +public struct MatrixRedactionEvent: MatrixEvent { public static var type = "m.room.redaction" public let content: Content @@ -23,7 +23,7 @@ public struct RoomRedactionEvent: RoomEvent { case redacts } - public struct Content: Decodable { + public struct Content: Codable { public let reason: String? } } diff --git a/Sources/MatrixClient/API/Events/RoomEvent.swift b/Sources/MatrixClient/API/Events/RoomEvent.swift index a2f2f14..d1e27ef 100644 --- a/Sources/MatrixClient/API/Events/RoomEvent.swift +++ b/Sources/MatrixClient/API/Events/RoomEvent.swift @@ -4,7 +4,7 @@ import AnyCodable /// A protocol that all room event structs should conform to in order /// to be decoded by the client. Don't forget to add any new structs /// to `Client.eventTypes` for included in the JSON decoder. -public protocol RoomEvent: Decodable { +public protocol MatrixEvent: Codable { static var type: String { get } // var content: Content { get } @@ -15,45 +15,47 @@ public protocol RoomEvent: Decodable { } /// The coding keys needed to determine an event's type before decoding. -enum RoomEventTypeKeys: CodingKey { +enum MatrixEventTypeKeys: CodingKey { case type } -enum RoomEventDecodableError: Error { +enum MatrixEventCodableError: Error { case missingTypes case unableToFindType(String) - case unableToCast(decoded: RoomEvent?, into: String) + case unableToCast(decoded: MatrixEvent?, into: String) } extension KeyedDecodingContainer { - // The synthesized decoding for RoomEventArray will throw if the key is missing. This fixes that. - func decode(_ type: DecodableRoomEvents.Type, forKey key: Self.Key) throws -> DecodableRoomEvents { - return try decodeIfPresent(type, forKey: key) ?? DecodableRoomEvents(wrappedValue: nil) + // The synthesized decoding for MatrixCodableEvents will throw if the key is missing. This fixes that. + func decode(_ type: MatrixCodableEvents.Type, forKey key: Self.Key) throws -> MatrixCodableEvents { + return try decodeIfPresent(type, forKey: key) ?? MatrixCodableEvents(wrappedValue: nil) } } extension CodingUserInfoKey { - /// The key used to determing the types of `RoomEvent` that can be decoded. - static var roomEventTypes: CodingUserInfoKey { - CodingUserInfoKey(rawValue: "uk.pixlwave.RoomEventTypes")! + /// The key used to determine the types of `MatrixEvent` that can be decoded. + static var matrixEventTypes: CodingUserInfoKey { + CodingUserInfoKey(rawValue: "MatrixCore.EventTypes")! } } +// TODO: encodable @propertyWrapper -public struct DecodableRoomEvents: Decodable where Value.Element == RoomEvent { +public struct MatrixCodableEvents: Codable where Value.Element == MatrixEvent { public var wrappedValue: Value? - private struct RoomEventWrapper: Decodable { + // TODO: encodable? + private struct EventWrapper: Codable { var wrappedEvent: T? init(from decoder: Decoder) throws { // these can throw as something has gone seriously wrong if the type key is missing - let container = try decoder.container(keyedBy: RoomEventTypeKeys.self) + let container = try decoder.container(keyedBy: MatrixEventTypeKeys.self) let typeID = try container.decode(String.self, forKey: .type) - guard let types = decoder.userInfo[.roomEventTypes] as? [RoomEvent.Type] else { + guard let types = decoder.userInfo[.matrixEventTypes] as? [MatrixEvent.Type] else { // the decoder must be supplied with some event types to decode - throw RoomEventDecodableError.missingTypes + throw MatrixEventCodableError.missingTypes } guard let matchingType = types.first(where: { $0.type == typeID }) else { @@ -62,17 +64,21 @@ public struct DecodableRoomEvents: Decodable where Value.Elem } guard let decoded = try? matchingType.init(from: decoder) else { - assertionFailure("Failed to decode RoomEvent as \(String(describing: T.self))") + 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 RoomEventDecodableError.unableToCast(decoded: decoded, into: String(describing: T.self)) + throw MatrixEventCodableError.unableToCast(decoded: decoded, into: String(describing: T.self)) } self.wrappedEvent = decoded } + + func encode(to encoder: Encoder) throws { + fatalError("Event encoding not implemented.") + } } @@ -84,9 +90,13 @@ public struct DecodableRoomEvents: Decodable where Value.Elem public init(from decoder: Decoder) { guard let container = try? decoder.singleValueContainer(), - let wrappers = try? container.decode([RoomEventWrapper].self) + let wrappers = try? container.decode([EventWrapper].self) else { return } wrappedValue = wrappers.compactMap(\.wrappedEvent) as? Value } + + public func encode(to encoder: Encoder) throws { + fatalError("Event encoding not implemented.") + } } diff --git a/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift b/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift index 033deb2..e26f424 100644 --- a/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift +++ b/Sources/MatrixClient/API/Events/State/RoomEncryptionEvent.swift @@ -1,7 +1,7 @@ import Foundation import AnyCodable -public struct RoomEncryptionEvent: RoomEvent { +public struct MatrixEncryptionEvent: MatrixEvent { public static var type = "m.room.encryption" public let content: Content @@ -23,6 +23,6 @@ public struct RoomEncryptionEvent: RoomEvent { case stateKey = "state_key" } - public struct Content: Decodable { } + public struct Content: Codable { } } diff --git a/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift b/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift index 6ed777d..c642982 100644 --- a/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift +++ b/Sources/MatrixClient/API/Events/State/RoomMemberEvent.swift @@ -1,7 +1,7 @@ import Foundation import AnyCodable -public struct RoomMemberEvent: RoomEvent { +public struct MatrixMemberEvent: MatrixEvent { public static var type = "m.room.member" public let content: Content @@ -23,10 +23,10 @@ public struct RoomMemberEvent: RoomEvent { case stateKey = "state_key" } - public struct Content: Decodable { + public struct Content: Codable { public let avatarURL: String? public let displayName: String? - public let membership: Membership? + public let membership: MatrixMembership? public let isDirect: Bool? enum CodingKeys: String, CodingKey { diff --git a/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift b/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift index aaec37c..8a279b2 100644 --- a/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift +++ b/Sources/MatrixClient/API/Events/State/RoomNameEvent.swift @@ -1,7 +1,7 @@ import Foundation import AnyCodable -public struct RoomNameEvent: RoomEvent { +public struct MatrixNameEvent: MatrixEvent { public static var type = "m.room.name" public let content: Content @@ -23,7 +23,7 @@ public struct RoomNameEvent: RoomEvent { case stateKey = "state_key" } - public struct Content: Decodable { + public struct Content: Codable { public let name: String? } } diff --git a/Sources/MatrixClient/API/Filter.swift b/Sources/MatrixClient/API/Filter.swift index d431a76..9649a90 100644 --- a/Sources/MatrixClient/API/Filter.swift +++ b/Sources/MatrixClient/API/Filter.swift @@ -26,7 +26,7 @@ public struct MatrixFilterRequest: MatrixRequest { public typealias URLParameters = MatrixFilterId - public func path(with parameters: MatrixFilterId) throws -> String { + public func components(for homeserver: MatrixHomeserver, with parameters: MatrixFilterId) throws -> URLComponents { // FIXME: url encode guard let userId = parameters.user, @@ -34,7 +34,10 @@ public struct MatrixFilterRequest: MatrixRequest { else { throw MatrixError.Unrecognized } - return "/_matrix/client/r0/user/\(userId)/filter/\(filterId)" + + var components = homeserver.url + components.path = "/_matrix/client/r0/user/\(userId)/filter/\(filterId)" + return components } public static var httpMethod = HttpMethod.GET @@ -245,9 +248,11 @@ extension MatrixFilter: MatrixRequest { public typealias URLParameters = String /// with -> userId - public func path(with parameters: String) -> String { + public func components(for homeserver: MatrixHomeserver, with parameters: String) -> URLComponents { // TODO: url encode - return "/_matrix/client/r0/user/\(parameters)/filter" + var components = homeserver.url + components.path = "/_matrix/client/r0/user/\(parameters)/filter" + return components } public static var httpMethod = HttpMethod.POST diff --git a/Sources/MatrixClient/API/HomeServer.swift b/Sources/MatrixClient/API/HomeServer.swift index 557d82b..b00ab64 100644 --- a/Sources/MatrixClient/API/HomeServer.swift +++ b/Sources/MatrixClient/API/HomeServer.swift @@ -34,8 +34,10 @@ public struct MatrixServerInfoRequest: MatrixRequest { public typealias URLParameters = () - public func path(with parameters: ()) -> String { - return "/_matrix/client/versions" + public func components(for homeserver: MatrixHomeserver, with parameters: ()) -> URLComponents { + var components = homeserver.url + components.path = "/_matrix/client/versions" + return components } public static var httpMethod = HttpMethod.GET @@ -62,8 +64,10 @@ public struct MatrixWellKnownRequest: MatrixRequest { public typealias URLParameters = () - public func path(with parameters: ()) -> String { - return "/.well-known/matrix/client" + public func components(for homeserver: MatrixHomeserver, with parameters: ()) -> URLComponents { + var components = homeserver.url + components.path = "/.well-known/matrix/client" + return components } public static var httpMethod = HttpMethod.GET diff --git a/Sources/MatrixClient/API/Login.swift b/Sources/MatrixClient/API/Login.swift index a69ad40..541c8fe 100644 --- a/Sources/MatrixClient/API/Login.swift +++ b/Sources/MatrixClient/API/Login.swift @@ -12,8 +12,10 @@ public struct MatrixLoginFlowRequest: MatrixRequest { public typealias URLParameters = () - public func path(with parameters: ()) -> String { - return "/_matrix/client/r0/login" + public func components(for homeserver: MatrixHomeserver, with parameters: ()) -> URLComponents { + var components = homeserver.url + components.path = "/_matrix/client/r0/login" + return components } public static var httpMethod = HttpMethod.GET @@ -241,8 +243,10 @@ extension MatrixLoginRequest: MatrixRequest { public typealias URLParameters = () - public func path(with parameters: ()) -> String { - return "/_matrix/client/r0/login" + public func components(for homeserver: MatrixHomeserver, with parameters: ()) -> URLComponents { + var components = homeserver.url + components.path = "/_matrix/client/r0/login" + return components } public static var httpMethod = HttpMethod.POST diff --git a/Sources/MatrixClient/API/Logout.swift b/Sources/MatrixClient/API/Logout.swift index 3929469..f41935a 100644 --- a/Sources/MatrixClient/API/Logout.swift +++ b/Sources/MatrixClient/API/Logout.swift @@ -12,12 +12,16 @@ public struct MatrixLogoutRequest: MatrixRequest { public typealias URLParameters = Bool - public func path(with all: Bool) -> String { + public func components(for homeserver: MatrixHomeserver, with all: Bool) -> URLComponents { + var components = homeserver.url + if all { - return "/_matrix/client/r0/logout/all" + components.path = "/_matrix/client/r0/logout/all" } else { - return "/_matrix/client/r0/logout" + components.path = "/_matrix/client/r0/logout" } + + return components } public static var httpMethod = HttpMethod.POST diff --git a/Sources/MatrixClient/API/Request.swift b/Sources/MatrixClient/API/Request.swift index be1751b..7e9218d 100644 --- a/Sources/MatrixClient/API/Request.swift +++ b/Sources/MatrixClient/API/Request.swift @@ -13,14 +13,14 @@ public enum HttpMethod: String, CaseIterable { case PUT = "PUT" case PATCH = "PATCH" - static var containsBoddy: [Self] = [ .POST, .PUT, .PATCH ] + static var containsBody: [Self] = [ .POST, .PUT, .PATCH ] } public protocol MatrixRequest: Codable { associatedtype Response: MatrixResponse associatedtype URLParameters - func path(with parameters: URLParameters) throws -> String + func components(for homeserver: MatrixHomeserver, with parameters: URLParameters) throws -> URLComponents static var httpMethod: HttpMethod { get } static var requiresAuth: Bool { get } @@ -30,7 +30,7 @@ public protocol MatrixRequest: Codable { public extension MatrixRequest { func request(on homeserver: MatrixHomeserver, withToken token: String? = nil, with parameters: URLParameters) throws -> URLRequest { - let components = homeserver.path(try self.path(with: parameters)) + let components = try components(for: homeserver, with: parameters) // components.queryItems = self.queryParameters var urlRequest = URLRequest(url: components.url!) @@ -43,7 +43,7 @@ public extension MatrixRequest { } urlRequest.httpMethod = Self.httpMethod.rawValue - if HttpMethod.containsBoddy.contains(Self.httpMethod) { + if HttpMethod.containsBody.contains(Self.httpMethod) { urlRequest.httpBody = try? JSONEncoder().encode(self) } @@ -74,6 +74,8 @@ public protocol MatrixResponse: Codable { public extension MatrixResponse { init(fromMatrixRequestData data: Data) throws { let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .millisecondsSince1970 + decoder.userInfo[.matrixEventTypes] = MatrixClient.eventTypes self = try decoder.decode(Self.self, from: data) } } diff --git a/Sources/MatrixClient/API/Sync.swift b/Sources/MatrixClient/API/Sync.swift new file mode 100644 index 0000000..626ca2e --- /dev/null +++ b/Sources/MatrixClient/API/Sync.swift @@ -0,0 +1,152 @@ +import Foundation + +public struct MatrixSyncRequest: MatrixRequest { + public func components(for homeserver: MatrixHomeserver, with parameters: URLParameters) throws -> URLComponents { + var components = homeserver.url + components.path = "/_matrix/client/r0/sync" + + var queryItems = [URLQueryItem]() + + if let filter = parameters.filter { + queryItems.append(URLQueryItem(name: "filter", value: filter)) + } + + if let since = parameters.since { + queryItems.append(URLQueryItem(name: "since", value: since)) + } + + // TODO: fullState + // TODO: presence + + if let timeout = parameters.timeout { + queryItems.append(URLQueryItem(name: "timeout", value: String(timeout))) + } + + components.queryItems = queryItems + + return components + } + + public typealias Response = MatrixSyncResponse + + public typealias URLParameters = Parameters + + public static var httpMethod: HttpMethod = .GET + + 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 enum Presence: String { + case online + case offline + case unavailable + } + + public init(filter: String? = nil, + since: String? = nil, + fullState: Bool? = nil, + presence: MatrixSyncRequest.Parameters.Presence? = nil, + timeout: Int? = nil) { + self.filter = filter + self.since = since + self.fullState = fullState + self.presence = presence + self.timeout = timeout + } + } +} + +public struct MatrixSyncResponse: MatrixResponse { + public let nextBatch: String + public let rooms: Rooms? + // public let presence: Presence + // public let account_data: AccountData + // public let to_device: ToDevice + // public let device_lists: DeviceLists + // public let device_one_time_keys_count: OneTimeKeysCount + + enum CodingKeys: String, CodingKey { + case nextBatch = "next_batch" + case rooms + } + + public struct Rooms: Codable { + public let joined: [String: JoinedRoom]? + // public let invite: [String: InvitedRoom]? + public let left: [String: LeftRoom]? + + enum CodingKeys: String, CodingKey { + case joined = "join" + case left = "leave" + } + } + + public struct JoinedRoom: Codable { + public let summary: RoomSummary? + public let state: State? + public let timeline: Timeline? + // public let ephemeral: Ephemeral? + // public let account_data: AccountData? + public let unreadNotifications: UnreadNotificationCounts? + + enum CodingKeys: String, CodingKey { + case summary + case state + case timeline + case unreadNotifications = "unread_notifications" + } + + public struct RoomSummary: Codable { + public let heroes: [String]? + public let joinedMemberCount: Int? + public let invitedMemberCount: Int? + + enum CodingKeys: String, CodingKey { + case heroes = "m.heroes" + case joinedMemberCount = "m.joined_member_count" + case invitedMemberCount = "m.invited_member_count" + } + } + + public struct State: Codable { + @MatrixCodableEvents + public var events: [MatrixEvent]? + } + + public struct Timeline: Codable { + @MatrixCodableEvents + public var events: [MatrixEvent]? + public let isLimited: Bool? + public let previousBatch: String? + + enum CodingKeys: String, CodingKey { + case events + case isLimited = "limited" + case previousBatch = "prev_batch" + } + } + + public struct UnreadNotificationCounts: Codable { + public let highlightCount: Int? + public let notificationCount: Int? + + enum CodingKeys: String, CodingKey { + case highlightCount = "highlight_count" + case notificationCount = "notification_count" + } + } + } + + public struct LeftRoom: Codable { + // public let state: State + // public let timeline: Timeline + // public let account_data: AccountData + } + +} diff --git a/Sources/MatrixClient/MatrixClient.swift b/Sources/MatrixClient/MatrixClient.swift index f7318e2..00beb32 100644 --- a/Sources/MatrixClient/MatrixClient.swift +++ b/Sources/MatrixClient/MatrixClient.swift @@ -9,11 +9,36 @@ import Foundation public struct MatrixClient { public var homeserver: MatrixHomeserver - public var urlSession: URLSession = URLSession(configuration: .default) + public var urlSession: URLSession /// Access token used to authorize api requests public var accessToken: String? - // 2.1 GET /_maftrix/client/versions + /// An array containing all of the event types that the client should decode. + /// By default this contains all of the events implemented within the library. + /// + /// Add any custom events you would like decoded to this array. + public static var eventTypes: [MatrixEvent.Type] = [ + MatrixEncryptionEvent.self, + MatrixMemberEvent.self, + MatrixMessageEvent.self, + MatrixNameEvent.self, + MatrixReactionEvent.self, + MatrixRedactionEvent.self + ] + + /// Initializes a Matrix client object with the specified parameters. + /// - Parameters: + /// - homeserver: The homeserver the client should contact. + /// - urlSession: An optional `URLSession` to use for requests. A new session is created if `nil`. + /// - accessToken: An optional access token to use for requests if already logged in. + public init(homeserver: MatrixHomeserver, urlSession: URLSession = URLSession(configuration: .default), accessToken: String? = nil) { + self.homeserver = homeserver + self.urlSession = urlSession + self.accessToken = accessToken + } + + + // 2.1 GET /_matrix/client/versions /// Gets the versions of the specification supported by the server. /// /// Values will take the form `rX.Y.Z`. @@ -195,6 +220,12 @@ public struct MatrixClient { try await MatrixFilterRequest() .response(on: homeserver, withToken: accessToken, with: id, withUrlSession: urlSession) } + + @available(swift, introduced: 5.5) + public func sync(parameters: MatrixSyncRequest.Parameters) async throws -> MatrixSyncResponse { + try await MatrixSyncRequest() + .response(on: homeserver, withToken: accessToken, with: parameters) + } }