Geo blocklist - reimplement with backend support (#1089)

Ticket: 1079 Support geoblocking channels/videos

## Changes
- Replaced the .env version with iapi version.
- Includes 'videos' blocking and custom messages.
This commit is contained in:
infinite-persistence 2022-03-14 12:15:30 -07:00 committed by GitHub
parent de29e323a8
commit 99f87e95e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 118 additions and 74 deletions

View file

@ -130,13 +130,6 @@ FIREBASE_APP_ID=1:638894153788:web:35b295b15297201bd2e339
FIREBASE_MEASUREMENT_ID=G-2MPJGFEEXC
FIREBASE_VAPID_KEY=BFayEBpwMTU9GQQpXgitIJkfx-SD8-ltrFb3wLTZWgA27MfBhG4948pe0eERl432NzPrMKsbkXnA7ap_vLPgLYk
# --- Geoblock ---
# Note: our current version of dotenv doesn't support multiline definition.
#
# FORMAT: "<channel_id>; type=[livestream|video|*]; country=[xx|*]; continent=[xx|*]; | ..."
#
GEOBLOCKED_CHANNELS="148bfcb49da2c2e781ae27387e45043a4bcbd51e; types=livestream; countries=FR,MS; continents=AF,EU; | 636098f86be74be8b609c38e643e48786d58f413; types=livestream,video; countries=EU,SA; continents=EU;"
# --- Development ---
REPORT_NEW_STRINGS=false
USE_LOCAL_HOMEPAGE_DATA=false

View file

@ -90,30 +90,6 @@ const config = {
AD_KEYWORD_BLOCKLIST_CHECK_DESCRIPTION: process.env.AD_KEYWORD_BLOCKLIST_CHECK_DESCRIPTION,
};
config.GEOBLOCKED_CHANNELS = {};
if (process.env.GEOBLOCKED_CHANNELS) {
const entries = process.env.GEOBLOCKED_CHANNELS.split('|');
entries.forEach((entry) => {
const fields = entry.split(';');
if (fields.length > 0) {
const channelId = fields[0].trim();
config.GEOBLOCKED_CHANNELS[channelId] = {};
for (let i = 1; i < fields.length; ++i) {
const kv = fields[i].split('=');
if (kv.length === 2) {
const key = kv[0].trim();
const values = kv[1].trim();
config.GEOBLOCKED_CHANNELS[channelId][key] = values.split(',');
}
}
}
});
}
config.SDK_API_PATH = `${config.LBRY_WEB_API}/api/v1`;
config.PROXY_URL = `${config.SDK_API_PATH}/proxy`;

View file

@ -1,5 +1,6 @@
declare type BlocklistState = {
blockedChannels: Array<string>
blockedChannels: Array<string>,
geoBlockedList: ?GBL,
};
declare type BlocklistAction = {
@ -8,3 +9,30 @@ declare type BlocklistAction = {
uri: string,
},
};
// ****************************************************************************
// Geo-blocked list (GBL)
// ****************************************************************************
declare type GeoChannelId = string;
declare type GeoRestriction = {
id: string,
trigger?: string,
reason?: string,
message?: string,
};
declare type GeoConfig = {
countries?: Array<GeoRestriction>,
continents?: Array<GeoRestriction>,
specials?: Array<GeoRestriction>,
};
declare type GBL = {
livestreams?: { [GeoChannelId]: GeoConfig },
videos?: { [GeoChannelId]: GeoConfig }
};
// ****************************************************************************
// ****************************************************************************

6
flow-typed/user.js vendored
View file

@ -41,9 +41,3 @@ declare type LocaleInfo = {
gdpr_required: boolean,
is_eu_member: boolean,
};
declare type GeoBlock = {
types: Array<string>,
countries: Array<string>,
continents: Array<string>,
};

View file

@ -2186,7 +2186,7 @@
"The minimum duration must not exceed Feb 8th, 2022.": "The minimum duration must not exceed Feb 8th, 2022.",
"No limit": "No limit",
"Search results are being filtered by language. Click here to change the setting.": "Search results are being filtered by language. Click here to change the setting.",
"This creator has requested that their livestream be blocked in your region.": "This creator has requested that their livestream be blocked in your region.",
"Content unavailable": "Content unavailable",
"There are language translations available for your location! Do you want to switch from English?": "There are language translations available for your location! Do you want to switch from English?",
"A homepage and language translations are available for your location! Do you want to switch?": "A homepage and language translations are available for your location! Do you want to switch?",
"A homepage is available for your location! Do you want to switch?": "A homepage is available for your location! Do you want to switch?",

View file

@ -430,6 +430,11 @@ export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
// Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
// Geo-block
export const FETCH_GBL_STARTED = 'FETCH_GBL_STARTED';
export const FETCH_GBL_FAILED = 'FETCH_GBL_FAILED';
export const FETCH_GBL_DONE = 'FETCH_GBL_DONE';
// Coin swap
export const ADD_COIN_SWAP = 'ADD_COIN_SWAP';
export const REMOVE_COIN_SWAP = 'REMOVE_COIN_SWAP';

View file

@ -14,6 +14,7 @@ import React, { Fragment, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app';
import { doFetchGeoBlockedList } from 'redux/actions/blocked';
import Lbry, { apiCall } from 'lbry';
import { isURIValid } from 'util/lbryURI';
import { setSearchApi } from 'redux/actions/search';
@ -246,6 +247,7 @@ function AppWrapper() {
app.store.dispatch(doUpdateIsNightAsync());
app.store.dispatch(doBlackListedOutpointsSubscribe());
app.store.dispatch(doFilteredOutpointsSubscribe());
app.store.dispatch(doFetchGeoBlockedList());
}, 25);
analytics.startupEvent(Date.now());

View file

@ -6,32 +6,11 @@ import analytics from 'analytics';
import LivestreamLayout from 'component/livestreamLayout';
import moment from 'moment';
import Page from 'component/page';
import Yrbl from 'component/yrbl';
import { GEOBLOCKED_CHANNELS } from 'config';
import * as SETTINGS from 'constants/settings';
import React from 'react';
import { useIsMobile } from 'effects/use-screensize';
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
function isLivestreamGeoAllowed(channelId: ?string, isLive: boolean) {
const locale: LocaleInfo = window[SETTINGS.LOCALE];
const geoBlock: GeoBlock = GEOBLOCKED_CHANNELS[channelId];
if (locale && geoBlock) {
const typeBlocked = geoBlock.types && geoBlock.types.includes('livestream');
const countryBlocked = geoBlock.countries && geoBlock.countries.includes(locale.country);
const europeanUnionOnly = geoBlock.continents && geoBlock.continents.includes('EU-UNION') && locale.is_eu_member;
const continentBlocked =
europeanUnionOnly || (geoBlock.continents && geoBlock.continents.includes(locale.continent));
return typeBlocked && !countryBlocked && !continentBlocked;
}
// If 'locale/get' fails, we don't know whether to block or not. Flaw?
return true;
}
type Props = {
activeLivestreamForChannel: any,
activeLivestreamInitialized: boolean,
@ -75,7 +54,6 @@ export default function LivestreamPage(props: Props) {
const claimId = claim && claim.claim_id;
const isCurrentClaimLive = isChannelBroadcasting && activeLivestreamForChannel.claimId === claimId;
const livestreamChannelId = channelClaimId || '';
const isGeoBlocked = !isLivestreamGeoAllowed(channelClaimId, isCurrentClaimLive);
// $FlowFixMe
const release = moment.unix(claim.value.release_time);
@ -182,7 +160,6 @@ export default function LivestreamPage(props: Props) {
livestream
chatDisabled={hideComments}
rightSide={
!isGeoBlocked &&
!hideComments &&
isInitialized && (
<React.Suspense fallback={null}>
@ -191,17 +168,7 @@ export default function LivestreamPage(props: Props) {
)
}
>
{isGeoBlocked && (
<div className="main--empty">
<Yrbl
title={__('This creator has requested that their livestream be blocked in your region.')}
type="sad"
alwaysShow
/>
</div>
)}
{isInitialized && !isGeoBlocked && (
{isInitialized && (
<LivestreamLayout
uri={uri}
hideComments={hideComments}

View file

@ -10,6 +10,7 @@ import {
selectClaimIsMine,
makeSelectClaimIsPending,
selectIsStreamPlaceholderForUri,
selectGeoRestrictionForUri,
} from 'redux/selectors/claims';
import {
makeSelectCollectionForId,
@ -86,6 +87,7 @@ const select = (state, props) => {
collectionId,
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
geoRestriction: selectGeoRestrictionForUri(state, uri),
};
};

View file

@ -36,6 +36,7 @@ type Props = {
collection: Collection,
collectionUrls: Array<string>,
isResolvingCollection: boolean,
geoRestriction: ?GeoRestriction,
doResolveUri: (uri: string, returnCached: boolean, resolveReposts: boolean, options: any) => void,
doBeginPublish: (name: ?string) => void,
doFetchItemsInCollection: ({ collectionId: string }) => void,
@ -57,6 +58,7 @@ export default function ShowPage(props: Props) {
collection,
collectionUrls,
isResolvingCollection,
geoRestriction,
doResolveUri,
doBeginPublish,
doFetchItemsInCollection,
@ -87,7 +89,7 @@ export default function ShowPage(props: Props) {
);
// changed this from 'isCollection' to resolve strangers' collections.
React.useEffect(() => {
useEffect(() => {
if (collectionId && !resolvedCollection) {
doFetchItemsInCollection({ collectionId });
}
@ -229,6 +231,14 @@ export default function ShowPage(props: Props) {
);
}
if (geoRestriction) {
return (
<div className="main--empty">
<Yrbl title={__('Content unavailable')} subtitle={__(geoRestriction.message || '')} type="sad" alwaysShow />
</div>
);
}
if (showLiveStream) {
return (
<React.Suspense fallback={null}>

View file

@ -1,4 +1,5 @@
// @flow
import { Lbryio } from 'lbryinc';
import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app';
@ -39,3 +40,22 @@ export function doChannelUnmute(uri: string, showLink: boolean = true) {
return dispatch(doToggleMuteChannel(uri, showLink, true));
};
}
export function doFetchGeoBlockedList() {
return (dispatch: Dispatch) => {
dispatch({ type: ACTIONS.FETCH_GBL_STARTED });
const success = (response: GBL) => {
dispatch({
type: ACTIONS.FETCH_GBL_DONE,
data: response,
});
};
const failure = (error) => {
dispatch({ type: ACTIONS.FETCH_GBL_FAILED, data: error });
};
Lbryio.call('geo', 'blocked_list').then(success, failure);
};
}

View file

@ -4,6 +4,7 @@ import { handleActions } from 'util/redux-utils';
const defaultState: BlocklistState = {
blockedChannels: [],
geoBlockedList: undefined,
};
export default handleActions(
@ -20,9 +21,16 @@ export default handleActions(
}
return {
...state,
blockedChannels: newBlockedChannels,
};
},
[ACTIONS.FETCH_GBL_DONE]: (state: BlocklistState, action: any): BlocklistState => {
return {
...state,
geoBlockedList: action.data,
};
},
[ACTIONS.USER_STATE_POPULATE]: (state: BlocklistState, action: { data: { blocked: ?Array<string> } }) => {
const { blocked } = action.data;
const sanitizedBlocked = blocked && blocked.filter((e) => typeof e === 'string');

View file

@ -7,6 +7,7 @@ type State = { blocked: BlocklistState };
const selectState = (state: State) => state.blocked || {};
export const selectMutedChannels = (state: State) => selectState(state).blockedChannels;
export const selectGeoBlockLists = (state: State) => selectState(state).geoBlockedList;
export const makeSelectChannelIsMuted = (uri: string) =>
createSelector(selectMutedChannels, (state: Array<string>) => {

View file

@ -1,12 +1,14 @@
// @flow
import { CHANNEL_CREATION_LIMIT } from 'config';
import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI';
import { selectGeoBlockLists } from 'redux/selectors/blocked';
import { selectYoutubeChannels } from 'redux/selectors/user';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect';
import { isClaimNsfw, filterClaims, getChannelIdFromClaim, isStreamPlaceholderClaim } from 'util/claim';
import * as CLAIM from 'constants/claim';
import * as SETTINGS from 'constants/settings';
import { INTERNAL_TAGS } from 'constants/tags';
type State = { claims: any, user: User };
@ -829,3 +831,39 @@ export const selectOdyseeMembershipForChannelId = function (state: State, channe
return matchingMembershipOfUser;
};
export const selectGeoRestrictionForUri = createCachedSelector(
selectClaimForUri,
selectGeoBlockLists,
(claim, geoBlockLists) => {
const locale: LocaleInfo = window[SETTINGS.LOCALE]; // <-- NOTE: not handled by redux updates
const channelId: ?string = getChannelIdFromClaim(claim);
if (locale && geoBlockLists && channelId && claim) {
let geoConfig: ?GeoConfig;
// --- livestreams
if (isStreamPlaceholderClaim(claim)) {
geoConfig = geoBlockLists.livestreams && geoBlockLists.livestreams[channelId];
}
// --- videos (a.k.a everything else)
else {
geoConfig = geoBlockLists.videos && geoBlockLists.videos[channelId];
}
if (geoConfig) {
const specials = geoConfig.specials || [];
const countries = geoConfig.countries || [];
const continents = geoConfig.continents || [];
return (
specials.find((x: GeoRestriction) => x.id === 'EU-ONLY' && locale.is_eu_member) ||
countries.find((x: GeoRestriction) => x.id === locale.country) ||
continents.find((x: GeoRestriction) => x.id === locale.continent)
);
}
}
return null;
}
)((state, uri) => String(uri));