diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js
index 26bd3b1a96..9ae37463bc 100644
--- a/test/matrix-to-test.js
+++ b/test/matrix-to-test.js
@@ -14,14 +14,51 @@ limitations under the License.
 import expect from 'expect';
 import peg from '../src/MatrixClientPeg';
 import {
-    makeEventPermalink,
     makeGroupPermalink,
     makeRoomPermalink,
     makeUserPermalink,
-    pickServerCandidates,
+    RoomPermaLinkCreator,
 } from "../src/matrix-to";
 import * as testUtils from "./test-utils";
 
+function mockRoom(roomId, members, serverACL) {
+    members.forEach(m => m.membership = "join");
+    const powerLevelsUsers = members.reduce((pl, member) => {
+        if (Number.isFinite(member.powerLevel)) {
+            pl[member.userId] = member.powerLevel;
+        }
+        return pl;
+    }, {});
+
+    return {
+        roomId,
+        getJoinedMembers: () => members,
+        getMember: (userId) => members.find(m => m.userId === userId),
+        currentState: {
+            getStateEvents: (type, key) => {
+                if (key) {
+                    return null;
+                }
+                let content;
+                switch (type) {
+                    case "m.room.server_acl":
+                        content = serverACL;
+                        break;
+                    case "m.room.power_levels":
+                        content = {users: powerLevelsUsers, users_default: 0};
+                        break;
+                }
+                if (content) {
+                    return {
+                        getContent: () => content,
+                    };
+                } else {
+                    return null;
+                }
+            },
+        },
+    };
+}
 
 describe('matrix-to', function() {
     let sandbox;
@@ -36,444 +73,320 @@ describe('matrix-to', 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).toBeTruthy();
-        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).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, []);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.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).toBeTruthy();
-        expect(pickedServers.length).toBe(3);
-        expect(pickedServers[0]).toBe("pl_95");
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:pl_50",
+                powerLevel: 50,
+            },
+            {
+                userId: "@alice:pl_75",
+                powerLevel: 75,
+            },
+            {
+                userId: "@alice:pl_95",
+                powerLevel: 95,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(3);
+        expect(creator._serverCandidates[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).toBeTruthy();
-        expect(pickedServers.length).toBe(3);
-        expect(pickedServers[0]).toBe("first");
-        expect(pickedServers[1]).toBe("second");
-        expect(pickedServers[2]).toBe("third");
+        const room = mockRoom(null, [
+            {
+                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 creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(3);
+        expect(creator._serverCandidates[0]).toBe("first");
+        expect(creator._serverCandidates[1]).toBe("second");
+        expect(creator._serverCandidates[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).toBeTruthy();
-        expect(pickedServers.length).toBe(3);
-        expect(pickedServers[0]).toBe("first");
-        expect(pickedServers[1]).toBe("second");
-        expect(pickedServers[2]).toBe("third");
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:first",
+                powerLevel: 100,
+            },
+            {
+                userId: "@alice:second",
+                powerLevel: 0,
+            },
+            {
+                userId: "@bob:second",
+                powerLevel: 0,
+            },
+            {
+                userId: "@charlie:third",
+                powerLevel: 0,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates.length).toBe(3);
+        expect(creator._serverCandidates[0]).toBe("first");
+        expect(creator._serverCandidates[1]).toBe("second");
+        expect(creator._serverCandidates[2]).toBe("third");
     });
 
     it('should pick a maximum of 3 candidate servers', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:alpha",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@alice:bravo",
-                        powerLevel: 0,
-                    },
-                    {
-                        userId: "@alice:charlie",
-                        powerLevel: 0,
-                    },
-                    {
-                        userId: "@alice:delta",
-                        powerLevel: 0,
-                    },
-                    {
-                        userId: "@alice:echo",
-                        powerLevel: 0,
-                    },
-                ],
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(3);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:alpha",
+                powerLevel: 100,
+            },
+            {
+                userId: "@alice:bravo",
+                powerLevel: 0,
+            },
+            {
+                userId: "@alice:charlie",
+                powerLevel: 0,
+            },
+            {
+                userId: "@alice:delta",
+                powerLevel: 0,
+            },
+            {
+                userId: "@alice:echo",
+                powerLevel: 0,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(3);
     });
 
     it('should not consider IPv4 hosts', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:127.0.0.1",
-                        powerLevel: 100,
-                    },
-                ],
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:127.0.0.1",
+                powerLevel: 100,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     it('should not consider IPv6 hosts', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:[::1]",
-                        powerLevel: 100,
-                    },
-                ],
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:[::1]",
+                powerLevel: 100,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     it('should not consider 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).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:127.0.0.1:8448",
+                powerLevel: 100,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     it('should not consider IPv6 hostnames with ports', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:[::1]:8448",
-                        powerLevel: 100,
-                    },
-                ],
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:[::1]:8448",
+                powerLevel: 100,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     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).toBeTruthy();
-        expect(pickedServers.length).toBe(1);
-        expect(pickedServers[0]).toBe("example.org:8448");
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:example.org:8448",
+                powerLevel: 100,
+            },
+        ]);
+
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(1);
+        expect(creator._serverCandidates[0]).toBe("example.org:8448");
     });
 
     it('should not consider servers explicitly denied by ACLs', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:evilcorp.com",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:chat.evilcorp.com",
-                        powerLevel: 0,
-                    },
-                ],
-                currentState: {
-                    getStateEvents: (type, key) => {
-                        if (type !== "m.room.server_acl" || key !== "") return null;
-                        return {
-                            getContent: () => {
-                                return {
-                                    deny: ["evilcorp.com", "*.evilcorp.com"],
-                                    allow: ["*"],
-                                };
-                            },
-                        };
-                    },
-                },
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:evilcorp.com",
+                powerLevel: 100,
+            },
+            {
+                userId: "@bob:chat.evilcorp.com",
+                powerLevel: 0,
+            },
+        ], {
+            deny: ["evilcorp.com", "*.evilcorp.com"],
+            allow: ["*"],
+        });
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     it('should not consider servers not allowed by ACLs', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:evilcorp.com",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:chat.evilcorp.com",
-                        powerLevel: 0,
-                    },
-                ],
-                currentState: {
-                    getStateEvents: (type, key) => {
-                        if (type !== "m.room.server_acl" || key !== "") return null;
-                        return {
-                            getContent: () => {
-                                return {
-                                    deny: [],
-                                    allow: [], // implies "ban everyone"
-                                };
-                            },
-                        };
-                    },
-                },
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(0);
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:evilcorp.com",
+                powerLevel: 100,
+            },
+            {
+                userId: "@bob:chat.evilcorp.com",
+                powerLevel: 0,
+            },
+        ], {
+            deny: [],
+            allow: [], // implies "ban everyone"
+        });
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(0);
     });
 
     it('should consider servers not explicitly banned by ACLs', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:evilcorp.com",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:chat.evilcorp.com",
-                        powerLevel: 0,
-                    },
-                ],
-                currentState: {
-                    getStateEvents: (type, key) => {
-                        if (type !== "m.room.server_acl" || key !== "") return null;
-                        return {
-                            getContent: () => {
-                                return {
-                                    deny: ["*.evilcorp.com"], // evilcorp.com is still good though
-                                    allow: ["*"],
-                                };
-                            },
-                        };
-                    },
-                },
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(1);
-        expect(pickedServers[0]).toEqual("evilcorp.com");
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:evilcorp.com",
+                powerLevel: 100,
+            },
+            {
+                userId: "@bob:chat.evilcorp.com",
+                powerLevel: 0,
+            },
+        ], {
+            deny: ["*.evilcorp.com"], // evilcorp.com is still good though
+            allow: ["*"],
+        });
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(1);
+        expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
     });
 
     it('should consider servers not disallowed by ACLs', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:evilcorp.com",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:chat.evilcorp.com",
-                        powerLevel: 0,
-                    },
-                ],
-                currentState: {
-                    getStateEvents: (type, key) => {
-                        if (type !== "m.room.server_acl" || key !== "") return null;
-                        return {
-                            getContent: () => {
-                                return {
-                                    deny: [],
-                                    allow: ["evilcorp.com"], // implies "ban everyone else"
-                                };
-                            },
-                        };
-                    },
-                },
-            };
-        };
-        const pickedServers = pickServerCandidates("!somewhere:example.org");
-        expect(pickedServers).toBeTruthy();
-        expect(pickedServers.length).toBe(1);
-        expect(pickedServers[0]).toEqual("evilcorp.com");
+        const room = mockRoom(null, [
+            {
+                userId: "@alice:evilcorp.com",
+                powerLevel: 100,
+            },
+            {
+                userId: "@bob:chat.evilcorp.com",
+                powerLevel: 0,
+            },
+        ], {
+            deny: [],
+            allow: ["evilcorp.com"], // implies "ban everyone else"
+        });
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        expect(creator._serverCandidates).toBeTruthy();
+        expect(creator._serverCandidates.length).toBe(1);
+        expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
     });
 
     it('should generate an event permalink for room IDs with no candidate servers', function() {
-        peg.get().getRoom = () => null;
-        const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
+        const room = mockRoom("!somewhere:example.org", []);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        const result = creator.forEvent("$something:example.com");
         expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
     });
 
     it('should generate an event permalink for room IDs with some candidate servers', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:first",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:second",
-                        powerLevel: 0,
-                    },
-                ],
-            };
-        };
-        const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
+        const room = mockRoom("!somewhere:example.org", [
+            {
+                userId: "@alice:first",
+                powerLevel: 100,
+            },
+            {
+                userId: "@bob:second",
+                powerLevel: 0,
+            },
+        ]);
+        const creator = new RoomPermaLinkCreator(room);
+        creator.load();
+        const result = creator.forEvent("$something:example.com");
         expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
     });
 
