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:
parent
de29e323a8
commit
99f87e95e3
14 changed files with 118 additions and 74 deletions
|
@ -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
|
||||
|
|
24
config.js
24
config.js
|
@ -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`;
|
||||
|
||||
|
|
30
flow-typed/Blocklist.js
vendored
30
flow-typed/Blocklist.js
vendored
|
@ -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
6
flow-typed/user.js
vendored
|
@ -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>,
|
||||
};
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>) => {
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in a new issue