client.ts (15533B)
1 import { 2 createClient, 3 EventTimeline, 4 EventType, 5 IndexedDBCryptoStore, 6 IndexedDBStore, 7 MatrixClient as MatrixClientSdk, 8 MatrixEvent, MatrixEventEvent, 9 Preset, 10 RoomCreateTypeField, 11 RoomType, 12 UNSTABLE_MSC3088_ENABLED, 13 UNSTABLE_MSC3088_PURPOSE, 14 UNSTABLE_MSC3089_TREE_SUBTYPE 15 } from 'matrix-js-sdk'; 16 import { 17 MEGOLM_ALGORITHM 18 } from 'matrix-js-sdk/lib/crypto/olmlib'; 19 import { 20 DEFAULT_TREE_POWER_LEVELS_TEMPLATE, 21 MSC3089TreeSpace 22 } from 'matrix-js-sdk/lib/models/MSC3089TreeSpace'; 23 import { 24 M_IMAGE 25 } from './events/ImageEvent'; 26 // @ts-ignore - `.ts` is needed here to make TS happy 27 import IndexedDBWorker from "./workers/indexeddb.worker.ts?worker"; 28 29 export class MatrixClient { 30 private events: MatrixEvent[] = []; 31 private currentUserDirectory?: MSC3089TreeSpace; 32 private rootDirectory?: MSC3089TreeSpace; 33 private constructor(private client: MatrixClientSdk) { } 34 35 public static async new(): Promise<MatrixClient> { 36 // @ts-ignore Known to be a thing 37 if (!global.Olm) { 38 console.error( 39 "global.Olm does not seem to be present." 40 + " Did you forget to add olm in the out directory?" 41 ); 42 } 43 44 let server; 45 if (window.localStorage.getItem("server") === null) { 46 server = import.meta.env.VITE_MATRIX_SERVER_URL; 47 } else { 48 server = window.localStorage.getItem("server"); 49 } 50 51 52 // TODO user id and token if logged in instead of new guest all the time 53 if (!server) { 54 throw new Error("No matrix server URL defined"); 55 } 56 57 let guest_client; 58 if (window.localStorage.getItem("mxid_guest") !== null) { 59 const mxid = window.localStorage.getItem("mxid_guest") ?? undefined; 60 const token = window.localStorage.getItem("access_token_guest") ?? undefined; 61 const device_id = window.localStorage.getItem("device_id_guest") ?? undefined; 62 63 guest_client = createClient({ 64 useAuthorizationHeader: true, 65 baseUrl: server, 66 userId: mxid, 67 accessToken: token, 68 deviceId: device_id, 69 // @ts-ignore - The function currently comes with incorrect types 70 store: new IndexedDBStore({ 71 indexedDB: window.indexedDB, 72 dbName: "matrix-art-sync:guest", 73 localStorage: window.localStorage, 74 workerFactory: () => new IndexedDBWorker(), 75 }), 76 cryptoStore: new IndexedDBCryptoStore( 77 window.indexedDB, "matrix-art:crypto", 78 ), 79 }); 80 guest_client.setGuest(true); 81 } { 82 83 const tmpClient = createClient({ baseUrl: import.meta.env.VITE_MATRIX_SERVER_URL }); 84 // @ts-ignore - The function currently comes with incorrect types 85 const { user_id, device_id, access_token } = await tmpClient.registerGuest(); 86 87 guest_client = createClient({ 88 useAuthorizationHeader: true, 89 baseUrl: server, 90 userId: user_id, 91 accessToken: access_token, 92 deviceId: device_id, 93 // @ts-ignore - The function currently comes with incorrect types 94 store: new IndexedDBStore({ 95 indexedDB: window.indexedDB, 96 dbName: "matrix-art-sync:guest", 97 localStorage: window.localStorage, 98 workerFactory: () => new IndexedDBWorker(), 99 }), 100 cryptoStore: new IndexedDBCryptoStore( 101 window.indexedDB, "matrix-art:crypto", 102 ), 103 }); 104 guest_client.setGuest(true); 105 window.localStorage.setItem("mxid_guest", user_id); 106 window.localStorage.setItem("access_token_guest", access_token); 107 window.localStorage.setItem("device_id_guest", device_id); 108 window.localStorage.setItem("server", server); 109 } 110 111 let client; 112 if (window.localStorage.getItem("mxid") !== null) { 113 const mxid = window.localStorage.getItem("mxid") ?? undefined; 114 const token = window.localStorage.getItem("access_token") ?? undefined; 115 const device_id = window.localStorage.getItem("device_id") ?? undefined; 116 117 client = createClient({ 118 useAuthorizationHeader: true, 119 baseUrl: server, 120 userId: mxid, 121 accessToken: token, 122 deviceId: device_id, 123 // @ts-ignore - The function currently comes with incorrect types 124 store: new IndexedDBStore({ 125 indexedDB: window.indexedDB, 126 dbName: "matrix-art-sync:guest", 127 localStorage: window.localStorage, 128 workerFactory: () => new IndexedDBWorker(), 129 }), 130 cryptoStore: new IndexedDBCryptoStore( 131 window.indexedDB, "matrix-art:crypto", 132 ), 133 }); 134 client.setGuest(false); 135 } 136 137 return new MatrixClient(client ?? guest_client); 138 } 139 140 public isLoggedIn(): boolean { 141 return this.client.isLoggedIn() && !this.client.isGuest(); 142 } 143 144 public async start(): Promise<void> { 145 //TODO Setup handlers 146 this.client.on(MatrixEventEvent.Decrypted, (event: MatrixEvent, _err?: Error) => { 147 const ext_ev = event.unstableExtensibleEvent; 148 if (ext_ev?.isEquivalentTo(M_IMAGE)) { 149 this.events.push(event); 150 } 151 }); 152 153 console.log("start"); 154 await this.client.store.startup(); 155 await this.client.initCrypto(); 156 await this.client.startClient(); 157 158 // Load the root 159 const room = await this.client.joinRoom(import.meta.env.VITE_MATRIX_ROOT_FOLDER); 160 // FIXME: This will break if the server is slower 161 await delay(1000); 162 this.rootDirectory = new MSC3089TreeSpace(this.client, room.roomId); 163 console.log("started"); 164 } 165 166 public async register(homeserver: string = import.meta.env.VITE_MATRIX_SERVER_URL, username: string, password: string, createProfile = false) { 167 this.client.stopClient(); 168 this.client = createClient({ 169 useAuthorizationHeader: true, 170 baseUrl: homeserver, 171 userId: username, 172 deviceId: "Matrix Art", 173 // @ts-ignore - The function currently comes with incorrect types 174 store: new IndexedDBStore({ 175 indexedDB: window.indexedDB, 176 dbName: "matrix-art-sync:loggedin", 177 localStorage: window.localStorage, 178 workerFactory: () => new IndexedDBWorker(), 179 }), 180 cryptoStore: new IndexedDBCryptoStore( 181 window.indexedDB, "matrix-art:crypto", 182 ), 183 }); 184 185 await this.client.register(username, password, null, { type: "m.login.dummy" }); 186 187 window.localStorage.setItem("server", homeserver); 188 window.localStorage.setItem("mxid", username); 189 window.localStorage.setItem("access_token", this.client.getAccessToken() ?? "unknown"); 190 window.localStorage.setItem("device_id", "Matrix Art"); 191 await this.start(); 192 if (createProfile) { 193 const subdirs = this.rootDirectory?.getDirectories(); 194 const id = this.client.getUserId()?.replace(":", "_"); 195 if (subdirs?.some((directory) => directory.room.name === id)) { 196 this.rootDirectory = subdirs?.find((directory) => directory.room.name === id); 197 } else { 198 await this.createProfileFolder(); 199 } 200 } 201 } 202 203 // Login and create the profile if wanted 204 public async login(homeserver: string, username: string, password: string, createProfile = false): Promise<void> { 205 this.client.stopClient(); 206 this.client = createClient({ 207 useAuthorizationHeader: true, 208 baseUrl: homeserver, 209 userId: username, 210 deviceId: "Matrix Art", 211 // @ts-ignore - The function currently comes with incorrect types 212 store: new IndexedDBStore({ 213 indexedDB: window.indexedDB, 214 dbName: "matrix-art-sync:loggedin", 215 localStorage: window.localStorage, 216 workerFactory: () => new IndexedDBWorker(), 217 }), 218 cryptoStore: new IndexedDBCryptoStore( 219 window.indexedDB, "matrix-art:crypto", 220 ), 221 }); 222 await this.client.loginWithPassword(username, password); 223 224 window.localStorage.setItem("server", homeserver); 225 window.localStorage.setItem("mxid", username); 226 window.localStorage.setItem("access_token", this.client.getAccessToken() ?? "unknown"); 227 window.localStorage.setItem("device_id", "Matrix Art"); 228 await this.start(); 229 if (createProfile) { 230 const subdirs = this.rootDirectory?.getDirectories(); 231 console.log(subdirs); 232 const id = this.client.getUserId()?.replace(":", "_"); 233 if (subdirs?.some((directory) => directory.room.name === id)) { 234 this.rootDirectory = subdirs?.find((directory) => directory.room.name === id); 235 } else { 236 await this.createProfileFolder(); 237 } 238 } 239 } 240 241 // creates the profile 242 private async createProfileFolder() { 243 if (this.client.isGuest()) { 244 throw new Error("Cannot create a file tree space as a guest"); 245 } 246 // Load the root as client changed 247 const room = await this.client.joinRoom(import.meta.env.VITE_MATRIX_ROOT_FOLDER); 248 await delay(1000); 249 this.rootDirectory = new MSC3089TreeSpace(this.client, room.roomId); 250 // Create the user folder and add it to the top folder 251 const id = this.client.getUserId()?.replace(":", "_"); 252 this.currentUserDirectory = await this.createPublicSubDirectory(this.rootDirectory, id ?? "unknown"); 253 // Create the public timeline for the user. We dont need it saved as we can get it again later using the users dir. 254 await this.createPublicSubDirectory(this.currentUserDirectory, "Timeline"); 255 } 256 257 /** 258 * Creates a new file tree space with the given name. The client will pick 259 * defaults for how it expects to be able to support the remaining API offered 260 * by the returned class. 261 * 262 * Note that this is UNSTABLE and may have breaking changes without notice. 263 * @param {string} name The name of the tree space. 264 * @returns {Promise<MSC3089TreeSpace>} Resolves to the created space. 265 * 266 * This is taken from https://github.com/matrix-org/matrix-js-sdk/blob/d6f1c6cfdc5a4f3d7b4ec67fe9f4d89d7319d8f7/src/client.ts#L8776 267 * License of the original file: Apache-2.0 268 */ 269 public async createPublicFileTree(name: string): Promise<MSC3089TreeSpace> { 270 if (this.client.isGuest()) { 271 throw new Error("Cannot create a file tree space as a guest"); 272 } 273 const { room_id: roomId } = await this.client.createRoom({ 274 name: name, 275 preset: Preset.PublicChat, 276 power_level_content_override: { 277 ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE, 278 users: { 279 // We want to be able to moderate this as the instance admin for legal reasons 280 [import.meta.env.VITE_MATRIX_INSTANCE_ADMIN]: 100, 281 // We initially need to use 100 to be able to create the room... 282 [this.client.getUserId() ?? "broken"]: 100, 283 }, 284 }, 285 invite: [ 286 import.meta.env.VITE_MATRIX_INSTANCE_ADMIN, 287 ], 288 creation_content: { 289 [RoomCreateTypeField]: RoomType.Space, 290 }, 291 initial_state: [ 292 { 293 type: UNSTABLE_MSC3088_PURPOSE.name, 294 state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.name, 295 content: { 296 [UNSTABLE_MSC3088_ENABLED.name]: true, 297 }, 298 }, 299 { 300 type: EventType.RoomEncryption, 301 state_key: "", 302 content: { 303 algorithm: MEGOLM_ALGORITHM, 304 }, 305 }, 306 { 307 type: EventType.RoomGuestAccess, 308 state_key: "", 309 content: { 310 guest_access: "can_join", 311 }, 312 }, 313 { 314 type: EventType.RoomHistoryVisibility, 315 state_key: "", 316 content: { 317 history_visibility: "world_readable", 318 }, 319 } 320 ], 321 }); 322 // Demote ourself 323 const room = this.client.getRoom(roomId); 324 const powerLevelEvent = room?.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(EventType.RoomPowerLevels, ""); 325 if (!powerLevelEvent) { 326 throw new Error("Failed to find PL event"); 327 } 328 await this.client.setPowerLevel(roomId, this.client.getUserId() ?? "unknown", 50, powerLevelEvent); 329 return new MSC3089TreeSpace(this.client, roomId); 330 } 331 332 /** 333 * Creates a directory under this tree space, represented as another tree space. 334 * @param {string} name The name for the directory. 335 * @returns {Promise<MSC3089TreeSpace>} Resolves to the created directory. 336 * 337 * This is taken from https://github.com/matrix-org/matrix-js-sdk/blob/feb83ba161c32c0519613b88027f573e22efa3aa/src/models/MSC3089TreeSpace.ts#L226 338 * License of the original file: Apache-2.0 339 */ 340 public async createPublicSubDirectory(topdirectory: MSC3089TreeSpace, name: string): Promise<MSC3089TreeSpace> { 341 const directory = await this.createPublicFileTree(name); 342 343 await this.client.sendStateEvent(topdirectory.roomId, EventType.SpaceChild, { 344 via: [this.client.getDomain()], 345 }, directory.roomId); 346 347 await this.client.sendStateEvent(directory.roomId, EventType.SpaceParent, { 348 via: [this.client.getDomain()], 349 }, topdirectory.roomId); 350 351 return directory; 352 } 353 } 354 355 /* Technical folder layout (https://github.com/matrix-org/matrix-spec-proposals/blob/travis/msc/trees/proposals/3089-file-tree-structures.md) 356 Idea by TravisR 357 358 Note that every user can create a user folder or delete themself from it again. 359 Every user owns their own user folder. 360 361 If possible users shall never remove relations to other users folders. 362 363 + 📂 Matrix Art User Dir (public, m.space) 364 + 📂 User A (public, m.space) 365 + 📂 Timeline (m.space) 366 - 📄 Image A 367 = Room A (invite protected, <no type>) 368 - 📄 Image B (counted as under the timeline) 369 + 📂 User B (public, m.space) 370 + 📂 Timeline (m.space) 371 - 📄 Image C 372 = Room B (invite protected, <no type>) 373 - 📄 Image D (counted as under the timeline) 374 */ 375 376 function delay(time: number): Promise<void> { 377 return new Promise(resolve => setTimeout(resolve, time)); 378 }