lbry-redux/src/lbryURI.js

347 lines
11 KiB
JavaScript
Raw Normal View History

// @flow
const isProduction = process.env.NODE_ENV === 'production';
const channelNameMinLength = 1;
2018-04-05 04:57:29 +02:00
const claimIdMaxLength = 40;
// see https://spec.lbry.com/#urls
2021-07-14 21:00:11 +02:00
export const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u;
export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
const regexPartProtocol = '^((?:lbry://)?)';
const regexPartStreamOrChannelName = '([^:$#/]*)';
const regexPartModifierSeparator = '([:$#]?)([^/]*)';
2019-08-24 04:43:49 +02:00
const queryStringBreaker = '^([\\S]+)([?][\\S]*)';
2019-10-08 16:14:50 +02:00
const separateQuerystring = new RegExp(queryStringBreaker);
2021-07-14 21:00:11 +02:00
const MOD_SEQUENCE_SEPARATOR = '*';
const MOD_CLAIM_ID_SEPARATOR_OLD = '#';
const MOD_CLAIM_ID_SEPARATOR = ':';
const MOD_BID_POSITION_SEPARATOR = '$';
2018-04-05 04:57:29 +02:00
/**
* Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names.
*
* Returns a dictionary with keys:
* - path (string)
2018-04-05 04:57:29 +02:00
* - isChannel (boolean)
* - streamName (string, if present)
* - streamClaimId (string, if present)
* - channelName (string, if present)
* - channelClaimId (string, if present)
* - primaryClaimSequence (int, if present)
* - secondaryClaimSequence (int, if present)
* - primaryBidPosition (int, if present)
* - secondaryBidPosition (int, if present)
2018-04-05 04:57:29 +02:00
*/
2020-10-20 17:47:37 +02:00
export function parseURI(url: string, requireProto: boolean = false): LbryUrlObj {
2018-04-05 04:57:29 +02:00
// Break into components. Empty sub-matches are converted to null
2019-08-24 04:43:49 +02:00
2018-04-05 04:57:29 +02:00
const componentsRegex = new RegExp(
regexPartProtocol + // protocol
regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end)
regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end)
'(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts
regexPartStreamOrChannelName +
regexPartModifierSeparator
2018-04-05 04:57:29 +02:00
);
2019-08-24 04:43:49 +02:00
// chop off the querystring first
let QSStrippedURL, qs;
2020-10-20 17:47:37 +02:00
const qsRegexResult = separateQuerystring.exec(url);
2019-08-24 04:43:49 +02:00
if (qsRegexResult) {
[QSStrippedURL, qs] = qsRegexResult.slice(1).map(match => match || null);
}
2018-04-05 04:57:29 +02:00
2020-10-20 17:47:37 +02:00
const cleanURL = QSStrippedURL || url;
2019-08-24 04:43:49 +02:00
const regexMatch = componentsRegex.exec(cleanURL) || [];
const [proto, ...rest] = regexMatch.slice(1).map(match => match || null);
const path = rest.join('');
const [
streamNameOrChannelName,
primaryModSeparator,
primaryModValue,
pathSep,
possibleStreamName,
secondaryModSeparator,
secondaryModValue,
] = rest;
2020-05-07 23:18:40 +02:00
const searchParams = new URLSearchParams(qs || '');
const startTime = searchParams.get('t');
2018-04-05 04:57:29 +02:00
// Validate protocol
if (requireProto && !proto) {
2019-12-21 21:32:55 +01:00
throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
2018-04-05 04:57:29 +02:00
}
// Validate and process name
if (!streamNameOrChannelName) {
2019-12-21 21:32:55 +01:00
throw new Error(__('URL does not include name.'));
2018-04-05 04:57:29 +02:00
}
2019-10-08 16:14:50 +02:00
rest.forEach(urlPiece => {
if (urlPiece && urlPiece.includes(' ')) {
2020-10-20 17:47:37 +02:00
throw new Error(__('URL can not include a space'));
2019-10-08 16:14:50 +02:00
}
});
const includesChannel = streamNameOrChannelName.startsWith('@');
const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
const channelName = includesChannel && streamNameOrChannelName.slice(1);
2018-04-05 04:57:29 +02:00
if (includesChannel) {
2018-04-05 04:57:29 +02:00
if (!channelName) {
2019-12-21 21:32:55 +01:00
throw new Error(__('No channel name after @.'));
2018-04-05 04:57:29 +02:00
}
if (channelName.length < channelNameMinLength) {
2019-12-21 21:32:55 +01:00
throw new Error(
2019-10-08 21:03:09 +02:00
__(`Channel names must be at least %channelNameMinLength% characters.`, {
channelNameMinLength,
})
);
2018-04-05 04:57:29 +02:00
}
}
// Validate and process modifier
const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
primaryModSeparator,
primaryModValue
);
const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(
secondaryModSeparator,
secondaryModValue
);
const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
const channelClaimId = includesChannel && primaryClaimId;
2018-04-05 04:57:29 +02:00
return {
isChannel,
path,
...(streamName ? { streamName } : {}),
...(streamClaimId ? { streamClaimId } : {}),
...(channelName ? { channelName } : {}),
...(channelClaimId ? { channelClaimId } : {}),
...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}),
...(secondaryClaimSequence
? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) }
: {}),
...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}),
...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}),
2020-05-07 23:18:40 +02:00
...(startTime ? { startTime: parseInt(startTime, 10) } : {}),
// The values below should not be used for new uses of parseURI
// They will not work properly with canonical_urls
claimName: streamNameOrChannelName,
claimId: primaryClaimId,
...(streamName ? { contentName: streamName } : {}),
2019-10-08 16:14:50 +02:00
...(qs ? { queryString: qs } : {}),
};
}
function parseURIModifier(modSeperator: ?string, modValue: ?string) {
2018-04-05 04:57:29 +02:00
let claimId;
let claimSequence;
let bidPosition;
2019-08-24 04:43:49 +02:00
if (modSeperator) {
if (!modValue) {
2019-12-21 21:32:55 +01:00
throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
2018-04-05 04:57:29 +02:00
}
2021-07-14 21:00:11 +02:00
if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) {
claimId = modValue;
2021-07-14 21:00:11 +02:00
} else if (modSeperator === MOD_SEQUENCE_SEPARATOR) {
claimSequence = modValue;
2021-07-14 21:00:11 +02:00
} else if (modSeperator === MOD_BID_POSITION_SEPARATOR) {
bidPosition = modValue;
2018-04-05 04:57:29 +02:00
}
}
if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
2019-12-21 21:32:55 +01:00
throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
2018-04-05 04:57:29 +02:00
}
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
2019-12-21 21:32:55 +01:00
throw new Error(__('Claim sequence must be a number.'));
2018-04-05 04:57:29 +02:00
}
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
2019-12-21 21:32:55 +01:00
throw new Error(__('Bid position must be a number.'));
2018-04-05 04:57:29 +02:00
}
return [claimId, claimSequence, bidPosition];
2018-04-05 04:57:29 +02:00
}
/**
* Takes an object in the same format returned by parse() and builds a URI.
*
* The channelName key will accept names with or without the @ prefix.
*/
export function buildURI(
UrlObj: LbryUrlObj,
includeProto: boolean = true,
protoDefault: string = 'lbry://'
): string {
const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
2020-05-07 23:18:40 +02:00
startTime,
...deprecatedParts
} = UrlObj;
const { claimId, claimName, contentName } = deprecatedParts;
2018-04-05 04:57:29 +02:00
if (!isProduction) {
if (claimId) {
console.error(
__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")
2018-04-05 04:57:29 +02:00
);
}
if (claimName) {
console.error(
2018-04-05 04:57:29 +02:00
__(
"'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead"
2018-04-05 04:57:29 +02:00
)
);
}
if (contentName) {
console.error(__("'contentName' should no longer be used. Use 'streamName' instead"));
}
2018-04-05 04:57:29 +02:00
}
if (!claimName && !channelName && !streamName) {
2019-08-30 18:28:36 +02:00
console.error(
__(
"'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url."
)
);
}
const formattedChannelName =
channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`);
2019-08-22 17:03:13 +02:00
const primaryClaimName = claimName || contentName || formattedChannelName || streamName;
const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId);
2019-08-22 17:03:13 +02:00
const secondaryClaimName =
(!claimName && contentName) || (formattedChannelName ? streamName : null);
const secondaryClaimId = secondaryClaimName && streamClaimId;
2018-04-05 04:57:29 +02:00
return (
(includeProto ? protoDefault : '') +
// primaryClaimName will always exist here because we throw above if there is no "name" value passed in
// $FlowFixMe
primaryClaimName +
(primaryClaimId ? `#${primaryClaimId}` : '') +
(primaryClaimSequence ? `:${primaryClaimSequence}` : '') +
(primaryBidPosition ? `${primaryBidPosition}` : '') +
(secondaryClaimName ? `/${secondaryClaimName}` : '') +
(secondaryClaimId ? `#${secondaryClaimId}` : '') +
(secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') +
2020-05-07 23:18:40 +02:00
(secondaryBidPosition ? `${secondaryBidPosition}` : '') +
(startTime ? `?t=${startTime}` : '')
2018-04-05 04:57:29 +02:00
);
}
/* Takes a parseable LBRY URL and converts it to standard, canonical format */
export function normalizeURI(URL: string) {
const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
2020-05-07 23:18:40 +02:00
startTime,
} = parseURI(URL);
return buildURI({
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
2020-05-07 23:18:40 +02:00
startTime,
});
2018-04-05 04:57:29 +02:00
}
export function isURIValid(URL: string): boolean {
2018-04-05 04:57:29 +02:00
try {
2019-10-08 16:15:51 +02:00
parseURI(normalizeURI(URL));
2018-04-05 04:57:29 +02:00
} catch (error) {
return false;
}
return true;
2018-04-05 04:57:29 +02:00
}
export function isNameValid(claimName: string) {
return !regexInvalidURI.test(claimName);
2018-04-05 04:57:29 +02:00
}
export function isURIClaimable(URL: string) {
2018-04-05 04:57:29 +02:00
let parts;
try {
parts = parseURI(normalizeURI(URL));
2018-04-05 04:57:29 +02:00
} catch (error) {
return false;
}
return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
2018-04-05 04:57:29 +02:00
}
export function convertToShareLink(URL: string) {
const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
} = parseURI(URL);
return buildURI(
{
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
},
true,
2019-05-10 19:29:51 +02:00
'https://open.lbry.com/'
);
}
2021-07-15 21:46:31 +02:00
export function splitBySeparator(uri: string) {
const protocolLength = 7;
return uri.startsWith('lbry://') ? uri.slice(protocolLength).split(/[#:*]/) : uri.split(/#:\*\$/);
}
export function isURIEqual(uriA: string, uriB: string) {
const parseA = parseURI(normalizeURI(uriA));
const parseB = parseURI(normalizeURI(uriB));
if (parseA.isChannel) {
if (parseB.isChannel && parseA.channelClaimId === parseB.channelClaimId) {
return true;
}
} else if (parseA.streamClaimId === parseB.streamClaimId) {
return true;
} else {
return false;
}
}