import _ from "lodash"; import { LinkDiscriminator, SafeLink, Link, LinkContent, Arguments, } from "./types"; /* * Verifiers are regexes which will match valid * identifiers to their type. (This is a lie, they * can return anything) */ type Verifier = [RegExp, A]; export const roomVerifiers: Verifier< LinkDiscriminator.Alias | LinkDiscriminator.RoomId >[] = [ [/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias], [/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId], ]; export const verifiers: Verifier[] = [ [/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, LinkDiscriminator.Permalink], [/^@([^\/:]+?):(.+)$/, LinkDiscriminator.UserId], [/^\+([^\/:]+?):(.+)$/, LinkDiscriminator.GroupId], ...roomVerifiers, ]; /* * parseLink takes a striped hash link (without the '#/' prefix) * and parses into a Link. If the parse failed the result will * be ParseFailed */ export function parseLink(link: string): Link { const [identifier, args] = link.split("?"); const kind = discriminate( identifier, verifiers, LinkDiscriminator.ParseFailed ); const { vias, sharer, extras } = parseArgs(args); let parsedLink: LinkContent = { identifier, arguments: { vias, sharer, extras, }, originalLink: link, }; if (kind === LinkDiscriminator.Permalink) { const { roomKind, roomLink, eventId } = parsePermalink(identifier); return { kind, ...parsedLink, roomKind, roomLink, eventId, }; } return { kind, ...parsedLink, }; } /* * Parses a permalink. * Assumes the permalink is correct. */ export function parsePermalink(identifier: string) { const [roomLink, eventId] = identifier.split("/"); const roomKind = discriminate( roomLink, roomVerifiers, // This is hacky but we're assuming identifier is a valid permalink LinkDiscriminator.Alias ); return { roomKind, roomLink, eventId, }; } /* * descriminate applies the verifiers to the identifier and * returns the identifier's type */ export function discriminate( identifier: string, verifiers: Verifier[], fail: F ): T | F { if (identifier !== encodeURI(identifier)) { return fail; } return verifiers.reduce((discriminator, verifier) => { if (discriminator !== fail) { return discriminator; } if (identifier.match(verifier[0])) { return verifier[1]; } return discriminator; }, fail); } /* * parseArgs parses the part of matrix.to links */ export function parseArgs(args: string): Arguments { const parsedArgTuples = _.groupBy( args .split("&") .map((x) => x.split("=")) .filter((x) => x.length == 2), (arg) => { return arg[0]; } ); const parsedArgs = _.mapValues(parsedArgTuples, (arg) => arg.map((x) => x[1]) ); const { via, sharer, ...extras } = parsedArgs; return { vias: via, sharer: (parsedArgs.sharer || [undefined])[0], extras, }; } /* * toURI converts a Link to uri. It's recommended * to use the original link instead of toURI if it existed. * This is handy function in case the Link was constructed. */ export function toURI(hostname: string, link: SafeLink): string { const cleanHostname = hostname.trim().replace(/\/+$/, ""); switch (link.kind) { case LinkDiscriminator.GroupId: case LinkDiscriminator.UserId: case LinkDiscriminator.RoomId: case LinkDiscriminator.Alias: case LinkDiscriminator.Permalink: const uri = encodeURI(cleanHostname + "/#/" + link.identifier); const vias = link.arguments.vias.map((s) => "via=" + s).join("&"); const sharer = link.arguments.sharer ? "sharer=" + link.arguments.sharer : ""; const extras = _.map(link.arguments.extras, (vals, key) => vals.map((v) => key + "=" + v).join("&") ).join("&"); const args = [vias, sharer, extras].filter(Boolean).join("&"); if (args) { return uri + "?" + args; } return uri; } }