c6856da247
Replays have issues since they are on v1 and timeout. This process needs to be improved on the backend. don't log all
379 lines
14 KiB
JavaScript
379 lines
14 KiB
JavaScript
// @flow
|
|
// import analytics from 'analytics';
|
|
import { FETCH_TIMEOUT } from 'constants/errors';
|
|
import { NO_AUTH, X_LBRY_AUTH_TOKEN } from 'constants/token';
|
|
import fetchWithTimeout from 'util/fetch';
|
|
|
|
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
|
|
resolve: (params) => daemonCallWithResult('resolve', params, searchRequiresAuth),
|
|
get: (params) => daemonCallWithResult('get', params),
|
|
claim_search: (params) => daemonCallWithResult('claim_search', params, searchRequiresAuth),
|
|
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);
|
|
}
|
|
}),
|
|
};
|
|
|
|
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);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Returns a customized error message for known scenarios.
|
|
*/
|
|
function resolveFetchErrorMsg(method: string, response: Response | string) {
|
|
if (typeof response === 'object') {
|
|
// prettier-ignore
|
|
switch (response.status) {
|
|
case 504: // Gateway timeout
|
|
case 524: // Cloudflare: a timeout occurred
|
|
switch (method) {
|
|
case 'publish':
|
|
return __('[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:
|
|
return `${method}: ${response.statusText} (${response.status})`;
|
|
}
|
|
default:
|
|
return `${method}: ${response.statusText} (${response.status})`;
|
|
}
|
|
} else if (response === FETCH_TIMEOUT) {
|
|
return `${method}: Your action timed out, but may have been completed.`;
|
|
} else {
|
|
return `${method}: fetch failed.`;
|
|
}
|
|
}
|
|
|
|
function checkAndParse(response: Response, method: string) {
|
|
if (!response.ok) {
|
|
const errMsg = resolveFetchErrorMsg(method, response);
|
|
throw Error(errMsg);
|
|
}
|
|
|
|
if (response.status >= 200 && response.status < 300) {
|
|
return response.json();
|
|
}
|
|
|
|
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)`));
|
|
});
|
|
}
|
|
|
|
export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) {
|
|
let apiRequestHeaders = Lbry.apiRequestHeaders;
|
|
|
|
if (params && params[NO_AUTH]) {
|
|
apiRequestHeaders = Object.assign({}, Lbry.apiRequestHeaders);
|
|
delete apiRequestHeaders[X_LBRY_AUTH_TOKEN];
|
|
delete params[NO_AUTH];
|
|
}
|
|
|
|
const counter = new Date().getTime();
|
|
const options = {
|
|
method: 'POST',
|
|
headers: apiRequestHeaders,
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
method,
|
|
params,
|
|
id: counter,
|
|
}),
|
|
};
|
|
|
|
if (ApiFailureMgr.isFailingAndShouldDrop(method, params)) {
|
|
return Promise.reject('Dropped due to successive failures.');
|
|
}
|
|
|
|
const connectionString = Lbry.methodsUsingAlternateConnectionString.includes(method)
|
|
? Lbry.alternateConnectionString
|
|
: Lbry.daemonConnectionString;
|
|
|
|
const SDK_FETCH_TIMEOUT_MS = 1800000;
|
|
return fetchWithTimeout(SDK_FETCH_TIMEOUT_MS, fetch(connectionString + '?m=' + method, options))
|
|
.then((response) => checkAndParse(response, method))
|
|
.then((response) => {
|
|
const error = response.error || (response.result && response.result.error);
|
|
if (error) {
|
|
ApiFailureMgr.logFailure(method, params, counter);
|
|
return reject(error);
|
|
} else {
|
|
ApiFailureMgr.logSuccess(method);
|
|
return resolve(response.result);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
ApiFailureMgr.logFailure(method, params, counter);
|
|
if (err?.message === FETCH_TIMEOUT) {
|
|
// analytics.error(`${method}: timed out after ${SDK_FETCH_TIMEOUT_MS / 1000}s`);
|
|
reject(resolveFetchErrorMsg(method, FETCH_TIMEOUT));
|
|
} else {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function daemonCallWithResult(
|
|
name: string,
|
|
params: ?{} = {},
|
|
checkAuthNeededFn: ?(?{}) => boolean = undefined
|
|
): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
const skipAuth = checkAuthNeededFn ? !checkAuthNeededFn(params) : false;
|
|
|
|
apiCall(
|
|
name,
|
|
skipAuth ? { ...params, [NO_AUTH]: true } : params,
|
|
(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);
|
|
});
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
|
|
export default lbryProxy;
|