Merge pull request #2671 from matrix-org/bwindels/permalinkperf
Improve permalink performance
This commit is contained in:
commit
68ba14909b
13 changed files with 628 additions and 530 deletions
|
@ -525,6 +525,7 @@ module.exports = React.createClass({
|
|||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
</li>,
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ import Promise from 'bluebird';
|
|||
import filesize from 'filesize';
|
||||
const classNames = require("classnames");
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from "../../matrix-to";
|
||||
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const ContentMessages = require("../../ContentMessages");
|
||||
|
@ -441,6 +442,11 @@ module.exports = React.createClass({
|
|||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
}
|
||||
|
||||
// stop tracking room changes to format permalinks
|
||||
if (this.state.permalinkCreator) {
|
||||
this.state.permalinkCreator.stop();
|
||||
}
|
||||
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
|
@ -652,6 +658,11 @@ module.exports = React.createClass({
|
|||
this._loadMembersIfJoined(room);
|
||||
this._calculateRecommendedVersion(room);
|
||||
this._updateE2EStatus(room);
|
||||
if (!this.state.permalinkCreator) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.start();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
},
|
||||
|
||||
_calculateRecommendedVersion: async function(room) {
|
||||
|
@ -1219,6 +1230,7 @@ module.exports = React.createClass({
|
|||
searchResult={result}
|
||||
searchHighlights={this.state.searchHighlights}
|
||||
resultLink={resultLink}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
onWidgetLoad={onWidgetLoad} />);
|
||||
}
|
||||
return ret;
|
||||
|
@ -1725,6 +1737,7 @@ module.exports = React.createClass({
|
|||
showApps={this.state.showApps}
|
||||
uploadAllowed={this.isFileUploadAllowed}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -1826,6 +1839,7 @@ module.exports = React.createClass({
|
|||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -1202,6 +1202,7 @@ var TimelinePanel = React.createClass({
|
|||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
|
|
|
@ -26,7 +26,6 @@ import { _t } from '../../../languageHandler';
|
|||
import Modal from '../../../Modal';
|
||||
import Resend from '../../../Resend';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {makeEventPermalink} from '../../../matrix-to';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -197,6 +196,7 @@ module.exports = React.createClass({
|
|||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
@ -305,10 +305,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
let permalink;
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
);
|
||||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(mxEvent.getRoomId(), mxEvent.getId())}
|
||||
<a href={permalink}
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
|
|
|
@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
|
@ -123,6 +123,14 @@ export default class ShareDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.target instanceof Room) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
|
||||
permalinkCreator.load();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
@ -146,9 +154,9 @@ export default class ShareDialog extends React.Component {
|
|||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
|
@ -169,9 +177,9 @@ export default class ShareDialog extends React.Component {
|
|||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
matrixToUrl = this.props.permalinkCreator.forRoom();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import dis from '../../../dispatcher';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||
|
@ -32,6 +32,7 @@ export default class ReplyThread extends React.Component {
|
|||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
||||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -85,7 +86,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
static getNestedReplyText(ev) {
|
||||
static getNestedReplyText(ev, permalinkCreator) {
|
||||
if (!ev) return null;
|
||||
|
||||
let {body, formatted_body: html} = ev.getContent();
|
||||
|
@ -94,7 +95,7 @@ export default class ReplyThread extends React.Component {
|
|||
if (html) html = this.stripHTMLReply(html);
|
||||
}
|
||||
|
||||
const evLink = makeEventPermalink(ev.getRoomId(), ev.getId());
|
||||
const evLink = permalinkCreator.forEvent(ev.getId());
|
||||
const userLink = makeUserPermalink(ev.getSender());
|
||||
const mxid = ev.getSender();
|
||||
|
||||
|
@ -159,11 +160,12 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||
static makeThread(parentEv, onWidgetLoad, permalinkCreator, ref) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad}
|
||||
ref={ref} permalinkCreator={permalinkCreator} />;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -294,6 +296,7 @@ export default class ReplyThread extends React.Component {
|
|||
<EventTile mxEvent={ev}
|
||||
tileShape="reply"
|
||||
onWidgetLoad={this.props.onWidgetLoad}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||
</blockquote>;
|
||||
});
|
||||
|
|
|
@ -18,8 +18,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import { makeEventPermalink } from '../../../matrix-to';
|
||||
import { RoomPermalinkCreator } from '../../../matrix-to';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomCreate',
|
||||
|
@ -47,13 +48,17 @@ module.exports = React.createClass({
|
|||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instaniated in this case
|
||||
}
|
||||
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
|
||||
const permalinkCreator = new RoomPermalinkCreator(prevRoom);
|
||||
permalinkCreator.load();
|
||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||
return <div className="mx_CreateEvent">
|
||||
<img className="mx_CreateEvent_image" src={require("../../../../res/img/room-continuation.svg")} />
|
||||
<div className="mx_CreateEvent_header">
|
||||
{_t("This room is a continuation of another conversation.")}
|
||||
</div>
|
||||
<a className="mx_CreateEvent_link"
|
||||
href={makeEventPermalink(predecessor['room_id'], predecessor['event_id'])}
|
||||
href={predecessorPermalink}
|
||||
onClick={this._onLinkClicked}
|
||||
>
|
||||
{_t("Click here to see older messages.")}
|
||||
|
|
|
@ -32,7 +32,6 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
|
|||
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
|
@ -329,6 +328,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
|
@ -544,7 +544,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
|
||||
let permalink = "#";
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
const readAvatars = this.getReadAvatars();
|
||||
|
||||
|
@ -697,6 +700,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
case 'reply':
|
||||
case 'reply_preview': {
|
||||
let thread;
|
||||
if (this.props.tileShape === 'reply_preview') {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onWidgetLoad,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ avatar }
|
||||
|
@ -706,10 +718,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{
|
||||
this.props.tileShape === 'reply_preview'
|
||||
&& ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
|
||||
}
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
@ -721,6 +730,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
);
|
||||
}
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onWidgetLoad,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
|
@ -732,7 +747,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
|
|
@ -415,7 +415,8 @@ export default class MessageComposer extends React.Component {
|
|||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
onInputStateChanged={this.onInputStateChanged}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
uploadButton,
|
||||
|
|
|
@ -1195,7 +1195,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// Part of Replies fallback support - prepend the text we're sending
|
||||
// with the text we're replying to
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv, this.props.permalinkCreator);
|
||||
if (nestedReply) {
|
||||
if (content.formatted_body) {
|
||||
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||
|
|
|
@ -56,6 +56,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (EventTile.haveTileForEvent(ev)) {
|
||||
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
highlightLink={this.props.resultLink}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />);
|
||||
}
|
||||
|
|
326
src/matrix-to.js
326
src/matrix-to.js
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,17 +25,213 @@ export const baseUrl = `https://${host}`;
|
|||
// to add to permalinks. The servers are appended as ?via=example.org
|
||||
const MAX_SERVER_CANDIDATES = 3;
|
||||
|
||||
export function makeEventPermalink(roomId, eventId) {
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
|
||||
|
||||
// If the roomId isn't actually a room ID, don't try to list the servers.
|
||||
// Aliases are already routable, and don't need extra information.
|
||||
if (roomId[0] !== '!') return permalinkBase;
|
||||
// Permalinks can have servers appended to them so that the user
|
||||
// receiving them can have a fighting chance at joining the room.
|
||||
// These servers are called "candidates" at this point because
|
||||
// it is unclear whether they are going to be useful to actually
|
||||
// join in the future.
|
||||
//
|
||||
// We pick 3 servers based on the following criteria:
|
||||
//
|
||||
// Server 1: The highest power level user in the room, provided
|
||||
// they are at least PL 50. We don't calculate "what is a moderator"
|
||||
// here because it is less relevant for the vast majority of rooms.
|
||||
// We also want to ensure that we get an admin or high-ranking mod
|
||||
// as they are less likely to leave the room. If no user happens
|
||||
// to meet this criteria, we'll pick the most popular server in the
|
||||
// room.
|
||||
//
|
||||
// Server 2: The next most popular server in the room (in user
|
||||
// distribution). This cannot be the same as Server 1. If no other
|
||||
// servers are available then we'll only return Server 1.
|
||||
//
|
||||
// Server 3: The next most popular server by user distribution. This
|
||||
// has the same rules as Server 2, with the added exception that it
|
||||
// must be unique from Server 1 and 2.
|
||||
|
||||
const serverCandidates = pickServerCandidates(roomId);
|
||||
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
|
||||
// Rationale for popular servers: It's hard to get rid of people when
|
||||
// they keep flocking in from a particular server. Sure, the server could
|
||||
// be ACL'd in the future or for some reason be evicted from the room
|
||||
// however an event like that is unlikely the larger the room gets. If
|
||||
// the server is ACL'd at the time of generating the link however, we
|
||||
// shouldn't pick them. We also don't pick IP addresses.
|
||||
|
||||
// Note: we don't pick the server the room was created on because the
|
||||
// homeserver should already be using that server as a last ditch attempt
|
||||
// and there's less of a guarantee that the server is a resident server.
|
||||
// Instead, we actively figure out which servers are likely to be residents
|
||||
// in the future and try to use those.
|
||||
|
||||
// Note: Users receiving permalinks that happen to have all 3 potential
|
||||
// servers fail them (in terms of joining) are somewhat expected to hunt
|
||||
// down the person who gave them the link to ask for a participating server.
|
||||
// The receiving user can then manually append the known-good server to
|
||||
// the list and magically have the link work.
|
||||
|
||||
export class RoomPermalinkCreator {
|
||||
constructor(room) {
|
||||
this._room = room;
|
||||
this._highestPlUserId = null;
|
||||
this._populationMap = null;
|
||||
this._bannedHostsRegexps = null;
|
||||
this._allowedHostsRegexps = null;
|
||||
this._serverCandidates = null;
|
||||
|
||||
this.onMembership = this.onMembership.bind(this);
|
||||
this.onRoomState = this.onRoomState.bind(this);
|
||||
}
|
||||
|
||||
load() {
|
||||
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);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._room.removeListener("RoomMember.membership", this.onMembership);
|
||||
this._room.removeListener("RoomState.events", this.onRoomState);
|
||||
}
|
||||
|
||||
forEvent(eventId) {
|
||||
const roomId = this._room.roomId;
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
|
||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
||||
}
|
||||
|
||||
forRoom() {
|
||||
const roomId = this._room.roomId;
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}`;
|
||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
||||
}
|
||||
|
||||
onRoomState(event) {
|
||||
switch (event.getType()) {
|
||||
case "m.room.server_acl":
|
||||
this._updateAllowedServers();
|
||||
this._updateHighestPlUser();
|
||||
this._updatePopulationMap();
|
||||
this._updateServerCandidates();
|
||||
return;
|
||||
case "m.room.power_levels":
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMembership(evt, member, oldMembership) {
|
||||
const userId = member.userId;
|
||||
const membership = member.membership;
|
||||
const serverName = getServerName(userId);
|
||||
const hasJoined = oldMembership !== "join" && membership === "join";
|
||||
const hasLeft = oldMembership === "join" && membership !== "join";
|
||||
|
||||
if (hasLeft) {
|
||||
this._populationMap[serverName]--;
|
||||
} else if (hasJoined) {
|
||||
this._populationMap[serverName]++;
|
||||
}
|
||||
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
}
|
||||
|
||||
_updateHighestPlUser() {
|
||||
const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (plEvent) {
|
||||
const content = plEvent.getContent();
|
||||
if (content) {
|
||||
const users = content.users;
|
||||
if (users) {
|
||||
const entries = Object.entries(users);
|
||||
const allowedEntries = entries.filter(([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);
|
||||
});
|
||||
const maxEntry = allowedEntries.reduce((max, entry) => {
|
||||
return (entry[1] > max[1]) ? entry : max;
|
||||
}, [null, 0]);
|
||||
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;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._highestPlUserId = null;
|
||||
}
|
||||
|
||||
_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 (aclEvent && aclEvent.getContent()) {
|
||||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
const denied = aclEvent.getContent().deny || [];
|
||||
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||
|
||||
const allowed = aclEvent.getContent().allow || [];
|
||||
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
this._bannedHostsRegexps = bannedHostsRegexps;
|
||||
this._allowedHostsRegexps = allowedHostsRegexps;
|
||||
}
|
||||
|
||||
_updatePopulationMap() {
|
||||
const populationMap: {[server:string]:number} = {};
|
||||
for (const member of this._room.getJoinedMembers()) {
|
||||
const serverName = getServerName(member.userId);
|
||||
if (!populationMap[serverName]) {
|
||||
populationMap[serverName] = 0;
|
||||
}
|
||||
populationMap[serverName]++;
|
||||
}
|
||||
this._populationMap = populationMap;
|
||||
}
|
||||
|
||||
_updateServerCandidates() {
|
||||
let candidates = [];
|
||||
if (this._highestPlUserId) {
|
||||
candidates.push(getServerName(this._highestPlUserId));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
|
||||
candidates = candidates.concat(remainingServers);
|
||||
|
||||
this._serverCandidates = candidates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function makeUserPermalink(userId) {
|
||||
return `${baseUrl}/#/${userId}`;
|
||||
}
|
||||
|
@ -47,8 +243,14 @@ export function makeRoomPermalink(roomId) {
|
|||
// Aliases are already routable, and don't need extra information.
|
||||
if (roomId[0] !== '!') return permalinkBase;
|
||||
|
||||
const serverCandidates = pickServerCandidates(roomId);
|
||||
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
return permalinkBase;
|
||||
}
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.forRoom();
|
||||
}
|
||||
|
||||
export function makeGroupPermalink(groupId) {
|
||||
|
@ -60,111 +262,13 @@ export function encodeServerCandidates(candidates) {
|
|||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
}
|
||||
|
||||
export function pickServerCandidates(roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) return [];
|
||||
|
||||
// Permalinks can have servers appended to them so that the user
|
||||
// receiving them can have a fighting chance at joining the room.
|
||||
// These servers are called "candidates" at this point because
|
||||
// it is unclear whether they are going to be useful to actually
|
||||
// join in the future.
|
||||
//
|
||||
// We pick 3 servers based on the following criteria:
|
||||
//
|
||||
// Server 1: The highest power level user in the room, provided
|
||||
// they are at least PL 50. We don't calculate "what is a moderator"
|
||||
// here because it is less relevant for the vast majority of rooms.
|
||||
// We also want to ensure that we get an admin or high-ranking mod
|
||||
// as they are less likely to leave the room. If no user happens
|
||||
// to meet this criteria, we'll pick the most popular server in the
|
||||
// room.
|
||||
//
|
||||
// Server 2: The next most popular server in the room (in user
|
||||
// distribution). This cannot be the same as Server 1. If no other
|
||||
// servers are available then we'll only return Server 1.
|
||||
//
|
||||
// Server 3: The next most popular server by user distribution. This
|
||||
// has the same rules as Server 2, with the added exception that it
|
||||
// must be unique from Server 1 and 2.
|
||||
|
||||
// Rationale for popular servers: It's hard to get rid of people when
|
||||
// they keep flocking in from a particular server. Sure, the server could
|
||||
// be ACL'd in the future or for some reason be evicted from the room
|
||||
// however an event like that is unlikely the larger the room gets. If
|
||||
// the server is ACL'd at the time of generating the link however, we
|
||||
// shouldn't pick them. We also don't pick IP addresses.
|
||||
|
||||
// Note: we don't pick the server the room was created on because the
|
||||
// homeserver should already be using that server as a last ditch attempt
|
||||
// and there's less of a guarantee that the server is a resident server.
|
||||
// Instead, we actively figure out which servers are likely to be residents
|
||||
// in the future and try to use those.
|
||||
|
||||
// Note: Users receiving permalinks that happen to have all 3 potential
|
||||
// servers fail them (in terms of joining) are somewhat expected to hunt
|
||||
// down the person who gave them the link to ask for a participating server.
|
||||
// The receiving user can then manually append the known-good server to
|
||||
// the list and magically have the link work.
|
||||
|
||||
const bannedHostsRegexps = [];
|
||||
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
|
||||
if (room.currentState) {
|
||||
const aclEvent = room.currentState.getStateEvents("m.room.server_acl", "");
|
||||
if (aclEvent && aclEvent.getContent()) {
|
||||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
const denied = aclEvent.getContent().deny || [];
|
||||
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||
|
||||
const allowed = aclEvent.getContent().allow || [];
|
||||
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
|
||||
const populationMap: {[server:string]:number} = {};
|
||||
const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
|
||||
|
||||
for (const member of room.getJoinedMembers()) {
|
||||
const serverName = member.userId.split(":").splice(1).join(":");
|
||||
if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName)
|
||||
&& !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) {
|
||||
highestPlUser.userId = member.userId;
|
||||
highestPlUser.powerLevel = member.powerLevel;
|
||||
highestPlUser.serverName = serverName;
|
||||
}
|
||||
|
||||
if (!populationMap[serverName]) populationMap[serverName] = 0;
|
||||
populationMap[serverName]++;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
if (highestPlUser.powerLevel >= 50) candidates.push(highestPlUser.serverName);
|
||||
|
||||
const beforePopulation = candidates.length;
|
||||
const serversByPopulation = Object.keys(populationMap)
|
||||
.sort((a, b) => populationMap[b] - populationMap[a])
|
||||
.filter(a => !candidates.includes(a) && !isHostnameIpAddress(a)
|
||||
&& !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps));
|
||||
for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) {
|
||||
const idx = i - beforePopulation;
|
||||
if (idx >= serversByPopulation.length) break;
|
||||
candidates.push(serversByPopulation[idx]);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
function getServerName(userId) {
|
||||
return userId.split(":").splice(1).join(":");
|
||||
}
|
||||
|
||||
function getHostnameFromMatrixDomain(domain) {
|
||||
if (!domain) return null;
|
||||
|
||||
// The hostname might have a port, so we convert it to a URL and
|
||||
// split out the real hostname.
|
||||
const parser = document.createElement('a');
|
||||
parser.href = "https://" + domain;
|
||||
return parser.hostname;
|
||||
return new URL(`https://${domain}`).hostname;
|
||||
}
|
||||
|
||||
function isHostInRegex(hostname, regexps) {
|
||||
|
|
|
@ -14,14 +14,51 @@ limitations under the License.
|
|||
import expect from 'expect';
|
||||
import peg from '../src/MatrixClientPeg';
|
||||
import {
|
||||
makeEventPermalink,
|
||||
makeGroupPermalink,
|
||||
makeRoomPermalink,
|
||||
makeUserPermalink,
|
||||
pickServerCandidates,
|
||||
RoomPermalinkCreator,
|
||||
} from "../src/matrix-to";
|
||||
import * as testUtils from "./test-utils";
|
||||
|
||||
function mockRoom(roomId, members, serverACL) {
|
||||
members.forEach(m => m.membership = "join");
|
||||
const powerLevelsUsers = members.reduce((pl, member) => {
|
||||
if (Number.isFinite(member.powerLevel)) {
|
||||
pl[member.userId] = member.powerLevel;
|
||||
}
|
||||
return pl;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
roomId,
|
||||
getJoinedMembers: () => members,
|
||||
getMember: (userId) => members.find(m => m.userId === userId),
|
||||
currentState: {
|
||||
getStateEvents: (type, key) => {
|
||||
if (key) {
|
||||
return null;
|
||||
}
|
||||
let content;
|
||||
switch (type) {
|
||||
case "m.room.server_acl":
|
||||
content = serverACL;
|
||||
break;
|
||||
case "m.room.power_levels":
|
||||
content = {users: powerLevelsUsers, users_default: 0};
|
||||
break;
|
||||
}
|
||||
if (content) {
|
||||
return {
|
||||
getContent: () => content,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('matrix-to', function() {
|
||||
let sandbox;
|
||||
|
@ -36,444 +73,347 @@ describe('matrix-to', function() {
|
|||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should pick no candidate servers when the room is not found', function() {
|
||||
peg.get().getRoom = () => null;
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should pick no candidate servers when the room has no members', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, []);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should pick a candidate server for the highest power level user in the room', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:pl_50",
|
||||
powerLevel: 50,
|
||||
},
|
||||
{
|
||||
userId: "@alice:pl_75",
|
||||
powerLevel: 75,
|
||||
},
|
||||
{
|
||||
userId: "@alice:pl_95",
|
||||
powerLevel: 95,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(3);
|
||||
expect(pickedServers[0]).toBe("pl_95");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:pl_50",
|
||||
powerLevel: 50,
|
||||
},
|
||||
{
|
||||
userId: "@alice:pl_75",
|
||||
powerLevel: 75,
|
||||
},
|
||||
{
|
||||
userId: "@alice:pl_95",
|
||||
powerLevel: 95,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(3);
|
||||
expect(creator._serverCandidates[0]).toBe("pl_95");
|
||||
// we don't check the 2nd and 3rd servers because that is done by the next test
|
||||
});
|
||||
|
||||
it('should pick candidate servers based on user population', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:third",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
it('should change candidate server when highest power level user leaves the room', function() {
|
||||
const member95 = {
|
||||
userId: "@alice:pl_95",
|
||||
powerLevel: 95,
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(3);
|
||||
expect(pickedServers[0]).toBe("first");
|
||||
expect(pickedServers[1]).toBe("second");
|
||||
expect(pickedServers[2]).toBe("third");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:pl_50",
|
||||
powerLevel: 50,
|
||||
},
|
||||
{
|
||||
userId: "@alice:pl_75",
|
||||
powerLevel: 75,
|
||||
},
|
||||
member95,
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates[0]).toBe("pl_95");
|
||||
member95.membership = "left";
|
||||
creator.onMembership({}, member95, "join");
|
||||
expect(creator._serverCandidates[0]).toBe("pl_75");
|
||||
member95.membership = "join";
|
||||
creator.onMembership({}, member95, "left");
|
||||
expect(creator._serverCandidates[0]).toBe("pl_95");
|
||||
});
|
||||
|
||||
it('should pick candidate servers based on user population', function() {
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:first",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:third",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(3);
|
||||
expect(creator._serverCandidates[0]).toBe("first");
|
||||
expect(creator._serverCandidates[1]).toBe("second");
|
||||
expect(creator._serverCandidates[2]).toBe("third");
|
||||
});
|
||||
|
||||
it('should pick prefer candidate servers with higher power levels', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@alice:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:third",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(3);
|
||||
expect(pickedServers[0]).toBe("first");
|
||||
expect(pickedServers[1]).toBe("second");
|
||||
expect(pickedServers[2]).toBe("third");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@alice:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@charlie:third",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates.length).toBe(3);
|
||||
expect(creator._serverCandidates[0]).toBe("first");
|
||||
expect(creator._serverCandidates[1]).toBe("second");
|
||||
expect(creator._serverCandidates[2]).toBe("third");
|
||||
});
|
||||
|
||||
it('should pick a maximum of 3 candidate servers', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:alpha",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@alice:bravo",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:charlie",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:delta",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:echo",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(3);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:alpha",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@alice:bravo",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:charlie",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:delta",
|
||||
powerLevel: 0,
|
||||
},
|
||||
{
|
||||
userId: "@alice:echo",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should not consider IPv4 hosts', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:127.0.0.1",
|
||||
powerLevel: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:127.0.0.1",
|
||||
powerLevel: 100,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not consider IPv6 hosts', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:[::1]",
|
||||
powerLevel: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:[::1]",
|
||||
powerLevel: 100,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not consider IPv4 hostnames with ports', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:127.0.0.1:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:127.0.0.1:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not consider IPv6 hostnames with ports', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:[::1]:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:[::1]:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should work with hostnames with ports', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:example.org:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(1);
|
||||
expect(pickedServers[0]).toBe("example.org:8448");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:example.org:8448",
|
||||
powerLevel: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(1);
|
||||
expect(creator._serverCandidates[0]).toBe("example.org:8448");
|
||||
});
|
||||
|
||||
it('should not consider servers explicitly denied by ACLs', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
currentState: {
|
||||
getStateEvents: (type, key) => {
|
||||
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||
return {
|
||||
getContent: () => {
|
||||
return {
|
||||
deny: ["evilcorp.com", "*.evilcorp.com"],
|
||||
allow: ["*"],
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
], {
|
||||
deny: ["evilcorp.com", "*.evilcorp.com"],
|
||||
allow: ["*"],
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not consider servers not allowed by ACLs', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
currentState: {
|
||||
getStateEvents: (type, key) => {
|
||||
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||
return {
|
||||
getContent: () => {
|
||||
return {
|
||||
deny: [],
|
||||
allow: [], // implies "ban everyone"
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(0);
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
], {
|
||||
deny: [],
|
||||
allow: [], // implies "ban everyone"
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should consider servers not explicitly banned by ACLs', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
currentState: {
|
||||
getStateEvents: (type, key) => {
|
||||
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||
return {
|
||||
getContent: () => {
|
||||
return {
|
||||
deny: ["*.evilcorp.com"], // evilcorp.com is still good though
|
||||
allow: ["*"],
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(1);
|
||||
expect(pickedServers[0]).toEqual("evilcorp.com");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
], {
|
||||
deny: ["*.evilcorp.com"], // evilcorp.com is still good though
|
||||
allow: ["*"],
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(1);
|
||||
expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
|
||||
});
|
||||
|
||||
it('should consider servers not disallowed by ACLs', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
currentState: {
|
||||
getStateEvents: (type, key) => {
|
||||
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||
return {
|
||||
getContent: () => {
|
||||
return {
|
||||
deny: [],
|
||||
allow: ["evilcorp.com"], // implies "ban everyone else"
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||
expect(pickedServers).toBeTruthy();
|
||||
expect(pickedServers.length).toBe(1);
|
||||
expect(pickedServers[0]).toEqual("evilcorp.com");
|
||||
const room = mockRoom(null, [
|
||||
{
|
||||
userId: "@alice:evilcorp.com",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:chat.evilcorp.com",
|
||||
powerLevel: 0,
|
||||
},
|
||||
], {
|
||||
deny: [],
|
||||
allow: ["evilcorp.com"], // implies "ban everyone else"
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator._serverCandidates).toBeTruthy();
|
||||
expect(creator._serverCandidates.length).toBe(1);
|
||||
expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
|
||||
});
|
||||
|
||||
it('should generate an event permalink for room IDs with no candidate servers', function() {
|
||||
peg.get().getRoom = () => null;
|
||||
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
|
||||
const room = mockRoom("!somewhere:example.org", []);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
const result = creator.forEvent("$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
|
||||
});
|
||||
|
||||
it('should generate an event permalink for room IDs with some candidate servers', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
|
||||
const room = mockRoom("!somewhere:example.org", [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
const result = creator.forEvent("$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
|
||||
});
|
||||
|
||||
it('should generate a room permalink for room IDs with no candidate servers', function() {
|
||||
peg.get().getRoom = () => null;
|
||||
const result = makeRoomPermalink("!somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org");
|
||||
});
|
||||
|
||||
it('should generate a room permalink for room IDs with some candidate servers', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
peg.get().getRoom = (roomId) => {
|
||||
return mockRoom(roomId, [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
};
|
||||
const result = makeRoomPermalink("!somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
|
||||
});
|
||||
|
||||
// Technically disallowed but we'll test it anyways
|
||||
it('should generate an event permalink for room aliases with no candidate servers', function() {
|
||||
peg.get().getRoom = () => null;
|
||||
const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
|
||||
});
|
||||
|
||||
// Technically disallowed but we'll test it anyways
|
||||
it('should generate an event permalink for room aliases without candidate servers', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
|
||||
});
|
||||
|
||||
it('should generate a room permalink for room aliases with no candidate servers', function() {
|
||||
peg.get().getRoom = () => null;
|
||||
const result = makeRoomPermalink("#somewhere:example.org");
|
||||
|
@ -481,19 +421,17 @@ describe('matrix-to', function() {
|
|||
});
|
||||
|
||||
it('should generate a room permalink for room aliases without candidate servers', function() {
|
||||
peg.get().getRoom = () => {
|
||||
return {
|
||||
getJoinedMembers: () => [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
peg.get().getRoom = (roomId) => {
|
||||
return mockRoom(roomId, [
|
||||
{
|
||||
userId: "@alice:first",
|
||||
powerLevel: 100,
|
||||
},
|
||||
{
|
||||
userId: "@bob:second",
|
||||
powerLevel: 0,
|
||||
},
|
||||
]);
|
||||
};
|
||||
const result = makeRoomPermalink("#somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
|
||||
|
|
Loading…
Reference in a new issue