2021-10-17 10:36:14 +02:00
// @flow
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 ;
2022-04-29 13:27:04 +02:00
2021-10-17 10:36:14 +02:00
// 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-04-29 13:27:04 +02:00
const ApiFailureMgr = {
MAX _FAILED _ATTEMPTS : 5 ,
MAX _FAILED _GAP _MS : 500 ,
BLOCKED _DURATION _MS : 60000 ,
METHODS _TO _LOG : [ 'claim_search' ] , // Can check all, but narrow do claim_search only for now.
failureTimestamps : { } , // { [key: string]: Array<timestamps: number> }
logFailure : function ( method : string , params : ? { } , timestamp : number ) {
if ( this . isListedMethod ( method ) ) {
const key = this . getKey ( method , params ) ;
const ts = this . failureTimestamps [ key ] || [ ] ;
ts . push ( timestamp ) ;
this . failureTimestamps [ key ] = ts ;
}
} ,
logSuccess : function ( method : string , params : ? { } ) {
if ( this . isListedMethod ( method ) ) {
const key = this . getKey ( method , params ) ;
delete this . failureTimestamps [ key ] ;
}
} ,
isFailingAndShouldDrop : function ( method : string , params : ? { } ) {
if ( this . isListedMethod ( method ) ) {
const key = this . getKey ( method , params ) ;
const fts = this . failureTimestamps [ key ] ;
if ( fts && fts . length > this . MAX _FAILED _ATTEMPTS ) {
const ts2 = fts [ fts . length - 1 ] ;
const ts1 = fts [ fts . length - this . MAX _FAILED _ATTEMPTS ] ;
const successivelyFailed = ts2 - ts1 < this . MAX _FAILED _GAP _MS ;
return successivelyFailed && Date . now ( ) - ts2 < this . BLOCKED _DURATION _MS ;
}
}
return false ;
} ,
getKey : function ( method : string , params : ? { } ) {
return method + '/' + JSON . stringify ( params || { } ) ;
} ,
isListedMethod : function ( method : string ) {
return this . METHODS _TO _LOG . includes ( method ) ;
} ,
} ;
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 ,
} ) ,
} ;
2022-04-29 13:27:04 +02:00
if ( ApiFailureMgr . isFailingAndShouldDrop ( method , params ) ) {
return Promise . reject ( 'Dropped due to successive failures.' ) ;
}
2021-10-17 10:36:14 +02:00
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-04-29 13:27:04 +02:00
if ( error ) {
ApiFailureMgr . logFailure ( method , params , counter ) ;
return reject ( error ) ;
} else {
ApiFailureMgr . logSuccess ( method ) ;
return resolve ( response . result ) ;
}
2021-10-17 10:36:14 +02:00
} )
2022-04-29 13:27:04 +02:00
. catch ( ( err ) => {
ApiFailureMgr . logFailure ( method , params , counter ) ;
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 ;