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

Commit 0bd1d6b

Browse files
authored
Merge pull request #2250 from matrix-org/travis/permalink-routing
Support routing matrix.to links to joinable rooms
2 parents 3b6a0f9 + 0857e2c commit 0bd1d6b

File tree

8 files changed

+360
-5
lines changed

8 files changed

+360
-5
lines changed

karma.conf.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,25 @@ module.exports = function (config) {
199199

200200
'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
201201
'sinon': 'sinon/pkg/sinon.js',
202+
203+
// To make webpack happy
204+
// Related: https://github.com/request/request/issues/1529
205+
// (there's no mock available for fs, so we fake a mock by using
206+
// an in-memory version of fs)
207+
"fs": "memfs",
202208
},
203209
modules: [
204210
path.resolve('./test'),
205211
"node_modules"
206212
],
207213
},
214+
node: {
215+
// Because webpack is made of fail
216+
// https://github.com/request/request/issues/1529
217+
// Note: 'mock' is the new 'empty'
218+
net: 'mock',
219+
tls: 'mock'
220+
},
208221
devtool: 'inline-source-map',
209222
externals: {
210223
// Don't try to bundle electron: leave it as a commonjs dependency

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"lodash": "^4.13.1",
7777
"lolex": "2.3.2",
7878
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
79+
"memfs": "^2.10.1",
7980
"optimist": "^0.6.1",
8081
"pako": "^1.0.5",
8182
"prop-types": "^15.5.8",

src/components/structures/LoggedInView.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ const LoggedInView = React.createClass({
6464

6565
teamToken: PropTypes.string,
6666

67+
// Used by the RoomView to handle joining rooms
68+
viaServers: PropTypes.arrayOf(PropTypes.string),
69+
6770
// and lots and lots of other stuff.
6871
},
6972

@@ -389,6 +392,7 @@ const LoggedInView = React.createClass({
389392
onRegistered={this.props.onRegistered}
390393
thirdPartyInvite={this.props.thirdPartyInvite}
391394
oobData={this.props.roomOobData}
395+
viaServers={this.props.viaServers}
392396
eventPixelOffset={this.props.initialEventPixelOffset}
393397
key={this.props.currentRoomId || 'roomview'}
394398
disabled={this.props.middleDisabled}

src/components/structures/MatrixChat.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ export default React.createClass({
840840
page_type: PageTypes.RoomView,
841841
thirdPartyInvite: roomInfo.third_party_invite,
842842
roomOobData: roomInfo.oob_data,
843+
viaServers: roomInfo.via_servers,
843844
};
844845

845846
if (roomInfo.room_alias) {
@@ -1489,9 +1490,21 @@ export default React.createClass({
14891490
inviterName: params.inviter_name,
14901491
};
14911492

1493+
// on our URLs there might be a ?via=matrix.org or similar to help
1494+
// joins to the room succeed. We'll pass these through as an array
1495+
// to other levels. If there's just one ?via= then params.via is a
1496+
// single string. If someone does something like ?via=one.com&via=two.com
1497+
// then params.via is an array of strings.
1498+
let via = [];
1499+
if (params.via) {
1500+
if (typeof(params.via) === 'string') via = [params.via];
1501+
else via = params.via;
1502+
}
1503+
14921504
const payload = {
14931505
action: 'view_room',
14941506
event_id: eventId,
1507+
via_servers: via,
14951508
// If an event ID is given in the URL hash, notify RoomViewStore to mark
14961509
// it as highlighted, which will propagate to RoomView and highlight the
14971510
// associated EventTile.

src/components/structures/RoomView.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ module.exports = React.createClass({
8888

8989
// is the RightPanel collapsed?
9090
collapsedRhs: PropTypes.bool,
91+
92+
// Servers the RoomView can use to try and assist joins
93+
viaServers: PropTypes.arrayOf(PropTypes.string),
9194
},
9295

9396
getInitialState: function() {
@@ -833,7 +836,7 @@ module.exports = React.createClass({
833836
action: 'do_after_sync_prepared',
834837
deferred_action: {
835838
action: 'join_room',
836-
opts: { inviteSignUrl: signUrl },
839+
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
837840
},
838841
});
839842

@@ -875,7 +878,7 @@ module.exports = React.createClass({
875878
this.props.thirdPartyInvite.inviteSignUrl : undefined;
876879
dis.dispatch({
877880
action: 'join_room',
878-
opts: { inviteSignUrl: signUrl },
881+
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
879882
});
880883
return Promise.resolve();
881884
});

src/components/structures/UserSettings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ module.exports = React.createClass({
12981298
// If the olmVersion is not defined then either crypto is disabled, or
12991299
// we are using a version old version of olm. We assume the former.
13001300
let olmVersionString = "<not-enabled>";
1301-
if (olmVersion !== undefined) {
1301+
if (olmVersion) {
13021302
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
13031303
}
13041304

src/matrix-to.js

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,111 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import MatrixClientPeg from "./MatrixClientPeg";
18+
1719
export const host = "matrix.to";
1820
export const baseUrl = `https://${host}`;
1921

22+
// The maximum number of servers to pick when working out which servers
23+
// to add to permalinks. The servers are appended as ?via=example.org
24+
const MAX_SERVER_CANDIDATES = 3;
25+
2026
export function makeEventPermalink(roomId, eventId) {
21-
return `${baseUrl}/#/${roomId}/${eventId}`;
27+
const serverCandidates = pickServerCandidates(roomId);
28+
return `${baseUrl}/#/${roomId}/${eventId}?${encodeServerCandidates(serverCandidates)}`;
2229
}
2330

2431
export function makeUserPermalink(userId) {
2532
return `${baseUrl}/#/${userId}`;
2633
}
2734

2835
export function makeRoomPermalink(roomId) {
29-
return `${baseUrl}/#/${roomId}`;
36+
const serverCandidates = pickServerCandidates(roomId);
37+
return `${baseUrl}/#/${roomId}?${encodeServerCandidates(serverCandidates)}`;
3038
}
3139

3240
export function makeGroupPermalink(groupId) {
3341
return `${baseUrl}/#/${groupId}`;
3442
}
43+
44+
export function encodeServerCandidates(candidates) {
45+
if (!candidates) return '';
46+
return `via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
47+
}
48+
49+
export function pickServerCandidates(roomId) {
50+
const client = MatrixClientPeg.get();
51+
const room = client.getRoom(roomId);
52+
if (!room) return [];
53+
54+
// Permalinks can have servers appended to them so that the user
55+
// receiving them can have a fighting chance at joining the room.
56+
// These servers are called "candidates" at this point because
57+
// it is unclear whether they are going to be useful to actually
58+
// join in the future.
59+
//
60+
// We pick 3 servers based on the following criteria:
61+
//
62+
// Server 1: The highest power level user in the room, provided
63+
// they are at least PL 50. We don't calculate "what is a moderator"
64+
// here because it is less relevant for the vast majority of rooms.
65+
// We also want to ensure that we get an admin or high-ranking mod
66+
// as they are less likely to leave the room. If no user happens
67+
// to meet this criteria, we'll pick the most popular server in the
68+
// room.
69+
//
70+
// Server 2: The next most popular server in the room (in user
71+
// distribution). This cannot be the same as Server 1. If no other
72+
// servers are available then we'll only return Server 1.
73+
//
74+
// Server 3: The next most popular server by user distribution. This
75+
// has the same rules as Server 2, with the added exception that it
76+
// must be unique from Server 1 and 2.
77+
78+
// Rationale for popular servers: It's hard to get rid of people when
79+
// they keep flocking in from a particular server. Sure, the server could
80+
// be ACL'd in the future or for some reason be evicted from the room
81+
// however an event like that is unlikely the larger the room gets.
82+
83+
// Note: we don't pick the server the room was created on because the
84+
// homeserver should already be using that server as a last ditch attempt
85+
// and there's less of a guarantee that the server is a resident server.
86+
// Instead, we actively figure out which servers are likely to be residents
87+
// in the future and try to use those.
88+
89+
// Note: Users receiving permalinks that happen to have all 3 potential
90+
// servers fail them (in terms of joining) are somewhat expected to hunt
91+
// down the person who gave them the link to ask for a participating server.
92+
// The receiving user can then manually append the known-good server to
93+
// the list and magically have the link work.
94+
95+
const populationMap: {[server:string]:number} = {};
96+
const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
97+
98+
for (const member of room.getJoinedMembers()) {
99+
const serverName = member.userId.split(":").splice(1).join(":");
100+
if (member.powerLevel > highestPlUser.powerLevel) {
101+
highestPlUser.userId = member.userId;
102+
highestPlUser.powerLevel = member.powerLevel;
103+
highestPlUser.serverName = serverName;
104+
}
105+
106+
if (!populationMap[serverName]) populationMap[serverName] = 0;
107+
populationMap[serverName]++;
108+
}
109+
110+
const candidates = [];
111+
if (highestPlUser.powerLevel >= 50) candidates.push(highestPlUser.serverName);
112+
113+
const beforePopulation = candidates.length;
114+
const serversByPopulation = Object.keys(populationMap)
115+
.sort((a, b) => populationMap[b] - populationMap[a])
116+
.filter(a => !candidates.includes(a));
117+
for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) {
118+
const idx = i - beforePopulation;
119+
if (idx >= serversByPopulation.length) break;
120+
candidates.push(serversByPopulation[idx]);
121+
}
122+
123+
return candidates;
124+
}

0 commit comments

Comments
 (0)