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

Support routing matrix.to links to joinable rooms #2250

Merged
merged 16 commits into from
Oct 26, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this is unrelated to the changeset here, but has a 50% chance of being the solution to the build failing in earlier commits. It seems worth keeping the change regardless of build status imo.

olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}

Expand Down
87 changes: 85 additions & 2 deletions src/matrix-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,104 @@ 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}`;

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:
turt2live marked this conversation as resolved.
Show resolved Hide resolved
//
// 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 will probably be matrix.org in most cases
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid mentioning matrix.org here; it's very much a misfeature that public rooms are dominated by matrix.org users today, plus this ignores usages in private federations.

// although it is certainly possible to be some other server. 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: 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 maxCandidates = 3;
Copy link
Member

@t3chguy t3chguy Oct 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this const should be outside of the method to make its purpose even clearer
(along with a comment in case anyone is hunting through the code which thing to tweak)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

const serversByPopulation = Object.keys(populationMap)
.sort((a, b) => populationMap[b] - populationMap[a])
.filter(a => !candidates.includes(a));
for (let i = beforePopulation; i <= maxCandidates; i++) {
const idx = i - beforePopulation;
if (idx >= serversByPopulation.length) break;
candidates.push(serversByPopulation[idx]);
}

return candidates;
}
Loading