From 1edb825a5981bd7155c0e3c0ba75a38d446bb94d Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 26 Jun 2024 10:21:27 +0100 Subject: [PATCH] chore(client): delete old controller code --- .../components/client/Controller.ts.mobx_src | 294 ------------------ .../components/client/Session.ts.mobx_src | 276 ---------------- 2 files changed, 570 deletions(-) delete mode 100644 packages/client/components/client/Controller.ts.mobx_src delete mode 100644 packages/client/components/client/Session.ts.mobx_src diff --git a/packages/client/components/client/Controller.ts.mobx_src b/packages/client/components/client/Controller.ts.mobx_src deleted file mode 100644 index 4691536b..00000000 --- a/packages/client/components/client/Controller.ts.mobx_src +++ /dev/null @@ -1,294 +0,0 @@ -import { detect } from "detect-browser"; -import { API, Client } from "revolt.js"; - -import { - CONFIGURATION, - getController, - registerController, -} from "@revolt/common"; -import { state } from "@revolt/state"; - -import Session, { SessionPrivate } from "./Session.ts.mobx_src"; -import { mapAnyError } from "./error"; - -/** - * Controls the lifecycle of clients - */ -export default class ClientController { - /** - * API client - */ - private apiClient: Client; - - /** - * Server configuration - */ - private configuration: API.RevoltConfig | null; - - /** - * Map of user IDs to sessions - */ - private sessions: ObservableMap; - - /** - * User ID of active session - */ - private current?: string; - - /** - * Create client controller - */ - constructor() { - this.apiClient = new Client(CONFIGURATION.DEFAULT_API_URL); - - /* this.apiClient - .fetchConfiguration() - .then(() => (this.configuration = this.apiClient.configuration!)); */ - - this.configuration = null; - this.sessions = new ObservableMap(); - delete this.current; - - makeAutoObservable(this); - - this.login = this.login.bind(this); - this.logoutCurrent = this.logoutCurrent.bind(this); - - registerController("client", this); - } - - /** - * Select the next available session - */ - pickNextSession() { - this.switchAccount( - this.current ?? this.sessions.keys().next().value ?? null - ); - } - - /** - * Get the currently selected session - * @returns Active Session - */ - getActiveSession() { - return this.sessions.get(this.current!); - } - - /** - * Get the currently ready client - * @returns Ready Client - */ - getReadyClient() { - const session = this.getActiveSession(); - return session && session.ready ? session.client! : undefined; - } - - /** - * Get the currently ready client - * @returns Ready Client - */ - getReadyClients() { - return [...this.sessions.values()] - .filter((session) => session.ready) - .map((session) => session.client!); - } - - /** - * Get an unauthenticated instance of the Revolt.js Client - * @returns API Client - */ - getAnonymousClient() { - return this.apiClient; - } - - /** - * Get the next available client (either from session or API) - * @returns Revolt.js Client - */ - getAvailableClient() { - return this.getActiveSession()?.client ?? this.apiClient; - } - - /** - * Fetch server configuration - * @returns Server Configuration - */ - getServerConfig() { - return this.configuration; - } - - /** - * Check whether we are logged in right now - * @returns Whether we are logged in - */ - isLoggedIn() { - return this.current !== null; - } - - /** - * Check whether we are currently ready - * @returns Whether we are ready to render - */ - isReady() { - return this.getActiveSession()?.ready; - } - - /** - * Start a new client lifecycle - * @param entry Session Information - * @param knowledge Whether the session is new or existing - */ - async addSession( - entry: { session: SessionPrivate; apiUrl?: string }, - knowledge: "new" | "existing" - ) { - const user_id = entry.session.user_id!; - - const session = new Session(); - this.sessions.set(user_id, session); - this.pickNextSession(); - - await session - .emit({ - action: "LOGIN", - session: entry.session, - apiUrl: entry.apiUrl, - configuration: this.configuration!, - knowledge, - }) - .catch((err) => { - const error = mapAnyError(err); - if (error === "Forbidden" || error === "Unauthorized") { - this.sessions.delete(user_id); - delete this.current; - this.pickNextSession(); - state.auth.removeSession(user_id); - getController("modal").push({ type: "signed_out" }); - session.destroy(); - } else { - getController("modal").push({ - type: "error", - error, - }); - } - }); - } - - /** - * Login given a set of credentials - * @param credentials Credentials - */ - async login(credentials: API.DataLogin) { - const browser = detect(); - - // Generate a friendly name for this browser - let friendly_name; - if (browser) { - let { name } = browser; - const { os } = browser; - let isiPad; - // TODO window.isNative - if (name === "ios") { - name = "safari"; - } else if (name === "fxios") { - name = "firefox"; - } else if (name === "crios") { - name = "chrome"; - } - if (os === "Mac OS" && navigator.maxTouchPoints > 0) isiPad = true; - friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`; - } else { - friendly_name = "Unknown Device"; - } - - // Try to login with given credentials - let session = await this.apiClient.api.post("/auth/session/login", { - ...credentials, - friendly_name, - }); - - // Prompt for MFA verification if necessary - if (session.result === "MFA") { - const { allowed_methods } = session; - while (session.result === "MFA") { - const mfa_response: API.MFAResponse | undefined = await new Promise( - (callback) => - getController("modal").push({ - type: "mfa_flow", - state: "unknown", - available_methods: allowed_methods, - callback, - }) - ); - - if (typeof mfa_response === "undefined") { - break; - } - - try { - session = await this.apiClient.api.post("/auth/session/login", { - mfa_response, - mfa_ticket: session.ticket, - friendly_name, - }); - } catch (err) { - console.error("Failed login:", err); - } - } - - if (session.result === "MFA") { - throw "Cancelled"; - } - } - - if (session.result !== "Disabled") { - // Start client lifecycle - this.addSession( - { - session, - }, - "new" - ); - - state.auth.setSession(session); - } - } - - /** - * Log out of a specific user session - * @param user_id Target User ID - */ - logout(user_id: string) { - const session = this.sessions.get(user_id); - if (session) { - if (user_id === this.current) { - delete this.current; - } - - this.sessions.delete(user_id); - this.pickNextSession(); - session.destroy(); - } - } - - /** - * Logout of the current session - */ - logoutCurrent() { - if (this.current) { - this.logout(this.current); - } - } - - /** - * Switch to another user session - * @param user_id Target User ID - */ - switchAccount(user_id: string) { - this.current = user_id; - - // This will allow account switching to work more seamlessly, - // maybe it'll be properly / fully implemented at some point. - // TODO resetMemberSidebarFetched(); - } -} diff --git a/packages/client/components/client/Session.ts.mobx_src b/packages/client/components/client/Session.ts.mobx_src deleted file mode 100644 index 575e3871..00000000 --- a/packages/client/components/client/Session.ts.mobx_src +++ /dev/null @@ -1,276 +0,0 @@ -import { API, Client } from "revolt.js"; - -import { CONFIGURATION, getController } from "@revolt/common"; - -/** - * Session authentication - */ -export type SessionPrivate = { - token: string; - user_id: string; -}; - -/** - * Current lifecycle state - */ -type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; - -/** - * Possible transitions between states - */ -type Transition = - | { - action: "LOGIN"; - apiUrl?: string; - session: SessionPrivate; - configuration?: API.RevoltConfig; - - knowledge: "new" | "existing"; - } - | { - action: - | "SUCCESS" - | "DISCONNECT" - | "RETRY" - | "LOGOUT" - | "ONLINE" - | "OFFLINE"; - }; - -/** - * Client lifecycle finite state machine - */ -export default class Session { - state: State = - window.navigator.onLine || import.meta.env.DEV ? "Ready" : "Offline"; - user_id: string | null = null; - client: Client | null = null; - - /** - * Create a new Session - */ - constructor() { - this.onDropped = this.onDropped.bind(this); - this.onReady = this.onReady.bind(this); - this.onOnline = this.onOnline.bind(this); - this.onOffline = this.onOffline.bind(this); - - window.addEventListener("online", this.onOnline); - window.addEventListener("offline", this.onOffline); - } - - /** - * Initiate logout and destroy client - */ - destroy() { - if (this.client) { - this.client.logout(false); - this.state = "Ready"; - this.client = null; - } - } - - /** - * Called when user's browser signals it is online - */ - private onOnline() { - this.emit({ - action: "ONLINE", - }); - } - - /** - * Called when user's browser signals it is offline - */ - private onOffline() { - this.emit({ - action: "OFFLINE", - }); - } - - /** - * Called when the client signals it has disconnected - */ - private onDropped() { - this.emit({ - action: "DISCONNECT", - }); - } - - /** - * Called when the client signals it has received the Ready packet - */ - private onReady() { - this.emit({ - action: "SUCCESS", - }); - } - - /** - * Create a new Revolt.js Client for this Session - * @param apiUrl Optionally specify an API URL - */ - private createClient(apiUrl?: string) { - this.client = new Client({ - unreads: true, - autoReconnect: false, - onPongTimeout: "EXIT", - apiURL: apiUrl ?? CONFIGURATION.DEFAULT_API_URL, - }); - - // this.client.addListener("dropped", this.onDropped); - // this.client.addListener("ready", this.onReady); - } - - /** - * Destroy the client including any listeners. - */ - private destroyClient() { - this.client!.removeAllListeners(); - this.client!.logout(); - this.user_id = null; - this.client = null; - } - - /** - * Ensure we are in one of the given states - * @param state Possible states - */ - private assert(...state: State[]) { - let found = false; - for (const target of state) { - if (this.state === target) { - found = true; - break; - } - } - - if (!found) { - throw `State must be ${state} in order to transition! (currently ${this.state})`; - } - } - - /** - * Continue logging in provided onboarding is successful - * @param data Transition Data - */ - private async continueLogin(data: Transition & { action: "LOGIN" }) { - try { - await this.client!.useExistingSession(data.session); - this.user_id = this.client!.user!._id; - // TODO state.auth.setSession(data.session); - } catch (err) { - this.state = "Ready"; - throw err; - } - } - - /** - * Transition to a new state by a certain action - * @param data Transition Data - */ - async emit(data: Transition) { - console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data); - - switch (data.action) { - // Login with session - case "LOGIN": { - this.assert("Ready"); - this.state = "Connecting"; - this.createClient(data.apiUrl); - - if (data.configuration) { - this.client!.configuration = data.configuration; - } - - if (data.knowledge === "new") { - await this.client!.fetchConfiguration(); - this.client!.session = data.session; - (this.client! as any).$updateHeaders(); - - const { onboarding } = await this.client!.api.get("/onboard/hello"); - - if (onboarding) { - getController("modal").push({ - type: "onboarding", - callback: async (username: string) => - this.client!.completeOnboarding({ username }, false).then(() => - this.continueLogin(data) - ), - }); - - return; - } - } - - await this.continueLogin(data); - - break; - } - // Ready successfully received - case "SUCCESS": { - this.assert("Connecting"); - this.state = "Online"; - break; - } - // Client got disconnected - case "DISCONNECT": { - if (navigator.onLine) { - this.assert("Online"); - this.state = "Disconnected"; - - setTimeout(() => { - // Check we are still disconnected before retrying. - if (this.state === "Disconnected") { - this.emit({ - action: "RETRY", - }); - } - }, 1000); - } - - break; - } - // We should try reconnecting - case "RETRY": { - this.assert("Disconnected"); - this.client!.websocket.connect(); - this.state = "Connecting"; - break; - } - // User instructed logout - case "LOGOUT": { - this.assert("Connecting", "Online", "Disconnected"); - this.state = "Ready"; - this.destroyClient(); - break; - } - // Browser went offline - case "OFFLINE": { - this.state = "Offline"; - break; - } - // Browser went online - case "ONLINE": { - this.assert("Offline"); - if (this.client) { - this.state = "Disconnected"; - this.emit({ - action: "RETRY", - }); - } else { - this.state = "Ready"; - } - break; - } - } - } - - /** - * Whether we are ready to render. - * @returns Boolean - */ - get ready() { - return !!this.client?.user; - } -}