Merge branch 'master' into bwindels/create-room

This commit is contained in:
Bruno Windels 2022-02-10 20:05:40 +01:00
commit a442b4b009
12 changed files with 436 additions and 67 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "hydrogen-view-sdk", "name": "hydrogen-view-sdk",
"description": "Embeddable matrix client library, including view components", "description": "Embeddable matrix client library, including view components",
"version": "0.0.4", "version": "0.0.5",
"main": "./hydrogen.es.js", "main": "./hydrogen.es.js",
"type": "module" "type": "module"
} }

View file

@ -26,3 +26,10 @@ export {RoomViewModel} from "./domain/session/room/RoomViewModel.js";
export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js";
export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js";
export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; 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";

View file

@ -30,6 +30,7 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod";
import {TokenLoginMethod} from "./login/TokenLoginMethod"; import {TokenLoginMethod} from "./login/TokenLoginMethod";
import {SSOLoginHelper} from "./login/SSOLoginHelper"; import {SSOLoginHelper} from "./login/SSOLoginHelper";
import {getDehydratedDevice} from "./e2ee/Dehydration.js"; import {getDehydratedDevice} from "./e2ee/Dehydration.js";
import {Registration} from "./registration/Registration";
export const LoadStatus = createEnum( export const LoadStatus = createEnum(
"NotLoading", "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} = {}) { async startWithLogin(loginMethod, {inspectAccountSetup} = {}) {
const currentStatus = this._status.get(); const currentStatus = this._status.get();
if (currentStatus !== LoadStatus.LoginFailed && if (currentStatus !== LoadStatus.LoginFailed &&

View file

@ -20,12 +20,13 @@ import {HomeServerRequest} from "./HomeServerRequest";
import type {IHomeServerRequest} from "./HomeServerRequest"; import type {IHomeServerRequest} from "./HomeServerRequest";
import type {Reconnector} from "./Reconnector"; import type {Reconnector} from "./Reconnector";
import type {EncodedBody} from "./common"; 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"; import type {ILogItem} from "../../logging/types";
type RequestMethod = "POST" | "GET" | "PUT"; type RequestMethod = "POST" | "GET" | "PUT";
const CS_R0_PREFIX = "/_matrix/client/r0"; const CS_R0_PREFIX = "/_matrix/client/r0";
const CS_V3_PREFIX = "/_matrix/client/v3";
const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2";
type Options = { type Options = {
@ -35,6 +36,14 @@ type Options = {
reconnector: Reconnector; reconnector: Reconnector;
}; };
type BaseRequestOptions = {
log?: ILogItem;
allowedStatusCodes?: number[];
uploadProgress?: (loadedBytes: number) => void;
timeout?: number;
prefix?: string;
};
export class HomeServerApi { export class HomeServerApi {
private readonly _homeserver: string; private readonly _homeserver: string;
private readonly _accessToken: string; private readonly _accessToken: string;
@ -54,18 +63,9 @@ export class HomeServerApi {
return this._homeserver + prefix + csPath; return this._homeserver + prefix + csPath;
} }
private _baseRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions, accessToken?: string): IHomeServerRequest { private _baseRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: BaseRequestOptions, accessToken?: string): IHomeServerRequest {
const queryString = encodeQueryParams(queryParams); const queryString = encodeQueryParams(queryParams);
url = `${url}?${queryString}`; 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"]; let encodedBody: EncodedBody["body"];
const headers: Map<string, string | number> = new Map(); const headers: Map<string, string | number> = new Map();
if (accessToken) { if (accessToken) {
@ -84,10 +84,11 @@ export class HomeServerApi {
body: encodedBody, body: encodedBody,
timeout: options?.timeout, timeout: options?.timeout,
uploadProgress: options?.uploadProgress, 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) { if (this._reconnector) {
hsRequest.response().catch(err => { hsRequest.response().catch(err => {
@ -104,27 +105,27 @@ export class HomeServerApi {
return hsRequest; return hsRequest;
} }
private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._baseRequest(method, url, queryParams, body, options); return this._baseRequest(method, url, queryParams, body, options);
} }
private _authedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { private _authedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._baseRequest(method, url, queryParams, body, options, this._accessToken); return this._baseRequest(method, url, queryParams, body, options, this._accessToken);
} }
private _post(csPath: string, queryParams: Record<string, any>, body: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { private _post(csPath: string, queryParams: Record<string, any>, body: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
} }
private _put(csPath: string, queryParams: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { private _put(csPath: string, queryParams: Record<string, any>, body?: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
} }
private _get(csPath: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { private _get(csPath: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); 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); 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. // params is from, dir and optionally to, limit, filter.
messages(roomId: string, params: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { messages(roomId: string, params: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options); return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options);
} }
// params is at, membership and not_membership // params is at, membership and not_membership
members(roomId: string, params: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { members(roomId: string, params: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, undefined, options); return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, undefined, options);
} }
send(roomId: string, eventType: string, txnId: string, content: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { send(roomId: string, eventType: string, txnId: string, content: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
} }
redact(roomId: string, eventId: string, txnId: string, content: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { redact(roomId: string, eventId: string, txnId: string, content: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); 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)}`, return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`,
{}, {}, options); {}, {}, 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); 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")); 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<string, any>, 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, { return this._unauthedRequest("POST", this._url("/login"), undefined, {
"type": "m.login.password", "type": "m.login.password",
"identifier": { "identifier": {
@ -175,7 +191,7 @@ export class HomeServerApi {
}, options); }, 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, { return this._unauthedRequest("POST", this._url("/login"), undefined, {
"type": "m.login.token", "type": "m.login.token",
"identifier": { "identifier": {
@ -187,15 +203,15 @@ export class HomeServerApi {
}, options); }, options);
} }
createFilter(userId: string, filter: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { createFilter(userId: string, filter: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post(`/user/${encodeURIComponent(userId)}/filter`, {}, filter, options); 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); return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, undefined, undefined, options);
} }
uploadKeys(dehydratedDeviceId: string, payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { uploadKeys(dehydratedDeviceId: string, payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
let path = "/keys/upload"; let path = "/keys/upload";
if (dehydratedDeviceId) { if (dehydratedDeviceId) {
path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; path = path + `/${encodeURIComponent(dehydratedDeviceId)}`;
@ -203,19 +219,19 @@ export class HomeServerApi {
return this._post(path, {}, payload, options); return this._post(path, {}, payload, options);
} }
queryKeys(queryRequest: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { queryKeys(queryRequest: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post("/keys/query", {}, queryRequest, options); return this._post("/keys/query", {}, queryRequest, options);
} }
claimKeys(payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { claimKeys(payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post("/keys/claim", {}, payload, options); return this._post("/keys/claim", {}, payload, options);
} }
sendToDevice(type: string, payload: Record<string, any>, txnId: string, options?: IRequestOptions): IHomeServerRequest { sendToDevice(type: string, payload: Record<string, any>, txnId: string, options?: BaseRequestOptions): IHomeServerRequest {
return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options); return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options);
} }
roomKeysVersion(version?: string, options?: IRequestOptions): IHomeServerRequest { roomKeysVersion(version?: string, options?: BaseRequestOptions): IHomeServerRequest {
let versionPart = ""; let versionPart = "";
if (version) { if (version) {
versionPart = `/${encodeURIComponent(version)}`; versionPart = `/${encodeURIComponent(version)}`;
@ -223,70 +239,70 @@ export class HomeServerApi {
return this._get(`/room_keys/version${versionPart}`, undefined, undefined, options); 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); return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options);
} }
uploadRoomKeysToBackup(version: string, payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { uploadRoomKeysToBackup(version: string, payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._put(`/room_keys/keys`, {version}, payload, options); 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); return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options);
} }
setPusher(pusher: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { setPusher(pusher: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post("/pushers/set", {}, pusher, options); return this._post("/pushers/set", {}, pusher, options);
} }
getPushers(options?: IRequestOptions): IHomeServerRequest { getPushers(options?: BaseRequestOptions): IHomeServerRequest {
return this._get("/pushers", undefined, undefined, options); 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); 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); 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); 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); return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, {}, {}, options);
} }
logout(options?: IRequestOptions): IHomeServerRequest { logout(options?: BaseRequestOptions): IHomeServerRequest {
return this._post(`/logout`, {}, {}, options); return this._post(`/logout`, {}, {}, options);
} }
getDehydratedDevice(options: IRequestOptions = {}): IHomeServerRequest { getDehydratedDevice(options: BaseRequestOptions = {}): IHomeServerRequest {
options.prefix = DEHYDRATION_PREFIX; options.prefix = DEHYDRATION_PREFIX;
return this._get(`/dehydrated_device`, undefined, undefined, options); return this._get(`/dehydrated_device`, undefined, undefined, options);
} }
createDehydratedDevice(payload: Record<string, any>, options: IRequestOptions = {}): IHomeServerRequest { createDehydratedDevice(payload: Record<string, any>, options: BaseRequestOptions = {}): IHomeServerRequest {
options.prefix = DEHYDRATION_PREFIX; options.prefix = DEHYDRATION_PREFIX;
return this._put(`/dehydrated_device`, {}, payload, options); return this._put(`/dehydrated_device`, {}, payload, options);
} }
claimDehydratedDevice(deviceId: string, options: IRequestOptions = {}): IHomeServerRequest { claimDehydratedDevice(deviceId: string, options: BaseRequestOptions = {}): IHomeServerRequest {
options.prefix = DEHYDRATION_PREFIX; options.prefix = DEHYDRATION_PREFIX;
return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options); 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)}`); return this._get(`/profile/${encodeURIComponent(userId)}`);
} }
createRoom(payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { createRoom(payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post(`/createRoom`, {}, payload, options); return this._post(`/createRoom`, {}, payload, options);
} }
setAccountData(ownUserId: string, type: string, content: Record<string, any>, options?: IRequestOptions): IHomeServerRequest { setAccountData(ownUserId: string, type: string, content: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options); return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options);
} }
} }

View file

@ -22,21 +22,32 @@ import type {ILogItem} from "../../logging/types";
export interface IHomeServerRequest { export interface IHomeServerRequest {
abort(): void; abort(): void;
response(): Promise<any>; response(): Promise<any>;
responseCode(): Promise<number>;
} }
type HomeServerRequestOptions = {
log?: ILogItem;
allowedStatusCodes?: number[];
};
export class HomeServerRequest implements IHomeServerRequest { export class HomeServerRequest implements IHomeServerRequest {
private readonly _log?: ILogItem; private readonly _log?: ILogItem;
private _sourceRequest?: RequestResult; private _sourceRequest?: RequestResult;
// as we add types for expected responses from hs, this could be a generic class instead // as we add types for expected responses from hs, this could be a generic class instead
private readonly _promise: Promise<any>; private readonly _promise: Promise<any>;
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._log = log;
this._sourceRequest = sourceRequest; this._sourceRequest = sourceRequest;
this._promise = sourceRequest.response().then(response => { this._promise = sourceRequest.response().then(response => {
log?.set("status", response.status); log?.set("status", response.status);
// ok? // ok?
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300 || options?.allowedStatusCodes?.includes(response.status)) {
log?.finish(); log?.finish();
return response.body; return response.body;
} else { } else {
@ -104,6 +115,11 @@ export class HomeServerRequest implements IHomeServerRequest {
response(): Promise<any> { response(): Promise<any> {
return this._promise; return this._promise;
} }
async responseCode(): Promise<number> {
const response = await this._sourceRequest.response();
return response.status;
}
} }
import {Request as MockRequest} from "../../mocks/Request.js"; import {Request as MockRequest} from "../../mocks/Request.js";

View file

@ -25,31 +25,60 @@ import type {IHomeServerRequest} from "./HomeServerRequest.js";
class Request implements IHomeServerRequest { class Request implements IHomeServerRequest {
public readonly methodName: string; public readonly methodName: string;
public readonly args: any[]; public readonly args: any[];
public resolve: (result: any) => void; private responseResolve: (result: any) => void;
public reject: (error: Error) => void; public responseReject: (error: Error) => void;
public requestResult?: IHomeServerRequest; private responseCodeResolve: (result: any) => void;
private responseCodeReject: (result: any) => void;
private _requestResult?: IHomeServerRequest;
private readonly _responsePromise: Promise<any>; private readonly _responsePromise: Promise<any>;
private _responseCodePromise: Promise<any>;
constructor(methodName: string, args: any[]) { constructor(methodName: string, args: any[]) {
this.methodName = methodName; this.methodName = methodName;
this.args = args; this.args = args;
this._responsePromise = new Promise((resolve, reject) => { this._responsePromise = new Promise((resolve, reject) => {
this.resolve = resolve; this.responseResolve = resolve;
this.reject = reject; this.responseReject = reject;
}); });
} }
abort(): void { abort(): void {
if (this.requestResult) { if (this._requestResult) {
this.requestResult.abort(); this._requestResult.abort();
} else { } else {
this.reject(new AbortError()); this.responseReject(new AbortError());
this.responseCodeReject?.(new AbortError());
} }
} }
response(): Promise<any> { response(): Promise<any> {
return this._responsePromise; return this._responsePromise;
} }
responseCode(): Promise<number> {
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 { class HomeServerApiWrapper {
@ -113,9 +142,7 @@ export class RequestScheduler {
request.methodName request.methodName
].apply(this._hsApi, request.args); ].apply(this._hsApi, request.args);
// so the request can be aborted // so the request can be aborted
request.requestResult = requestResult; await request.setRequestResult(requestResult);
const response = await requestResult.response();
request.resolve(response);
return; return;
} catch (err) { } catch (err) {
if ( if (
@ -135,7 +162,7 @@ export class RequestScheduler {
await retryDelay.waitForRetry(); await retryDelay.waitForRetry();
} }
} else { } else {
request.reject(err); request.responseReject(err);
return; return;
} }
} }

View file

@ -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<BaseRegistrationStage> {
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<BaseRegistrationStage | undefined> {
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;
}
}

View file

@ -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<string, any>
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;
}
}

View file

@ -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";
}
}

View file

@ -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"];
}
}

View file

@ -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<string, any>;
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;
}

View file

@ -24,8 +24,6 @@ export interface IRequestOptions {
body?: EncodedBody; body?: EncodedBody;
headers?: Map<string, string|number>; headers?: Map<string, string|number>;
cache?: boolean; cache?: boolean;
log?: ILogItem;
prefix?: string;
method?: string; method?: string;
format?: string; format?: string;
} }