-    it('should generate a room permalink for room IDs with no candidate servers', function() {
-        peg.get().getRoom = () => null;
-        const result = makeRoomPermalink("!somewhere:example.org");
-        expect(result).toBe("https://matrix.to/#/!somewhere:example.org");
-    });
-
     it('should generate a room permalink for room IDs with some candidate servers', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:first",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:second",
-                        powerLevel: 0,
-                    },
-                ],
-            };
+        peg.get().getRoom = (roomId) => {
+            return mockRoom(roomId, [
+                {
+                    userId: "@alice:first",
+                    powerLevel: 100,
+                },
+                {
+                    userId: "@bob:second",
+                    powerLevel: 0,
+                },
+            ]);
         };
         const result = makeRoomPermalink("!somewhere:example.org");
         expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
     });
 
-    // Technically disallowed but we'll test it anyways
-    it('should generate an event permalink for room aliases with no candidate servers', function() {
-        peg.get().getRoom = () => null;
-        const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
-        expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
-    });
-
-    // Technically disallowed but we'll test it anyways
-    it('should generate an event permalink for room aliases without candidate servers', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:first",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:second",
-                        powerLevel: 0,
-                    },
-                ],
-            };
-        };
-        const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
-        expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
-    });
-
     it('should generate a room permalink for room aliases with no candidate servers', function() {
         peg.get().getRoom = () => null;
         const result = makeRoomPermalink("#somewhere:example.org");
@@ -481,19 +394,17 @@ describe('matrix-to', function() {
     });
 
     it('should generate a room permalink for room aliases without candidate servers', function() {
-        peg.get().getRoom = () => {
-            return {
-                getJoinedMembers: () => [
-                    {
-                        userId: "@alice:first",
-                        powerLevel: 100,
-                    },
-                    {
-                        userId: "@bob:second",
-                        powerLevel: 0,
-                    },
-                ],
-            };
+        peg.get().getRoom = (roomId) => {
+            return mockRoom(roomId, [
+                {
+                    userId: "@alice:first",
+                    powerLevel: 100,
+                },
+                {
+                    userId: "@bob:second",
+                    powerLevel: 0,
+                },
+            ]);
         };
         const result = makeRoomPermalink("#somewhere:example.org");
         expect(result).toBe("https://matrix.to/#/#somewhere:example.org");