Parse matrix-schemed URIs (#7453)
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com> Co-authored-by: Dariusz Niemczyk <dariuszn@element.io> Co-authored-by: Timo K <toger5@hotmail.de> With this pr all href use matrix matrix.to links. As a consequence right-click copy link will always return get you a sharable matrix.to link.
This commit is contained in:
parent
f59ea6d7ad
commit
6712a5b1c5
12 changed files with 254 additions and 100 deletions
105
src/utils/permalinks/MatrixSchemePermalinkConstructor.ts
Normal file
105
src/utils/permalinks/MatrixSchemePermalinkConstructor.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
|
||||
|
||||
/**
|
||||
* Generates matrix: scheme permalinks
|
||||
*/
|
||||
export default class MatrixSchemePermalinkConstructor extends PermalinkConstructor {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private encodeEntity(entity: string): string {
|
||||
if (entity[0] === "!") {
|
||||
return `roomid/${entity.slice(1)}`;
|
||||
} else if (entity[0] === "#") {
|
||||
return `r/${entity.slice(1)}`;
|
||||
} else if (entity[0] === "@") {
|
||||
return `u/${entity.slice(1)}`;
|
||||
} else if (entity[0] === "$") {
|
||||
return `e/${entity.slice(1)}`;
|
||||
}
|
||||
|
||||
throw new Error("Cannot encode entity: " + entity);
|
||||
}
|
||||
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
return `matrix:${this.encodeEntity(roomId)}` +
|
||||
`/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
return `matrix:${this.encodeEntity(roomIdOrAlias)}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
}
|
||||
|
||||
forUser(userId: string): string {
|
||||
return `matrix:${this.encodeEntity(userId)}`;
|
||||
}
|
||||
|
||||
forGroup(groupId: string): string {
|
||||
throw new Error("Deliberately not implemented");
|
||||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
return `matrix:${this.encodeEntity(entityId)}`;
|
||||
}
|
||||
|
||||
isPermalinkHost(testHost: string): boolean {
|
||||
// TODO: Change API signature to accept the URL for checking
|
||||
return testHost === "";
|
||||
}
|
||||
|
||||
encodeServerCandidates(candidates: string[]) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
}
|
||||
|
||||
parsePermalink(fullUrl: string): PermalinkParts {
|
||||
if (!fullUrl || !fullUrl.startsWith("matrix:")) {
|
||||
throw new Error("Does not appear to be a permalink");
|
||||
}
|
||||
|
||||
const parts = fullUrl.substring("matrix:".length).split('/');
|
||||
|
||||
const identifier = parts[0];
|
||||
const entityNoSigil = parts[1];
|
||||
if (identifier === 'u') {
|
||||
// Probably a user, no further parsing needed.
|
||||
return PermalinkParts.forUser(`@${entityNoSigil}`);
|
||||
} else if (identifier === 'r' || identifier === 'roomid') {
|
||||
const sigil = identifier === 'r' ? '#' : '!';
|
||||
|
||||
if (parts.length === 2) { // room without event permalink
|
||||
const [roomId, query = ""] = entityNoSigil.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
return PermalinkParts.forRoom(`${sigil}${roomId}`, via);
|
||||
}
|
||||
|
||||
if (parts[2] === 'e') { // event permalink
|
||||
const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join('/') : "";
|
||||
const [eventId, query = ""] = eventIdAndQuery.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
return PermalinkParts.forEvent(`${sigil}${entityNoSigil}`, `$${eventId}`, via);
|
||||
}
|
||||
|
||||
throw new Error("Faulty room permalink");
|
||||
} else {
|
||||
throw new Error("Unknown entity type in permalink");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ export const baseUrl = `https://${host}`;
|
|||
/**
|
||||
* Generates matrix.to permalinks
|
||||
*/
|
||||
export default class SpecPermalinkConstructor extends PermalinkConstructor {
|
||||
export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
|
@ -23,11 +23,12 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor";
|
||||
import MatrixToPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./MatrixToPermalinkConstructor";
|
||||
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
|
||||
import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { ELEMENT_URL_PATTERN } from "../../linkify-matrix";
|
||||
import MatrixSchemePermalinkConstructor from "./MatrixSchemePermalinkConstructor";
|
||||
|
||||
// The maximum number of servers to pick when working out which servers
|
||||
// to add to permalinks. The servers are appended as ?via=example.org
|
||||
|
@ -312,14 +313,14 @@ export function makeGroupPermalink(groupId: string): string {
|
|||
export function isPermalinkHost(host: string): boolean {
|
||||
// Always check if the permalink is a spec permalink (callers are likely to call
|
||||
// parsePermalink after this function).
|
||||
if (new SpecPermalinkConstructor().isPermalinkHost(host)) return true;
|
||||
if (new MatrixToPermalinkConstructor().isPermalinkHost(host)) return true;
|
||||
return getPermalinkConstructor().isPermalinkHost(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an entity (permalink, room alias, user ID, etc) into a local URL
|
||||
* if possible. If the given entity is not found to be valid enough to be converted
|
||||
* then a null value will be returned.
|
||||
* if possible. If it is already a permalink (matrix.to) it gets returned
|
||||
* unchanged.
|
||||
* @param {string} entity The entity to transform.
|
||||
* @returns {string|null} The transformed permalink or null if unable.
|
||||
*/
|
||||
|
@ -327,12 +328,31 @@ export function tryTransformEntityToPermalink(entity: string): string {
|
|||
if (!entity) return null;
|
||||
|
||||
// Check to see if it is a bare entity for starters
|
||||
if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);
|
||||
{if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);}
|
||||
if (entity[0] === '@') return makeUserPermalink(entity);
|
||||
if (entity[0] === '+') return makeGroupPermalink(entity);
|
||||
|
||||
// Then try and merge it into a permalink
|
||||
return tryTransformPermalinkToLocalHref(entity);
|
||||
if (entity.slice(0, 7) === "matrix:") {
|
||||
try {
|
||||
const permalinkParts = parsePermalink(entity);
|
||||
if (permalinkParts) {
|
||||
if (permalinkParts.roomIdOrAlias) {
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
|
||||
let pl = matrixtoBaseUrl+`/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
|
||||
if (permalinkParts.viaServers.length > 0) {
|
||||
pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
}
|
||||
return pl;
|
||||
} else if (permalinkParts.groupId) {
|
||||
return matrixtoBaseUrl + `/#/${permalinkParts.groupId}`;
|
||||
} else if (permalinkParts.userId) {
|
||||
return matrixtoBaseUrl + `/#/${permalinkParts.userId}`;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -342,7 +362,7 @@ export function tryTransformEntityToPermalink(entity: string): string {
|
|||
* @returns {string} The transformed permalink or original URL if unable.
|
||||
*/
|
||||
export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
||||
if (!permalink.startsWith("http:") && !permalink.startsWith("https:")) {
|
||||
if (!permalink.startsWith("http:") && !permalink.startsWith("https:") && !permalink.startsWith("matrix:")) {
|
||||
return permalink;
|
||||
}
|
||||
|
||||
|
@ -364,7 +384,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
|||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
|
||||
permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
|
||||
if (permalinkParts.viaServers.length > 0) {
|
||||
permalink += new SpecPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
permalink += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
}
|
||||
} else if (permalinkParts.groupId) {
|
||||
permalink = `#/group/${permalinkParts.groupId}`;
|
||||
|
@ -411,13 +431,15 @@ function getPermalinkConstructor(): PermalinkConstructor {
|
|||
return new ElementPermalinkConstructor(elementPrefix);
|
||||
}
|
||||
|
||||
return new SpecPermalinkConstructor();
|
||||
return new MatrixToPermalinkConstructor();
|
||||
}
|
||||
|
||||
export function parsePermalink(fullUrl: string): PermalinkParts {
|
||||
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
|
||||
if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) {
|
||||
return new SpecPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
|
||||
return new MatrixToPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
|
||||
} else if (fullUrl.startsWith("matrix:")) {
|
||||
return new MatrixSchemePermalinkConstructor().parsePermalink(fullUrl);
|
||||
} else if (elementPrefix && fullUrl.startsWith(elementPrefix)) {
|
||||
return new ElementPermalinkConstructor(elementPrefix).parsePermalink(fullUrl);
|
||||
}
|
||||
|
@ -425,23 +447,6 @@ export function parsePermalink(fullUrl: string): PermalinkParts {
|
|||
return null; // not a permalink we can handle
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an app local link (`#/(user|room|group)/identifer`) to a Matrix entity
|
||||
* (room, user, group). Such links are produced by `HtmlUtils` when encountering
|
||||
* links, which calls `tryTransformPermalinkToLocalHref` in this module.
|
||||
* @param {string} localLink The app local link
|
||||
* @returns {PermalinkParts}
|
||||
*/
|
||||
export function parseAppLocalLink(localLink: string): PermalinkParts {
|
||||
try {
|
||||
const segments = localLink.replace("#/", "");
|
||||
return ElementPermalinkConstructor.parseAppRoute(segments);
|
||||
} catch (e) {
|
||||
// Ignore failures
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getServerName(userId: string): string {
|
||||
return userId.split(":").splice(1).join(":");
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import Pill from "../components/views/elements/Pill";
|
||||
import { parseAppLocalLink } from "./permalinks/Permalinks";
|
||||
import { parsePermalink } from "./permalinks/Permalinks";
|
||||
|
||||
/**
|
||||
* Recurses depth-first through a DOM tree, converting matrix.to links
|
||||
|
@ -46,7 +46,7 @@ export function pillifyLinks(nodes: ArrayLike<Element>, mxEvent: MatrixEvent, pi
|
|||
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
const parts = parseAppLocalLink(href);
|
||||
const parts = parsePermalink(href);
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
// We don't want to pill event permalinks, so those are ignored.
|
||||
if (parts && !parts.eventId) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue