cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit 49becbdb392509dff8ac10c9591f67e9d32f7c2f
parent 3081db8ce62670475ed897bc4bb9e5b7e034a29a
Author: MTRNord <mtrnord1@gmail.com>
Date:   Tue, 19 Sep 2023 11:26:58 +0200

Add more errors and remove throws

Diffstat:
Msrc/app/sdk/client.ts | 33++++++++++++++++++++-------------
Msrc/app/sdk/ownUser.ts | 44+++++++++++++++++++++++---------------------
Msrc/app/sdk/room.ts | 22+++++++++++-----------
Msrc/app/sdk/slidingSync.ts | 7+++----
Msrc/app/sdk/utils.ts | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/components/login/login.tsx | 9++++-----
Msrc/pages/LoginPage.tsx | 6+++++-
7 files changed, 141 insertions(+), 56 deletions(-)

diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -20,7 +20,7 @@ import { } from "idb"; import { DeviceId, UserId } from "@mtrnord/matrix-sdk-crypto-js"; import { MatrixSlidingSync } from "./slidingSync"; -import { NotLogeedInError, SDKError } from './utils'; +import { AccessTokenMissingError, HostnameMissingError, NotLogeedInError, ProfileFetchError, SDKError } from './utils'; export interface MatrixClientEvents { // Used to notify about changes to the room list @@ -119,8 +119,11 @@ export class MatrixClient extends EventEmitter { this.emit("rooms", rooms); } - public async passwordLogin(username: string, password: string) { - await this.user.passwordLogin(username, password); + public async passwordLogin(username: string, password: string): Promise<SDKError | void> { + const loginResp = await this.user.passwordLogin(username, password); + if (loginResp instanceof SDKError) { + return loginResp + } this.sync.on("rooms", (rooms) => this.onSyncRooms(rooms)); } @@ -241,10 +244,13 @@ export class MatrixClient extends EventEmitter { return decryptedEvent; } - public async logout() { + public async logout(): Promise<void | SDKError> { this.sync.logout(); this.sync.off("rooms", this.onSyncRooms); - await this.user.logout(); + const error = await this.user.logout(); + if (error instanceof SDKError) { + return error; + } this.user.e2ee.logoutE2ee(); if (this.user.mxid) { const syncInfoTX = this.database?.transaction('syncInfo', 'readwrite'); @@ -383,7 +389,7 @@ export class MatrixClient extends EventEmitter { await this.sync.startSync(); } - public async fetchProfileInfo(userId: string): Promise<IProfileInfo> { + public async fetchProfileInfo(userId: string): Promise<SDKError | IProfileInfo> { // @ts-ignore if (globalThis.IS_STORYBOOK) { await new Promise(r => setTimeout(r, 5000)) @@ -392,13 +398,13 @@ export class MatrixClient extends EventEmitter { return this.profileInfo; } if (!this.user.hostname) { - throw Error("Hostname must be set first"); + return new HostnameMissingError() } if (!this.database) { await this.createDatabase(); } if (!this.user.access_token) { - throw Error("Access token must be set first"); + return new AccessTokenMissingError(); } const resp = await fetch(`${this.user.hostname}/_matrix/client/v3/profile/${userId}`, { headers: { @@ -409,8 +415,7 @@ export class MatrixClient extends EventEmitter { if (resp.status === 404 || resp.status === 403) { return {} as IProfileInfo; } - console.error(resp); - throw Error("Error fetching profile info. See console for error."); + return new ProfileFetchError(resp); } const json = await resp.json() as IProfileInfo; if (json.avatar_url) { @@ -496,10 +501,12 @@ export function useProfile() { useEffect(() => { client.fetchProfileInfo(client.mxid!).then((profile) => { - if (!profile.displayname) { - profile.displayname = client.mxid || "Unknown"; + if (!(profile instanceof SDKError)) { + if (!profile.displayname) { + profile.displayname = client.mxid || "Unknown"; + } + setProfile(profile); } - setProfile(profile); }) }, []) return profile; diff --git a/src/app/sdk/ownUser.ts b/src/app/sdk/ownUser.ts @@ -2,7 +2,7 @@ import { DeviceId, UserId } from "@mtrnord/matrix-sdk-crypto-js"; import { IErrorResp, ILoginFlows, ILoginResponse, IWellKnown } from "./api/apiTypes"; import { MatrixClient, isRateLimitError } from "./client"; import { MatrixE2EE } from "./e2ee"; -import { HostnameMissingError, NotLogeedInError, SDKError } from "./utils"; +import { HostnameMissingError, HostnameMissingHTTPSError, LoginError, LoginFlowRequestError, LogoutError, NotLogeedInError, PasswordLoginNotSupportedError, PasswordMissingError, SDKError, SlidingSyncProxyNotFoundError, UsernameMissingError } from "./utils"; export class OwnUser { public access_token?: string; @@ -36,8 +36,7 @@ export class OwnUser { }, }); if (!resp.ok) { - console.error(resp); - throw Error("Error logging out. See console for error."); + return new LogoutError(resp); } this.access_token = undefined; @@ -47,9 +46,9 @@ export class OwnUser { - public async setHostname(hostname: string) { + public async setHostname(hostname: string): Promise<SDKError | void> { if (!hostname.startsWith("https://")) { - throw Error("Hostname must start with 'https://'"); + return new HostnameMissingHTTPSError(); } if (!this.client.database) { await this.client.createDatabase(); @@ -70,47 +69,48 @@ export class OwnUser { this.hostname = hostname; } - private async getLoginFlows(): Promise<ILoginFlows> { + private async getLoginFlows(): Promise<ILoginFlows | SDKError> { if (!this.hostname) { - throw Error("Hostname must be set first"); + return new HostnameMissingError(); } const resp = await fetch(`${this.hostname}/_matrix/client/v3/login`); if (!resp.ok) { - console.error(resp); - throw Error("Error requesting login flows. See console for error."); + return new LoginFlowRequestError(resp); } const json = await resp.json() as ILoginFlows; return json; } - private async getWellKnown(): Promise<IWellKnown> { + private async getWellKnown(): Promise<IWellKnown | SDKError> { if (!this.hostname) { - throw Error("Hostname must be set first"); + return new HostnameMissingError(); } const resp = await fetch(`${this.hostname}/.well-known/matrix/client`); if (!resp.ok) { - console.error(resp); - throw Error("Error requesting login flows. See console for error."); + return new LoginFlowRequestError(resp); } const json = await resp.json() as IWellKnown; return json; } - public async passwordLogin(username: string, password: string, triesLeft = 5) { + public async passwordLogin(username: string, password: string, triesLeft = 5): Promise<SDKError | void> { if (!this.client.database) { await this.client.createDatabase(); } if (!username) { - throw Error("Username must be set"); + return new UsernameMissingError(); } if (!password) { - throw Error("Password must be set"); + return new PasswordMissingError(); } this.mxid = username; await this.setHostname(`https://${username.split(':')[1]}`); try { const well_known = await this.getWellKnown(); + if (well_known instanceof SDKError) { + return well_known; + } if (well_known["m.homeserver"]?.base_url) { await this.setHostname(well_known["m.homeserver"].base_url); } @@ -129,15 +129,18 @@ export class OwnUser { // Set the sliding sync proxy this.slidingSyncHostname = well_known["org.matrix.msc3575.proxy"].url; } else { - throw Error("No sliding sync proxy found"); + return new SlidingSyncProxyNotFoundError(); } } catch (e: any) { console.warn(`No well-known found for ${this.hostname}:\n${e}`); } const loginFlows = await this.getLoginFlows(); + if (loginFlows instanceof SDKError) { + return loginFlows; + } if ((loginFlows.flows.filter((flow) => flow.type === 'm.login.password')?.length || 0) == 0) { - throw Error("Password login is not supported by this homeserver"); + return new PasswordLoginNotSupportedError(); } const resp = await fetch(`${this.hostname}/_matrix/client/v3/login`, { @@ -156,12 +159,11 @@ export class OwnUser { }) }); if (!resp.ok) { - console.error(resp); - throw Error("Error logging in. See console for error."); + return new LoginError(resp); } const json = await resp.json(); if (isErrorResp(json)) { - throw Error(`Error logging in: ${json.errcode}: ${json.error}`); + return new LoginError(undefined, json); } if (isRateLimitError(json)) { console.error(`Rate limited. Retrying in ${json.retry_after_ms}ms. ${triesLeft} tries left.`); diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -15,6 +15,7 @@ import { useEffect, useState } from "react"; import { EncryptionAlgorithm, EncryptionSettings, RoomId } from "@mtrnord/matrix-sdk-crypto-js"; import { OnlineState } from "./api/otherEnums"; import { MatrixE2EE } from "./e2ee"; +import { AccessTokenMissingError, FailedSendingError, HostnameMissingError, ProfileFetchError, SDKError } from "./utils"; export interface RoomEvents { // Used to notify about changes to the event list @@ -251,13 +252,13 @@ export class Room extends EventEmitter { return isBot; } - public async joinedMembers(): Promise<IRoomMemberEvent[]> { + public async joinedMembers(): Promise<IRoomMemberEvent[] | SDKError> { if (!this.has_all_users) { if (!this.client.hostname) { - throw Error("Hostname must be set first"); + return new HostnameMissingError(); } if (!this.client.accessToken) { - throw Error("Access token must be set first"); + return new AccessTokenMissingError(); } // We dont have all members so we need to fetch them const resp = await fetch(`${this.client.hostname}/_matrix/client/v3/rooms/${this.roomID}/members`, { @@ -273,8 +274,7 @@ export class Room extends EventEmitter { } }); } - console.error(resp); - throw Error("Error fetching profile info. See console for error."); + return new ProfileFetchError(resp); } const json = await resp.json() as { chunk: IRoomMemberEvent[] }; this.stateEvents = [...this.stateEvents, ...json.chunk]; @@ -305,7 +305,7 @@ export class Room extends EventEmitter { this.emit("events", this.getEvents()); } - public async sendHtmlMessage(html: string, plainText: string, callbackLocalEcho: () => void): Promise<string> { + public async sendHtmlMessage(html: string, plainText: string, callbackLocalEcho: () => void): Promise<string | SDKError> { const txn_id = Date.now().toString(); // @ts-ignore: Intentionally incomplete const event = { @@ -338,7 +338,7 @@ export class Room extends EventEmitter { }); if (!resp.ok) { this.deletePendingByEventID(event.event_id); - throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); + return new FailedSendingError(resp); } const json = await resp.json(); this.deletePendingByEventID(event.event_id); @@ -362,7 +362,7 @@ export class Room extends EventEmitter { }); if (!resp.ok) { this.deletePendingByEventID(event.event_id); - throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); + return new FailedSendingError(resp); } const json = await resp.json(); this.deletePendingByEventID(event.event_id); @@ -370,7 +370,7 @@ export class Room extends EventEmitter { } } - public async sendTextMessage(text: string, callbackLocalEcho: () => void): Promise<string> { + public async sendTextMessage(text: string, callbackLocalEcho: () => void): Promise<string | SDKError> { const txn_id = Date.now().toString(); // @ts-ignore: Intentionally incomplete const event = { @@ -401,7 +401,7 @@ export class Room extends EventEmitter { }); if (!resp.ok) { this.deletePendingByEventID(event.event_id); - throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); + return new FailedSendingError(resp); } const json = await resp.json(); this.deletePendingByEventID(event.event_id); @@ -425,7 +425,7 @@ export class Room extends EventEmitter { }); if (!resp.ok) { this.deletePendingByEventID(event.event_id); - throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); + return new FailedSendingError(resp); } const json = await resp.json(); this.deletePendingByEventID(event.event_id); diff --git a/src/app/sdk/slidingSync.ts b/src/app/sdk/slidingSync.ts @@ -5,7 +5,7 @@ import { Room } from "./room"; import { OwnUser } from "./ownUser"; import { DeviceLists, UserId } from "@mtrnord/matrix-sdk-crypto-js"; import { IRoomEvent, IRoomStateEvent, isRoomStateEvent } from "./api/events"; -import { HostnameMissingError, NotLogeedInError, SDKError } from "./utils"; +import { HostnameMissingError, NotLogeedInError, SDKError, SyncError } from "./utils"; export interface MatrixSlidingSyncEvents { // Used to notify about changes to the room list @@ -462,10 +462,9 @@ export class MatrixSlidingSync extends EventEmitter { return; } else if (resp.status === 401) { await this.logout(); - console.error(resp); - console.error("Error syncing. See console for error."); + return new SyncError(resp); } else { - throw new Error(`Error syncing. See console for error:\n\n${await resp.text()}`); + return new SyncError(resp); } } const json = await resp.json() as ISlidingSyncResp; diff --git a/src/app/sdk/utils.ts b/src/app/sdk/utils.ts @@ -1,3 +1,5 @@ +import { IErrorResp } from "./api/apiTypes" + export class SDKError extends Error { protected constructor(msg?: string) { if (msg) { @@ -20,8 +22,80 @@ export class HostnameMissingError extends SDKError { } } -export class OlmMachineNotSetup extends SDKError { +export class HostnameMissingHTTPSError extends SDKError { + constructor() { + super("Hostname must start with 'https://'") + } +} + +export class OlmMachineNotSetupError extends SDKError { constructor() { super("Olm machine must be set first") } +} + +export class AccessTokenMissingError extends SDKError { + constructor() { + super("Access token must be set first") + } +} + +export class ProfileFetchError extends SDKError { + constructor(public readonly response: Response) { + super("Error fetching profile info.") + } +} + +export class LogoutError extends SDKError { + constructor(public readonly response: Response) { + super("Error logging out.") + } +} + +export class LoginFlowRequestError extends SDKError { + constructor(public readonly response: Response) { + super("Error requesting login flows.") + } +} + +export class UsernameMissingError extends SDKError { + constructor() { + super("Username must be set") + } +} + +export class PasswordMissingError extends SDKError { + constructor() { + super("Password must be set") + } +} + +export class SlidingSyncProxyNotFoundError extends SDKError { + constructor() { + super("No sliding sync proxy found") + } +} + +export class LoginError extends SDKError { + constructor(public readonly response?: Response, public readonly matrix_error?: IErrorResp) { + super("Error logging in.") + } +} + +export class PasswordLoginNotSupportedError extends SDKError { + constructor() { + super("Password login is not supported by this homeserver") + } +} + +export class FailedSendingError extends SDKError { + constructor(public readonly response: Response) { + super(`Failed to send message: ${response.status} ${response.statusText}`) + } +} + +export class SyncError extends SDKError { + constructor(public readonly response: Response) { + super(`Error syncing`) + } } \ No newline at end of file diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx @@ -18,11 +18,10 @@ const Login = memo(() => { } } const startLogin = async () => { - try { - setLoginPending(true); - await matrixClient.passwordLogin(username, password); - } catch (e: any) { - setLoginError(e.toString()); + setLoginPending(true); + const error = await matrixClient.passwordLogin(username, password); + if (error) { + setLoginError(error.toString()); } setLoginPending(false); } diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx @@ -2,6 +2,7 @@ import './LoginPage.scss'; import Login from '../components/login/login'; import { memo, useContext, useEffect, useState } from 'react'; import { MatrixContext } from '../app/sdk/client'; +import { SDKError } from '../app/sdk/utils'; const LoginPage = memo(() => { const matrixClient = useContext(MatrixContext); @@ -10,7 +11,10 @@ const LoginPage = memo(() => { useEffect(() => { if (loading) { // Ensure logout worked - matrixClient.logout().then(() => { + matrixClient.logout().then((error) => { + if (error instanceof SDKError) { + console.error(error) + } setLoading(false) }); }