Un-authenticated resolve (#341)

* apiCall: add option to not send the auth header

## Why
Want an option to make un-authenticated `resolve` calls where appropriate, to improve caching.

## How
All `apiCall`s are authenticated by default, but when clients add NO_AUTH to the params, `apiCall` will exclude the X_LBRY_AUTH_TOKEN. It will also strip NO_AUTH from the param object before sending it out.

* Add hook for 'resolve' and 'claim_search' to check and skip auth...

... if the params does not contain anything that requires the wallet.

* doResolveUri, doClaimSearch: let clients decide when to include_my_output

- No more hardcoding 'include_purchase_receipt' and 'include_is_my_output'
- doResolveUri: include these params when opening a file page. This was the only place that was doing that prior to this PR.

* is_my_output: use the signing_channel as alternative

## Notes
`is_my_output` is more expensive to resolve, so it is not being requested all the time.

## Change
Looking at the signing channel as the additional fallback, on top of `myClaimIds`.

## Aside
I think using `myClaimIds` here is redundant, as it is usually populated from `is_my_ouput`. But leaving as is for now...
This commit is contained in:
infinite-persistence 2021-11-24 06:33:34 -08:00 committed by GitHub
parent c74dd49bc5
commit 4267c1ccf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 46 deletions

View file

@ -42,6 +42,7 @@ function CommentMenuList(props: Props) {
const { const {
uri, uri,
claim, claim,
claimIsMine,
authorUri, authorUri,
commentIsMine, commentIsMine,
commentId, commentId,
@ -104,7 +105,7 @@ function CommentMenuList(props: Props) {
function getBlockOptionElem() { function getBlockOptionElem() {
const isPersonalBlockTheOnlyOption = !activeChannelIsModerator && !activeChannelIsAdmin; const isPersonalBlockTheOnlyOption = !activeChannelIsModerator && !activeChannelIsAdmin;
const isTimeoutBlockAvailable = (claim && claim.is_my_output) || activeChannelIsModerator; const isTimeoutBlockAvailable = claimIsMine || activeChannelIsModerator;
const personalPermanentBlockOnly = isPersonalBlockTheOnlyOption && !isTimeoutBlockAvailable; const personalPermanentBlockOnly = isPersonalBlockTheOnlyOption && !isTimeoutBlockAvailable;
function getSubtitle() { function getSubtitle() {

View file

@ -1 +1,5 @@
export const X_LBRY_AUTH_TOKEN = 'X-Lbry-Auth-Token'; export const X_LBRY_AUTH_TOKEN = 'X-Lbry-Auth-Token';
// Additional parameter for apiCall() to skip sending the auth token.
// NO_AUTH will be stripped from the parameter object before sending out.
export const NO_AUTH = 'no_auth';

View file

@ -1,4 +1,6 @@
// @flow // @flow
import { NO_AUTH, X_LBRY_AUTH_TOKEN } from 'constants/token';
require('proxy-polyfill'); require('proxy-polyfill');
const CHECK_DAEMON_STARTED_TRY_NUMBER = 200; const CHECK_DAEMON_STARTED_TRY_NUMBER = 200;
@ -75,9 +77,9 @@ const Lbry = {
version: () => daemonCallWithResult('version', {}), version: () => daemonCallWithResult('version', {}),
// Claim fetching and manipulation // Claim fetching and manipulation
resolve: (params) => daemonCallWithResult('resolve', params), resolve: (params) => daemonCallWithResult('resolve', params, searchRequiresAuth),
get: (params) => daemonCallWithResult('get', params), get: (params) => daemonCallWithResult('get', params),
claim_search: (params) => daemonCallWithResult('claim_search', params), claim_search: (params) => daemonCallWithResult('claim_search', params, searchRequiresAuth),
claim_list: (params) => daemonCallWithResult('claim_list', params), claim_list: (params) => daemonCallWithResult('claim_list', params),
channel_create: (params) => daemonCallWithResult('channel_create', params), channel_create: (params) => daemonCallWithResult('channel_create', params),
channel_update: (params) => daemonCallWithResult('channel_update', params), channel_update: (params) => daemonCallWithResult('channel_update', params),
@ -192,10 +194,18 @@ function checkAndParse(response) {
} }
export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) { 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 counter = new Date().getTime();
const options = { const options = {
method: 'POST', method: 'POST',
headers: Lbry.apiRequestHeaders, headers: apiRequestHeaders,
body: JSON.stringify({ body: JSON.stringify({
jsonrpc: '2.0', jsonrpc: '2.0',
method, method,
@ -220,11 +230,17 @@ export function apiCall(method: string, params: ?{}, resolve: Function, reject:
.catch(reject); .catch(reject);
} }
function daemonCallWithResult(name: string, params: ?{} = {}): Promise<any> { function daemonCallWithResult(
name: string,
params: ?{} = {},
checkAuthNeededFn: ?(?{}) => boolean = undefined
): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const skipAuth = checkAuthNeededFn ? !checkAuthNeededFn(params) : false;
apiCall( apiCall(
name, name,
params, skipAuth ? { ...params, [NO_AUTH]: true } : params,
(result) => { (result) => {
resolve(result); resolve(result);
}, },
@ -248,4 +264,16 @@ const lbryProxy = new Proxy(Lbry, {
}, },
}); });
/**
* 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; export default lbryProxy;

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { selectClaimForUri, selectClaimIsMine } from 'redux/selectors/claims';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { doCommentModBlock, doCommentModBlockAsAdmin, doCommentModBlockAsModerator } from 'redux/actions/comments'; import { doCommentModBlock, doCommentModBlockAsAdmin, doCommentModBlockAsModerator } from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
@ -7,11 +7,15 @@ import { selectModerationDelegatorsById } from 'redux/selectors/comments';
import ModalBlockChannel from './view'; import ModalBlockChannel from './view';
const select = (state, props) => ({ const select = (state, props) => {
activeChannelClaim: selectActiveChannelClaim(state), const contentClaim = selectClaimForUri(state, props.contentUri);
contentClaim: makeSelectClaimForUri(props.contentUri)(state), return {
moderationDelegatorsById: selectModerationDelegatorsById(state), activeChannelClaim: selectActiveChannelClaim(state),
}); contentClaim,
contentClaimIsMine: selectClaimIsMine(state, contentClaim),
moderationDelegatorsById: selectModerationDelegatorsById(state),
};
};
const perform = { const perform = {
doHideModal, doHideModal,

View file

@ -31,6 +31,7 @@ type Props = {
// --- redux --- // --- redux ---
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
contentClaim: ?Claim, contentClaim: ?Claim,
contentClaimIsMine: ?boolean,
moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } }, moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
doHideModal: () => void, doHideModal: () => void,
doCommentModBlock: (commenterUri: string, offendingCommentId: ?string, timeoutSec: ?number) => void, doCommentModBlock: (commenterUri: string, offendingCommentId: ?string, timeoutSec: ?number) => void,
@ -55,6 +56,7 @@ export default function ModalBlockChannel(props: Props) {
offendingCommentId, offendingCommentId,
activeChannelClaim, activeChannelClaim,
contentClaim, contentClaim,
contentClaimIsMine,
moderationDelegatorsById, moderationDelegatorsById,
doHideModal, doHideModal,
doCommentModBlock, doCommentModBlock,
@ -78,7 +80,7 @@ export default function ModalBlockChannel(props: Props) {
const [timeoutSec, setTimeoutSec] = React.useState(-1); const [timeoutSec, setTimeoutSec] = React.useState(-1);
const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin;
const isTimeoutAvail = (contentClaim && contentClaim.is_my_output) || activeChannelIsModerator; const isTimeoutAvail = contentClaimIsMine || activeChannelIsModerator;
const blockButtonDisabled = blockType === BLOCK.TIMEOUT && timeoutSec < 1; const blockButtonDisabled = blockType === BLOCK.TIMEOUT && timeoutSec < 1;
// ************************************************************************** // **************************************************************************

View file

@ -88,7 +88,8 @@ const select = (state, props) => {
}; };
const perform = (dispatch) => ({ const perform = (dispatch) => ({
resolveUri: (uri) => dispatch(doResolveUri(uri)), resolveUri: (uri, returnCached, resolveRepost, options) =>
dispatch(doResolveUri(uri, returnCached, resolveRepost, options)),
beginPublish: (name) => { beginPublish: (name) => {
dispatch(doClearPublish()); dispatch(doClearPublish());
dispatch(doPrepareEdit({ name })); dispatch(doPrepareEdit({ name }));

View file

@ -23,7 +23,7 @@ const isDev = process.env.NODE_ENV !== 'production';
type Props = { type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
resolveUri: (string) => void, resolveUri: (string, boolean, boolean, any) => void,
isSubscribed: boolean, isSubscribed: boolean,
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
@ -107,7 +107,12 @@ function ShowPage(props: Props) {
(resolveUri && !isResolvingUri && uri && haventFetchedYet) || (resolveUri && !isResolvingUri && uri && haventFetchedYet) ||
(claimExists && !claimIsPending && (!canonicalUrl || isMine === undefined)) (claimExists && !claimIsPending && (!canonicalUrl || isMine === undefined))
) { ) {
resolveUri(uri); resolveUri(
uri,
false,
true,
isMine === undefined ? { include_is_my_output: true, include_purchase_receipt: true } : {}
);
} }
}, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, isMine, claimIsPending, search]); }, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, isMine, claimIsPending, search]);

View file

@ -27,7 +27,8 @@ let checkPendingInterval;
export function doResolveUris( export function doResolveUris(
uris: Array<string>, uris: Array<string>,
returnCachedClaims: boolean = false, returnCachedClaims: boolean = false,
resolveReposts: boolean = true resolveReposts: boolean = true,
additionalOptions: any = {}
) { ) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const normalizedUris = uris.map(normalizeURI); const normalizedUris = uris.map(normalizeURI);
@ -47,13 +48,6 @@ export function doResolveUris(
return; return;
} }
const options: { include_is_my_output?: boolean, include_purchase_receipt: boolean } = {
include_purchase_receipt: true,
};
if (urisToResolve.length === 1) {
options.include_is_my_output = true;
}
dispatch({ dispatch({
type: ACTIONS.RESOLVE_URIS_STARTED, type: ACTIONS.RESOLVE_URIS_STARTED,
data: { uris: normalizedUris }, data: { uris: normalizedUris },
@ -70,7 +64,7 @@ export function doResolveUris(
const collectionIds: Array<string> = []; const collectionIds: Array<string> = [];
return Lbry.resolve({ urls: urisToResolve, ...options }).then(async (result: ResolveResponse) => { return Lbry.resolve({ urls: urisToResolve, ...additionalOptions }).then(async (result: ResolveResponse) => {
let repostedResults = {}; let repostedResults = {};
const repostsToResolve = []; const repostsToResolve = [];
const fallbackResolveInfo = { const fallbackResolveInfo = {
@ -127,7 +121,7 @@ export function doResolveUris(
type: ACTIONS.RESOLVE_URIS_STARTED, type: ACTIONS.RESOLVE_URIS_STARTED,
data: { uris: repostsToResolve, debug: 'reposts' }, data: { uris: repostsToResolve, debug: 'reposts' },
}); });
repostedResults = await Lbry.resolve({ urls: repostsToResolve, ...options }); repostedResults = await Lbry.resolve({ urls: repostsToResolve, ...additionalOptions });
} }
processResult(repostedResults, resolveInfo); processResult(repostedResults, resolveInfo);
@ -145,8 +139,13 @@ export function doResolveUris(
}; };
} }
export function doResolveUri(uri: string) { export function doResolveUri(
return doResolveUris([uri]); uri: string,
returnCachedClaims: boolean = false,
resolveReposts: boolean = true,
additionalOptions: any = {}
) {
return doResolveUris([uri], returnCachedClaims, resolveReposts, additionalOptions);
} }
export function doFetchClaimListMine( export function doFetchClaimListMine(
@ -660,10 +659,7 @@ export function doClaimSearch(
return false; return false;
}; };
return await Lbry.claim_search({ return await Lbry.claim_search(options).then(success, failure);
...options,
include_purchase_receipt: true,
}).then(success, failure);
}; };
} }

View file

@ -3,7 +3,7 @@ import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect'; import { createCachedSelector } from 're-reselect';
import { isClaimNsfw, filterClaims } from 'util/claim'; import { isClaimNsfw, filterClaims, getChannelIdFromClaim } from 'util/claim';
import * as CLAIM from 'constants/claim'; import * as CLAIM from 'constants/claim';
type State = { claims: any }; type State = { claims: any };
@ -216,20 +216,26 @@ const selectNormalizedAndVerifiedUri = createCachedSelector(
export const selectClaimIsMine = (state: State, claim: ?Claim) => { export const selectClaimIsMine = (state: State, claim: ?Claim) => {
if (claim) { if (claim) {
// The original code seems to imply that 'is_my_output' could be false even
// when it is yours and there is a need to double-check with 'myActiveClaims'.
// I'm retaining that logic. Otherwise, we could have just return
// is_my_output directly when it is defined and skip the fallback.
if (claim.is_my_output) { if (claim.is_my_output) {
return true; return true;
} else {
// 'is_my_output' is false or undefined.
const myActiveClaims = selectMyActiveClaims(state);
return claim.claim_id && myActiveClaims.has(claim.claim_id);
} }
} else {
return false; const signingChannelId = getChannelIdFromClaim(claim);
const myChannelIds = selectMyChannelClaimIds(state);
if (signingChannelId && myChannelIds) {
if (myChannelIds.includes(signingChannelId)) {
return true;
}
} else {
const myActiveClaims = selectMyActiveClaims(state);
if (claim.claim_id && myActiveClaims.has(claim.claim_id)) {
return true;
}
}
} }
return false;
}; };
export const selectClaimIsMineForUri = (state: State, rawUri: string) => { export const selectClaimIsMineForUri = (state: State, rawUri: string) => {

View file

@ -4,7 +4,12 @@ import { createCachedSelector } from 're-reselect';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
import { selectClaimsById, selectMyClaimIdsRaw, selectClaimIdForUri } from 'redux/selectors/claims'; import {
selectClaimsById,
selectMyClaimIdsRaw,
selectMyChannelClaimIds,
selectClaimIdForUri,
} from 'redux/selectors/claims';
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw } from 'util/claim';
type State = { claims: any, comments: CommentsState }; type State = { claims: any, comments: CommentsState };
@ -181,6 +186,7 @@ export const selectCommentIdsForUri = (state: State, uri: string) => {
const filterCommentsDepOnList = { const filterCommentsDepOnList = {
claimsById: selectClaimsById, claimsById: selectClaimsById,
myClaimIds: selectMyClaimIdsRaw, myClaimIds: selectMyClaimIdsRaw,
myChannelClaimIds: selectMyChannelClaimIds,
mutedChannels: selectMutedChannels, mutedChannels: selectMutedChannels,
personalBlockList: selectModerationBlockList, personalBlockList: selectModerationBlockList,
blacklistedMap: selectBlacklistedOutpointMap, blacklistedMap: selectBlacklistedOutpointMap,
@ -264,6 +270,7 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
const { const {
claimsById, claimsById,
myClaimIds, myClaimIds,
myChannelClaimIds,
mutedChannels, mutedChannels,
personalBlockList, personalBlockList,
blacklistedMap, blacklistedMap,
@ -282,8 +289,12 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author // Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
if (channelClaim) { if (channelClaim) {
if (myClaimIds && myClaimIds.size > 0) { if ((myClaimIds && myClaimIds.size > 0) || (myChannelClaimIds && myChannelClaimIds.length > 0)) {
const claimIsMine = channelClaim.is_my_output || myClaimIds.includes(channelClaim.claim_id); const claimIsMine =
channelClaim.is_my_output ||
myChannelClaimIds.includes(channelClaim.claim_id) ||
myClaimIds.includes(channelClaim.claim_id);
// TODO: I believe 'myClaimIds' does not include channels, so it seems wasteful to include it here? ^
if (claimIsMine) { if (claimIsMine) {
return true; return true;
} }