|
| 1 | +import ArgumentParser |
| 2 | +import Foundation |
| 3 | +import MatrixRustSDK |
| 4 | +import KeychainAccess |
| 5 | + |
| 6 | +class Walkthrough { |
| 7 | + // MARK: - Step 1 |
| 8 | + // Authenticate the user. |
| 9 | + |
| 10 | + var client: Client! |
| 11 | + |
| 12 | + func step1Login() async throws -> WalkthroughUser { |
| 13 | + let storeID = UUID().uuidString |
| 14 | + |
| 15 | + // Create a client for a particular homeserver. |
| 16 | + // Note that we can pass a server name (the second part of a Matrix user ID) instead of the direct URL. |
| 17 | + // This allows the SDK to discover the homeserver's well-known configuration for Sliding Sync support. |
| 18 | + let client = try await ClientBuilder() |
| 19 | + .serverNameOrHomeserverUrl(serverNameOrUrl: "matrix.org") |
| 20 | + .sessionPaths(dataPath: URL.sessionData(for: storeID).path(percentEncoded: false), |
| 21 | + cachePath: URL.sessionCaches(for: storeID).path(percentEncoded: false)) |
| 22 | + .slidingSyncVersionBuilder(versionBuilder: .discoverNative) |
| 23 | + .build() |
| 24 | + |
| 25 | + // Login using password authentication. |
| 26 | + try await client.login(username: "alice", password: "secret", initialDeviceName: nil, deviceId: nil) |
| 27 | + |
| 28 | + self.client = client |
| 29 | + |
| 30 | + // This data should be stored securely in the keychain. |
| 31 | + return try WalkthroughUser(session: client.session(), storeID: storeID) |
| 32 | + } |
| 33 | + |
| 34 | + // Or, if the user has previously authenticated we can restore their session instead. |
| 35 | + |
| 36 | + func step1Restore(_ walkthroughUser: WalkthroughUser) async throws { |
| 37 | + let session = walkthroughUser.session |
| 38 | + let sessionID = walkthroughUser.storeID |
| 39 | + |
| 40 | + // Build a client for the homeserver. |
| 41 | + let client = try await ClientBuilder() |
| 42 | + .sessionPaths(dataPath: URL.sessionData(for: sessionID).path(percentEncoded: false), |
| 43 | + cachePath: URL.sessionCaches(for: sessionID).path(percentEncoded: false)) |
| 44 | + .homeserverUrl(url: session.homeserverUrl) |
| 45 | + .build() |
| 46 | + |
| 47 | + // Restore the client using the session. |
| 48 | + try await client.restoreSession(session: session) |
| 49 | + |
| 50 | + self.client = client |
| 51 | + } |
| 52 | + |
| 53 | + // MARK: - Step 2 |
| 54 | + // Build the room list. |
| 55 | + |
| 56 | + class AllRoomsListener: RoomListEntriesListener { |
| 57 | + /// The user's list of rooms. |
| 58 | + var rooms: [Room] = [] |
| 59 | + |
| 60 | + func onUpdate(roomEntriesUpdate: [RoomListEntriesUpdate]) { |
| 61 | + // Update the user's room list on each update. |
| 62 | + for update in roomEntriesUpdate { |
| 63 | + switch update { |
| 64 | + case .append(let values): |
| 65 | + rooms.append(contentsOf: values) |
| 66 | + case .clear: |
| 67 | + rooms.removeAll() |
| 68 | + case .pushFront(let room): |
| 69 | + rooms.insert(room, at: 0) |
| 70 | + case .pushBack(let room): |
| 71 | + rooms.append(room) |
| 72 | + case .popFront: |
| 73 | + rooms.removeFirst() |
| 74 | + case .popBack: |
| 75 | + rooms.removeLast() |
| 76 | + case .insert(let index, let room): |
| 77 | + rooms.insert(room, at: Int(index)) |
| 78 | + case .set(let index, let room): |
| 79 | + rooms[Int(index)] = room |
| 80 | + case .remove(let index): |
| 81 | + rooms.remove(at: Int(index)) |
| 82 | + case .truncate(let length): |
| 83 | + rooms.removeSubrange(Int(length)..<rooms.count) |
| 84 | + case .reset(values: let values): |
| 85 | + rooms = values |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + var syncService: SyncService! |
| 92 | + var roomListService: RoomListService! |
| 93 | + var allRoomsListener: AllRoomsListener! |
| 94 | + var roomListEntriesHandle: RoomListEntriesWithDynamicAdaptersResult! |
| 95 | + |
| 96 | + func step2StartSync() async throws { |
| 97 | + // Create a sync service which controls the sync loop. |
| 98 | + syncService = try await client.syncService().finish() |
| 99 | + |
| 100 | + // Listen to room list updates. |
| 101 | + allRoomsListener = AllRoomsListener() |
| 102 | + roomListService = syncService.roomListService() |
| 103 | + roomListEntriesHandle = try await roomListService.allRooms().entriesWithDynamicAdapters(pageSize: 100, listener: allRoomsListener) |
| 104 | + _ = roomListEntriesHandle.controller().setFilter(kind: .all(filters: [])) |
| 105 | + |
| 106 | + // Start the sync loop. |
| 107 | + await syncService.start() |
| 108 | + } |
| 109 | + |
| 110 | + // MARK: - Step 3 |
| 111 | + // Create a timeline. |
| 112 | + |
| 113 | + class TimelineItemListener: TimelineListener { |
| 114 | + /// The loaded items for this room's timeline |
| 115 | + var timelineItems: [TimelineItem] = [] |
| 116 | + |
| 117 | + func onUpdate(diff: [TimelineDiff]) { |
| 118 | + // Update the timeline items on each update. |
| 119 | + for update in diff { |
| 120 | + switch update { |
| 121 | + case .append(let values): |
| 122 | + timelineItems.append(contentsOf: values) |
| 123 | + case .clear: |
| 124 | + timelineItems.removeAll() |
| 125 | + case .pushFront(let room): |
| 126 | + timelineItems.insert(room, at: 0) |
| 127 | + case .pushBack(let room): |
| 128 | + timelineItems.append(room) |
| 129 | + case .popFront: |
| 130 | + timelineItems.removeFirst() |
| 131 | + case .popBack: |
| 132 | + timelineItems.removeLast() |
| 133 | + case .insert(let index, let room): |
| 134 | + timelineItems.insert(room, at: Int(index)) |
| 135 | + case .set(let index, let room): |
| 136 | + timelineItems[Int(index)] = room |
| 137 | + case .remove(let index): |
| 138 | + timelineItems.remove(at: Int(index)) |
| 139 | + case .truncate(let length): |
| 140 | + timelineItems.removeSubrange(Int(length)..<timelineItems.count) |
| 141 | + case .reset(values: let values): |
| 142 | + timelineItems = values |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + var timeline: Timeline! |
| 149 | + var timelineItemsListener: TimelineItemListener! |
| 150 | + var timelineHandle: TaskHandle! |
| 151 | + |
| 152 | + func step3LoadRoomTimeline() async throws { |
| 153 | + let roomID = "!someroomid:matrix.org" |
| 154 | + |
| 155 | + // Wait for the rooms array to contain the desired room… |
| 156 | + while !allRoomsListener.rooms.contains(where: { $0.id() == roomID }) { |
| 157 | + try await Task.sleep(for: .milliseconds(250)) |
| 158 | + } |
| 159 | + |
| 160 | + // Fetch the room from the listener and initialise it's timeline. |
| 161 | + let room = allRoomsListener.rooms.first { $0.id() == roomID }! |
| 162 | + timeline = try await room.timeline() |
| 163 | + |
| 164 | + // Listen to timeline item updates. |
| 165 | + timelineItemsListener = TimelineItemListener() |
| 166 | + timelineHandle = await timeline.addListener(listener: timelineItemsListener) |
| 167 | + |
| 168 | + // Wait for the items array to be updated… |
| 169 | + while timelineItemsListener.timelineItems.isEmpty { |
| 170 | + try await Task.sleep(for: .milliseconds(250)) |
| 171 | + } |
| 172 | + |
| 173 | + // Get the event contents from an item. |
| 174 | + let timelineItem = timelineItemsListener.timelineItems.last! |
| 175 | + if case let .msgLike(content: messageEvent) = timelineItem.asEvent()?.content, |
| 176 | + case let .message(content: messageContent) = messageEvent.kind { |
| 177 | + print(messageContent.body) |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + // MARK: - Step 4 |
| 182 | + // Sending events. |
| 183 | + |
| 184 | + var sendHandle: SendHandle? |
| 185 | + |
| 186 | + func step4SendMessage() async throws { |
| 187 | + // Create the message content from a markdown string. |
| 188 | + let message = messageEventContentFromMarkdown(md: "Hello, World!") |
| 189 | + |
| 190 | + // Send the message content via the room's timeline (so that we show a local echo). |
| 191 | + sendHandle = try await timeline.send(msg: message) |
| 192 | + } |
| 193 | +} |
| 194 | + |
| 195 | +// MARK: - @main |
| 196 | + |
| 197 | +let applicationID = "org.matrix.swift.walkthrough" |
| 198 | +let keychainSessionKey = "WalkthroughUser" |
| 199 | + |
| 200 | +@main |
| 201 | +struct WalkthroughCommand: AsyncParsableCommand { |
| 202 | + static let configuration = CommandConfiguration(abstract: "A basic example of using Matrix Rust SDK in Swift.") |
| 203 | + |
| 204 | + func run() async throws { |
| 205 | + let walkthrough = Walkthrough() |
| 206 | + |
| 207 | + if let walkthroughUser = try loadUserFromKeychain() { |
| 208 | + try await walkthrough.step1Restore(walkthroughUser) |
| 209 | + } else { |
| 210 | + let walkthroughUser = try await walkthrough.step1Login() |
| 211 | + try saveUserToKeychain(walkthroughUser) |
| 212 | + } |
| 213 | + |
| 214 | + try await walkthrough.step2StartSync() |
| 215 | + try await walkthrough.step3LoadRoomTimeline() |
| 216 | + try await walkthrough.step4SendMessage() |
| 217 | + |
| 218 | + // Don't exit immediately otherwise the message won't be sent (the await only suspends until the event is queued). |
| 219 | + _ = readLine() |
| 220 | + } |
| 221 | + |
| 222 | + func saveUserToKeychain(_ walkthroughUser: WalkthroughUser) throws { |
| 223 | + let keychainData = try JSONEncoder().encode(walkthroughUser) |
| 224 | + let keychain = Keychain(service: applicationID) |
| 225 | + try keychain.set(keychainData, key: keychainSessionKey) |
| 226 | + } |
| 227 | + |
| 228 | + func loadUserFromKeychain() throws -> WalkthroughUser? { |
| 229 | + let keychain = Keychain(service: applicationID) |
| 230 | + guard let keychainData = try keychain.getData(keychainSessionKey) else { return nil } |
| 231 | + return try JSONDecoder().decode(WalkthroughUser.self, from: keychainData) |
| 232 | + } |
| 233 | + |
| 234 | + private func reset() throws { |
| 235 | + if let walkthroughUser = try loadUserFromKeychain() { |
| 236 | + try? FileManager.default.removeItem(at: .sessionData(for: walkthroughUser.storeID)) |
| 237 | + try? FileManager.default.removeItem(at: .sessionCaches(for: walkthroughUser.storeID)) |
| 238 | + let keychain = Keychain(service: applicationID) |
| 239 | + try keychain.removeAll() |
| 240 | + } |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +struct WalkthroughUser: Codable { |
| 245 | + let accessToken: String |
| 246 | + let refreshToken: String? |
| 247 | + let userID: String |
| 248 | + let deviceID: String |
| 249 | + let homeserverURL: String |
| 250 | + let oidcData: String? |
| 251 | + let storeID: String |
| 252 | + |
| 253 | + init(session: Session, storeID: String) { |
| 254 | + self.accessToken = session.accessToken |
| 255 | + self.refreshToken = session.refreshToken |
| 256 | + self.userID = session.userId |
| 257 | + self.deviceID = session.deviceId |
| 258 | + self.homeserverURL = session.homeserverUrl |
| 259 | + self.oidcData = session.oidcData |
| 260 | + self.storeID = storeID |
| 261 | + } |
| 262 | + |
| 263 | + var session: Session { |
| 264 | + Session(accessToken: accessToken, |
| 265 | + refreshToken: refreshToken, |
| 266 | + userId: userID, |
| 267 | + deviceId: deviceID, |
| 268 | + homeserverUrl: homeserverURL, |
| 269 | + oidcData: oidcData, |
| 270 | + slidingSyncVersion: .native) |
| 271 | + |
| 272 | + } |
| 273 | +} |
| 274 | + |
| 275 | +extension URL { |
| 276 | + static func sessionData(for sessionID: String) -> URL { |
| 277 | + applicationSupportDirectory |
| 278 | + .appending(component: applicationID) |
| 279 | + .appending(component: sessionID) |
| 280 | + } |
| 281 | + |
| 282 | + static func sessionCaches(for sessionID: String) -> URL { |
| 283 | + cachesDirectory |
| 284 | + .appending(component: applicationID) |
| 285 | + .appending(component: sessionID) |
| 286 | + } |
| 287 | +} |
0 commit comments