Merge branch 'develop' into handle-encoded-urls

This commit is contained in:
Travis Ralston 2021-04-28 09:53:16 -06:00
commit f0f875d1e2
146 changed files with 5594 additions and 1985 deletions

View file

@ -1,6 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019-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.
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import {_t, _td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils";
import SdkConfig from '../SdkConfig';
const LIVELINESS_DISCOVERY_ERRORS = [
const LIVELINESS_DISCOVERY_ERRORS: string[] = [
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
];
@ -40,17 +39,23 @@ export class ValidatedServerConfig {
warning: string;
}
export interface IAuthComponentState {
serverIsAlive: boolean;
serverErrorIsFatal: boolean;
serverDeadError?: ReactNode;
}
export default class AutoDiscoveryUtils {
/**
* Checks if a given error or error message is considered an error
* relating to the liveliness of the server. Must be an error returned
* from this AutoDiscoveryUtils class.
* @param {string|Error} error The error to check
* @param {string | Error} error The error to check
* @returns {boolean} True if the error is a liveliness error.
*/
static isLivelinessError(error: string|Error): boolean {
static isLivelinessError(error: string | Error): boolean {
if (!error) return false;
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message);
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message);
}
/**
@ -61,7 +66,7 @@ export default class AutoDiscoveryUtils {
* implementation for known values.
* @returns {*} The state for the component, given the error.
*/
static authComponentStateForError(err: string | Error | null, pageName = "login"): Object {
static authComponentStateForError(err: string | Error | null, pageName = "login"): IAuthComponentState {
if (!err) {
return {
serverIsAlive: true,
@ -70,7 +75,7 @@ export default class AutoDiscoveryUtils {
};
}
let title = _t("Cannot reach homeserver");
let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
let body: ReactNode = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
if (!AutoDiscoveryUtils.isLivelinessError(err)) {
const brand = SdkConfig.get().brand;
title = _t("Your %(brand)s is misconfigured", { brand });
@ -92,7 +97,7 @@ export default class AutoDiscoveryUtils {
}
let isFatalError = true;
const errorMessage = err.message ? err.message : err;
const errorMessage = typeof err === "string" ? err : err.message;
if (errorMessage === AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER) {
isFatalError = false;
title = _t("Cannot reach identity server");
@ -141,7 +146,10 @@ export default class AutoDiscoveryUtils {
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
*/
static async validateServerConfigWithStaticUrls(
homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig {
homeserverUrl: string,
identityUrl?: string,
syntaxOnly = false,
): Promise<ValidatedServerConfig> {
if (!homeserverUrl) {
throw newTranslatableError(_td("No homeserver URL provided"));
}
@ -171,7 +179,7 @@ export default class AutoDiscoveryUtils {
* @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate.
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
*/
static async validateServerName(serverName: string): ValidatedServerConfig {
static async validateServerName(serverName: string): Promise<ValidatedServerConfig> {
const result = await AutoDiscovery.findClientConfig(serverName);
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}

View file

@ -55,6 +55,15 @@ export default class DMRoomMap {
return DMRoomMap.sharedInstance;
}
/**
* Set the shared instance to the instance supplied
* Used by tests
* @param inst the new shared instance
*/
public static setShared(inst: DMRoomMap) {
DMRoomMap.sharedInstance = inst;
}
/**
* Returns a shared instance of the class
* that uses the singleton matrix client

View file

@ -49,12 +49,6 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
}
}
export function messageForSendError(errorData) {
if (errorData.errcode === "M_TOO_LARGE") {
return _t("The message you are trying to send is too large.");
}
}
export function messageForSyncError(err) {
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const limitError = messageForResourceLimitError(

50
src/utils/Mouse.ts Normal file
View file

@ -0,0 +1,50 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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.
*/
/**
* Different browsers use different deltaModes. This causes different behaviour.
* To avoid that we use this function to convert any event to pixels.
* @param {WheelEvent} event to normalize
* @returns {WheelEvent} normalized event event
*/
export function normalizeWheelEvent(event: WheelEvent): WheelEvent {
const LINE_HEIGHT = 18;
let deltaX;
let deltaY;
let deltaZ;
if (event.deltaMode === 1) { // Units are lines
deltaX = (event.deltaX * LINE_HEIGHT);
deltaY = (event.deltaY * LINE_HEIGHT);
deltaZ = (event.deltaZ * LINE_HEIGHT);
} else {
deltaX = event.deltaX;
deltaY = event.deltaY;
deltaZ = event.deltaZ;
}
return new WheelEvent(
"syntheticWheel",
{
deltaMode: 0,
deltaY: deltaY,
deltaX: deltaX,
deltaZ: deltaZ,
...event,
},
);
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019-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.
@ -32,15 +32,15 @@ try {
const SYNC_STORE_NAME = "riot-web-sync";
const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
function log(msg) {
function log(msg: string) {
console.log(`StorageManager: ${msg}`);
}
function error(msg) {
console.error(`StorageManager: ${msg}`);
function error(msg: string, ...args: string[]) {
console.error(`StorageManager: ${msg}`, ...args);
}
function track(action) {
function track(action: string) {
Analytics.trackEvent("StorageManager", action);
}
@ -73,7 +73,7 @@ export async function checkConsistency() {
dataInLocalStorage = localStorage.length > 0;
log(`Local storage contains data? ${dataInLocalStorage}`);
cryptoInited = localStorage.getItem("mx_crypto_initialised");
cryptoInited = !!localStorage.getItem("mx_crypto_initialised");
log(`Crypto initialised? ${cryptoInited}`);
} else {
healthy = false;

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018, 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.
@ -26,44 +26,48 @@ Once a timer is finished or aborted, it can't be started again
a new one through `clone()` or `cloneIfRun()`.
*/
export default class Timer {
constructor(timeout) {
this._timeout = timeout;
this._onTimeout = this._onTimeout.bind(this);
this._setNotStarted();
private timerHandle: NodeJS.Timeout;
private startTs: number;
private promise: Promise<void>;
private resolve: () => void;
private reject: (Error) => void;
constructor(private timeout: number) {
this.setNotStarted();
}
_setNotStarted() {
this._timerHandle = null;
this._startTs = null;
this._promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
private setNotStarted() {
this.timerHandle = null;
this.startTs = null;
this.promise = new Promise<void>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
}).finally(() => {
this._timerHandle = null;
this.timerHandle = null;
});
}
_onTimeout() {
private onTimeout = () => {
const now = Date.now();
const elapsed = now - this._startTs;
if (elapsed >= this._timeout) {
this._resolve();
this._setNotStarted();
const elapsed = now - this.startTs;
if (elapsed >= this.timeout) {
this.resolve();
this.setNotStarted();
} else {
const delta = this._timeout - elapsed;
this._timerHandle = setTimeout(this._onTimeout, delta);
const delta = this.timeout - elapsed;
this.timerHandle = setTimeout(this.onTimeout, delta);
}
}
changeTimeout(timeout) {
if (timeout === this._timeout) {
changeTimeout(timeout: number) {
if (timeout === this.timeout) {
return;
}
const isSmallerTimeout = timeout < this._timeout;
this._timeout = timeout;
const isSmallerTimeout = timeout < this.timeout;
this.timeout = timeout;
if (this.isRunning() && isSmallerTimeout) {
clearTimeout(this._timerHandle);
this._onTimeout();
clearTimeout(this.timerHandle);
this.onTimeout();
}
}
@ -73,8 +77,8 @@ export default class Timer {
*/
start() {
if (!this.isRunning()) {
this._startTs = Date.now();
this._timerHandle = setTimeout(this._onTimeout, this._timeout);
this.startTs = Date.now();
this.timerHandle = setTimeout(this.onTimeout, this.timeout);
}
return this;
}
@ -89,7 +93,7 @@ export default class Timer {
// can be called in fast succession,
// instead just take note and compare
// when the already running timeout expires
this._startTs = Date.now();
this.startTs = Date.now();
return this;
} else {
return this.start();
@ -103,9 +107,9 @@ export default class Timer {
*/
abort() {
if (this.isRunning()) {
clearTimeout(this._timerHandle);
this._reject(new Error("Timer was aborted."));
this._setNotStarted();
clearTimeout(this.timerHandle);
this.reject(new Error("Timer was aborted."));
this.setNotStarted();
}
return this;
}
@ -116,10 +120,10 @@ export default class Timer {
*@return {Promise}
*/
finished() {
return this._promise;
return this.promise;
}
isRunning() {
return this._timerHandle !== null;
return this.timerHandle !== null;
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020, 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.
@ -15,23 +15,47 @@ limitations under the License.
*/
/**
* Quickly resample an array to have less data points. This isn't a perfect representation,
* though this does work best if given a large array to downsample to a much smaller array.
* @param {number[]} input The input array to downsample.
* Quickly resample an array to have less/more data points. If an input which is larger
* than the desired size is provided, it will be downsampled. Similarly, if the input
* is smaller than the desired size then it will be upsampled.
* @param {number[]} input The input array to resample.
* @param {number} points The number of samples to end up with.
* @returns {number[]} The downsampled array.
* @returns {number[]} The resampled array.
*/
export function arrayFastResample(input: number[], points: number): number[] {
// Heavily inpired by matrix-media-repo (used with permission)
if (input.length === points) return input; // short-circuit a complicated call
// Heavily inspired by matrix-media-repo (used with permission)
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
const everyNth = Math.round(input.length / points);
const samples: number[] = [];
for (let i = 0; i < input.length; i += everyNth) {
samples.push(input[i]);
let samples: number[] = [];
if (input.length > points) {
// Danger: this loop can cause out of memory conditions if the input is too small.
const everyNth = Math.round(input.length / points);
for (let i = 0; i < input.length; i += everyNth) {
samples.push(input[i]);
}
} else {
// Smaller inputs mean we have to spread the values over the desired length. We
// end up overshooting the target length in doing this, so we'll resample down
// before returning. This recursion is risky, but mathematically should not go
// further than 1 level deep.
const spreadFactor = Math.ceil(points / input.length);
for (const val of input) {
samples.push(...arraySeed(val, spreadFactor));
}
samples = arrayFastResample(samples, points);
}
// Sanity fill, just in case
while (samples.length < points) {
samples.push(input[input.length - 1]);
}
// Sanity trim, just in case
if (samples.length > points) {
samples = samples.slice(0, points);
}
return samples;
}
@ -49,12 +73,32 @@ export function arraySeed<T>(val: T, length: number): T[] {
return a;
}
/**
* Trims or fills the array to ensure it meets the desired length. The seed array
* given is pulled from to fill any missing slots - it is recommended that this be
* at least `len` long. The resulting array will be exactly `len` long, either
* trimmed from the source or filled with the some/all of the seed array.
* @param {T[]} a The array to trim/fill.
* @param {number} len The length to trim or fill to, as needed.
* @param {T[]} seed Values to pull from if the array needs filling.
* @returns {T[]} The resulting array of `len` length.
*/
export function arrayTrimFill<T>(a: T[], len: number, seed: T[]): T[] {
// Dev note: we do length checks because the spread operator can result in some
// performance penalties in more critical code paths. As a utility, it should be
// as fast as possible to not cause a problem for the call stack, no matter how
// critical that stack is.
if (a.length === len) return a;
if (a.length > len) return a.slice(0, len);
return a.concat(seed.slice(0, len - a.length));
}
/**
* Clones an array as fast as possible, retaining references of the array's values.
* @param a The array to clone. Must be defined.
* @returns A copy of the array.
*/
export function arrayFastClone(a: any[]): any[] {
export function arrayFastClone<T>(a: T[]): T[] {
return a.slice(0, a.length);
}
@ -178,6 +222,13 @@ export class GroupedArray<K, T> {
constructor(private val: Map<K, T[]>) {
}
/**
* The value of this group, after all applicable alterations.
*/
public get value(): Map<K, T[]> {
return this.val;
}
/**
* Orders the grouping into an array using the provided key order.
* @param keyOrder The key order.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020, 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.
@ -19,11 +19,23 @@ limitations under the License.
* @param e The enum.
* @returns The enum values.
*/
export function getEnumValues<T>(e: any): T[] {
export function getEnumValues(e: any): (string | number)[] {
// String-based enums will simply be objects ({Key: "value"}), but number-based
// enums will instead map themselves twice: in one direction for {Key: 12} and
// the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
// the key is a string, not a number.
//
// For this reason, we try to determine what kind of enum we're dealing with.
const keys = Object.keys(e);
return keys
.filter(k => ['string', 'number'].includes(typeof(e[k])))
.map(k => e[k]);
const values: (string | number)[] = [];
for (const key of keys) {
const value = e[key];
if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
values.push(value);
}
}
return values;
}
/**

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020, 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.
@ -141,3 +141,21 @@ export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
export function objectClone<O extends {}>(obj: O): O {
return JSON.parse(JSON.stringify(obj));
}
/**
* Converts a series of entries to an object.
* @param entries The entries to convert.
* @returns The converted object.
*/
// NOTE: Deprecated once we have Object.fromEntries() support.
// @ts-ignore - return type is complaining about non-string keys, but we know better
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
const obj: {
// @ts-ignore - same as return type
[k: K]: V} = {};
for (const e of entries) {
// @ts-ignore - same as return type
obj[e[0]] = e[1];
}
return obj;
}

View file

@ -20,31 +20,31 @@ import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
* Generates permalinks that self-reference the running webapp
*/
export default class ElementPermalinkConstructor extends PermalinkConstructor {
_elementUrl: string;
private elementUrl: string;
constructor(elementUrl: string) {
super();
this._elementUrl = elementUrl;
this.elementUrl = elementUrl;
if (!this._elementUrl.startsWith("http:") && !this._elementUrl.startsWith("https:")) {
if (!this.elementUrl.startsWith("http:") && !this.elementUrl.startsWith("https:")) {
throw new Error("Element prefix URL does not appear to be an HTTP(S) URL");
}
}
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
return `${this._elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
return `${this.elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
}
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
return `${this._elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
forRoom(roomIdOrAlias: string, serverCandidates?: string[]): string {
return `${this.elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
}
forUser(userId: string): string {
return `${this._elementUrl}/#/user/${userId}`;
return `${this.elementUrl}/#/user/${userId}`;
}
forGroup(groupId: string): string {
return `${this._elementUrl}/#/group/${groupId}`;
return `${this.elementUrl}/#/group/${groupId}`;
}
forEntity(entityId: string): string {
@ -58,11 +58,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
}
isPermalinkHost(testHost: string): boolean {
const parsedUrl = new URL(this._elementUrl);
const parsedUrl = new URL(this.elementUrl);
return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match
}
encodeServerCandidates(candidates: string[]) {
encodeServerCandidates(candidates?: string[]) {
if (!candidates || candidates.length === 0) return '';
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
}
@ -71,11 +71,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
// https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
// Adapted for Element's URL format
parsePermalink(fullUrl: string): PermalinkParts {
if (!fullUrl || !fullUrl.startsWith(this._elementUrl)) {
if (!fullUrl || !fullUrl.startsWith(this.elementUrl)) {
throw new Error("Does not appear to be a permalink");
}
const parts = fullUrl.substring(`${this._elementUrl}/#/`.length);
const parts = fullUrl.substring(`${this.elementUrl}/#/`.length);
return ElementPermalinkConstructor.parseAppRoute(parts);
}

View file

@ -17,6 +17,9 @@ limitations under the License.
import isIp from "is-ip";
import * as utils from "matrix-js-sdk/src/utils";
import {Room} from "matrix-js-sdk/src/models/room";
import {EventType} from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
@ -74,29 +77,35 @@ const MAX_SERVER_CANDIDATES = 3;
// the list and magically have the link work.
export class RoomPermalinkCreator {
private room: Room;
private roomId: string;
private highestPlUserId: string;
private populationMap: { [serverName: string]: number };
private bannedHostsRegexps: RegExp[];
private allowedHostsRegexps: RegExp[];
private _serverCandidates: string[];
private started: boolean;
// We support being given a roomId as a fallback in the event the `room` object
// doesn't exist or is not healthy for us to rely on. For example, loading a
// permalink to a room which the MatrixClient doesn't know about.
constructor(room, roomId = null) {
this._room = room;
this._roomId = room ? room.roomId : roomId;
this._highestPlUserId = null;
this._populationMap = null;
this._bannedHostsRegexps = null;
this._allowedHostsRegexps = null;
constructor(room: Room, roomId: string = null) {
this.room = room;
this.roomId = room ? room.roomId : roomId;
this.highestPlUserId = null;
this.populationMap = null;
this.bannedHostsRegexps = null;
this.allowedHostsRegexps = null;
this._serverCandidates = null;
this._started = false;
this.started = false;
if (!this._roomId) {
if (!this.roomId) {
throw new Error("Failed to resolve a roomId for the permalink creator to use");
}
this.onMembership = this.onMembership.bind(this);
this.onRoomState = this.onRoomState.bind(this);
}
load() {
if (!this._room || !this._room.currentState) {
if (!this.room || !this.room.currentState) {
// Under rare and unknown circumstances it is possible to have a room with no
// currentState, at least potentially at the early stages of joining a room.
// To avoid breaking everything, we'll just warn rather than throw as well as
@ -104,23 +113,23 @@ export class RoomPermalinkCreator {
console.warn("Tried to load a permalink creator with no room state");
return;
}
this._updateAllowedServers();
this._updateHighestPlUser();
this._updatePopulationMap();
this._updateServerCandidates();
this.updateAllowedServers();
this.updateHighestPlUser();
this.updatePopulationMap();
this.updateServerCandidates();
}
start() {
this.load();
this._room.on("RoomMember.membership", this.onMembership);
this._room.on("RoomState.events", this.onRoomState);
this._started = true;
this.room.on("RoomMember.membership", this.onMembership);
this.room.on("RoomState.events", this.onRoomState);
this.started = true;
}
stop() {
this._room.removeListener("RoomMember.membership", this.onMembership);
this._room.removeListener("RoomState.events", this.onRoomState);
this._started = false;
this.room.removeListener("RoomMember.membership", this.onMembership);
this.room.removeListener("RoomState.events", this.onRoomState);
this.started = false;
}
get serverCandidates() {
@ -128,44 +137,44 @@ export class RoomPermalinkCreator {
}
isStarted() {
return this._started;
return this.started;
}
forEvent(eventId) {
return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
forEvent(eventId: string): string {
return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates);
}
forShareableRoom() {
if (this._room) {
forShareableRoom(): string {
if (this.room) {
// Prefer to use canonical alias for permalink if possible
const alias = this._room.getCanonicalAlias();
const alias = this.room.getCanonicalAlias();
if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
}
}
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
}
forRoom() {
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
forRoom(): string {
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
}
onRoomState(event) {
private onRoomState = (event: MatrixEvent) => {
switch (event.getType()) {
case "m.room.server_acl":
this._updateAllowedServers();
this._updateHighestPlUser();
this._updatePopulationMap();
this._updateServerCandidates();
case EventType.RoomServerAcl:
this.updateAllowedServers();
this.updateHighestPlUser();
this.updatePopulationMap();
this.updateServerCandidates();
return;
case "m.room.power_levels":
this._updateHighestPlUser();
this._updateServerCandidates();
case EventType.RoomPowerLevels:
this.updateHighestPlUser();
this.updateServerCandidates();
return;
}
}
onMembership(evt, member, oldMembership) {
private onMembership = (evt: MatrixEvent, member: RoomMember, oldMembership: string) => {
const userId = member.userId;
const membership = member.membership;
const serverName = getServerName(userId);
@ -173,17 +182,17 @@ export class RoomPermalinkCreator {
const hasLeft = oldMembership === "join" && membership !== "join";
if (hasLeft) {
this._populationMap[serverName]--;
this.populationMap[serverName]--;
} else if (hasJoined) {
this._populationMap[serverName]++;
this.populationMap[serverName]++;
}
this._updateHighestPlUser();
this._updateServerCandidates();
this.updateHighestPlUser();
this.updateServerCandidates();
}
_updateHighestPlUser() {
const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", "");
private updateHighestPlUser() {
const plEvent = this.room.currentState.getStateEvents("m.room.power_levels", "");
if (plEvent) {
const content = plEvent.getContent();
if (content) {
@ -191,14 +200,14 @@ export class RoomPermalinkCreator {
if (users) {
const entries = Object.entries(users);
const allowedEntries = entries.filter(([userId]) => {
const member = this._room.getMember(userId);
const member = this.room.getMember(userId);
if (!member || member.membership !== "join") {
return false;
}
const serverName = getServerName(userId);
return !isHostnameIpAddress(serverName) &&
!isHostInRegex(serverName, this._bannedHostsRegexps) &&
isHostInRegex(serverName, this._allowedHostsRegexps);
!isHostInRegex(serverName, this.bannedHostsRegexps) &&
isHostInRegex(serverName, this.allowedHostsRegexps);
});
const maxEntry = allowedEntries.reduce((max, entry) => {
return (entry[1] > max[1]) ? entry : max;
@ -206,20 +215,20 @@ export class RoomPermalinkCreator {
const [userId, powerLevel] = maxEntry;
// object wasn't empty, and max entry wasn't a demotion from the default
if (userId !== null && powerLevel >= 50) {
this._highestPlUserId = userId;
this.highestPlUserId = userId;
return;
}
}
}
}
this._highestPlUserId = null;
this.highestPlUserId = null;
}
_updateAllowedServers() {
private updateAllowedServers() {
const bannedHostsRegexps = [];
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
if (this._room.currentState) {
const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", "");
if (this.room.currentState) {
const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", "");
if (aclEvent && aclEvent.getContent()) {
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
@ -231,35 +240,35 @@ export class RoomPermalinkCreator {
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
}
}
this._bannedHostsRegexps = bannedHostsRegexps;
this._allowedHostsRegexps = allowedHostsRegexps;
this.bannedHostsRegexps = bannedHostsRegexps;
this.allowedHostsRegexps = allowedHostsRegexps;
}
_updatePopulationMap() {
private updatePopulationMap() {
const populationMap: { [server: string]: number } = {};
for (const member of this._room.getJoinedMembers()) {
for (const member of this.room.getJoinedMembers()) {
const serverName = getServerName(member.userId);
if (!populationMap[serverName]) {
populationMap[serverName] = 0;
}
populationMap[serverName]++;
}
this._populationMap = populationMap;
this.populationMap = populationMap;
}
_updateServerCandidates() {
private updateServerCandidates() {
let candidates = [];
if (this._highestPlUserId) {
candidates.push(getServerName(this._highestPlUserId));
if (this.highestPlUserId) {
candidates.push(getServerName(this.highestPlUserId));
}
const serversByPopulation = Object.keys(this._populationMap)
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
const serversByPopulation = Object.keys(this.populationMap)
.sort((a, b) => this.populationMap[b] - this.populationMap[a])
.filter(a => {
return !candidates.includes(a) &&
!isHostnameIpAddress(a) &&
!isHostInRegex(a, this._bannedHostsRegexps) &&
isHostInRegex(a, this._allowedHostsRegexps);
!isHostInRegex(a, this.bannedHostsRegexps) &&
isHostInRegex(a, this.allowedHostsRegexps);
});
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
@ -273,11 +282,11 @@ export function makeGenericPermalink(entityId: string): string {
return getPermalinkConstructor().forEntity(entityId);
}
export function makeUserPermalink(userId) {
export function makeUserPermalink(userId: string): string {
return getPermalinkConstructor().forUser(userId);
}
export function makeRoomPermalink(roomId) {
export function makeRoomPermalink(roomId: string): string {
if (!roomId) {
throw new Error("can't permalink a falsey roomId");
}
@ -296,7 +305,7 @@ export function makeRoomPermalink(roomId) {
return permalinkCreator.forRoom();
}
export function makeGroupPermalink(groupId) {
export function makeGroupPermalink(groupId: string): string {
return getPermalinkConstructor().forGroup(groupId);
}
@ -428,24 +437,24 @@ export function parseAppLocalLink(localLink: string): PermalinkParts {
return null;
}
function getServerName(userId) {
function getServerName(userId: string): string {
return userId.split(":").splice(1).join(":");
}
function getHostnameFromMatrixDomain(domain) {
function getHostnameFromMatrixDomain(domain: string): string {
if (!domain) return null;
return new URL(`https://${domain}`).hostname;
}
function isHostInRegex(hostname, regexps) {
function isHostInRegex(hostname: string, regexps: RegExp[]) {
hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return true; // assumed
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]);
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
return regexps.filter(h => h.test(hostname)).length > 0;
}
function isHostnameIpAddress(hostname) {
function isHostnameIpAddress(hostname: string): boolean {
hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return false;