use canonical_url for everything #187

Merged
neb-b merged 2 commits from canonical_url into master 2019-08-22 17:04:50 +02:00
21 changed files with 544 additions and 350 deletions

View file

@ -6,6 +6,7 @@
./flow-typed ./flow-typed
[options] [options]
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
module.system.node.resolve_dirname=./src module.system.node.resolve_dirname=./src
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/redux\1' module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/redux\1'
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/util\1' module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/util\1'

353
dist/bundle.es.js vendored
View file

@ -897,45 +897,46 @@ const getSearchQueryString = (query, options = {}, includeUserOptions = false) =
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
const channelNameMinLength = 1; const channelNameMinLength = 1;
const claimIdMaxLength = 40; const claimIdMaxLength = 40;
// see https://spec.lbry.com/#urls // see https://spec.lbry.com/#urls
const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u; const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u;
const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/; const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
const regexPartProtocol = '^((?:lbry://)?)';
const regexPartStreamOrChannelName = '([^:$#/]*)';
const regexPartModifierSeparator = '([:$#]?)([^/]*)';
/** /**
* Parses a LBRY name into its component parts. Throws errors with user-friendly * Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names. * messages for invalid names.
* *
* N.B. that "name" indicates the value in the name position of the URI. For
* claims for channel content, this will actually be the channel name, and
* the content name is in the path (e.g. lbry://@channel/content)
*
* In most situations, you'll want to use the contentName and channelName keys
* and ignore the name key.
*
* Returns a dictionary with keys: * Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this * - path (string)
* could be either content name or channel name; see above.
* - path (string, if persent)
* - claimSequence (int, if present)
* - bidPosition (int, if present)
* - claimId (string, if present)
* - isChannel (boolean) * - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path * - streamName (string, if present)
* - channelName (string, if present): Channel name without @ * - 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)
*/ */
function parseURI(URI, requireProto = false) {
// Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp('^((?:lbry://)?)' + // protocol
'([^:$#/]*)' + // claim name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path
);
const [proto, claimName, modSep, modVal, pathSep, path] = componentsRegex.exec(URI).slice(1).map(match => match || null);
let contentName; function parseURI(URL, requireProto = false) {
// 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);
const regexMatch = componentsRegex.exec(URL) || [];
const [proto, ...rest] = regexMatch.slice(1).map(match => match || null);
const path = rest.join('');
const [streamNameOrChannelName, primaryModSeparator, primaryModValue, pathSep, possibleStreamName, secondaryModSeparator, secondaryModValue] = rest;
// Validate protocol // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
@ -943,14 +944,15 @@ function parseURI(URI, requireProto = false) {
} }
// Validate and process name // Validate and process name
if (!claimName) { if (!streamNameOrChannelName) {
throw new Error(__('URI does not include name.')); throw new Error(__('URI does not include name.'));
} }
const isChannel = claimName.startsWith('@'); const includesChannel = streamNameOrChannelName.startsWith('@');
const channelName = isChannel ? claimName.slice(1) : claimName; const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
const channelName = includesChannel && streamNameOrChannelName.slice(1);
if (isChannel) { if (includesChannel) {
if (!channelName) { if (!channelName) {
throw new Error(__('No channel name after @.')); throw new Error(__('No channel name after @.'));
} }
@ -958,30 +960,42 @@ function parseURI(URI, requireProto = false) {
if (channelName.length < channelNameMinLength) { if (channelName.length < channelNameMinLength) {
throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength));
} }
contentName = path;
} }
const nameBadChars = (channelName || claimName).match(regexInvalidURI); // Validate and process modifier
if (nameBadChars) { const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(primaryModSeparator, primaryModValue);
throw new Error(__(`Invalid character %s in name: %s.`, nameBadChars.length === 1 ? '' : 's', nameBadChars.join(', '))); const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(secondaryModSeparator, secondaryModValue);
} const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
const channelClaimId = includesChannel && primaryClaimId;
// Validate and process modifier (claim ID, bid position or claim sequence) return _extends({
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) } : {}, {
// 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 } : {});
}
function parseURIModifier(modSeperator, modValue) {
let claimId; let claimId;
let claimSequence; let claimSequence;
let bidPosition; let bidPosition;
if (modSep) { if (modSeperator) {
if (!modVal) { if (!modValue) {
throw new Error(__(`No modifier provided after separator %s.`, modSep)); throw new Error(__(`No modifier provided after separator %s.`, modSeperator));
} }
if (modSep === '#') { if (modSeperator === '#') {
claimId = modVal; claimId = modValue;
} else if (modSep === ':') { } else if (modSeperator === ':') {
claimSequence = modVal; claimSequence = modValue;
} else if (modSep === '$') { } else if (modSeperator === '$') {
bidPosition = modVal; bidPosition = modValue;
} }
} }
@ -997,27 +1011,7 @@ function parseURI(URI, requireProto = false) {
throw new Error(__('Bid position must be a number.')); throw new Error(__('Bid position must be a number.'));
} }
// Validate and process path return [claimId, claimSequence, bidPosition];
if (path) {
if (!isChannel) {
throw new Error(__('Only channel URIs may have a path.'));
}
const pathBadChars = path.match(regexInvalidURI);
if (pathBadChars) {
throw new Error(__(`Invalid character in path: %s`, pathBadChars.join(', ')));
}
contentName = path;
} else if (pathSep) {
throw new Error(__('No path provided after /'));
}
return _extends({
claimName,
path,
isChannel
}, contentName ? { contentName } : {}, channelName ? { channelName } : {}, claimSequence ? { claimSequence: parseInt(claimSequence, 10) } : {}, bidPosition ? { bidPosition: parseInt(bidPosition, 10) } : {}, claimId ? { claimId } : {}, path ? { path } : {});
} }
/** /**
@ -1025,67 +1019,107 @@ function parseURI(URI, requireProto = false) {
* *
* The channelName key will accept names with or without the @ prefix. * The channelName key will accept names with or without the @ prefix.
*/ */
function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') {
const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition
} = UrlObj,
deprecatedParts = _objectWithoutProperties(UrlObj, ['streamName', 'streamClaimId', 'channelName', 'channelClaimId', 'primaryClaimSequence', 'primaryBidPosition', 'secondaryClaimSequence', 'secondaryBidPosition']);
const { claimId, claimName, contentName } = deprecatedParts;
let { claimName, path } = URIObj; if (!claimName && !channelName && !streamName) {
throw new Error(__("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url."));
if (channelName) {
const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`;
if (!claimName) {
claimName = channelNameFormatted;
} else if (claimName !== channelNameFormatted) {
throw new Error(__('Received a channel content URI, but claim name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.'));
}
} }
if (contentName) { const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`);
if (!claimName) { const primaryClaimName = claimName || contentName || formattedChannelName || streamName;
claimName = contentName; const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId);
} else if (!path) { const secondaryClaimName = !claimName && contentName || (formattedChannelName ? streamName : null);
path = contentName; const secondaryClaimId = secondaryClaimName && streamClaimId;
}
if (path && path !== contentName) {
throw new Error(__('Path and contentName do not match. Only one is required; most likely you wanted contentName.'));
}
}
return (includeProto ? protoDefault : '') + claimName + (claimId ? `#${claimId}` : '') + (claimSequence ? `:${claimSequence}` : '') + (bidPosition ? `${bidPosition}` : '') + (path ? `/${path}` : ''); 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}` : '');
} }
/* Takes a parseable LBRY URI and converts it to standard, canonical format */ /* Takes a parseable LBRY URL and converts it to standard, canonical format */
function normalizeURI(URI) { function normalizeURI(URL) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition
} = parseURI(URL);
return buildURI({
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition
});
} }
function isURIValid(URI) { function isURIValid(URL) {
let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.claimName;
return true;
} }
function isNameValid(claimName) { function isNameValid(claimName) {
return !regexInvalidURI.test(claimName); return !regexInvalidURI.test(claimName);
} }
function isURIClaimable(URI) { function isURIClaimable(URL) {
let parts; let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parts = parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.claimName && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path;
return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
} }
function convertToShareLink(URI) { function convertToShareLink(URL) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }, true, 'https://open.lbry.com/'); 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/');
} }
var _extends$1 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _extends$1 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
@ -1130,13 +1164,13 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
let searchSuggestions = []; let searchSuggestions = [];
try { try {
const uri = normalizeURI(query); const uri = normalizeURI(query);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
searchSuggestions.push({ searchSuggestions.push({
value: claimName, value: streamName,
type: SEARCH_TYPES.SEARCH type: SEARCH_TYPES.SEARCH
}, { }, {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
}); });
} catch (e) { } catch (e) {
@ -1157,11 +1191,11 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
// determine if it's a channel // determine if it's a channel
try { try {
const uri = normalizeURI(suggestion); const uri = normalizeURI(suggestion);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
return { return {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
}; };
} catch (e) { } catch (e) {
@ -1377,7 +1411,7 @@ const selectTransactionListFilter = reselect.createSelector(selectState$1, state
var _extends$2 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _extends$2 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _objectWithoutProperties$1(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
const matureTagMap = MATURE_TAGS.reduce((acc, tag) => _extends$2({}, acc, { [tag]: true }), {}); const matureTagMap = MATURE_TAGS.reduce((acc, tag) => _extends$2({}, acc, { [tag]: true }), {});
@ -1404,7 +1438,7 @@ const isClaimNsfw = claim => {
function createNormalizedClaimSearchKey(options) { function createNormalizedClaimSearchKey(options) {
// Ignore page because we don't care what the last page searched was, we want everything // Ignore page because we don't care what the last page searched was, we want everything
// Ignore release_time because that will change depending on when you call claim_search ex: release_time: ">12344567" // Ignore release_time because that will change depending on when you call claim_search ex: release_time: ">12344567"
const rest = _objectWithoutProperties(options, ['page', 'release_time']); const rest = _objectWithoutProperties$1(options, ['page', 'release_time']);
const query = JSON.stringify(rest); const query = JSON.stringify(rest);
return query; return query;
} }
@ -1445,8 +1479,10 @@ const selectPendingClaims = reselect.createSelector(selectState$2, state => Obje
const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => { const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => {
let claimId; let claimId;
try { try {
({ claimId } = parseURI(uri)); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
claimId = isChannel ? channelClaimId : streamClaimId;
} catch (e) {} } catch (e) {}
if (claimId) { if (claimId) {
@ -1455,7 +1491,8 @@ const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingByI
}); });
const makeSelectPendingByUri = uri => reselect.createSelector(selectPendingById, pendingById => { const makeSelectPendingByUri = uri => reselect.createSelector(selectPendingById, pendingById => {
const { claimId } = parseURI(uri); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
const claimId = isChannel ? channelClaimId : streamClaimId;
return pendingById[claimId]; return pendingById[claimId];
}); });
@ -1464,13 +1501,16 @@ const makeSelectClaimForUri = uri => reselect.createSelector(selectClaimsByUri,
// It won't be in claimsByUri because resolving it will return nothing // It won't be in claimsByUri because resolving it will return nothing
let valid; let valid;
let claimId; let channelClaimId;
let streamClaimId;
let isChannel;
try { try {
({ claimId } = parseURI(uri)); ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
valid = true; valid = true;
} catch (e) {} } catch (e) {}
if (valid) { if (valid) {
const claimId = isChannel ? channelClaimId : streamClaimId;
const pendingClaim = pendingById[claimId]; const pendingClaim = pendingById[claimId];
if (pendingClaim) { if (pendingClaim) {
@ -1720,15 +1760,18 @@ const selectClaimSearchByQueryLastPageReached = reselect.createSelector(selectSt
const makeSelectShortUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.short_url); const makeSelectShortUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.short_url);
const makeSelectCanonicalUrlForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), claim => claim && claim.canonical_url);
const makeSelectSupportsForUri = uri => reselect.createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim) => { const makeSelectSupportsForUri = uri => reselect.createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim) => {
if (!claim || !claim.is_mine) { if (!claim || !claim.is_mine) {
return null; return null;
} }
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
let total = parseFloat("0.0"); let total = 0;
Object.values(byOutpoint).forEach(support => { Object.values(byOutpoint).forEach(support => {
// $FlowFixMe
const { claim_id, amount } = support; const { claim_id, amount } = support;
total = claim_id === claimId && amount ? total + parseFloat(amount) : total; total = claim_id === claimId && amount ? total + parseFloat(amount) : total;
}); });
@ -2433,10 +2476,10 @@ function doClaimSearch(options = {
const success = data => { const success = data => {
const resolveInfo = {}; const resolveInfo = {};
const uris = []; const urls = [];
data.items.forEach(stream => { data.items.forEach(stream => {
resolveInfo[stream.permanent_url] = { stream }; resolveInfo[stream.canonical_url] = { stream };
uris.push(stream.permanent_url); urls.push(stream.canonical_url);
}); });
dispatch({ dispatch({
@ -2444,7 +2487,7 @@ function doClaimSearch(options = {
data: { data: {
query, query,
resolveInfo, resolveInfo,
uris, urls,
append: options.page && options.page !== 1, append: options.page && options.page !== 1,
pageSize: options.page_size pageSize: options.page_size
} }
@ -2858,12 +2901,12 @@ function doSetFileListSort(page, value) {
}; };
} }
function _objectWithoutProperties$1(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _objectWithoutProperties$2(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
const selectState$5 = state => state.publish || {}; const selectState$5 = state => state.publish || {};
const selectPublishFormValues = reselect.createSelector(selectState$5, state => { const selectPublishFormValues = reselect.createSelector(selectState$5, state => {
const formValues = _objectWithoutProperties$1(state, ['pendingPublish']); const formValues = _objectWithoutProperties$2(state, ['pendingPublish']);
return formValues; return formValues;
}); });
const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]); const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]);
@ -2876,8 +2919,16 @@ const selectIsStillEditing = reselect.createSelector(selectPublishFormValues, pu
return false; return false;
} }
const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); const {
const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); isChannel: currentIsChannel,
claimName: currentClaimName,
contentName: currentContentName
} = parseURI(uri);
const {
isChannel: editIsChannel,
claimName: editClaimName,
contentName: editContentName
} = parseURI(editingURI);
// Depending on the previous/current use of a channel, we need to compare different things // Depending on the previous/current use of a channel, we need to compare different things
// ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName // ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName
@ -2904,7 +2955,7 @@ const selectIsResolvingPublishUris = reselect.createSelector(selectState$5, sele
let isResolvingShortUri; let isResolvingShortUri;
if (isChannel) { if (isChannel) {
const shortUri = buildURI({ contentName: name }); const shortUri = buildURI({ streamName: name });
isResolvingShortUri = resolvingUris.includes(shortUri); isResolvingShortUri = resolvingUris.includes(shortUri);
} }
@ -3376,13 +3427,21 @@ from, isBackgroundSearch = false) => (dispatch, getState) => {
const actions = []; const actions = [];
data.forEach(result => { data.forEach(result => {
if (result.name) { if (result) {
const uri = buildURI({ const { name, claimId } = result;
claimName: result.name, const urlObj = {};
claimId: result.claimId
}); if (name.startsWith('@')) {
actions.push(doResolveUri(uri)); urlObj.channelName = name;
uris.push(uri); urlObj.channelClaimId = claimId;
} else {
urlObj.streamName = name;
urlObj.streamClaimId = claimId;
}
const url = buildURI(urlObj);
actions.push(doResolveUri(url));
uris.push(url);
} }
}); });
@ -3561,26 +3620,25 @@ function handleClaimAction(state, action) {
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
Object.entries(resolveInfo).forEach(([uri, resolveResponse]) => { Object.entries(resolveInfo).forEach(([url, resolveResponse]) => {
// $FlowFixMe // $FlowFixMe
if (resolveResponse.claimsInChannel) { const { claimsInChannel, stream, channel } = resolveResponse;
// $FlowFixMe if (claimsInChannel) {
channelClaimCounts[uri] = resolveResponse.claimsInChannel; channelClaimCounts[url] = claimsInChannel;
} }
});
// $FlowFixMe
Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => {
if (stream) { if (stream) {
byId[stream.claim_id] = stream; byId[stream.claim_id] = stream;
byUri[uri] = stream.claim_id; byUri[url] = stream.claim_id;
} }
if (channel) { if (channel) {
byId[channel.claim_id] = channel; byId[channel.claim_id] = channel;
byUri[stream ? channel.permanent_url : uri] = channel.claim_id; byUri[stream ? channel.canonical_url : url] = channel.claim_id;
} }
if (!stream && !channel) { if (!stream && !channel) {
byUri[uri] = null; byUri[url] = null;
} }
}); });
@ -3792,17 +3850,17 @@ reducers[CLAIM_SEARCH_COMPLETED] = (state, action) => {
const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery); const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery);
const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery); const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery);
const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached); const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached);
const { append, query, uris, pageSize } = action.data; const { append, query, urls, pageSize } = action.data;
if (append) { if (append) {
// todo: check for duplicate uris when concatenating? // todo: check for duplicate urls when concatenating?
claimSearchByQuery[query] = claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(uris) : uris; claimSearchByQuery[query] = claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(urls) : urls;
} else { } else {
claimSearchByQuery[query] = uris; claimSearchByQuery[query] = urls;
} }
// the returned number of uris is less than the page size, so we're on the last page // the returned number of urls is less than the page size, so we're on the last page
claimSearchByQueryLastPageReached[query] = uris.length < pageSize; claimSearchByQueryLastPageReached[query] = urls.length < pageSize;
delete fetchingClaimSearchByQuery[query]; delete fetchingClaimSearchByQuery[query];
@ -4232,7 +4290,7 @@ const notificationsReducer = handleActions({
var _extends$a = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _extends$a = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _objectWithoutProperties$2(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _objectWithoutProperties$3(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
const defaultState$6 = { const defaultState$6 = {
editingURI: undefined, editingURI: undefined,
@ -4282,7 +4340,7 @@ const publishReducer = handleActions({
publishSuccess: true publishSuccess: true
}), }),
[DO_PREPARE_EDIT]: (state, action) => { [DO_PREPARE_EDIT]: (state, action) => {
const publishData = _objectWithoutProperties$2(action.data, []); const publishData = _objectWithoutProperties$3(action.data, []);
const { channel, name, uri } = publishData; const { channel, name, uri } = publishData;
// The short uri is what is presented to the user // The short uri is what is presented to the user
@ -4892,6 +4950,7 @@ exports.isNameValid = isNameValid;
exports.isURIClaimable = isURIClaimable; exports.isURIClaimable = isURIClaimable;
exports.isURIValid = isURIValid; exports.isURIValid = isURIValid;
exports.makeSelectAmountForUri = makeSelectAmountForUri; exports.makeSelectAmountForUri = makeSelectAmountForUri;
exports.makeSelectCanonicalUrlForUri = makeSelectCanonicalUrlForUri;
exports.makeSelectChannelForClaimUri = makeSelectChannelForClaimUri; exports.makeSelectChannelForClaimUri = makeSelectChannelForClaimUri;
exports.makeSelectClaimForUri = makeSelectClaimForUri; exports.makeSelectClaimForUri = makeSelectClaimForUri;
exports.makeSelectClaimIsMine = makeSelectClaimIsMine; exports.makeSelectClaimIsMine = makeSelectClaimIsMine;

View file

@ -23,6 +23,7 @@ declare type GenericClaim = {
decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044
timestamp?: number, // date of last transaction timestamp?: number, // date of last transaction
height: number, // block height the tx was confirmed height: number, // block height the tx was confirmed
is_mine: boolean,
name: string, name: string,
normalized_name: string, // `name` normalized via unicode NFD spec, normalized_name: string, // `name` normalized via unicode NFD spec,
nout: number, // index number for an output of a tx nout: number, // index number for an output of a tx

View file

@ -65,7 +65,7 @@ declare type VersionResponse = {
declare type ResolveResponse = { declare type ResolveResponse = {
// Keys are the url(s) passed to resolve // Keys are the url(s) passed to resolve
[string]: Claim | { error?: {} }, [string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, claimsInChannel?: number },
}; };
declare type GetResponse = FileListItem & { error?: string }; declare type GetResponse = FileListItem & { error?: string };

2
dist/flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1,2 @@
// @flow
declare function __(a: string, b?: string | number): string;

1
flow-typed/Claim.js vendored
View file

@ -23,6 +23,7 @@ declare type GenericClaim = {
decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044
timestamp?: number, // date of last transaction timestamp?: number, // date of last transaction
height: number, // block height the tx was confirmed height: number, // block height the tx was confirmed
is_mine: boolean,
name: string, name: string,
normalized_name: string, // `name` normalized via unicode NFD spec, normalized_name: string, // `name` normalized via unicode NFD spec,
nout: number, // index number for an output of a tx nout: number, // index number for an output of a tx

2
flow-typed/Lbry.js vendored
View file

@ -65,7 +65,7 @@ declare type VersionResponse = {
declare type ResolveResponse = { declare type ResolveResponse = {
// Keys are the url(s) passed to resolve // Keys are the url(s) passed to resolve
[string]: Claim | { error?: {} }, [string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, claimsInChannel?: number },
}; };
declare type GetResponse = FileListItem & { error?: string }; declare type GetResponse = FileListItem & { error?: string };

2
flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1,2 @@
// @flow
declare function __(a: string, b?: string | number): string;

20
flow-typed/lbryURI.js vendored Normal file
View file

@ -0,0 +1,20 @@
// @flow
declare type LbryUrlObj = {
// Path and channel will always exist when calling parseURI
// But they may not exist when code calls buildURI
isChannel?: boolean,
path?: string,
streamName?: string,
streamClaimId?: string,
channelName?: string,
channelClaimId?: string,
primaryClaimSequence?: number,
secondaryClaimSequence?: number,
primaryBidPosition?: number,
secondaryBidPosition?: number,
// Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url
claimName?: string,
claimId?: string,
contentName?: string,
};

View file

@ -21,7 +21,7 @@
"main": "dist/bundle.es.js", "main": "dist/bundle.es.js",
"module": "dist/bundle.es.js", "module": "dist/bundle.es.js",
"scripts": { "scripts": {
"build": "rollup --config", "build": "NODE_ENV=production rollup --config",
"dev": "rollup --config --watch", "dev": "rollup --config --watch",
"precommit": "flow check && lint-staged", "precommit": "flow check && lint-staged",
"lint": "eslint 'src/**/*.js' --fix", "lint": "eslint 'src/**/*.js' --fix",
@ -59,7 +59,8 @@
"rollup-plugin-copy": "^1.1.0", "rollup-plugin-copy": "^1.1.0",
"rollup-plugin-eslint": "^5.1.0", "rollup-plugin-eslint": "^5.1.0",
"rollup-plugin-flow": "^1.1.1", "rollup-plugin-flow": "^1.1.1",
"rollup-plugin-includepaths": "^0.2.3" "rollup-plugin-includepaths": "^0.2.3",
"rollup-plugin-replace": "^2.2.0"
}, },
"engines": { "engines": {
"yarn": "^1.3" "yarn": "^1.3"

View file

@ -2,6 +2,7 @@ import babel from 'rollup-plugin-babel';
import flow from 'rollup-plugin-flow'; import flow from 'rollup-plugin-flow';
import includePaths from 'rollup-plugin-includepaths'; import includePaths from 'rollup-plugin-includepaths';
import copy from 'rollup-plugin-copy'; import copy from 'rollup-plugin-copy';
import replace from 'rollup-plugin-replace';
let includePathOptions = { let includePathOptions = {
include: {}, include: {},
@ -10,6 +11,8 @@ let includePathOptions = {
extensions: ['.js'], extensions: ['.js'],
}; };
const production = process.env.NODE_ENV === 'production';
export default { export default {
input: 'src/index.js', input: 'src/index.js',
output: { output: {
@ -24,5 +27,10 @@ export default {
presets: ['stage-2'], presets: ['stage-2'],
}), }),
copy({ targets: ['flow-typed'] }), copy({ targets: ['flow-typed'] }),
replace({
'process.env.NODE_ENV': production
? JSON.stringify('production')
: JSON.stringify('development'),
}),
], ],
}; };

View file

@ -172,6 +172,7 @@ export {
makeSelectPendingByUri, makeSelectPendingByUri,
makeSelectClaimsInChannelForCurrentPageState, makeSelectClaimsInChannelForCurrentPageState,
makeSelectShortUrlForUri, makeSelectShortUrlForUri,
makeSelectCanonicalUrlForUri,
makeSelectSupportsForUri, makeSelectSupportsForUri,
selectPendingById, selectPendingById,
selectClaimsById, selectClaimsById,

View file

@ -1,46 +1,55 @@
// @flow
const isProduction = process.env.NODE_ENV === 'production';
const channelNameMinLength = 1; const channelNameMinLength = 1;
const claimIdMaxLength = 40; const claimIdMaxLength = 40;
// see https://spec.lbry.com/#urls // 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 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}$/; export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
const regexPartProtocol = '^((?:lbry://)?)';
const regexPartStreamOrChannelName = '([^:$#/]*)';
const regexPartModifierSeparator = '([:$#]?)([^/]*)';
/** /**
* Parses a LBRY name into its component parts. Throws errors with user-friendly * Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names. * messages for invalid names.
* *
* N.B. that "name" indicates the value in the name position of the URI. For
* claims for channel content, this will actually be the channel name, and
* the content name is in the path (e.g. lbry://@channel/content)
*
* In most situations, you'll want to use the contentName and channelName keys
* and ignore the name key.
*
* Returns a dictionary with keys: * Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this * - path (string)
* could be either content name or channel name; see above.
* - path (string, if persent)
* - claimSequence (int, if present)
* - bidPosition (int, if present)
* - claimId (string, if present)
* - isChannel (boolean) * - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path * - streamName (string, if present)
* - channelName (string, if present): Channel name without @ * - 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(URI, requireProto = false) {
export function parseURI(URL: string, requireProto: boolean = false): LbryUrlObj {
// Break into components. Empty sub-matches are converted to null // Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp( const componentsRegex = new RegExp(
'^((?:lbry://)?)' + // protocol regexPartProtocol + // protocol
'([^:$#/]*)' + // claim name (stops at the first separator or end) regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts
regexPartStreamOrChannelName +
regexPartModifierSeparator
); );
const [proto, claimName, modSep, modVal, pathSep, path] = componentsRegex
.exec(URI)
.slice(1)
.map(match => match || null);
let contentName; const regexMatch = componentsRegex.exec(URL) || [];
const [proto, ...rest] = regexMatch.slice(1).map(match => match || null);
const path = rest.join('');
const [
streamNameOrChannelName,
primaryModSeparator,
primaryModValue,
pathSep,
possibleStreamName,
secondaryModSeparator,
secondaryModValue,
] = rest;
// Validate protocol // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
@ -48,14 +57,15 @@ export function parseURI(URI, requireProto = false) {
} }
// Validate and process name // Validate and process name
if (!claimName) { if (!streamNameOrChannelName) {
throw new Error(__('URI does not include name.')); throw new Error(__('URI does not include name.'));
} }
const isChannel = claimName.startsWith('@'); const includesChannel = streamNameOrChannelName.startsWith('@');
kauffj commented 2019-08-20 16:45:04 +02:00 (Migrated from github.com)
Review

This could possibly be removed or moved. The function is called parseURI, not parseAndValidateURI. It's one thing to error if invalid characters make it impossible to parse, but I think erroring on a validly structured URI is probably incorrect here.

This could possibly be removed or moved. The function is called parseURI, not parseAndValidateURI. It's one thing to error if invalid characters make it impossible to parse, but I think erroring on a validly structured URI is probably incorrect here.
neb-b commented 2019-08-20 18:52:19 +02:00 (Migrated from github.com)
Review

I like removing validation from this. We already have isURIValid to validate

I like removing validation from this. We already have `isURIValid` to validate
const channelName = isChannel ? claimName.slice(1) : claimName; const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
const channelName = includesChannel && streamNameOrChannelName.slice(1);
if (isChannel) { if (includesChannel) {
if (!channelName) { if (!channelName) {
neb-b commented 2019-08-19 06:04:26 +02:00 (Migrated from github.com)
Review

Not sure the best way to do this.

Should path include the claimId? Should there be path and pathClaimId?

Not sure the best way to do this. Should `path` include the claimId? Should there be `path` and `pathClaimId`?
kauffj commented 2019-08-20 16:49:01 +02:00 (Migrated from github.com)
Review

According to https://lbry.tech/spec#urls, the path should include the channel name, claim name, and any modifiers. The path is everything between the scheme and any query parameters.

Channel name, claim name, and any modifiers would be sub-properties of the path (and could potentially be returned/parsed out by this function as well).

@lyoshenka, please correct me if I'm wrong about this.

@seanyesmunt we should try to have this function use the same names and standards that the spec specifies. If you think we should do something different than what the spec says, we can also consider amending the spec.

According to https://lbry.tech/spec#urls, the `path` should include the channel name, claim name, and any modifiers. The path is everything between the scheme and any query parameters. Channel name, claim name, and any modifiers would be sub-properties of the path (and could potentially be returned/parsed out by this function as well). @lyoshenka, please correct me if I'm wrong about this. @seanyesmunt we should try to have this function use the same names and standards that the spec specifies. If you think we should do something different than what the spec says, we can also consider amending the spec.
throw new Error(__('No channel name after @.')); throw new Error(__('No channel name after @.'));
} }
@ -63,36 +73,58 @@ export function parseURI(URI, requireProto = false) {
if (channelName.length < channelNameMinLength) { if (channelName.length < channelNameMinLength) {
throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength)); throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength));
} }
contentName = path;
} }
const nameBadChars = (channelName || claimName).match(regexInvalidURI); // Validate and process modifier
if (nameBadChars) { const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
throw new Error( primaryModSeparator,
__( primaryModValue
`Invalid character %s in name: %s.`,
nameBadChars.length === 1 ? '' : 's',
nameBadChars.join(', ')
)
); );
} const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(
secondaryModSeparator,
secondaryModValue
);
const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
const channelClaimId = includesChannel && primaryClaimId;
// Validate and process modifier (claim ID, bid position or claim sequence) 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) } : {}),
// 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 } : {}),
};
}
function parseURIModifier(modSeperator: ?string, modValue: ?string) {
let claimId; let claimId;
let claimSequence; let claimSequence;
let bidPosition; let bidPosition;
if (modSep) { if (modSeperator) {
if (!modVal) { if (!modValue) {
throw new Error(__(`No modifier provided after separator %s.`, modSep)); throw new Error(__(`No modifier provided after separator %s.`, modSeperator));
} }
if (modSep === '#') { if (modSeperator === '#') {
claimId = modVal; claimId = modValue;
} else if (modSep === ':') { } else if (modSeperator === ':') {
claimSequence = modVal; claimSequence = modValue;
} else if (modSep === '$') { } else if (modSeperator === '$') {
bidPosition = modVal; bidPosition = modValue;
} }
} }
@ -108,33 +140,7 @@ export function parseURI(URI, requireProto = false) {
throw new Error(__('Bid position must be a number.')); throw new Error(__('Bid position must be a number.'));
} }
// Validate and process path return [claimId, claimSequence, bidPosition];
if (path) {
if (!isChannel) {
throw new Error(__('Only channel URIs may have a path.'));
}
const pathBadChars = path.match(regexInvalidURI);
if (pathBadChars) {
throw new Error(__(`Invalid character in path: %s`, pathBadChars.join(', ')));
}
contentName = path;
} else if (pathSep) {
throw new Error(__('No path provided after /'));
}
return {
claimName,
path,
isChannel,
...(contentName ? { contentName } : {}),
...(channelName ? { channelName } : {}),
...(claimSequence ? { claimSequence: parseInt(claimSequence, 10) } : {}),
...(bidPosition ? { bidPosition: parseInt(bidPosition, 10) } : {}),
...(claimId ? { claimId } : {}),
...(path ? { path } : {}),
};
} }
/** /**
@ -142,91 +148,145 @@ export function parseURI(URI, requireProto = false) {
* *
* The channelName key will accept names with or without the @ prefix. * The channelName key will accept names with or without the @ prefix.
*/ */
export function buildURI(URIObj, includeProto = true, protoDefault = 'lbry://') { export function buildURI(
const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj; UrlObj: LbryUrlObj,
includeProto: boolean = true,
protoDefault: string = 'lbry://'
): string {
const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
...deprecatedParts
} = UrlObj;
const { claimId, claimName, contentName } = deprecatedParts;
let { claimName, path } = URIObj; if (!isProduction) {
if (claimId) {
if (channelName) { console.error(
const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`; __("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")
if (!claimName) { );
claimName = channelNameFormatted; }
} else if (claimName !== channelNameFormatted) { if (claimName) {
throw new Error( console.error(
__( __(
'Received a channel content URI, but claim name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.' "'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead"
) )
); );
} }
}
if (contentName) { if (contentName) {
if (!claimName) { console.error(__("'contentName' should no longer be used. Use 'streamName' instead"));
claimName = contentName;
} else if (!path) {
path = contentName;
} }
if (path && path !== contentName) { }
if (!claimName && !channelName && !streamName) {
throw new Error( throw new Error(
__( __(
'Path and contentName do not match. Only one is required; most likely you wanted contentName.' "'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 ( return (
(includeProto ? protoDefault : '') + (includeProto ? protoDefault : '') +
claimName + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in
(claimId ? `#${claimId}` : '') + // $FlowFixMe
(claimSequence ? `:${claimSequence}` : '') + primaryClaimName +
(bidPosition ? `${bidPosition}` : '') + (primaryClaimId ? `#${primaryClaimId}` : '') +
(path ? `/${path}` : '') (primaryClaimSequence ? `:${primaryClaimSequence}` : '') +
(primaryBidPosition ? `${primaryBidPosition}` : '') +
(secondaryClaimName ? `/${secondaryClaimName}` : '') +
(secondaryClaimId ? `#${secondaryClaimId}` : '') +
(secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') +
(secondaryBidPosition ? `${secondaryBidPosition}` : '')
); );
} }
/* Takes a parseable LBRY URI and converts it to standard, canonical format */ /* Takes a parseable LBRY URL and converts it to standard, canonical format */
export function normalizeURI(URI) { export function normalizeURI(URL: string) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
return buildURI({ claimName, path, claimSequence, bidPosition, claimId }); streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
} = parseURI(URL);
return buildURI({
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryClaimSequence,
primaryBidPosition,
secondaryClaimSequence,
secondaryBidPosition,
});
} }
export function isURIValid(URI) { export function isURIValid(URL: string): boolean {
let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.claimName;
return true;
} }
export function isNameValid(claimName) { export function isNameValid(claimName: string) {
return !regexInvalidURI.test(claimName); return !regexInvalidURI.test(claimName);
} }
export function isURIClaimable(URI) { export function isURIClaimable(URL: string) {
let parts; let parts;
try { try {
parts = parseURI(normalizeURI(URI)); parts = parseURI(normalizeURI(URL));
} catch (error) { } catch (error) {
return false; return false;
} }
return (
parts && return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
parts.claimName &&
!parts.claimId &&
!parts.bidPosition &&
!parts.claimSequence &&
!parts.isChannel &&
!parts.path
);
} }
export function convertToShareLink(URI) { export function convertToShareLink(URL: string) {
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI); const {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
} = parseURI(URL);
return buildURI( return buildURI(
{ claimName, path, claimSequence, bidPosition, claimId }, {
streamName,
streamClaimId,
channelName,
channelClaimId,
primaryBidPosition,
primaryClaimSequence,
secondaryBidPosition,
secondaryClaimSequence,
},
true, true,
'https://open.lbry.com/' 'https://open.lbry.com/'
); );

View file

@ -328,10 +328,10 @@ export function doClaimSearch(
kauffj commented 2019-08-20 16:49:52 +02:00 (Migrated from github.com)
Review

why keep two sets of URIs?

why keep two sets of URIs?
const success = (data: ClaimSearchResponse) => { const success = (data: ClaimSearchResponse) => {
const resolveInfo = {}; const resolveInfo = {};
const uris = []; const urls = [];
data.items.forEach((stream: Claim) => { data.items.forEach((stream: Claim) => {
resolveInfo[stream.permanent_url] = { stream }; resolveInfo[stream.canonical_url] = { stream };
uris.push(stream.permanent_url); urls.push(stream.canonical_url);
}); });
dispatch({ dispatch({
@ -339,7 +339,7 @@ export function doClaimSearch(
data: { data: {
query, query,
resolveInfo, resolveInfo,
uris, urls,
append: options.page && options.page !== 1, append: options.page && options.page !== 1,
pageSize: options.page_size, pageSize: options.page_size,
}, },

View file

@ -112,18 +112,26 @@ export const doSearch = (
fetch(`${CONNECTION_STRING}search?${queryWithOptions}`) fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
.then(handleFetchResponse) .then(handleFetchResponse)
.then((data: Array<{ name: String, claimId: string }>) => { .then((data: Array<{ name: string, claimId: string }>) => {
const uris = []; const uris = [];
const actions = []; const actions = [];
data.forEach(result => { data.forEach(result => {
if (result.name) { if (result) {
const uri = buildURI({ const { name, claimId } = result;
claimName: result.name, const urlObj: LbryUrlObj = {};
claimId: result.claimId,
}); if (name.startsWith('@')) {
actions.push(doResolveUri(uri)); urlObj.channelName = name;
uris.push(uri); urlObj.channelClaimId = claimId;
} else {
urlObj.streamName = name;
urlObj.streamClaimId = claimId;
}
const url = buildURI(urlObj);
actions.push(doResolveUri(url));
uris.push(url);
} }
}); });

View file

@ -66,26 +66,25 @@ function handleClaimAction(state: State, action: any): State {
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
kauffj commented 2019-08-20 16:50:34 +02:00 (Migrated from github.com)
Review

URI or URL? are we consistent with when we use one vs. the other?

URI or URL? are we consistent with when we use one vs. the other?
Object.entries(resolveInfo).forEach(([uri: string, resolveResponse: Claim]) => { Object.entries(resolveInfo).forEach(([url: string, resolveResponse: ResolveResponse]) => {
// $FlowFixMe // $FlowFixMe
if (resolveResponse.claimsInChannel) { const { claimsInChannel, stream, channel } = resolveResponse;
// $FlowFixMe if (claimsInChannel) {
channelClaimCounts[uri] = resolveResponse.claimsInChannel; channelClaimCounts[url] = claimsInChannel;
} }
});
// $FlowFixMe
Object.entries(resolveInfo).forEach(([uri, { channel, stream }]) => {
if (stream) { if (stream) {
byId[stream.claim_id] = stream; byId[stream.claim_id] = stream;
byUri[uri] = stream.claim_id; byUri[url] = stream.claim_id;
} }
if (channel) { if (channel) {
byId[channel.claim_id] = channel; byId[channel.claim_id] = channel;
byUri[stream ? channel.permanent_url : uri] = channel.claim_id; byUri[stream ? channel.canonical_url : url] = channel.claim_id;
} }
if (!stream && !channel) { if (!stream && !channel) {
byUri[uri] = null; byUri[url] = null;
} }
}); });
@ -305,20 +304,20 @@ reducers[ACTIONS.CLAIM_SEARCH_COMPLETED] = (state: State, action: any): State =>
{}, {},
state.claimSearchByQueryLastPageReached state.claimSearchByQueryLastPageReached
); );
const { append, query, uris, pageSize } = action.data; const { append, query, urls, pageSize } = action.data;
if (append) { if (append) {
// todo: check for duplicate uris when concatenating? // todo: check for duplicate urls when concatenating?
claimSearchByQuery[query] = claimSearchByQuery[query] =
claimSearchByQuery[query] && claimSearchByQuery[query].length claimSearchByQuery[query] && claimSearchByQuery[query].length
? claimSearchByQuery[query].concat(uris) ? claimSearchByQuery[query].concat(urls)
: uris; : urls;
} else { } else {
claimSearchByQuery[query] = uris; claimSearchByQuery[query] = urls;
} }
// the returned number of uris is less than the page size, so we're on the last page // the returned number of urls is less than the page size, so we're on the last page
claimSearchByQueryLastPageReached[query] = uris.length < pageSize; claimSearchByQueryLastPageReached[query] = urls.length < pageSize;
delete fetchingClaimSearchByQuery[query]; delete fetchingClaimSearchByQuery[query];

View file

@ -62,8 +62,10 @@ export const makeSelectClaimIsPending = (uri: string) =>
selectPendingById, selectPendingById,
pendingById => { pendingById => {
let claimId; let claimId;
try { try {
({ claimId } = parseURI(uri)); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
claimId = isChannel ? channelClaimId : streamClaimId;
} catch (e) {} } catch (e) {}
if (claimId) { if (claimId) {
@ -76,7 +78,8 @@ export const makeSelectPendingByUri = (uri: string) =>
createSelector( createSelector(
selectPendingById, selectPendingById,
pendingById => { pendingById => {
const { claimId } = parseURI(uri); const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
const claimId = isChannel ? channelClaimId : streamClaimId;
return pendingById[claimId]; return pendingById[claimId];
} }
); );
@ -90,13 +93,16 @@ export const makeSelectClaimForUri = (uri: string) =>
// It won't be in claimsByUri because resolving it will return nothing // It won't be in claimsByUri because resolving it will return nothing
let valid; let valid;
let claimId; let channelClaimId;
let streamClaimId;
let isChannel;
try { try {
({ claimId } = parseURI(uri)); ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
valid = true; valid = true;
} catch (e) {} } catch (e) {}
if (valid) { if (valid) {
const claimId = isChannel ? channelClaimId : streamClaimId;
const pendingClaim = pendingById[claimId]; const pendingClaim = pendingById[claimId];
if (pendingClaim) { if (pendingClaim) {
@ -521,23 +527,18 @@ export const selectClaimSearchByQueryLastPageReached = createSelector(
state => state.claimSearchByQueryLastPageReached || {} state => state.claimSearchByQueryLastPageReached || {}
); );
export const makeSelectClaimSearchUrisByOptions = (options: {}) =>
createSelector(
selectClaimSearchByQuery,
byQuery => {
// We don't care what options are passed to this selector. Just forward them.
// $FlowFixMe
const query = createNormalizedClaimSearchKey(options);
return byQuery[query];
}
);
export const makeSelectShortUrlForUri = (uri: string) => export const makeSelectShortUrlForUri = (uri: string) =>
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
claim => claim && claim.short_url claim => claim && claim.short_url
); );
export const makeSelectCanonicalUrlForUri = (uri: string) =>
createSelector(
makeSelectClaimForUri(uri),
claim => claim && claim.canonical_url
);
export const makeSelectSupportsForUri = (uri: string) => export const makeSelectSupportsForUri = (uri: string) =>
createSelector( createSelector(
selectSupportsByOutpoint, selectSupportsByOutpoint,
@ -548,11 +549,12 @@ export const makeSelectSupportsForUri = (uri: string) =>
} }
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
let total = parseFloat("0.0"); let total = 0;
Object.values(byOutpoint).forEach(support => { Object.values(byOutpoint).forEach(support => {
const { claim_id, amount } = support // $FlowFixMe
total = (claim_id === claimId && amount) ? total + parseFloat(amount) : total; const { claim_id, amount } = support;
total = claim_id === claimId && amount ? total + parseFloat(amount) : total;
}); });
return total; return total;

View file

@ -33,8 +33,16 @@ export const selectIsStillEditing = createSelector(
return false; return false;
} }
const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); const {
const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); isChannel: currentIsChannel,
claimName: currentClaimName,
contentName: currentContentName,
} = parseURI(uri);
const {
isChannel: editIsChannel,
claimName: editClaimName,
contentName: editContentName,
} = parseURI(editingURI);
// Depending on the previous/current use of a channel, we need to compare different things // Depending on the previous/current use of a channel, we need to compare different things
// ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName // ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName
@ -60,7 +68,9 @@ export const selectMyClaimForUri = createSelector(
return isStillEditing return isStillEditing
? claimsById[editClaimId] ? claimsById[editClaimId]
: myClaims.find(claim => : myClaims.find(claim =>
!contentName ? claim.name === claimName : claim.name === contentName || claim.name === claimName !contentName
? claim.name === claimName
: claim.name === contentName || claim.name === claimName
); );
} }
); );
@ -75,7 +85,7 @@ export const selectIsResolvingPublishUris = createSelector(
let isResolvingShortUri; let isResolvingShortUri;
if (isChannel) { if (isChannel) {
const shortUri = buildURI({ contentName: name }); const shortUri = buildURI({ streamName: name });
isResolvingShortUri = resolvingUris.includes(shortUri); isResolvingShortUri = resolvingUris.includes(shortUri);
} }

View file

@ -77,15 +77,15 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
let searchSuggestions = []; let searchSuggestions = [];
try { try {
const uri = normalizeURI(query); const uri = normalizeURI(query);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
searchSuggestions.push( searchSuggestions.push(
{ {
value: claimName, value: streamName,
type: SEARCH_TYPES.SEARCH, type: SEARCH_TYPES.SEARCH,
}, },
{ {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
} }
); );
@ -110,11 +110,11 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
// determine if it's a channel // determine if it's a channel
try { try {
const uri = normalizeURI(suggestion); const uri = normalizeURI(suggestion);
const { claimName, isChannel } = parseURI(uri); const { channelName, streamName, isChannel } = parseURI(uri);
return { return {
value: uri, value: uri,
shorthand: isChannel ? claimName.slice(1) : claimName, shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
}; };
} catch (e) { } catch (e) {

View file

@ -1,13 +0,0 @@
// @flow
import { parseURI } from 'lbryURI';
export const formatLbryUriForWeb = (uri: string) => {
const { claimName, claimId } = parseURI(uri);
let webUrl = `/${claimName}`;
if (claimId) {
webUrl += `/${claimId}`;
}
return webUrl;
};

View file

@ -1911,6 +1911,11 @@ estree-walker@^0.6.0:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae"
integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw== integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw==
estree-walker@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
esutils@^2.0.2: esutils@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@ -3440,6 +3445,13 @@ lru-cache@^4.0.1:
pseudomap "^1.0.2" pseudomap "^1.0.2"
yallist "^2.1.2" yallist "^2.1.2"
magic-string@^0.25.2:
version "0.25.3"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9"
integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==
dependencies:
sourcemap-codec "^1.4.4"
make-dir@^1.0.0: make-dir@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
@ -4379,6 +4391,14 @@ rollup-plugin-includepaths@^0.2.3:
resolved "https://registry.yarnpkg.com/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.3.tgz#244d21b9669a0debe476d825e4a02ed08c06b258" resolved "https://registry.yarnpkg.com/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.3.tgz#244d21b9669a0debe476d825e4a02ed08c06b258"
integrity sha512-4QbSIZPDT+FL4SViEVCRi4cGCA64zQJu7u5qmCkO3ecHy+l9EQBsue15KfCpddfb6Br0q47V/v2+E2YUiqts9g== integrity sha512-4QbSIZPDT+FL4SViEVCRi4cGCA64zQJu7u5qmCkO3ecHy+l9EQBsue15KfCpddfb6Br0q47V/v2+E2YUiqts9g==
rollup-plugin-replace@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3"
integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==
dependencies:
magic-string "^0.25.2"
rollup-pluginutils "^2.6.0"
rollup-pluginutils@^1.5.0, rollup-pluginutils@^1.5.1: rollup-pluginutils@^1.5.0, rollup-pluginutils@^1.5.1:
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
@ -4395,6 +4415,13 @@ rollup-pluginutils@^2.3.0:
estree-walker "^0.6.0" estree-walker "^0.6.0"
micromatch "^3.1.10" micromatch "^3.1.10"
rollup-pluginutils@^2.6.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97"
integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==
dependencies:
estree-walker "^0.6.1"
rollup@^1.8.0: rollup@^1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.8.0.tgz#e3ce8b708ad4325166717f74f244f691595d35e2" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.8.0.tgz#e3ce8b708ad4325166717f74f244f691595d35e2"
@ -4613,6 +4640,11 @@ source-map@^0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.4:
version "1.4.6"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"
integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==
spdx-correct@^3.0.0: spdx-correct@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"