Add special support for building channel claims in lbryuri module

Extends lbryuri.build() and lbryuri.parse() to support special
keys, contentName and channelName. These put the right values in the
"name" and "path" position for both anonymous claims and channel
content claims, which lets us write code that can deal with either type
without special logic.
This commit is contained in:
Alex Liebowitz 2017-04-19 13:56:17 -04:00
parent 7a4e9ad656
commit ca6d55da21
5 changed files with 65 additions and 34 deletions

View file

@ -19,6 +19,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* New welcome flow for new users * New welcome flow for new users
* Redesigned UI for Discover * Redesigned UI for Discover
* Handle more of price calculations at the daemon layer to improve page load time * Handle more of price calculations at the daemon layer to improve page load time
* Add special support for building channel claims in lbryuri module
### Changed ### Changed
* Update process now easier and more reliable * Update process now easier and more reliable

View file

@ -19,6 +19,7 @@ const UriIndicator = React.createClass({
const channelUriObj = Object.assign({}, uriObj); const channelUriObj = Object.assign({}, uriObj);
delete channelUriObj.path; delete channelUriObj.path;
delete channelUriObj.contentName;
const channelUri = lbryuri.build(channelUriObj, false); const channelUri = lbryuri.build(channelUriObj, false);
let icon, modifier; let icon, modifier;

View file

@ -7,14 +7,23 @@ const lbryuri = {};
* 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) * - name (string): The value in the "name" position in the URI. Note that this
* - properName (string; strips off @ for channels) * could be either content name or channel name; see above.
* - isChannel (boolean) * - path (string, if persent)
* - claimSequence (int, if present) * - claimSequence (int, if present)
* - bidPosition (int, if present) * - bidPosition (int, if present)
* - claimId (string, if present) * - claimId (string, if present)
* - path (string, if persent) * - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path
* - channelName (string, if present): Channel name without @
*/ */
lbryuri.parse = function(uri, requireProto=false) { lbryuri.parse = function(uri, requireProto=false) {
// Break into components. Empty sub-matches are converted to null // Break into components. Empty sub-matches are converted to null
@ -26,6 +35,8 @@ lbryuri.parse = function(uri, requireProto=false) {
); );
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null);
let contentName;
// Validate protocol // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
throw new Error('LBRY URIs must include a protocol prefix (lbry://).'); throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
@ -36,20 +47,22 @@ lbryuri.parse = function(uri, requireProto=false) {
throw new Error('URI does not include name.'); throw new Error('URI does not include name.');
} }
const isChannel = name[0] == '@'; const isChannel = name.startsWith('@');
const properName = isChannel ? name.substr(1) : name; const channelName = isChannel ? name.slice(1) : name;
if (isChannel) { if (isChannel) {
if (!properName) { if (!channelName) {
throw new Error('No channel name after @.'); throw new Error('No channel name after @.');
} }
if (properName.length < CHANNEL_NAME_MIN_LEN) { if (channelName.length < CHANNEL_NAME_MIN_LEN) {
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`); throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
} }
contentName = path;
} }
const nameBadChars = properName.match(/[^A-Za-z0-9-]/g); const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
if (nameBadChars) { if (nameBadChars) {
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`); throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
} }
@ -82,7 +95,7 @@ lbryuri.parse = function(uri, requireProto=false) {
throw new Error('Bid position must be a number.'); throw new Error('Bid position must be a number.');
} }
// Validate path // Validate and process path
if (path) { if (path) {
if (!isChannel) { if (!isChannel) {
throw new Error('Only channel URIs may have a path.'); throw new Error('Only channel URIs may have a path.');
@ -92,12 +105,16 @@ lbryuri.parse = function(uri, requireProto=false) {
if (pathBadChars) { if (pathBadChars) {
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`); throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
} }
contentName = path;
} else if (pathSep) { } else if (pathSep) {
throw new Error('No path provided after /'); throw new Error('No path provided after /');
} }
return { return {
name, properName, isChannel, name, path, isChannel,
... contentName ? {contentName} : {},
... channelName ? {channelName} : {},
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {}, ... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {}, ... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
... claimId ? {claimId} : {}, ... claimId ? {claimId} : {},
@ -105,20 +122,45 @@ lbryuri.parse = function(uri, requireProto=false) {
}; };
} }
lbryuri.build = function(uriObj, includeProto=true) { /**
const {name, claimId, claimSequence, bidPosition, path} = uriObj; * Takes an object in the same format returned by lbryuri.parse() and builds a URI.
*
* The channelName key will accept names with or without the @ prefix.
*/
lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) {
let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj;
if (channelName) {
const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName;
if (!name) {
name = channelNameFormatted;
} else if (name !== channelNameFormatted) {
throw new Error('Received a channel content URI, but 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) {
if (!path) {
path = contentName;
} else if (path !== contentName) {
throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.');
}
}
return (includeProto ? 'lbry://' : '') + name + return (includeProto ? 'lbry://' : '') + name +
(claimId ? `#${claimId}` : '') + (claimId ? `#${claimId}` : '') +
(claimSequence ? `:${claimSequence}` : '') + (claimSequence ? `:${claimSequence}` : '') +
(bidPosition ? `\$${bidPosition}` : '') + (bidPosition ? `\$${bidPosition}` : '') +
(path ? `/${path}` : ''); (path ? `/${path}` : '');
} }
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of making sure it has a lbry:// prefix) */ * consists of adding the lbry:// prefix if needed) */
lbryuri.normalize= function(uri) { lbryuri.normalize= function(uri) {
return lbryuri.build(lbryuri.parse(uri)); const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri);
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
} }
window.lbryuri = lbryuri;
export default lbryuri; export default lbryuri;

View file

@ -47,19 +47,11 @@ var SearchResults = React.createClass({
var rows = [], var rows = [],
seenNames = {}; //fix this when the search API returns claim IDs seenNames = {}; //fix this when the search API returns claim IDs
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
let uri; const uri = lbryuri.build({
if (channel_name) { channelName: channel_name,
uri = lbryuri.build({ contentName: name,
name: channel_name, claimId: channel_id || claim_id,
path: name, });
claimId: channel_id,
});
} else {
uri = lbryuri.build({
name: name,
claimId: claim_id,
})
}
rows.push( rows.push(
<FileTileStream key={name} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} /> <FileTileStream key={name} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />

View file

@ -196,12 +196,7 @@ export let FileList = React.createClass({
} }
let uri; const uri = lbryuri.build({contentName: name, channelName: channel_name});
if (!channel_name) {
uri = lbryuri.build({name});
} else {
uri = lbryuri.build({name: channel_name, path: name});
}
seenUris[name] = true; seenUris[name] = true;
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true} content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true}
hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type} hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}