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

View file

@ -1 +1,5 @@
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
import { NO_AUTH, X_LBRY_AUTH_TOKEN } from 'constants/token';
require('proxy-polyfill');
const CHECK_DAEMON_STARTED_TRY_NUMBER = 200;
@ -75,9 +77,9 @@ const Lbry = {
version: () => daemonCallWithResult('version', {}),
// Claim fetching and manipulation
resolve: (params) => daemonCallWithResult('resolve', params),
resolve: (params) => daemonCallWithResult('resolve', params, searchRequiresAuth),
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),
channel_create: (params) => daemonCallWithResult('channel_create', 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) {
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: Lbry.apiRequestHeaders,
headers: apiRequestHeaders,
body: JSON.stringify({
jsonrpc: '2.0',
method,
@ -220,11 +230,17 @@ export function apiCall(method: string, params: ?{}, resolve: Function, 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) => {
const skipAuth = checkAuthNeededFn ? !checkAuthNeededFn(params) : false;
apiCall(
name,
params,
skipAuth ? { ...params, [NO_AUTH]: true } : params,
(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;

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ const isDev = process.env.NODE_ENV !== 'production';
type Props = {
isResolvingUri: boolean,
resolveUri: (string) => void,
resolveUri: (string, boolean, boolean, any) => void,
isSubscribed: boolean,
uri: string,
claim: StreamClaim,
@ -107,7 +107,12 @@ function ShowPage(props: Props) {
(resolveUri && !isResolvingUri && uri && haventFetchedYet) ||
(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]);

View file

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

View file

@ -3,7 +3,7 @@ import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from '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';
type State = { claims: any };
@ -216,20 +216,26 @@ const selectNormalizedAndVerifiedUri = createCachedSelector(
export const selectClaimIsMine = (state: State, claim: ?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) {
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) => {

View file

@ -4,7 +4,12 @@ import { createCachedSelector } from 're-reselect';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectShowMatureContent } from 'redux/selectors/settings';
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';
type State = { claims: any, comments: CommentsState };
@ -181,6 +186,7 @@ export const selectCommentIdsForUri = (state: State, uri: string) => {
const filterCommentsDepOnList = {
claimsById: selectClaimsById,
myClaimIds: selectMyClaimIdsRaw,
myChannelClaimIds: selectMyChannelClaimIds,
mutedChannels: selectMutedChannels,
personalBlockList: selectModerationBlockList,
blacklistedMap: selectBlacklistedOutpointMap,
@ -264,6 +270,7 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
const {
claimsById,
myClaimIds,
myChannelClaimIds,
mutedChannels,
personalBlockList,
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
if (channelClaim) {
if (myClaimIds && myClaimIds.size > 0) {
const claimIsMine = channelClaim.is_my_output || myClaimIds.includes(channelClaim.claim_id);
if ((myClaimIds && myClaimIds.size > 0) || (myChannelClaimIds && myChannelClaimIds.length > 0)) {
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) {
return true;
}