matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

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 }