diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index d3e21d7b..3ee2ca3b 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.4", + "version": "0.0.5", "main": "./hydrogen.es.js", "type": "module" } diff --git a/src/lib.ts b/src/lib.ts index 0aa1bb44..3e191d45 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -26,3 +26,10 @@ export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; +export {Navigation} from "./domain/navigation/Navigation.js"; +export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; +export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; +export {TemplateView} from "./platform/web/ui/general/TemplateView"; +export {ViewModel} from "./domain/ViewModel.js"; +export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; +export {AvatarView} from "./platform/web/ui/AvatarView.js"; diff --git a/src/matrix/Client.js b/src/matrix/Client.js index f070d24c..b24c1ec9 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -30,6 +30,7 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod"; import {TokenLoginMethod} from "./login/TokenLoginMethod"; import {SSOLoginHelper} from "./login/SSOLoginHelper"; import {getDehydratedDevice} from "./e2ee/Dehydration.js"; +import {Registration} from "./registration/Registration"; export const LoadStatus = createEnum( "NotLoading", @@ -131,6 +132,17 @@ export class Client { }); } + async startRegistration(homeserver, username, password, initialDeviceDisplayName) { + const request = this._platform.request; + const hsApi = new HomeServerApi({homeserver, request}); + const registration = new Registration(hsApi, { + username, + password, + initialDeviceDisplayName, + }); + return registration; + } + async startWithLogin(loginMethod, {inspectAccountSetup} = {}) { const currentStatus = this._status.get(); if (currentStatus !== LoadStatus.LoginFailed && diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index a6749cc4..e9902ef8 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -20,12 +20,13 @@ import {HomeServerRequest} from "./HomeServerRequest"; import type {IHomeServerRequest} from "./HomeServerRequest"; import type {Reconnector} from "./Reconnector"; import type {EncodedBody} from "./common"; -import type {IRequestOptions, RequestFunction} from "../../platform/types/types"; +import type {RequestFunction} from "../../platform/types/types"; import type {ILogItem} from "../../logging/types"; type RequestMethod = "POST" | "GET" | "PUT"; const CS_R0_PREFIX = "/_matrix/client/r0"; +const CS_V3_PREFIX = "/_matrix/client/v3"; const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; type Options = { @@ -35,6 +36,14 @@ type Options = { reconnector: Reconnector; }; +type BaseRequestOptions = { + log?: ILogItem; + allowedStatusCodes?: number[]; + uploadProgress?: (loadedBytes: number) => void; + timeout?: number; + prefix?: string; +}; + export class HomeServerApi { private readonly _homeserver: string; private readonly _accessToken: string; @@ -54,18 +63,9 @@ export class HomeServerApi { return this._homeserver + prefix + csPath; } - private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions, accessToken?: string): IHomeServerRequest { + private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions, accessToken?: string): IHomeServerRequest { const queryString = encodeQueryParams(queryParams); url = `${url}?${queryString}`; - let log: ILogItem | undefined; - if (options?.log) { - const parent = options?.log; - log = parent.child({ - t: "network", - url, - method, - }, parent.level.Info); - } let encodedBody: EncodedBody["body"]; const headers: Map = new Map(); if (accessToken) { @@ -84,10 +84,11 @@ export class HomeServerApi { body: encodedBody, timeout: options?.timeout, uploadProgress: options?.uploadProgress, - format: "json" // response format + format: "json", // response format + cache: method !== "GET", }); - const hsRequest = new HomeServerRequest(method, url, requestResult, log); + const hsRequest = new HomeServerRequest(method, url, requestResult, options); if (this._reconnector) { hsRequest.response().catch(err => { @@ -104,27 +105,27 @@ export class HomeServerApi { return hsRequest; } - private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._baseRequest(method, url, queryParams, body, options); } - private _authedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _authedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._baseRequest(method, url, queryParams, body, options, this._accessToken); } - private _post(csPath: string, queryParams: Record, body: Record, options?: IRequestOptions): IHomeServerRequest { + private _post(csPath: string, queryParams: Record, body: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - private _put(csPath: string, queryParams: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _put(csPath: string, queryParams: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - private _get(csPath: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _get(csPath: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - sync(since: string, filter: string, timeout: number, options?: IRequestOptions): IHomeServerRequest { + sync(since: string, filter: string, timeout: number, options?: BaseRequestOptions): IHomeServerRequest { return this._get("/sync", {since, timeout, filter}, undefined, options); } @@ -133,29 +134,29 @@ export class HomeServerApi { } // params is from, dir and optionally to, limit, filter. - messages(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + messages(roomId: string, params: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options); } // params is at, membership and not_membership - members(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + members(roomId: string, params: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, undefined, options); } - send(roomId: string, eventType: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + send(roomId: string, eventType: string, txnId: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); } - redact(roomId: string, eventId: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + redact(roomId: string, eventId: string, txnId: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); } - receipt(roomId: string, receiptType: string, eventId: string, options?: IRequestOptions): IHomeServerRequest { + receipt(roomId: string, receiptType: string, eventId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, {}, {}, options); } - state(roomId: string, eventType: string, stateKey: string, options?: IRequestOptions): IHomeServerRequest { + state(roomId: string, eventType: string, stateKey: string, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, undefined, options); } @@ -163,7 +164,22 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: BaseRequestOptions = {}): IHomeServerRequest { + options.allowedStatusCodes = [401]; + const body: any = { + auth, + password, + initial_device_displayname: initialDeviceDisplayName, + inhibit_login: inhibitLogin, + }; + if (username) { + // username is optional for registration + body.username = username; + } + return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, options); + } + + passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.password", "identifier": { @@ -175,7 +191,7 @@ export class HomeServerApi { }, options); } - tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.token", "identifier": { @@ -187,15 +203,15 @@ export class HomeServerApi { }, options); } - createFilter(userId: string, filter: Record, options?: IRequestOptions): IHomeServerRequest { + createFilter(userId: string, filter: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/user/${encodeURIComponent(userId)}/filter`, {}, filter, options); } - versions(options?: IRequestOptions): IHomeServerRequest { + versions(options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, undefined, undefined, options); } - uploadKeys(dehydratedDeviceId: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + uploadKeys(dehydratedDeviceId: string, payload: Record, options?: BaseRequestOptions): IHomeServerRequest { let path = "/keys/upload"; if (dehydratedDeviceId) { path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; @@ -203,19 +219,19 @@ export class HomeServerApi { return this._post(path, {}, payload, options); } - queryKeys(queryRequest: Record, options?: IRequestOptions): IHomeServerRequest { + queryKeys(queryRequest: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/keys/query", {}, queryRequest, options); } - claimKeys(payload: Record, options?: IRequestOptions): IHomeServerRequest { + claimKeys(payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/keys/claim", {}, payload, options); } - sendToDevice(type: string, payload: Record, txnId: string, options?: IRequestOptions): IHomeServerRequest { + sendToDevice(type: string, payload: Record, txnId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options); } - roomKeysVersion(version?: string, options?: IRequestOptions): IHomeServerRequest { + roomKeysVersion(version?: string, options?: BaseRequestOptions): IHomeServerRequest { let versionPart = ""; if (version) { versionPart = `/${encodeURIComponent(version)}`; @@ -223,70 +239,70 @@ export class HomeServerApi { return this._get(`/room_keys/version${versionPart}`, undefined, undefined, options); } - roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: IRequestOptions): IHomeServerRequest { + roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options); } - uploadRoomKeysToBackup(version: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + uploadRoomKeysToBackup(version: string, payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/room_keys/keys`, {version}, payload, options); } - uploadAttachment(blob: Blob, filename: string, options?: IRequestOptions): IHomeServerRequest { + uploadAttachment(blob: Blob, filename: string, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); } - setPusher(pusher: Record, options?: IRequestOptions): IHomeServerRequest { + setPusher(pusher: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/pushers/set", {}, pusher, options); } - getPushers(options?: IRequestOptions): IHomeServerRequest { + getPushers(options?: BaseRequestOptions): IHomeServerRequest { return this._get("/pushers", undefined, undefined, options); } - join(roomId: string, options?: IRequestOptions): IHomeServerRequest { + join(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options); } - joinIdOrAlias(roomIdOrAlias: string, options?: IRequestOptions): IHomeServerRequest { + joinIdOrAlias(roomIdOrAlias: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, {}, {}, options); } - leave(roomId: string, options?: IRequestOptions): IHomeServerRequest { + leave(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, {}, {}, options); } - forget(roomId: string, options?: IRequestOptions): IHomeServerRequest { + forget(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, {}, {}, options); } - logout(options?: IRequestOptions): IHomeServerRequest { + logout(options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/logout`, {}, {}, options); } - getDehydratedDevice(options: IRequestOptions = {}): IHomeServerRequest { + getDehydratedDevice(options: BaseRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._get(`/dehydrated_device`, undefined, undefined, options); } - createDehydratedDevice(payload: Record, options: IRequestOptions = {}): IHomeServerRequest { + createDehydratedDevice(payload: Record, options: BaseRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._put(`/dehydrated_device`, {}, payload, options); } - claimDehydratedDevice(deviceId: string, options: IRequestOptions = {}): IHomeServerRequest { + claimDehydratedDevice(deviceId: string, options: BaseRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options); } - profile(userId: string, options?: IRequestOptions): IHomeServerRequest { + profile(userId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/profile/${encodeURIComponent(userId)}`); } - createRoom(payload: Record, options?: IRequestOptions): IHomeServerRequest { + createRoom(payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/createRoom`, {}, payload, options); } - setAccountData(ownUserId: string, type: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + setAccountData(ownUserId: string, type: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options); } } diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index ea5d2e40..d6745d03 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -22,21 +22,32 @@ import type {ILogItem} from "../../logging/types"; export interface IHomeServerRequest { abort(): void; response(): Promise; + responseCode(): Promise; } +type HomeServerRequestOptions = { + log?: ILogItem; + allowedStatusCodes?: number[]; +}; + export class HomeServerRequest implements IHomeServerRequest { private readonly _log?: ILogItem; private _sourceRequest?: RequestResult; // as we add types for expected responses from hs, this could be a generic class instead private readonly _promise: Promise; - constructor(method: string, url: string, sourceRequest: RequestResult, log?: ILogItem) { + constructor(method: string, url: string, sourceRequest: RequestResult, options?: HomeServerRequestOptions) { + let log: ILogItem | undefined; + if (options?.log) { + const parent = options?.log; + log = parent.child({ t: "network", url, method, }, parent.level.Info); + } this._log = log; this._sourceRequest = sourceRequest; this._promise = sourceRequest.response().then(response => { log?.set("status", response.status); // ok? - if (response.status >= 200 && response.status < 300) { + if (response.status >= 200 && response.status < 300 || options?.allowedStatusCodes?.includes(response.status)) { log?.finish(); return response.body; } else { @@ -104,6 +115,11 @@ export class HomeServerRequest implements IHomeServerRequest { response(): Promise { return this._promise; } + + async responseCode(): Promise { + const response = await this._sourceRequest.response(); + return response.status; + } } import {Request as MockRequest} from "../../mocks/Request.js"; diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index 45405da3..dc5c501b 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -25,31 +25,60 @@ import type {IHomeServerRequest} from "./HomeServerRequest.js"; class Request implements IHomeServerRequest { public readonly methodName: string; public readonly args: any[]; - public resolve: (result: any) => void; - public reject: (error: Error) => void; - public requestResult?: IHomeServerRequest; + private responseResolve: (result: any) => void; + public responseReject: (error: Error) => void; + private responseCodeResolve: (result: any) => void; + private responseCodeReject: (result: any) => void; + private _requestResult?: IHomeServerRequest; private readonly _responsePromise: Promise; + private _responseCodePromise: Promise; constructor(methodName: string, args: any[]) { this.methodName = methodName; this.args = args; this._responsePromise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; + this.responseResolve = resolve; + this.responseReject = reject; }); } abort(): void { - if (this.requestResult) { - this.requestResult.abort(); + if (this._requestResult) { + this._requestResult.abort(); } else { - this.reject(new AbortError()); + this.responseReject(new AbortError()); + this.responseCodeReject?.(new AbortError()); } } response(): Promise { return this._responsePromise; } + + responseCode(): Promise { + if (this.requestResult) { + return this.requestResult.responseCode(); + } + if (!this._responseCodePromise) { + this._responseCodePromise = new Promise((resolve, reject) => { + this.responseCodeResolve = resolve; + this.responseCodeReject = reject; + }); + } + return this._responseCodePromise; + } + + async setRequestResult(result) { + this._requestResult = result; + const response = await this._requestResult?.response(); + this.responseResolve(response); + const responseCode = await this._requestResult?.responseCode(); + this.responseCodeResolve(responseCode); + } + + get requestResult() { + return this._requestResult; + } } class HomeServerApiWrapper { @@ -113,9 +142,7 @@ export class RequestScheduler { request.methodName ].apply(this._hsApi, request.args); // so the request can be aborted - request.requestResult = requestResult; - const response = await requestResult.response(); - request.resolve(response); + await request.setRequestResult(requestResult); return; } catch (err) { if ( @@ -135,7 +162,7 @@ export class RequestScheduler { await retryDelay.waitForRetry(); } } else { - request.reject(err); + request.responseReject(err); return; } } diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts new file mode 100644 index 00000000..c9c9af87 --- /dev/null +++ b/src/matrix/registration/Registration.ts @@ -0,0 +1,119 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type {HomeServerApi} from "../net/HomeServerApi"; +import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; +import {DummyAuth} from "./stages/DummyAuth"; +import {TermsAuth} from "./stages/TermsAuth"; +import type { + AccountDetails, + RegistrationFlow, + RegistrationResponseMoreDataNeeded, + RegistrationResponse, + RegistrationResponseSuccess, + RegistrationParams, +} from "./types"; + +type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; + +export class Registration { + private readonly _hsApi: HomeServerApi; + private readonly _accountDetails: AccountDetails; + private readonly _flowSelector: FlowSelector; + private _sessionInfo?: RegistrationResponseSuccess + + constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { + this._hsApi = hsApi; + this._accountDetails = accountDetails; + this._flowSelector = flowSelector ?? (flows => flows[0]); + } + + async start(): Promise { + const response = await this._hsApi.register( + this._accountDetails.username, + this._accountDetails.password, + this._accountDetails.initialDeviceDisplayName, + undefined, + this._accountDetails.inhibitLogin).response(); + return this.parseStagesFromResponse(response); + } + + /** + * Finish a registration stage, return value is: + * - the next stage if this stage was completed successfully + * - undefined if registration is completed + */ + async submitStage(stage: BaseRegistrationStage): Promise { + const auth = stage.generateAuthenticationData(); + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; + const request = this._hsApi.register(username, password, initialDeviceDisplayName, auth, inhibitLogin); + const response = await request.response(); + const status = await request.responseCode(); + const registrationResponse: RegistrationResponse = { ...response, status }; + return this.parseRegistrationResponse(registrationResponse, stage); + } + + private parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { + const { session, params } = response; + const flow = this._flowSelector(response.flows); + if (!flow) { + throw new Error("flowSelector did not return any flow!"); + } + let firstStage: BaseRegistrationStage | undefined; + let lastStage: BaseRegistrationStage | undefined; + for (const stage of flow.stages) { + const registrationStage = this._createRegistrationStage(stage, session, params); + if (!firstStage) { + firstStage = registrationStage; + lastStage = registrationStage; + } else { + lastStage!.setNextStage(registrationStage); + lastStage = registrationStage; + } + } + return firstStage!; + } + + private async parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { + switch (response.status) { + case 200: + this._sessionInfo = response; + return undefined; + case 401: + if (response.completed?.includes(currentStage.type)) { + return currentStage.nextStage; + } + else { + throw new Error("This stage could not be completed!"); + } + } + } + + private _createRegistrationStage(type: string, session: string, params?: RegistrationParams) { + switch (type) { + case "m.login.dummy": + return new DummyAuth(session, params?.[type]); + case "m.login.terms": + return new TermsAuth(session, params?.[type]); + default: + throw new Error(`Unknown stage: ${type}`); + } + } + + get sessionInfo(): RegistrationResponseSuccess | undefined { + return this._sessionInfo; + } +} diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts new file mode 100644 index 00000000..cc5f46c1 --- /dev/null +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -0,0 +1,48 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type {AuthenticationData, RegistrationParams} from "../types"; + +export abstract class BaseRegistrationStage { + protected _session: string; + protected _nextStage: BaseRegistrationStage; + protected readonly _params?: Record + + constructor(session: string, params?: RegistrationParams) { + this._session = session; + this._params = params; + } + + /** + * eg: m.login.recaptcha or m.login.dummy + */ + abstract get type(): string; + + /** + * This method should return auth part that must be provided to + * /register endpoint to successfully complete this stage + */ + /** @internal */ + abstract generateAuthenticationData(): AuthenticationData; + + setNextStage(stage: BaseRegistrationStage) { + this._nextStage = stage; + } + + get nextStage(): BaseRegistrationStage { + return this._nextStage; + } +} diff --git a/src/matrix/registration/stages/DummyAuth.ts b/src/matrix/registration/stages/DummyAuth.ts new file mode 100644 index 00000000..b7f0a6ff --- /dev/null +++ b/src/matrix/registration/stages/DummyAuth.ts @@ -0,0 +1,31 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {AuthenticationData} from "../types"; +import {BaseRegistrationStage} from "./BaseRegistrationStage"; + +export class DummyAuth extends BaseRegistrationStage { + generateAuthenticationData(): AuthenticationData { + return { + session: this._session, + type: this.type, + }; + } + + get type(): string { + return "m.login.dummy"; + } +} diff --git a/src/matrix/registration/stages/TermsAuth.ts b/src/matrix/registration/stages/TermsAuth.ts new file mode 100644 index 00000000..bf54dd4d --- /dev/null +++ b/src/matrix/registration/stages/TermsAuth.ts @@ -0,0 +1,40 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {AuthenticationData} from "../types"; +import {BaseRegistrationStage} from "./BaseRegistrationStage"; + +export class TermsAuth extends BaseRegistrationStage { + generateAuthenticationData(): AuthenticationData { + return { + session: this._session, + type: this.type, + // No other auth data needed for m.login.terms + }; + } + + get type(): string { + return "m.login.terms"; + } + + get privacyPolicy() { + return this._params?.policies["privacy_policy"]; + } + + get termsOfService() { + return this._params?.policies["terms_of_service"]; + } +} diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts new file mode 100644 index 00000000..f1ddbe98 --- /dev/null +++ b/src/matrix/registration/types.ts @@ -0,0 +1,55 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export type AccountDetails = { + username: string | null; + password: string; + initialDeviceDisplayName: string; + inhibitLogin: boolean; +} + +export type RegistrationResponse = RegistrationResponseMoreDataNeeded | RegistrationResponseSuccess; + +export type RegistrationResponseMoreDataNeeded = { + completed?: string[]; + flows: RegistrationFlow[]; + params: Record; + session: string; + status: 401; +} + +export type RegistrationResponseSuccess = { + user_id: string; + device_id: string; + access_token?: string; + status: 200; +} + +export type RegistrationFlow = { + stages: string[]; +} + +/* Types for Registration Stage */ +export type AuthenticationData = { + type: string; + session: string; + [key: string]: any; +} + +// contains additional data needed to complete a stage, eg: link to privacy policy +export type RegistrationParams = { + [key: string]: any; +} diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 91884117..da8ec8e7 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -24,8 +24,6 @@ export interface IRequestOptions { body?: EncodedBody; headers?: Map; cache?: boolean; - log?: ILogItem; - prefix?: string; method?: string; format?: string; }