Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2250 from matrix-org/travis/permalink-routing
Browse files Browse the repository at this point in the history
Support routing matrix.to links to joinable rooms
  • Loading branch information
turt2live authored Oct 26, 2018
2 parents 3b6a0f9 + 0857e2c commit 0bd1d6b
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 5 deletions.
13 changes: 13 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,25 @@ module.exports = function (config) {

'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.js',

// To make webpack happy
// Related: https://github.com/request/request/issues/1529
// (there's no mock available for fs, so we fake a mock by using
// an in-memory version of fs)
"fs": "memfs",
},
modules: [
path.resolve('./test'),
"node_modules"
],
},
node: {
// Because webpack is made of fail
// https://github.com/request/request/issues/1529
// Note: 'mock' is the new 'empty'
net: 'mock',
tls: 'mock'
},
devtool: 'inline-source-map',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"memfs": "^2.10.1",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"prop-types": "^15.5.8",
Expand Down
4 changes: 4 additions & 0 deletions src/components/structures/LoggedInView.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const LoggedInView = React.createClass({

teamToken: PropTypes.string,

// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),

// and lots and lots of other stuff.
},

Expand Down Expand Up @@ -389,6 +392,7 @@ const LoggedInView = React.createClass({
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
Expand Down
13 changes: 13 additions & 0 deletions src/components/structures/MatrixChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export default React.createClass({
page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
};

if (roomInfo.room_alias) {
Expand Down Expand Up @@ -1489,9 +1490,21 @@ export default React.createClass({
inviterName: params.inviter_name,
};

// on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array
// to other levels. If there's just one ?via= then params.via is a
// single string. If someone does something like ?via=one.com&via=two.com
// then params.via is an array of strings.
let via = [];
if (params.via) {
if (typeof(params.via) === 'string') via = [params.via];
else via = params.via;
}

const payload = {
action: 'view_room',
event_id: eventId,
via_servers: via,
// If an event ID is given in the URL hash, notify RoomViewStore to mark
// it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile.
Expand Down
7 changes: 5 additions & 2 deletions src/components/structures/RoomView.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ module.exports = React.createClass({

// is the RightPanel collapsed?
collapsedRhs: PropTypes.bool,

// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
},

getInitialState: function() {
Expand Down Expand Up @@ -833,7 +836,7 @@ module.exports = React.createClass({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
},
});

Expand Down Expand Up @@ -875,7 +878,7 @@ module.exports = React.createClass({
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
});
return Promise.resolve();
});
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/UserSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1298,7 +1298,7 @@ module.exports = React.createClass({
// If the olmVersion is not defined then either crypto is disabled, or
// we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) {
if (olmVersion) {
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}

Expand Down
94 changes: 92 additions & 2 deletions src/matrix-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,111 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import MatrixClientPeg from "./MatrixClientPeg";

export const host = "matrix.to";
export const baseUrl = `https://${host}`;

// The maximum number of servers to pick when working out which servers
// to add to permalinks. The servers are appended as ?via=example.org
const MAX_SERVER_CANDIDATES = 3;

export function makeEventPermalink(roomId, eventId) {
return `${baseUrl}/#/${roomId}/${eventId}`;
const serverCandidates = pickServerCandidates(roomId);
return `${baseUrl}/#/${roomId}/${eventId}?${encodeServerCandidates(serverCandidates)}`;
}

export function makeUserPermalink(userId) {
return `${baseUrl}/#/${userId}`;
}

export function makeRoomPermalink(roomId) {
return `${baseUrl}/#/${roomId}`;
const serverCandidates = pickServerCandidates(roomId);
return `${baseUrl}/#/${roomId}?${encodeServerCandidates(serverCandidates)}`;
}

export function makeGroupPermalink(groupId) {
return `${baseUrl}/#/${groupId}`;
}

export function encodeServerCandidates(candidates) {
if (!candidates) return '';
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.

// 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 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) {
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));
for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) {
const idx = i - beforePopulation;
if (idx >= serversByPopulation.length) break;
candidates.push(serversByPopulation[idx]);
}

return candidates;
}
Loading

0 comments on commit 0bd1d6b

Please sign in to comment.