const MATRIX_TO_BASE = 'https://matrix.to'; export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`; const withViaServers = (fragment: string, viaServers: string[]): string => `${fragment}?${viaServers.map((server) => `via=${server}`).join('&')}`; export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): string => { let fragment = roomIdOrAlias; if (Array.isArray(viaServers) && viaServers.length > 0) { fragment = withViaServers(fragment, viaServers); } return `${MATRIX_TO_BASE}/#/${fragment}`; }; export const getMatrixToRoomEvent = ( roomId: string, eventId: string, viaServers?: string[] ): string => { let fragment = `${roomId}/${eventId}`; if (Array.isArray(viaServers) && viaServers.length > 0) { fragment = withViaServers(fragment, viaServers); } return `${MATRIX_TO_BASE}/#/${fragment}`; }; export type MatrixToRoom = { roomIdOrAlias: string; viaServers?: string[]; }; export type MatrixToRoomEvent = MatrixToRoom & { eventId: string; }; const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/; export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href); // Matrix room IDs start with `!` (and aliases with `#`) — characters that // some URL builders percent-encode in path segments. Go's `id.MatrixURI` // builder (mautrix-go id/matrixuri.go) uses `url.PathEscape`, which emits // `%21` for `!` — so every matrix.to URL produced by a mautrix bridge // arrives here as `https://matrix.to/#/%21abc:server`. Our regexes below // match literal `!`/`#` only, so without a decode pass every bridge- // generated permalink would silently fail to parse — both the in-chat // linkifier (`plugins/react-custom-html-parser.tsx`) and the widget // «open-matrix-to» action would drop the URL on the floor. // // Element Web does the same `decodeURIComponent` step before parsing in // `apps/web/src/utils/permalinks/Permalinks.ts::parsePermalink`; we // mirror that contract here. `decodeURIComponent` throws synchronously on // malformed `%XX` sequences (e.g. lone `%`), so wrap it; a malformed URL // is dropped the same way as a non-matching one (undefined). const tryDecodeHref = (href: string): string => { try { return decodeURIComponent(href); } catch { return href; } }; const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/; const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/?(\?[\S]*)?$/; const MATRIX_TO_ROOM_EVENT = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; export const parseMatrixToUser = (href: string): string | undefined => { const match = tryDecodeHref(href).match(MATRIX_TO_USER); if (!match) return undefined; const userId = match[1]; return userId; }; export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => { const match = tryDecodeHref(href).match(MATRIX_TO_ROOM); if (!match) return undefined; const roomIdOrAlias = match[1]; const viaSearchStr = match[2]; const viaServers = new URLSearchParams(viaSearchStr).getAll('via'); return { roomIdOrAlias, viaServers: viaServers.length === 0 ? undefined : viaServers, }; }; export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => { const match = tryDecodeHref(href).match(MATRIX_TO_ROOM_EVENT); if (!match) return undefined; const roomIdOrAlias = match[1]; const eventId = match[2]; const viaSearchStr = match[3]; const viaServers = new URLSearchParams(viaSearchStr).getAll('via'); return { roomIdOrAlias, eventId, viaServers: viaServers.length === 0 ? undefined : viaServers, }; };