From 8f7ef0165b5e7c9a8a507f73bd0a21a7fe7e0b7b Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:31:03 +0000 Subject: [PATCH] Permalinks should be able to decode and encode via servers. (#300) * Permalinks should be able to decode and encode via servers. Previously it was skipping `&via` and just joining `via` when there were more servers. And it also just would not attempt to parse via servers when there was no event id to go with a room. https://github.com/turt2live/matrix-bot-sdk/issues/299 Signed-off-by: gnuxie * Accurate documentation. * Use encodeURIComponent for Permalink components. * Edge cases in Permalinks. * Test Permalinks are URI encoded but can still decode bodged URIs. * Revert "Use encodeURIComponent for Permalink components." This reverts commit f4e106c6c00f2b961720578f279286a428c16ad2. * Revert "Test Permalinks are URI encoded but can still decode bodged URIs." This reverts commit 2f7c7245431ed8eccafc07067d20e310d49d2bdf. * Update test/helpers/PermalinksTest.ts --------- Signed-off-by: gnuxie Co-authored-by: Travis Ralston Co-authored-by: Travis Ralston --- src/helpers/Permalinks.ts | 43 ++++++++++++++-------------------- test/helpers/PermalinksTest.ts | 29 +++++++++++++++++------ 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/helpers/Permalinks.ts b/src/helpers/Permalinks.ts index 476a66b4..ce255955 100644 --- a/src/helpers/Permalinks.ts +++ b/src/helpers/Permalinks.ts @@ -5,22 +5,22 @@ */ export interface PermalinkParts { /** - * The room ID or alias the permalink references. May be null. + * The room ID or alias the permalink references. May be undefined. */ roomIdOrAlias: string; /** - * The user ID the permalink references. May be null. + * The user ID the permalink references. May be undefined. */ userId: string; /** - * The event ID the permalink references. May be null. + * The event ID the permalink references. May be undefined. */ eventId: string; /** - * The servers the permalink is routed through. May be null or empty. + * The servers the permalink is routed through. May be undefined or empty. */ viaServers: string[]; } @@ -38,7 +38,7 @@ export class Permalinks { private static encodeViaArgs(servers: string[]): string { if (!servers || !servers.length) return ""; - return `?via=${servers.join("via=")}`; + return `?via=${servers.join("&via=")}`; } /** @@ -77,32 +77,25 @@ export class Permalinks { * @returns {PermalinkParts} The parts of the permalink. */ public static parseUrl(matrixTo: string): PermalinkParts { - if (!matrixTo || !matrixTo.startsWith("https://matrix.to/#/")) { + const matrixToRegexp = /^https:\/\/matrix\.to\/#\/(?[^/?]+)\/?(?[^?]+)?(?\?[^]*)?$/; + + const url = matrixToRegexp.exec(matrixTo)?.groups; + if (!url) { throw new Error("Not a valid matrix.to URL"); } - const parts = matrixTo.substring("https://matrix.to/#/".length).split("/"); - - const entity = decodeURIComponent(parts[0]); + const entity = decodeURIComponent(url.entity); if (entity[0] === '@') { return { userId: entity, roomIdOrAlias: undefined, eventId: undefined, viaServers: undefined }; } else if (entity[0] === '#' || entity[0] === '!') { - if (parts.length === 1) { - return { roomIdOrAlias: entity, userId: undefined, eventId: undefined, viaServers: [] }; - } - - // rejoin the rest because v3 room can have slashes - const eventIdAndQuery = decodeURIComponent(parts.length > 1 ? parts.slice(1).join('/') : ""); - const secondaryParts = eventIdAndQuery.split('?'); - - const eventId = secondaryParts[0]; - const query = secondaryParts.length > 1 ? secondaryParts[1] : ""; - - const via = query.split("via=").filter(p => !!p); - - return { roomIdOrAlias: entity, eventId: eventId, viaServers: via, userId: undefined }; + return { + userId: undefined, + roomIdOrAlias: entity, + eventId: url.eventId && decodeURIComponent(url.eventId), + viaServers: new URLSearchParams(url.query).getAll('via'), + }; + } else { + throw new Error("Unexpected entity"); } - - throw new Error("Unexpected entity"); } } diff --git a/test/helpers/PermalinksTest.ts b/test/helpers/PermalinksTest.ts index e66660fd..d65fc148 100644 --- a/test/helpers/PermalinksTest.ts +++ b/test/helpers/PermalinksTest.ts @@ -17,14 +17,14 @@ describe('Permalinks', () => { it('should generate a URL for a room ID with via', () => { const roomId = "!test:example.org"; const via = ['one.example.org', 'two.example.org']; - const expected = `https://matrix.to/#/${roomId}?via=${via.join("via=")}`; + const expected = `https://matrix.to/#/${roomId}?via=${via.join("&via=")}`; expect(Permalinks.forRoom(roomId, via)).toBe(expected); }); it('should generate a URL for a room alias with via', () => { const roomAlias = "#test:example.org"; const via = ['one.example.org', 'two.example.org']; - const expected = `https://matrix.to/#/${roomAlias}?via=${via.join("via=")}`; + const expected = `https://matrix.to/#/${roomAlias}?via=${via.join("&via=")}`; expect(Permalinks.forRoom(roomAlias, via)).toBe(expected); }); }); @@ -48,7 +48,7 @@ describe('Permalinks', () => { const roomId = "!test:example.org"; const eventId = "$test:example.org"; const via = ['one.example.org', 'two.example.org']; - const expected = `https://matrix.to/#/${roomId}/${eventId}?via=${via.join("via=")}`; + const expected = `https://matrix.to/#/${roomId}/${eventId}?via=${via.join("&via=")}`; expect(Permalinks.forEvent(roomId, eventId, via)).toBe(expected); }); @@ -56,7 +56,7 @@ describe('Permalinks', () => { const roomAlias = "#test:example.org"; const eventId = "$test:example.org"; const via = ['one.example.org', 'two.example.org']; - const expected = `https://matrix.to/#/${roomAlias}/${eventId}?via=${via.join("via=")}`; + const expected = `https://matrix.to/#/${roomAlias}/${eventId}?via=${via.join("&via=")}`; expect(Permalinks.forEvent(roomAlias, eventId, via)).toBe(expected); }); }); @@ -81,6 +81,7 @@ describe('Permalinks', () => { const parsed = Permalinks.parseUrl(`https://matrix.to/#/${userId}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forUser(userId))).toMatchObject(expected); }); it('should parse room alias URLs', () => { @@ -94,6 +95,7 @@ describe('Permalinks', () => { const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forRoom(roomId))).toMatchObject(expected); }); it('should parse room ID URLs', () => { @@ -107,6 +109,7 @@ describe('Permalinks', () => { const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forRoom(roomId))).toMatchObject(expected); }); it('should parse room alias permalink URLs', () => { @@ -116,6 +119,7 @@ describe('Permalinks', () => { const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forEvent(roomId, eventId))).toMatchObject(expected); }); it('should parse room ID permalink URLs', () => { @@ -125,6 +129,7 @@ describe('Permalinks', () => { const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forEvent(roomId, eventId))).toMatchObject(expected); }); it('should parse room alias permalink URLs with via servers', () => { @@ -132,9 +137,9 @@ describe('Permalinks', () => { const eventId = "$ev:example.org"; const via = ["one.example.org", "two.example.org"]; const expected: PermalinkParts = { userId: undefined, roomIdOrAlias: roomId, viaServers: via, eventId }; - const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}?via=${via.join("via=")}`); - + const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}?via=${via.join("&via=")}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forEvent(roomId, eventId, via))).toMatchObject(expected); }); it('should parse room ID permalink URLs with via servers', () => { @@ -142,9 +147,19 @@ describe('Permalinks', () => { const eventId = "$ev:example.org"; const via = ["one.example.org", "two.example.org"]; const expected: PermalinkParts = { userId: undefined, roomIdOrAlias: roomId, viaServers: via, eventId }; - const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}?via=${via.join("via=")}`); + const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/${eventId}?via=${via.join("&via=")}`); + + expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forEvent(roomId, eventId, via))).toMatchObject(expected); + }); + it('should parse room ID URLs with via servers', () => { + const roomId = "!example:example.org"; + const via = ["example.org"]; + const expected: PermalinkParts = { userId: undefined, roomIdOrAlias: roomId, viaServers: via, eventId: undefined }; + const parsed = Permalinks.parseUrl(`https://matrix.to/#/${roomId}/?via=${via.join("&via=")}`); expect(parsed).toMatchObject(expected); + expect(Permalinks.parseUrl(Permalinks.forRoom(roomId, via))).toMatchObject(expected); }); }); });