From 4267c1ccf7bd2fae6e04d1d8800799be192b6058 Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Wed, 24 Nov 2021 06:33:34 -0800 Subject: [PATCH] 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... --- ui/component/commentMenuList/view.jsx | 3 ++- ui/constants/token.js | 4 +++ ui/lbry.js | 38 +++++++++++++++++++++++---- ui/modal/modalBlockChannel/index.js | 16 ++++++----- ui/modal/modalBlockChannel/view.jsx | 4 ++- ui/page/show/index.js | 3 ++- ui/page/show/view.jsx | 9 +++++-- ui/redux/actions/claims.js | 28 +++++++++----------- ui/redux/selectors/claims.js | 28 ++++++++++++-------- ui/redux/selectors/comments.js | 17 +++++++++--- 10 files changed, 104 insertions(+), 46 deletions(-) diff --git a/ui/component/commentMenuList/view.jsx b/ui/component/commentMenuList/view.jsx index e2a2265ae..e87867fc7 100644 --- a/ui/component/commentMenuList/view.jsx +++ b/ui/component/commentMenuList/view.jsx @@ -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() { diff --git a/ui/constants/token.js b/ui/constants/token.js index ebc0c04af..fcf606f98 100644 --- a/ui/constants/token.js +++ b/ui/constants/token.js @@ -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'; diff --git a/ui/lbry.js b/ui/lbry.js index ac9768711..26b56d2a5 100644 --- a/ui/lbry.js +++ b/ui/lbry.js @@ -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 { +function daemonCallWithResult( + name: string, + params: ?{} = {}, + checkAuthNeededFn: ?(?{}) => boolean = undefined +): Promise { 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; diff --git a/ui/modal/modalBlockChannel/index.js b/ui/modal/modalBlockChannel/index.js index 9e02ceb5a..45a43156d 100644 --- a/ui/modal/modalBlockChannel/index.js +++ b/ui/modal/modalBlockChannel/index.js @@ -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, diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx index be24b7dee..8b1a514d0 100644 --- a/ui/modal/modalBlockChannel/view.jsx +++ b/ui/modal/modalBlockChannel/view.jsx @@ -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; // ************************************************************************** diff --git a/ui/page/show/index.js b/ui/page/show/index.js index 764017600..30d0bb2e2 100644 --- a/ui/page/show/index.js +++ b/ui/page/show/index.js @@ -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 })); diff --git a/ui/page/show/view.jsx b/ui/page/show/view.jsx index 865f22607..a58ce7f08 100644 --- a/ui/page/show/view.jsx +++ b/ui/page/show/view.jsx @@ -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]); diff --git a/ui/redux/actions/claims.js b/ui/redux/actions/claims.js index 9ef7e579d..a905963f3 100644 --- a/ui/redux/actions/claims.js +++ b/ui/redux/actions/claims.js @@ -27,7 +27,8 @@ let checkPendingInterval; export function doResolveUris( uris: Array, 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 = []; - 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); }; } diff --git a/ui/redux/selectors/claims.js b/ui/redux/selectors/claims.js index e9f62c2e9..ea0ca588e 100644 --- a/ui/redux/selectors/claims.js +++ b/ui/redux/selectors/claims.js @@ -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) => { diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 7f0e47ba4..a07c56478 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -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, claimId?: string, filterInputs const { claimsById, myClaimIds, + myChannelClaimIds, mutedChannels, personalBlockList, blacklistedMap, @@ -282,8 +289,12 @@ const filterComments = (comments: Array, 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; }