2018-04-05 04:57:29 +02:00
const channelNameMinLength = 1 ;
const claimIdMaxLength = 40 ;
2019-03-29 01:40:40 +01:00
// see https://spec.lbry.com/#urls
2019-07-30 18:15:29 +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 ;
2019-03-17 23:30:17 +01:00
export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/ ;
2018-07-11 06:38:32 +02:00
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 .
*
* 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 :
* - name ( string ) : The value in the "name" position in the URI . Note that this
* 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 )
* - contentName ( string ) : For anon claims , the name ; for channel claims , the path
* - channelName ( string , if present ) : Channel name without @
* /
export 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 ;
// Validate protocol
if ( requireProto && ! proto ) {
throw new Error ( _ _ ( 'LBRY URIs must include a protocol prefix (lbry://).' ) ) ;
}
// Validate and process name
if ( ! claimName ) {
throw new Error ( _ _ ( 'URI does not include name.' ) ) ;
}
const isChannel = claimName . startsWith ( '@' ) ;
const channelName = isChannel ? claimName . slice ( 1 ) : claimName ;
if ( isChannel ) {
if ( ! channelName ) {
throw new Error ( _ _ ( 'No channel name after @.' ) ) ;
}
if ( channelName . length < channelNameMinLength ) {
throw new Error ( _ _ ( ` Channel names must be at least %s characters. ` , channelNameMinLength ) ) ;
}
contentName = path ;
}
const nameBadChars = ( channelName || claimName ) . match ( regexInvalidURI ) ;
if ( nameBadChars ) {
throw new Error (
_ _ (
` Invalid character %s in name: %s. ` ,
nameBadChars . length === 1 ? '' : 's' ,
nameBadChars . join ( ', ' )
)
) ;
}
// Validate and process modifier (claim ID, bid position or claim sequence)
let claimId ;
let claimSequence ;
let bidPosition ;
if ( modSep ) {
if ( ! modVal ) {
throw new Error ( _ _ ( ` No modifier provided after separator %s. ` , modSep ) ) ;
}
if ( modSep === '#' ) {
claimId = modVal ;
} else if ( modSep === ':' ) {
claimSequence = modVal ;
} else if ( modSep === '$' ) {
bidPosition = modVal ;
}
}
2018-10-25 19:48:23 +02:00
if ( claimId && ( claimId . length > claimIdMaxLength || ! claimId . match ( /^[0-9a-f]+$/ ) ) ) {
2018-04-05 04:57:29 +02:00
throw new Error ( _ _ ( ` Invalid claim ID %s. ` , 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.' ) ) ;
}
// Validate and process path
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 } : { } ) ,
} ;
}
/ * *
* 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 .
* /
2018-05-21 19:20:22 +02:00
export function buildURI ( URIObj , includeProto = true , protoDefault = 'lbry://' ) {
2018-04-05 04:57:29 +02:00
const { claimId , claimSequence , bidPosition , contentName , channelName } = URIObj ;
let { claimName , path } = URIObj ;
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 ) {
if ( ! claimName ) {
claimName = contentName ;
} else if ( ! path ) {
path = contentName ;
}
if ( path && path !== contentName ) {
throw new Error (
_ _ (
'Path and contentName do not match. Only one is required; most likely you wanted contentName.'
)
) ;
}
}
return (
2018-05-21 19:20:22 +02:00
( includeProto ? protoDefault : '' ) +
2018-04-05 04:57:29 +02:00
claimName +
( claimId ? ` # ${ claimId } ` : '' ) +
( claimSequence ? ` : ${ claimSequence } ` : '' ) +
( bidPosition ? ` ${ bidPosition } ` : '' ) +
( path ? ` / ${ path } ` : '' )
) ;
}
/* Takes a parseable LBRY URI and converts it to standard, canonical format */
export function normalizeURI ( URI ) {
const { claimName , path , bidPosition , claimSequence , claimId } = parseURI ( URI ) ;
return buildURI ( { claimName , path , claimSequence , bidPosition , claimId } ) ;
}
export function isURIValid ( URI ) {
let parts ;
try {
parts = parseURI ( normalizeURI ( URI ) ) ;
} catch ( error ) {
return false ;
}
return parts && parts . claimName ;
}
2019-06-03 04:55:52 +02:00
export function isNameValid ( claimName ) {
return ! regexInvalidURI . test ( claimName ) ;
2018-04-05 04:57:29 +02:00
}
export function isURIClaimable ( URI ) {
let parts ;
try {
parts = parseURI ( normalizeURI ( URI ) ) ;
} catch ( error ) {
return false ;
}
return (
parts &&
parts . claimName &&
! parts . claimId &&
! parts . bidPosition &&
! parts . claimSequence &&
! parts . isChannel &&
! parts . path
) ;
}
2018-05-21 19:20:22 +02:00
export function convertToShareLink ( URI ) {
2018-05-31 07:58:25 +02:00
const { claimName , path , bidPosition , claimSequence , claimId } = parseURI ( URI ) ;
2018-10-25 19:48:23 +02:00
return buildURI (
{ claimName , path , claimSequence , bidPosition , claimId } ,
true ,
2019-05-10 19:29:51 +02:00
'https://open.lbry.com/'
2018-10-25 19:48:23 +02:00
) ;
2018-05-21 19:20:22 +02:00
}