refactor pickServerCandidates into statefull class
so we don't need to iterate over all members for every permalinkpull/21833/head
							parent
							
								
									a26b62ef94
								
							
						
					
					
						commit
						c2791b9470
					
				
							
								
								
									
										305
									
								
								src/matrix-to.js
								
								
								
								
							
							
						
						
									
										305
									
								
								src/matrix-to.js
								
								
								
								
							|  | @ -25,17 +25,211 @@ export const baseUrl = `https://${host}`; | |||
| // to add to permalinks. The servers are appended as ?via=example.org
 | ||||
| const MAX_SERVER_CANDIDATES = 3; | ||||
| 
 | ||||
| export function makeEventPermalink(roomId, eventId) { | ||||
|     const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`; | ||||
| 
 | ||||
|     // If the roomId isn't actually a room ID, don't try to list the servers.
 | ||||
|     // Aliases are already routable, and don't need extra information.
 | ||||
|     if (roomId[0] !== '!') return permalinkBase; | ||||
| // 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.
 | ||||
| 
 | ||||
|     const serverCandidates = pickServerCandidates(roomId); | ||||
|     return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`; | ||||
| // 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. If
 | ||||
| // the server is ACL'd at the time of generating the link however, we
 | ||||
| // shouldn't pick them. We also don't pick IP addresses.
 | ||||
| 
 | ||||
| // 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.
 | ||||
| 
 | ||||
| export class RoomPermaLinkCreator { | ||||
|     constructor(room) { | ||||
|         this._room = room; | ||||
|         this._highestPlUserId = null; | ||||
|         this._populationMap = null; | ||||
|         this._bannedHostsRegexps = null; | ||||
|         this._allowedHostsRegexps = null; | ||||
|         this._serverCandidates = null; | ||||
| 
 | ||||
|         this.onPowerlevel = this.onPowerlevel.bind(this); | ||||
|         this.onMembership = this.onMembership.bind(this); | ||||
|         this.onRoomState = this.onRoomState.bind(this); | ||||
|     } | ||||
| 
 | ||||
|     load() { | ||||
|         this._updateAllowedServers(); | ||||
|         this._updatePopulationMap(); | ||||
|         this._updateServerCandidates(); | ||||
|     } | ||||
| 
 | ||||
|     start() { | ||||
|         this.load(); | ||||
|         this._room.on("RoomMember.membership", this.onMembership); | ||||
|         this._room.on("RoomMember.powerLevel", this.onPowerlevel); | ||||
|         this._room.on("RoomState.events", this.onRoomState); | ||||
|     } | ||||
| 
 | ||||
|     stop() { | ||||
|         this._room.off("RoomMember.membership", this.onMembership); | ||||
|         this._room.off("RoomMember.powerLevel", this.onPowerlevel); | ||||
|         this._room.off("RoomState.events", this.onRoomState); | ||||
|     } | ||||
| 
 | ||||
|     forEvent(eventId) { | ||||
|         const roomId = this._room.roomId; | ||||
|         const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`; | ||||
| 
 | ||||
|         // If the roomId isn't actually a room ID, don't try to list the servers.
 | ||||
|         // Aliases are already routable, and don't need extra information.
 | ||||
|         if (roomId[0] !== '!') return permalinkBase; | ||||
|         return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`; | ||||
|     } | ||||
| 
 | ||||
|     forRoom() { | ||||
|         const roomId = this._room.roomId; | ||||
|         const permalinkBase = `${baseUrl}/#/${roomId}`; | ||||
|         return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`; | ||||
|     } | ||||
| 
 | ||||
|     onRoomState(event) { | ||||
|         if (event.getType() === "m.room.server_acl") { | ||||
|             this._updateAllowedServers(); | ||||
|             this._updatePopulationMap(); | ||||
|             this._updateServerCandidates(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onMembership(evt, member, oldMembership) { | ||||
|         const userId = member.userId; | ||||
|         const membership = member.membership; | ||||
|         const serverName = getServerName(userId); | ||||
|         const hasJoined = oldMembership !== "join" && membership === "join"; | ||||
|         const hasLeft = oldMembership === "join" && membership !== "join"; | ||||
| 
 | ||||
|         if (hasLeft) { | ||||
|             this._populationMap[serverName]--; | ||||
|         } else if (hasJoined) { | ||||
|             this._populationMap[serverName]++; | ||||
|         } | ||||
| 
 | ||||
|         this._updateHighestPlUser(); | ||||
|         this._updateServerCandidates(); | ||||
|     } | ||||
| 
 | ||||
|     onPowerlevel() { | ||||
|         this._updateHighestPlUser(); | ||||
|         this._updateServerCandidates(); | ||||
|     } | ||||
| 
 | ||||
|     _updateHighestPlUser() { | ||||
|         const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", ""); | ||||
|         const content = plEvent.getContent(); | ||||
|         if (content) { | ||||
|             const users = content.users; | ||||
|             if (users) { | ||||
|                 const entries = Object.entries(users); | ||||
|                 const allowedEntries = entries.filter(([userId]) => { | ||||
|                     const member = this._room.getMember(userId); | ||||
|                     if (!member || member.membership !== "join") { | ||||
|                         return false; | ||||
|                     } | ||||
|                     const serverName = getServerName(userId); | ||||
|                     return !isHostnameIpAddress(serverName) && | ||||
|                            !isHostInRegex(serverName, this._bannedHostsRegexps) && | ||||
|                            isHostInRegex(serverName, this._allowedHostsRegexps); | ||||
|                 }); | ||||
|                 const maxEntry = allowedEntries.reduce((max, entry) => { | ||||
|                     return (entry[1] > max[1]) ? entry : max; | ||||
|                 }, [null, 0]); | ||||
|                 const [userId, powerLevel] = maxEntry; | ||||
|                 // object wasn't empty, and max entry wasn't a demotion from the default
 | ||||
|                 if (userId !== null && powerLevel > (content.users_default || 0)) { | ||||
|                     this._highestPlUserId = userId; | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         this._highestPlUserId = null; | ||||
|     } | ||||
| 
 | ||||
|     _updateAllowedServers() { | ||||
|         const bannedHostsRegexps = []; | ||||
|         let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
 | ||||
|         if (this._room.currentState) { | ||||
|             const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", ""); | ||||
|             if (aclEvent && aclEvent.getContent()) { | ||||
|                 const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); | ||||
| 
 | ||||
|                 const denied = aclEvent.getContent().deny || []; | ||||
|                 denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); | ||||
| 
 | ||||
|                 const allowed = aclEvent.getContent().allow || []; | ||||
|                 allowedHostsRegexps = []; // we don't want to use the default rule here
 | ||||
|                 allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); | ||||
|             } | ||||
|         } | ||||
|         this._bannedHostsRegexps = bannedHostsRegexps; | ||||
|         this._allowedHostsRegexps = allowedHostsRegexps; | ||||
|     } | ||||
| 
 | ||||
|     _updatePopulationMap() { | ||||
|         const populationMap: {[server:string]:number} = {}; | ||||
|         for (const member of this._room.getJoinedMembers()) { | ||||
|             const serverName = getServerName(member.userId); | ||||
|             if (!populationMap[serverName]) { | ||||
|                 populationMap[serverName] = 0; | ||||
|             } | ||||
|             populationMap[serverName]++; | ||||
|         } | ||||
|         this._populationMap = populationMap; | ||||
|     } | ||||
| 
 | ||||
|     _updateServerCandidates() { | ||||
|         let candidates = []; | ||||
|         if (this._highestPlUserId) { | ||||
|             candidates.push(getServerName(this._highestPlUserId)); | ||||
|         } | ||||
| 
 | ||||
|         const serversByPopulation = Object.keys(this._populationMap) | ||||
|             .sort((a, b) => this._populationMap[b] - this._populationMap[a]) | ||||
|             .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) | ||||
|                 && !isHostInRegex(a, this._bannedHostsRegexps) && isHostInRegex(a, this._allowedHostsRegexps)); | ||||
| 
 | ||||
|         const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length); | ||||
|         candidates = candidates.concat(remainingServers); | ||||
| 
 | ||||
|         this._serverCandidates = candidates; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export function makeUserPermalink(userId) { | ||||
|     return `${baseUrl}/#/${userId}`; | ||||
| } | ||||
|  | @ -60,101 +254,8 @@ export function encodeServerCandidates(candidates) { | |||
|     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. If
 | ||||
|     // the server is ACL'd at the time of generating the link however, we
 | ||||
|     // shouldn't pick them. We also don't pick IP addresses.
 | ||||
| 
 | ||||
|     // 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 bannedHostsRegexps = []; | ||||
|     let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
 | ||||
|     if (room.currentState) { | ||||
|         const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); | ||||
|         if (aclEvent && aclEvent.getContent()) { | ||||
|             const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); | ||||
| 
 | ||||
|             const denied = aclEvent.getContent().deny || []; | ||||
|             denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); | ||||
| 
 | ||||
|             const allowed = aclEvent.getContent().allow || []; | ||||
|             allowedHostsRegexps = []; // we don't want to use the default rule here
 | ||||
|             allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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 && !isHostnameIpAddress(serverName) | ||||
|             && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { | ||||
|             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) && !isHostnameIpAddress(a) | ||||
|             && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); | ||||
|     for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { | ||||
|         const idx = i - beforePopulation; | ||||
|         if (idx >= serversByPopulation.length) break; | ||||
|         candidates.push(serversByPopulation[idx]); | ||||
|     } | ||||
| 
 | ||||
|     return candidates; | ||||
| function getServerName(userId) { | ||||
|     return userId.split(":").splice(1).join(":"); | ||||
| } | ||||
| 
 | ||||
| function getHostnameFromMatrixDomain(domain) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Bruno Windels
						Bruno Windels