ownUser.ts (7068B)
1 import { DeviceId, UserId } from "@mtrnord/matrix-sdk-crypto-js"; 2 import { IErrorResp, ILoginFlows, ILoginResponse, IWellKnown } from "./api/apiTypes"; 3 import { MatrixClient, isRateLimitError } from "./client"; 4 import { MatrixE2EE } from "./e2ee"; 5 import { HostnameMissingError, HostnameMissingHTTPSError, LoginError, LoginFlowRequestError, LogoutError, NotLogeedInError, PasswordLoginNotSupportedError, PasswordMissingError, SDKError, SlidingSyncProxyNotFoundError, UsernameMissingError } from "./utils"; 6 7 export class OwnUser { 8 public access_token?: string; 9 public device_id?: string; 10 public mxid?: string; 11 // Hostname including "https://" 12 public hostname?: string; 13 public slidingSyncHostname?: string; 14 public e2ee: MatrixE2EE; 15 16 constructor(private client: MatrixClient) { 17 this.e2ee = new MatrixE2EE(this.client, this); 18 } 19 20 // TODO: call logout endpoint on logout 21 public async logout(): Promise<void | SDKError> { 22 if (!this.mxid) { 23 return new NotLogeedInError(); 24 } 25 if (!this.access_token) { 26 return new NotLogeedInError(); 27 } 28 if (!this.hostname) { 29 return new HostnameMissingError(); 30 } 31 const resp = await fetch(`${this.hostname}/_matrix/client/v3/logout`, { 32 method: "POST", 33 headers: { 34 "Content-Type": "application/json", 35 "Authorization": `Bearer ${this.access_token}` 36 }, 37 }); 38 if (!resp.ok) { 39 return new LogoutError(resp); 40 } 41 42 this.access_token = undefined; 43 this.device_id = undefined; 44 this.slidingSyncHostname = undefined; 45 } 46 47 48 49 public async setHostname(hostname: string): Promise<SDKError | void> { 50 if (!hostname.startsWith("https://")) { 51 return new HostnameMissingHTTPSError(); 52 } 53 if (!this.client.database) { 54 await this.client.createDatabase(); 55 } 56 57 // Write to database 58 const tx = this.client.database?.transaction('loginInfo', 'readwrite'); 59 await tx?.store.put({ 60 userId: this.mxid!, 61 hostname: hostname, 62 slidingSyncHostname: this.slidingSyncHostname, 63 access_token: this.access_token, 64 device_id: this.device_id, 65 }); 66 await tx?.done 67 68 // Set in memory 69 this.hostname = hostname; 70 } 71 72 private async getLoginFlows(): Promise<ILoginFlows | SDKError> { 73 if (!this.hostname) { 74 return new HostnameMissingError(); 75 } 76 const resp = await fetch(`${this.hostname}/_matrix/client/v3/login`); 77 if (!resp.ok) { 78 return new LoginFlowRequestError(resp); 79 } 80 const json = await resp.json() as ILoginFlows; 81 return json; 82 } 83 84 private async getWellKnown(): Promise<IWellKnown | SDKError> { 85 if (!this.hostname) { 86 return new HostnameMissingError(); 87 } 88 const resp = await fetch(`${this.hostname}/.well-known/matrix/client`); 89 if (!resp.ok) { 90 return new LoginFlowRequestError(resp); 91 } 92 const json = await resp.json() as IWellKnown; 93 return json; 94 } 95 96 public async passwordLogin(username: string, password: string, triesLeft = 5): Promise<SDKError | void> { 97 if (!this.client.database) { 98 await this.client.createDatabase(); 99 } 100 if (!username) { 101 return new UsernameMissingError(); 102 } 103 if (!password) { 104 return new PasswordMissingError(); 105 } 106 this.mxid = username; 107 await this.setHostname(`https://${username.split(':')[1]}`); 108 109 try { 110 const well_known = await this.getWellKnown(); 111 if (well_known instanceof SDKError) { 112 return well_known; 113 } 114 if (well_known["m.homeserver"]?.base_url) { 115 await this.setHostname(well_known["m.homeserver"].base_url); 116 } 117 if (well_known["org.matrix.msc3575.proxy"]?.url) { 118 // Write to database 119 const tx = this.client.database?.transaction('loginInfo', 'readwrite'); 120 await tx?.store.put({ 121 userId: this.mxid!, 122 hostname: this.hostname, 123 slidingSyncHostname: well_known["org.matrix.msc3575.proxy"].url, 124 access_token: this.access_token, 125 device_id: this.device_id, 126 }); 127 await tx?.done 128 129 // Set the sliding sync proxy 130 this.slidingSyncHostname = well_known["org.matrix.msc3575.proxy"].url; 131 } else { 132 return new SlidingSyncProxyNotFoundError(); 133 } 134 } catch (e: any) { 135 console.warn(`No well-known found for ${this.hostname}:\n${e}`); 136 } 137 138 const loginFlows = await this.getLoginFlows(); 139 if (loginFlows instanceof SDKError) { 140 return loginFlows; 141 } 142 if ((loginFlows.flows.filter((flow) => flow.type === 'm.login.password')?.length || 0) == 0) { 143 return new PasswordLoginNotSupportedError(); 144 } 145 146 const resp = await fetch(`${this.hostname}/_matrix/client/v3/login`, { 147 method: "POST", 148 headers: { 149 "Content-Type": "application/json" 150 }, 151 body: JSON.stringify({ 152 type: "m.login.password", 153 identifier: { 154 type: 'm.id.user', 155 user: username, 156 }, 157 user: username, 158 password: password 159 }) 160 }); 161 if (!resp.ok) { 162 return new LoginError(resp); 163 } 164 const json = await resp.json(); 165 if (isErrorResp(json)) { 166 return new LoginError(undefined, json); 167 } 168 if (isRateLimitError(json)) { 169 console.error(`Rate limited. Retrying in ${json.retry_after_ms}ms. ${triesLeft} tries left.`); 170 await this.passwordLogin(username, password, triesLeft - 1); 171 } 172 if (isLoginResponse(json)) { 173 // Write to database 174 const tx = this.client.database?.transaction('loginInfo', 'readwrite'); 175 await tx?.store.put({ 176 userId: json.user_id!, 177 hostname: this.hostname, 178 slidingSyncHostname: this.slidingSyncHostname, 179 access_token: json.access_token, 180 device_id: json.device_id, 181 }); 182 await tx?.done 183 this.access_token = json.access_token; 184 this.device_id = json.device_id; 185 this.mxid = json.user_id; 186 187 await this.e2ee.initOlmMachine(new UserId(this.mxid), new DeviceId(this.device_id)); 188 } 189 } 190 } 191 192 function isLoginResponse(arg: any): arg is ILoginResponse { 193 return arg.access_token !== undefined; 194 } 195 196 function isErrorResp(arg: any): arg is IErrorResp { 197 return arg.errcode !== undefined; 198 }