2021-10-17 10:36:14 +02:00
// @flow
2022-03-18 09:32:00 +01:00
import analytics from 'analytics' ;
2021-11-24 15:33:34 +01:00
import { NO _AUTH , X _LBRY _AUTH _TOKEN } from 'constants/token' ;
2021-10-17 10:36:14 +02:00
require ( 'proxy-polyfill' ) ;
const CHECK _DAEMON _STARTED _TRY _NUMBER = 200 ;
//
// Basic LBRY sdk connection config
// Offers a proxy to call LBRY sdk methods
//
const Lbry = {
isConnected : false ,
connectPromise : null ,
daemonConnectionString : 'http://localhost:5279' ,
alternateConnectionString : '' ,
methodsUsingAlternateConnectionString : [ ] ,
apiRequestHeaders : { 'Content-Type' : 'application/json-rpc' } ,
// Allow overriding daemon connection string (e.g. to `/api/proxy` for lbryweb)
setDaemonConnectionString : ( value : string ) => {
Lbry . daemonConnectionString = value ;
} ,
setApiHeader : ( key : string , value : string ) => {
Lbry . apiRequestHeaders = Object . assign ( Lbry . apiRequestHeaders , { [ key ] : value } ) ;
} ,
unsetApiHeader : ( key ) => {
Object . keys ( Lbry . apiRequestHeaders ) . includes ( key ) && delete Lbry . apiRequestHeaders [ key ] ;
} ,
// Allow overriding Lbry methods
overrides : { } ,
setOverride : ( methodName , newMethod ) => {
Lbry . overrides [ methodName ] = newMethod ;
} ,
getApiRequestHeaders : ( ) => Lbry . apiRequestHeaders ,
// Returns a human readable media type based on the content type or extension of a file that is returned by the sdk
getMediaType : ( contentType : ? string , fileName : ? string ) => {
if ( fileName ) {
const formats = [
[ /\.(mp4|m4v|webm|flv|f4v|ogv)$/i , 'video' ] ,
[ /\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i , 'audio' ] ,
[ /\.(jpeg|jpg|png|gif|svg|webp)$/i , 'image' ] ,
[ /\.(h|go|ja|java|js|jsx|c|cpp|cs|css|rb|scss|sh|php|py)$/i , 'script' ] ,
[ /\.(html|json|csv|txt|log|md|markdown|docx|pdf|xml|yml|yaml)$/i , 'document' ] ,
[ /\.(pdf|odf|doc|docx|epub|org|rtf)$/i , 'e-book' ] ,
[ /\.(stl|obj|fbx|gcode)$/i , '3D-file' ] ,
[ /\.(cbr|cbt|cbz)$/i , 'comic-book' ] ,
[ /\.(lbry)$/i , 'application' ] ,
] ;
const res = formats . reduce ( ( ret , testpair ) => {
switch ( testpair [ 0 ] . test ( ret ) ) {
case true :
return testpair [ 1 ] ;
default :
return ret ;
}
} , fileName ) ;
return res === fileName ? 'unknown' : res ;
} else if ( contentType ) {
// $FlowFixMe
return /^[^/]+/ . exec ( contentType ) [ 0 ] ;
}
return 'unknown' ;
} ,
//
// Lbry SDK Methods
// https://lbry.tech/api/sdk
//
status : ( params = { } ) => daemonCallWithResult ( 'status' , params ) ,
stop : ( ) => daemonCallWithResult ( 'stop' , { } ) ,
version : ( ) => daemonCallWithResult ( 'version' , { } ) ,
// Claim fetching and manipulation
2021-11-24 15:33:34 +01:00
resolve : ( params ) => daemonCallWithResult ( 'resolve' , params , searchRequiresAuth ) ,
2021-10-17 10:36:14 +02:00
get : ( params ) => daemonCallWithResult ( 'get' , params ) ,
2021-11-24 15:33:34 +01:00
claim _search : ( params ) => daemonCallWithResult ( 'claim_search' , params , searchRequiresAuth ) ,
2021-10-17 10:36:14 +02:00
claim _list : ( params ) => daemonCallWithResult ( 'claim_list' , params ) ,
channel _create : ( params ) => daemonCallWithResult ( 'channel_create' , params ) ,
channel _update : ( params ) => daemonCallWithResult ( 'channel_update' , params ) ,
channel _import : ( params ) => daemonCallWithResult ( 'channel_import' , params ) ,
channel _list : ( params ) => daemonCallWithResult ( 'channel_list' , params ) ,
stream _abandon : ( params ) => daemonCallWithResult ( 'stream_abandon' , params ) ,
stream _list : ( params ) => daemonCallWithResult ( 'stream_list' , params ) ,
channel _abandon : ( params ) => daemonCallWithResult ( 'channel_abandon' , params ) ,
channel _sign : ( params ) => daemonCallWithResult ( 'channel_sign' , params ) ,
support _create : ( params ) => daemonCallWithResult ( 'support_create' , params ) ,
support _list : ( params ) => daemonCallWithResult ( 'support_list' , params ) ,
stream _repost : ( params ) => daemonCallWithResult ( 'stream_repost' , params ) ,
collection _resolve : ( params ) => daemonCallWithResult ( 'collection_resolve' , params ) ,
collection _list : ( params ) => daemonCallWithResult ( 'collection_list' , params ) ,
collection _create : ( params ) => daemonCallWithResult ( 'collection_create' , params ) ,
collection _update : ( params ) => daemonCallWithResult ( 'collection_update' , params ) ,
// File fetching and manipulation
file _list : ( params = { } ) => daemonCallWithResult ( 'file_list' , params ) ,
file _delete : ( params = { } ) => daemonCallWithResult ( 'file_delete' , params ) ,
file _set _status : ( params = { } ) => daemonCallWithResult ( 'file_set_status' , params ) ,
blob _delete : ( params = { } ) => daemonCallWithResult ( 'blob_delete' , params ) ,
blob _list : ( params = { } ) => daemonCallWithResult ( 'blob_list' , params ) ,
file _reflect : ( params = { } ) => daemonCallWithResult ( 'file_reflect' , params ) ,
// Wallet utilities
wallet _balance : ( params = { } ) => daemonCallWithResult ( 'wallet_balance' , params ) ,
wallet _decrypt : ( ) => daemonCallWithResult ( 'wallet_decrypt' , { } ) ,
wallet _encrypt : ( params = { } ) => daemonCallWithResult ( 'wallet_encrypt' , params ) ,
wallet _unlock : ( params = { } ) => daemonCallWithResult ( 'wallet_unlock' , params ) ,
wallet _list : ( params = { } ) => daemonCallWithResult ( 'wallet_list' , params ) ,
wallet _send : ( params = { } ) => daemonCallWithResult ( 'wallet_send' , params ) ,
wallet _status : ( params = { } ) => daemonCallWithResult ( 'wallet_status' , params ) ,
address _is _mine : ( params = { } ) => daemonCallWithResult ( 'address_is_mine' , params ) ,
address _unused : ( params = { } ) => daemonCallWithResult ( 'address_unused' , params ) ,
address _list : ( params = { } ) => daemonCallWithResult ( 'address_list' , params ) ,
transaction _list : ( params = { } ) => daemonCallWithResult ( 'transaction_list' , params ) ,
utxo _release : ( params = { } ) => daemonCallWithResult ( 'utxo_release' , params ) ,
support _abandon : ( params = { } ) => daemonCallWithResult ( 'support_abandon' , params ) ,
purchase _list : ( params = { } ) => daemonCallWithResult ( 'purchase_list' , params ) ,
txo _list : ( params = { } ) => daemonCallWithResult ( 'txo_list' , params ) ,
account _list : ( params = { } ) => daemonCallWithResult ( 'account_list' , params ) ,
account _set : ( params = { } ) => daemonCallWithResult ( 'account_set' , params ) ,
sync _hash : ( params = { } ) => daemonCallWithResult ( 'sync_hash' , params ) ,
sync _apply : ( params = { } ) => daemonCallWithResult ( 'sync_apply' , params ) ,
// Preferences
preference _get : ( params = { } ) => daemonCallWithResult ( 'preference_get' , params ) ,
preference _set : ( params = { } ) => daemonCallWithResult ( 'preference_set' , params ) ,
// Comments
comment _list : ( params = { } ) => daemonCallWithResult ( 'comment_list' , params ) ,
comment _create : ( params = { } ) => daemonCallWithResult ( 'comment_create' , params ) ,
comment _hide : ( params = { } ) => daemonCallWithResult ( 'comment_hide' , params ) ,
comment _abandon : ( params = { } ) => daemonCallWithResult ( 'comment_abandon' , params ) ,
comment _update : ( params = { } ) => daemonCallWithResult ( 'comment_update' , params ) ,
// Connect to the sdk
connect : ( ) => {
if ( Lbry . connectPromise === null ) {
// $FlowFixMe
Lbry . connectPromise = new Promise ( ( resolve , reject ) => {
let tryNum = 0 ;
// Check every half second to see if the daemon is accepting connections
function checkDaemonStarted ( ) {
tryNum += 1 ;
Lbry . status ( )
. then ( resolve )
. catch ( ( ) => {
if ( tryNum <= CHECK _DAEMON _STARTED _TRY _NUMBER ) {
setTimeout ( checkDaemonStarted , tryNum < 50 ? 400 : 1000 ) ;
} else {
reject ( new Error ( 'Unable to connect to LBRY' ) ) ;
}
} ) ;
}
checkDaemonStarted ( ) ;
} ) ;
}
// Flow thinks this could be empty, but it will always reuturn a promise
// $FlowFixMe
return Lbry . connectPromise ;
} ,
publish : ( params = { } ) =>
new Promise ( ( resolve , reject ) => {
if ( Lbry . overrides . publish ) {
Lbry . overrides . publish ( params ) . then ( resolve , reject ) ;
} else {
apiCall ( 'publish' , params , resolve , reject ) ;
}
} ) ,
} ;
2022-03-18 09:42:52 +01:00
function checkAndParse ( response : Response , method : string ) {
if ( ! response . ok ) {
// prettier-ignore
switch ( response . status ) {
case 504 : // Gateway timeout
case 524 : // Cloudflare: a timeout occurred
switch ( method ) {
case 'publish' :
throw Error ( _ _ ( '[Publish]: Your action timed out, but may have been completed. Refresh and check your Uploads or Wallet page to confirm after a few minutes.' ) ) ;
default :
throw Error ( ` ${ method } : ${ response . statusText } ( ${ response . status } ) ` ) ;
}
default :
throw Error ( ` ${ method } : ${ response . statusText } ( ${ response . status } ) ` ) ;
}
}
2021-10-17 10:36:14 +02:00
if ( response . status >= 200 && response . status < 300 ) {
return response . json ( ) ;
}
2022-03-18 09:42:10 +01:00
2022-03-18 09:42:52 +01:00
return response
. json ( )
. then ( ( json ) => {
if ( json . error ) {
const errorMessage = typeof json . error === 'object' ? json . error . message : json . error ;
return Promise . reject ( new Error ( errorMessage ) ) ;
} else {
return Promise . reject ( new Error ( 'Protocol error with unknown response signature' ) ) ;
}
} )
. catch ( ( ) => {
// If not parsable, throw the initial response rather than letting
// the json failure ("unexpected token at..") pass through.
return Promise . reject ( new Error ( ` ${ method } : ${ response . statusText } ( ${ response . status } , JSON) ` ) ) ;
} ) ;
2021-10-17 10:36:14 +02:00
}
export function apiCall ( method : string , params : ? { } , resolve : Function , reject : Function ) {
2021-11-24 15:33:34 +01:00
let apiRequestHeaders = Lbry . apiRequestHeaders ;
if ( params && params [ NO _AUTH ] ) {
apiRequestHeaders = Object . assign ( { } , Lbry . apiRequestHeaders ) ;
delete apiRequestHeaders [ X _LBRY _AUTH _TOKEN ] ;
delete params [ NO _AUTH ] ;
}
2021-10-17 10:36:14 +02:00
const counter = new Date ( ) . getTime ( ) ;
const options = {
method : 'POST' ,
2021-11-24 15:33:34 +01:00
headers : apiRequestHeaders ,
2021-10-17 10:36:14 +02:00
body : JSON . stringify ( {
jsonrpc : '2.0' ,
method ,
params ,
id : counter ,
} ) ,
} ;
const connectionString = Lbry . methodsUsingAlternateConnectionString . includes ( method )
? Lbry . alternateConnectionString
: Lbry . daemonConnectionString ;
2022-03-18 09:42:10 +01:00
2021-10-17 10:36:14 +02:00
return fetch ( connectionString + '?m=' + method , options )
2022-03-18 09:42:52 +01:00
. then ( ( response ) => checkAndParse ( response , method ) )
2021-10-17 10:36:14 +02:00
. then ( ( response ) => {
const error = response . error || ( response . result && response . result . error ) ;
2022-03-18 09:42:10 +01:00
return error ? reject ( error ) : resolve ( response . result ) ;
2021-10-17 10:36:14 +02:00
} )
2022-03-18 09:32:00 +01:00
. catch ( ( err ) => {
if ( err . message === 'Failed to fetch' ) {
analytics . error ( ` \` ${ method } \` : Failed to fetch ` ) ;
}
return reject ( err ) ;
} ) ;
2021-10-17 10:36:14 +02:00
}
2021-11-24 15:33:34 +01:00
function daemonCallWithResult (
name : string ,
params : ? { } = { } ,
checkAuthNeededFn : ? ( ? { } ) => boolean = undefined
) : Promise < any > {
2021-10-17 10:36:14 +02:00
return new Promise ( ( resolve , reject ) => {
2021-11-24 15:33:34 +01:00
const skipAuth = checkAuthNeededFn ? ! checkAuthNeededFn ( params ) : false ;
2021-10-17 10:36:14 +02:00
apiCall (
name ,
2021-11-24 15:33:34 +01:00
skipAuth ? { ... params , [ NO _AUTH ] : true } : params ,
2021-10-17 10:36:14 +02:00
( result ) => {
resolve ( result ) ;
} ,
reject
) ;
} ) ;
}
// This is only for a fallback
// If there is a Lbry method that is being called by an app, it should be added to /flow-typed/Lbry.js
const lbryProxy = new Proxy ( Lbry , {
get ( target : LbryTypes , name : string ) {
if ( name in target ) {
return target [ name ] ;
}
return ( params = { } ) =>
new Promise ( ( resolve , reject ) => {
apiCall ( name , params , resolve , reject ) ;
} ) ;
} ,
} ) ;
2021-11-24 15:33:34 +01:00
/ * *
* daemonCallWithResult hook that checks if the search option requires the
* auth - token . This hook works for 'resolve' and 'claim_search' .
*
* @ param options
* @ returns { boolean }
* /
function searchRequiresAuth ( options : any ) {
const KEYS _REQUIRE _AUTH = [ 'include_purchase_receipt' , 'include_is_my_output' ] ;
return options && KEYS _REQUIRE _AUTH . some ( ( k ) => options . hasOwnProperty ( k ) ) ;
}
2021-10-17 10:36:14 +02:00
export default lbryProxy ;