Merge remote-tracking branch 'origin/develop' into develop
						commit
						18f4edcd7a
					
				|  | @ -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
 | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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.
 | ||||
|     }, | ||||
| 
 | ||||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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) { | ||||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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() { | ||||
|  | @ -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 }, | ||||
|                 }, | ||||
|             }); | ||||
| 
 | ||||
|  | @ -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(); | ||||
|         }); | ||||
|  |  | |||
|  | @ -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]}`; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,11 +14,18 @@ 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) { | ||||
|  | @ -26,9 +33,92 @@ export function makeUserPermalink(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; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,231 @@ | |||
| /* | ||||
| Copyright 2018 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. | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| import expect from 'expect'; | ||||
| import peg from '../src/MatrixClientPeg'; | ||||
| import {pickServerCandidates} from "../src/matrix-to"; | ||||
| import * as testUtils from "./test-utils"; | ||||
| 
 | ||||
| 
 | ||||
| describe('matrix-to', function() { | ||||
|     let sandbox; | ||||
| 
 | ||||
|     beforeEach(function() { | ||||
|         testUtils.beforeEach(this); | ||||
|         sandbox = testUtils.stubClient(); | ||||
|         peg.get().credentials = { userId: "@test:example.com" }; | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(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).toExist(); | ||||
|         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).toExist(); | ||||
|         expect(pickedServers.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).toExist(); | ||||
|         expect(pickedServers.length).toBe(3); | ||||
|         expect(pickedServers[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, | ||||
|                     }, | ||||
|                 ], | ||||
|             }; | ||||
|         }; | ||||
|         const pickedServers = pickServerCandidates("!somewhere:example.org"); | ||||
|         expect(pickedServers).toExist(); | ||||
|         expect(pickedServers.length).toBe(3); | ||||
|         expect(pickedServers[0]).toBe("first"); | ||||
|         expect(pickedServers[1]).toBe("second"); | ||||
|         expect(pickedServers[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).toExist(); | ||||
|         expect(pickedServers.length).toBe(3); | ||||
|         expect(pickedServers[0]).toBe("first"); | ||||
|         expect(pickedServers[1]).toBe("second"); | ||||
|         expect(pickedServers[2]).toBe("third"); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with IPv4 hostnames', function() { | ||||
|         peg.get().getRoom = () => { | ||||
|             return { | ||||
|                 getJoinedMembers: () => [ | ||||
|                     { | ||||
|                         userId: "@alice:127.0.0.1", | ||||
|                         powerLevel: 100, | ||||
|                     }, | ||||
|                 ], | ||||
|             }; | ||||
|         }; | ||||
|         const pickedServers = pickServerCandidates("!somewhere:example.org"); | ||||
|         expect(pickedServers).toExist(); | ||||
|         expect(pickedServers.length).toBe(1); | ||||
|         expect(pickedServers[0]).toBe("127.0.0.1"); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with IPv6 hostnames', function() { | ||||
|         peg.get().getRoom = () => { | ||||
|             return { | ||||
|                 getJoinedMembers: () => [ | ||||
|                     { | ||||
|                         userId: "@alice:[::1]", | ||||
|                         powerLevel: 100, | ||||
|                     }, | ||||
|                 ], | ||||
|             }; | ||||
|         }; | ||||
|         const pickedServers = pickServerCandidates("!somewhere:example.org"); | ||||
|         expect(pickedServers).toExist(); | ||||
|         expect(pickedServers.length).toBe(1); | ||||
|         expect(pickedServers[0]).toBe("[::1]"); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with 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).toExist(); | ||||
|         expect(pickedServers.length).toBe(1); | ||||
|         expect(pickedServers[0]).toBe("127.0.0.1:8448"); | ||||
|     }); | ||||
| 
 | ||||
|     it('should work with IPv6 hostnames with ports', function() { | ||||
|         peg.get().getRoom = () => { | ||||
|             return { | ||||
|                 getJoinedMembers: () => [ | ||||
|                     { | ||||
|                         userId: "@alice:[::1]:8448", | ||||
|                         powerLevel: 100, | ||||
|                     }, | ||||
|                 ], | ||||
|             }; | ||||
|         }; | ||||
|         const pickedServers = pickServerCandidates("!somewhere:example.org"); | ||||
|         expect(pickedServers).toExist(); | ||||
|         expect(pickedServers.length).toBe(1); | ||||
|         expect(pickedServers[0]).toBe("[::1]:8448"); | ||||
|     }); | ||||
| 
 | ||||
|     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).toExist(); | ||||
|         expect(pickedServers.length).toBe(1); | ||||
|         expect(pickedServers[0]).toBe("example.org:8448"); | ||||
|     }); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	 Weblate
						Weblate