Merge pull request #860 from lbryio/master

cuts staging from master
This commit is contained in:
jessopb 2019-01-07 18:12:14 -05:00 committed by GitHub
commit 827db9b458
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 249 additions and 81 deletions

View file

@ -2,5 +2,6 @@ client/build
node_modules/ node_modules/
public/bundle public/bundle
server/render/build server/render/build
server/bundle
test/ test/
server/chainquery server/chainquery

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2018 LBRY Inc. Copyright (c) 2017-2019 LBRY Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,

View file

@ -17,7 +17,8 @@
"ipAddress": "", "ipAddress": "",
"host": "https://www.example.com", "host": "https://www.example.com",
"description": "A decentralized hosting platform built on LBRY", "description": "A decentralized hosting platform built on LBRY",
"twitter": false "twitter": false,
"blockListEndpoint": "https://api.lbry.io/file/list_blocked"
}, },
"publishing": { "publishing": {
"primaryClaimAddress": null, "primaryClaimAddress": null,

View file

@ -2,6 +2,16 @@
position: relative; position: relative;
} }
.asset-preview__blocked {
box-sizing: border-box;
background: black;
color: white;
height: 80%;
padding: 5px;
//remove margin-bottom after mystery 5px on wrapper is gone.
margin-bottom: 5px;
}
.asset-preview__image { .asset-preview__image {
width : 100%; width : 100%;
padding: 0; padding: 0;

View file

@ -2,16 +2,27 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '../../../../utils/createCanonicalLink';
const ClaimPending = () => {
return (
<div className='claim-pending'>PENDING</div>
);
};
const AssetPreview = ({ defaultThumbnail, claimData }) => { const AssetPreview = ({ defaultThumbnail, claimData }) => {
const {name, fileExt, contentType, thumbnail, title, pending} = claimData; const {name, fileExt, contentType, thumbnail, title, blocked} = claimData;
const showUrl = createCanonicalLink({asset: {...claimData}}); const showUrl = createCanonicalLink({asset: {...claimData}});
const embedUrl = `${showUrl}.${fileExt}`; const embedUrl = `${showUrl}.${fileExt}`;
/*
This blocked section shouldn't be necessary after pagination is reworked,
though it might be useful for channel_mine situations.
*/
if (blocked) {
return (
<div className='asset-preview'>
<div className='asset-preview__blocked'>
<h3>Error 451</h3>
<p>This content is blocked for legal reasons.</p>
</div>
<h3 className='asset-preview__title'>Blocked Content</h3>
</div>
);
} else {
switch (contentType) { switch (contentType) {
case 'image/jpeg': case 'image/jpeg':
case 'image/jpg': case 'image/jpg':
@ -19,35 +30,32 @@ const AssetPreview = ({ defaultThumbnail, claimData }) => {
case 'image/gif': case 'image/gif':
return ( return (
<Link to={showUrl} className='asset-preview'> <Link to={showUrl} className='asset-preview'>
<div>
<img <img
className={'asset-preview__image'} className={'asset-preview__image'}
src={embedUrl} src={embedUrl}
alt={name} alt={name}
/> />
<h3 className='asset-preview__title'>{title}</h3> <h3 className='asset-preview__title'>{title}</h3>
</div>
</Link> </Link>
); );
case 'video/mp4': case 'video/mp4':
return ( return (
<Link to={showUrl} className='asset-preview'> <Link to={showUrl} className='asset-preview'>
<div>
<div className='asset-preview__play-wrapper'> <div className='asset-preview__play-wrapper'>
<img <img
className={'asset-preview__video'} className={'asset-preview__video'}
src={thumbnail || defaultThumbnail} src={thumbnail || defaultThumbnail}
alt={name} alt={name}
/> />
<div className='asset-preview__play-overlay'></div> <div className='asset-preview__play-overlay' />
</div> </div>
<h3 className='asset-preview__title'>{title}</h3> <h3 className='asset-preview__title'>{title}</h3>
</div>
</Link> </Link>
); );
default: default:
return null; return null;
} }
}
}; };
export default AssetPreview; export default AssetPreview;

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = (props) => {
const {show} = props;
const asset = selectAsset(show);
return {
asset,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -0,0 +1,58 @@
import React from 'react';
import createCanonicalLink from '../../../../utils/createCanonicalLink';
import HorizontalSplit from '@components/HorizontalSplit';
/*
This component shouldn't be necessary after pagination is reworked,
though it might be useful for channel_mine situations.
*/
class BlockedLeft extends React.PureComponent {
render () {
return (
<React.Fragment>
<img className='asset-image' src={'https://upload.wikimedia.org/wikipedia/commons/archive/a/af/20120315000030%21OR_451.svg'} alt={'451 image'} />
</React.Fragment>
);
}
}
class BlockedRight extends React.PureComponent {
render () {
return (
<React.Fragment>
<p>In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.</p>
<p><a href={'https://lbry.io/faq/dmca'} >Click here</a> for more information.</p>
</React.Fragment>
);
}
}
class AssetBlocked extends React.Component {
componentDidMount () {
/*
This function and fetch exists to send the browser the appropriate 451 error.
*/
const { asset } = this.props;
const { claimData: { contentType, outpoint } } = asset;
let fileExt;
if (typeof contentType === 'string') {
fileExt = contentType.split('/')[1] || 'jpg';
}
const sourceUrl = `${createCanonicalLink({ asset: asset.claimData })}.${fileExt}?${outpoint}`;
fetch(sourceUrl)
.catch();
}
render () {
return (
<div>
<HorizontalSplit
collapseOnMobile
leftSide={<BlockedLeft />}
rightSide={<BlockedRight />}
/>
</div>
);
}
};
export default AssetBlocked;

View file

@ -2,6 +2,7 @@ import React from 'react';
import PageLayout from '@components/PageLayout'; import PageLayout from '@components/PageLayout';
import * as Icon from 'react-feather'; import * as Icon from 'react-feather';
import AssetDisplay from '@containers/AssetDisplay'; import AssetDisplay from '@containers/AssetDisplay';
import AssetBlocked from '@containers/AssetBlocked';
import AssetInfo from '@containers/AssetInfo'; import AssetInfo from '@containers/AssetInfo';
import ErrorPage from '@pages/ErrorPage'; import ErrorPage from '@pages/ErrorPage';
import AssetTitle from '@containers/AssetTitle'; import AssetTitle from '@containers/AssetTitle';
@ -29,7 +30,8 @@ class ShowAssetDetails extends React.Component {
render () { render () {
const { asset } = this.props; const { asset } = this.props;
if (asset) { if (asset) {
const { claimData: { name } } = asset; const { claimData: { name, blocked } } = asset;
if (!blocked) {
return ( return (
<PageLayout <PageLayout
pageTitle={`${name} - details`} pageTitle={`${name} - details`}
@ -38,7 +40,6 @@ class ShowAssetDetails extends React.Component {
<div className="asset-main"> <div className="asset-main">
<AssetDisplay /> <AssetDisplay />
<AssetTitle /> <AssetTitle />
<button className='collapse-button' onClick={this.collapse}> <button className='collapse-button' onClick={this.collapse}>
{this.state.closed ? <Icon.PlusCircle className='plus-icon' /> : <Icon.MinusCircle />} {this.state.closed ? <Icon.PlusCircle className='plus-icon' /> : <Icon.MinusCircle />}
</button> </button>
@ -46,6 +47,15 @@ class ShowAssetDetails extends React.Component {
{!this.state.closed && <AssetInfo />} {!this.state.closed && <AssetInfo />}
</PageLayout> </PageLayout>
); );
} else {
return (
<PageLayout>
<div className="asset-main">
<AssetBlocked />
</div>
</PageLayout>
);
}
} }
return ( return (
<ErrorPage error={'loading asset data...'} /> <ErrorPage error={'loading asset data...'} />

View file

@ -5,6 +5,7 @@ const getClaimData = require('server/utils/getClaimData');
const chainquery = require('chainquery').default; const chainquery = require('chainquery').default;
const db = require('../../../../models'); const db = require('../../../../models');
const waitOn = require('wait-on'); const waitOn = require('wait-on');
const logger = require('winston');
/* /*
@ -17,32 +18,35 @@ const claimGet = async ({ ip, originalUrl, params }, res) => {
const claimId = params.claimId; const claimId = params.claimId;
try { try {
let claimData = await chainquery.claim.queries.resolveClaim(name, claimId).catch(() => {}); let claimInfo = await chainquery.claim.queries.resolveClaim(name, claimId).catch(() => {});
if (!claimData) { if (claimInfo) {
claimData = await db.Claim.resolveClaim(name, claimId); logger.info('claim/get: claim resolved in chainquery');
} }
if (!claimInfo) {
if (!claimData) { claimInfo = await db.Claim.resolveClaim(name, claimId);
throw new Error('No matching uri found in Claim table'); }
if (!claimInfo) {
throw new Error('claim/get: resolveClaim: No matching uri found in Claim table');
} }
let lbrynetResult = await getClaim(`${name}#${claimId}`); let lbrynetResult = await getClaim(`${name}#${claimId}`);
if (!lbrynetResult) { if (!lbrynetResult) {
throw new Error(`Unable to Get ${name}#${claimId}`); throw new Error(`claim/get: getClaim Unable to Get ${name}#${claimId}`);
}
const claimData = await getClaimData(claimInfo);
if (!claimData) {
throw new Error('claim/get: getClaimData failed to get file blobs');
}
await waitOn({
resources: [ lbrynetResult.download_path ],
timeout : 10000, // 10 seconds
window : 500,
});
const fileData = await createFileRecordDataAfterGet(claimData, lbrynetResult);
if (!fileData) {
throw new Error('claim/get: createFileRecordDataAfterGet failed to create file in time');
} }
let fileData = await createFileRecordDataAfterGet(await getClaimData(claimData), lbrynetResult);
const upsertCriteria = { name, claimId }; const upsertCriteria = { name, claimId };
await db.upsert(db.File, fileData, upsertCriteria, 'File'); await db.upsert(db.File, fileData, upsertCriteria, 'File');
try {
await waitOn({
resources: [ lbrynetResult.file_name ],
delay : 500,
timeout : 10000, // 10 seconds
});
} catch (e) {}
const { message, completed } = lbrynetResult; const { message, completed } = lbrynetResult;
res.status(200).json({ res.status(200).json({
success: true, success: true,
@ -53,5 +57,4 @@ const claimGet = async ({ ip, originalUrl, params }, res) => {
handleErrorResponse(originalUrl, ip, error, res); handleErrorResponse(originalUrl, ip, error, res);
} }
}; };
module.exports = claimGet; module.exports = claimGet;

View file

@ -16,6 +16,7 @@ const createDatabaseIfNotExists = require('./models/utils/createDatabaseIfNotExi
const { getWalletBalance } = require('./lbrynet/index'); const { getWalletBalance } = require('./lbrynet/index');
const configureLogging = require('./utils/configureLogging'); const configureLogging = require('./utils/configureLogging');
const configureSlack = require('./utils/configureSlack'); const configureSlack = require('./utils/configureSlack');
const { setupBlockList } = require('./utils/blockList');
const speechPassport = require('./speechPassport'); const speechPassport = require('./speechPassport');
const processTrending = require('./utils/processTrending'); const processTrending = require('./utils/processTrending');
@ -25,7 +26,7 @@ const {
} = require('./middleware/logMetricsMiddleware'); } = require('./middleware/logMetricsMiddleware');
const { const {
details: { port: PORT }, details: { port: PORT, blockListEndpoint },
startup: { startup: {
performChecks, performChecks,
performUpdates, performUpdates,
@ -34,6 +35,9 @@ const {
const { sessionKey } = require('@private/authConfig.json'); const { sessionKey } = require('@private/authConfig.json');
// configure.js doesn't handle new keys in config.json files yet. Make sure it doens't break.
let bLE;
function Server () { function Server () {
this.initialize = () => { this.initialize = () => {
// configure logging // configure logging
@ -166,19 +170,37 @@ function Server () {
logger.info('Starting LBC balance:', walletBalance); logger.info('Starting LBC balance:', walletBalance);
}); });
}; };
this.performUpdates = () => { this.performUpdates = () => {
if (!performUpdates) { if (!performUpdates) {
return; return;
} }
if (blockListEndpoint) {
bLE = blockListEndpoint;
} else if (!blockListEndpoint) {
if (typeof (blockListEndpoint) !== 'string') {
logger.warn('blockListEndpoint is null due to outdated siteConfig file. \n' +
'Continuing with default LBRY blocklist api endpoint. \n ' +
'(Specify /"blockListEndpoint" : ""/ to disable.')
bLE = 'https://api.lbry.io/file/list_blocked';
}
}
logger.info(`Peforming updates...`); logger.info(`Peforming updates...`);
if (!bLE) {
logger.info('Configured for no Block List')
db.Tor.refreshTable().then( (updatedTorList) => {
logger.info('Tor list updated, length:', updatedTorList.length);
});
} else {
return Promise.all([ return Promise.all([
db.Blocked.refreshTable(), db.Blocked.refreshTable(bLE),
db.Tor.refreshTable(), db.Tor.refreshTable()])
])
.then(([updatedBlockedList, updatedTorList]) => { .then(([updatedBlockedList, updatedTorList]) => {
logger.info('Blocked list updated, length:', updatedBlockedList.length); logger.info('Blocked list updated, length:', updatedBlockedList.length);
logger.info('Tor list updated, length:', updatedTorList.length); logger.info('Tor list updated, length:', updatedTorList.length);
}); })
}
}; };
this.start = () => { this.start = () => {
this.initialize(); this.initialize();
@ -194,6 +216,9 @@ function Server () {
this.performUpdates(), this.performUpdates(),
]); ]);
}) })
.then(() => {
return setupBlockList();
})
.then(() => { .then(() => {
logger.info('Spee.ch startup is complete'); logger.info('Spee.ch startup is complete');

View file

@ -1,5 +1,4 @@
const logger = require('winston'); const logger = require('winston');
const BLOCKED_CLAIM = 'BLOCKED_CLAIM'; const BLOCKED_CLAIM = 'BLOCKED_CLAIM';
module.exports = (sequelize, { STRING }) => { module.exports = (sequelize, { STRING }) => {
@ -16,6 +15,14 @@ module.exports = (sequelize, { STRING }) => {
} }
); );
Blocked.getBlockList = function () {
logger.debug('returning full block list');
return new Promise((resolve, reject) => {
this.findAll()
.then(list => { return resolve(list) });
});
};
Blocked.isNotBlocked = function (outpoint) { Blocked.isNotBlocked = function (outpoint) {
logger.debug(`checking to see if ${outpoint} is not blocked`); logger.debug(`checking to see if ${outpoint} is not blocked`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -37,9 +44,10 @@ module.exports = (sequelize, { STRING }) => {
}); });
}; };
Blocked.refreshTable = function () { Blocked.refreshTable = function (blockEndpoint) {
let blockedList = []; let blockedList = [];
return fetch('https://api.lbry.io/file/list_blocked')
return fetch(blockEndpoint)
.then(response => { .then(response => {
return response.json(); return response.json();
}) })
@ -50,9 +58,7 @@ module.exports = (sequelize, { STRING }) => {
if (!jsonResponse.data.outpoints) { if (!jsonResponse.data.outpoints) {
throw new Error('no outpoints in list_blocked response'); throw new Error('no outpoints in list_blocked response');
} }
return jsonResponse.data.outpoints; let outpoints = jsonResponse.data.outpoints;
})
.then(outpoints => {
logger.debug('total outpoints:', outpoints.length); logger.debug('total outpoints:', outpoints.length);
// prep the records // prep the records
for (let i = 0; i < outpoints.length; i++) { for (let i = 0; i < outpoints.length; i++) {

26
server/utils/blockList.js Normal file
View file

@ -0,0 +1,26 @@
const logger = require('winston');
const db = require('../models');
let blockList = new Set();
const setupBlockList = (intervalInSeconds = 60) => {
const fetchList = () => {
return new Promise((resolve, reject) => {
db.Blocked.getBlockList()
.then((result) => {
blockList.clear();
if (result.length > 0) {
result.map((item) => { blockList.add(item.dataValues.outpoint) });
resolve();
} else reject();
})
.catch(e => { console.error('list was empty', e) });
});
};
setInterval(() => { fetchList() }, intervalInSeconds * 1000);
return fetchList();
};
module.exports = {
isBlocked: (outpoint) => { return blockList.has(outpoint) },
setupBlockList,
};

View file

@ -1,6 +1,7 @@
const { details: { host } } = require('@config/siteConfig'); const { details: { host } } = require('@config/siteConfig');
const chainquery = require('chainquery').default; const chainquery = require('chainquery').default;
const { getClaim } = require('server/lbrynet'); const { getClaim } = require('server/lbrynet');
const { isBlocked } = require('./blockList');
module.exports = async (data, chName = null, chShortId = null) => { module.exports = async (data, chName = null, chShortId = null) => {
// TODO: Refactor getching the channel name out; requires invasive changes. // TODO: Refactor getching the channel name out; requires invasive changes.
@ -9,6 +10,11 @@ module.exports = async (data, chName = null, chShortId = null) => {
let lbrynetFileExt = null; let lbrynetFileExt = null;
let channelShortId = chShortId; let channelShortId = chShortId;
let channelName = chName; let channelName = chName;
let blocked;
const outPoint = `${data.transaction_hash_id}:${data.vout}`;
if (isBlocked(outPoint)) {
blocked = true;
}
if (!chName && certificateId && !channelName) { if (!chName && certificateId && !channelName) {
channelName = await chainquery.claim.queries.getClaimChannelName(certificateId).catch(() => { channelName = await chainquery.claim.queries.getClaimChannelName(certificateId).catch(() => {
@ -38,8 +44,9 @@ module.exports = async (data, chName = null, chShortId = null) => {
fileExt : data.generated_extension || data.fileExt || lbrynetFileExt, fileExt : data.generated_extension || data.fileExt || lbrynetFileExt,
description: data.description, description: data.description,
thumbnail : data.generated_thumbnail || data.thumbnail_url || data.thumbnail, thumbnail : data.generated_thumbnail || data.thumbnail_url || data.thumbnail,
outpoint : `${data.transaction_hash_id}:${data.vout}` || data.outpoint, outpoint : outPoint || data.outpoint,
host, host,
pending : Boolean(data.height === 0), pending : Boolean(data.height === 0),
blocked : blocked,
}); });
}; };