@@ -14,21 +14,111 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17+ import MatrixClientPeg from "./MatrixClientPeg" ;
18+
1719export const host = "matrix.to" ;
1820export 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+
2026export function makeEventPermalink ( roomId , eventId ) {
21- return `${ baseUrl } /#/${ roomId } /${ eventId } ` ;
27+ const serverCandidates = pickServerCandidates ( roomId ) ;
28+ return `${ baseUrl } /#/${ roomId } /${ eventId } ?${ encodeServerCandidates ( serverCandidates ) } ` ;
2229}
2330
2431export function makeUserPermalink ( userId ) {
2532 return `${ baseUrl } /#/${ userId } ` ;
2633}
2734
2835export function makeRoomPermalink ( roomId ) {
29- return `${ baseUrl } /#/${ roomId } ` ;
36+ const serverCandidates = pickServerCandidates ( roomId ) ;
37+ return `${ baseUrl } /#/${ roomId } ?${ encodeServerCandidates ( serverCandidates ) } ` ;
3038}
3139
3240export 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