7b621b7417
No point for it to keep appearing if nobody cares? Anyway, added option to hide it via environment variable for those who are annoyed by it.
331 lines
10 KiB
JavaScript
331 lines
10 KiB
JavaScript
// @flow
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
const channelNameMinLength = 1;
|
|
const claimIdMaxLength = 40;
|
|
|
|
// see https://spec.lbry.com/#urls
|
|
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 = '([:$#]?)([^/]*)';
|
|
const queryStringBreaker = '^([\\S]+)([?][\\S]*)';
|
|
const separateQuerystring = new RegExp(queryStringBreaker);
|
|
|
|
const MOD_SEQUENCE_SEPARATOR = '*';
|
|
const MOD_CLAIM_ID_SEPARATOR_OLD = '#';
|
|
const MOD_CLAIM_ID_SEPARATOR = ':';
|
|
const MOD_BID_POSITION_SEPARATOR = '$';
|
|
|
|
/**
|
|
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
|
* messages for invalid names.
|
|
*
|
|
* Returns a dictionary with keys:
|
|
* - path (string)
|
|
* - 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)
|
|
*/
|
|
|
|
export function parseURI(url: string, requireProto: boolean = false): LbryUrlObj {
|
|
// Break into components. Empty sub-matches are converted to null
|
|
|
|
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
|
|
);
|
|
// chop off the querystring first
|
|
let QSStrippedURL, qs;
|
|
const qsRegexResult = separateQuerystring.exec(url);
|
|
if (qsRegexResult) {
|
|
[QSStrippedURL, qs] = qsRegexResult.slice(1).map((match) => match || null);
|
|
}
|
|
|
|
const cleanURL = QSStrippedURL || url;
|
|
const regexMatch = componentsRegex.exec(cleanURL) || [];
|
|
const [proto, ...rest] = regexMatch.slice(1).map((match) => match || null);
|
|
const path = rest.join('');
|
|
const [
|
|
streamNameOrChannelName,
|
|
primaryModSeparator,
|
|
primaryModValue,
|
|
pathSep, // eslint-disable-line no-unused-vars
|
|
possibleStreamName,
|
|
secondaryModSeparator,
|
|
secondaryModValue,
|
|
] = rest;
|
|
const searchParams = new URLSearchParams(qs || '');
|
|
const startTime = searchParams.get('t');
|
|
|
|
// Validate protocol
|
|
if (requireProto && !proto) {
|
|
throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
|
|
}
|
|
|
|
// Validate and process name
|
|
if (!streamNameOrChannelName) {
|
|
throw new Error(__('URL does not include name.'));
|
|
}
|
|
|
|
rest.forEach((urlPiece) => {
|
|
if (urlPiece && urlPiece.includes(' ')) {
|
|
throw new Error(__('URL can not include a space'));
|
|
}
|
|
});
|
|
|
|
const includesChannel = streamNameOrChannelName.startsWith('@');
|
|
const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
|
|
const channelName = includesChannel && streamNameOrChannelName.slice(1);
|
|
|
|
if (includesChannel) {
|
|
if (!channelName) {
|
|
throw new Error(__('No channel name after @.'));
|
|
}
|
|
|
|
if (channelName.length < channelNameMinLength) {
|
|
throw new Error(
|
|
__(`Channel names must be at least %channelNameMinLength% characters.`, {
|
|
channelNameMinLength,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
|
|
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) } : {}),
|
|
...(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 } : {}),
|
|
...(qs ? { queryString: qs } : {}),
|
|
};
|
|
}
|
|
|
|
function parseURIModifier(modSeperator: ?string, modValue: ?string) {
|
|
let claimId;
|
|
let claimSequence;
|
|
let bidPosition;
|
|
|
|
if (modSeperator) {
|
|
if (!modValue) {
|
|
throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
|
|
}
|
|
|
|
if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) {
|
|
claimId = modValue;
|
|
} else if (modSeperator === MOD_SEQUENCE_SEPARATOR) {
|
|
claimSequence = modValue;
|
|
} else if (modSeperator === MOD_BID_POSITION_SEPARATOR) {
|
|
bidPosition = modValue;
|
|
}
|
|
}
|
|
|
|
if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
|
|
throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
|
|
}
|
|
|
|
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
|
|
throw new Error(__('Claim sequence must be a number.'));
|
|
}
|
|
|
|
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
|
|
throw new Error(__('Bid position must be a number.'));
|
|
}
|
|
|
|
return [claimId, claimSequence, bidPosition];
|
|
}
|
|
|
|
const errorHistory = [];
|
|
|
|
function logErrorOnce(err: string) {
|
|
if (!errorHistory.includes(err)) {
|
|
errorHistory.push(err);
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
startTime,
|
|
...deprecatedParts
|
|
} = UrlObj;
|
|
const { claimId, claimName, contentName } = deprecatedParts;
|
|
|
|
// @ifndef IGNORE_BUILD_URI_WARNINGS
|
|
if (!isProduction) {
|
|
if (claimId) {
|
|
logErrorOnce("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead");
|
|
}
|
|
if (claimName) {
|
|
logErrorOnce("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead");
|
|
}
|
|
if (contentName) {
|
|
logErrorOnce("'contentName' should no longer be used. Use 'streamName' instead");
|
|
}
|
|
}
|
|
// @endif
|
|
|
|
if (!claimName && !channelName && !streamName) {
|
|
console.error("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.");
|
|
}
|
|
|
|
const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`);
|
|
const primaryClaimName = claimName || contentName || formattedChannelName || streamName;
|
|
const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId);
|
|
const secondaryClaimName = (!claimName && contentName) || (formattedChannelName ? streamName : null);
|
|
const secondaryClaimId = secondaryClaimName && streamClaimId;
|
|
|
|
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}` : '') +
|
|
(secondaryBidPosition ? `${secondaryBidPosition}` : '') +
|
|
(startTime ? `?t=${startTime}` : '')
|
|
);
|
|
}
|
|
|
|
/* 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,
|
|
startTime,
|
|
} = parseURI(URL);
|
|
|
|
return buildURI({
|
|
streamName,
|
|
streamClaimId,
|
|
channelName,
|
|
channelClaimId,
|
|
primaryClaimSequence,
|
|
primaryBidPosition,
|
|
secondaryClaimSequence,
|
|
secondaryBidPosition,
|
|
startTime,
|
|
});
|
|
}
|
|
|
|
export function isURIValid(URL: string, normalize: boolean = true): boolean {
|
|
try {
|
|
parseURI(normalize ? normalizeURI(URL) : URL);
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function isNameValid(claimName: string) {
|
|
return !regexInvalidURI.test(claimName);
|
|
}
|
|
|
|
export function isURIClaimable(URL: string) {
|
|
let parts;
|
|
try {
|
|
parts = parseURI(normalizeURI(URL));
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
|
|
return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
|
|
}
|
|
|
|
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,
|
|
'https://open.lbry.com/'
|
|
);
|
|
}
|
|
|
|
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 a = uriA && uriA.replace(/:/g, '#');
|
|
const b = uriB && uriB.replace(/:/g, '#');
|
|
return a === b;
|
|
}
|