From 815d0437ac1abf76ec4b920d9394b11eef463d5f Mon Sep 17 00:00:00 2001 From: jessop Date: Tue, 5 Feb 2019 21:34:45 -0500 Subject: [PATCH 1/8] refactors publishValidate --- cli/defaults/siteConfig.json | 13 +++++- utils/validateFileForPublish.js | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 utils/validateFileForPublish.js diff --git a/cli/defaults/siteConfig.json b/cli/defaults/siteConfig.json index 15243fce..dd9f7066 100644 --- a/cli/defaults/siteConfig.json +++ b/cli/defaults/siteConfig.json @@ -23,7 +23,7 @@ "publishing": { "primaryClaimAddress": null, "uploadDirectory": "/home/lbry/Uploads", - "lbrynetHome": "/home/lbry", + "lbrynetHome": "/CURRENTLYUNUSED", "thumbnailChannel": null, "thumbnailChannelId": null, "additionalClaimAddresses": [], @@ -36,6 +36,17 @@ "publishingChannelWhitelist": [], "channelClaimBidAmount": "0.1", "fileClaimBidAmount": "0.01", + "fileSizeLimits": { + "image": 50000000, + "video": 50000000, + "audio": 50000000, + "text": 50000000, + "model": 50000000, + "application": 50000000, + "customByContentType": { + "application/octet-stream": 50000000 + } + }, "maxSizeImage": 10000000, "maxSizeGif": 50000000, "maxSizeVideo": 50000000 diff --git a/utils/validateFileForPublish.js b/utils/validateFileForPublish.js new file mode 100644 index 00000000..b2e8f87d --- /dev/null +++ b/utils/validateFileForPublish.js @@ -0,0 +1,75 @@ +import { publishing } from '@config/siteConfig.json'; + +const { + fileSizeLimits: { + image: maxSizeImage = 10000000, + video: maxSizeVideo = 50000000, + audio: maxSizeAudio = 50000000, + text: maxSizeText = 50000000, + model: maxSizeModel = 50000000, + application: maxSizeApplication = 50000000, + customByContentType, + }, +} = publishing; + +const SIZE_MB = 1000000; + +export const validateFileForPublish = file => { + let contentType = file.type; + let mediaType = contentType ? contentType.substr(0, contentType.indexOf('/')) : ''; + + if (!file) { + throw new Error('no file provided'); + } + + if (/'/.test(file.name)) { + throw new Error('apostrophes are not allowed in the file name'); + } + + if (Object.keys(customByContentType).includes(contentType)) { + if (file.size > customByContentType[contentType]) { + throw new Error( + `Sorry, type ${contentType} is limited to ${customByContentType[contentType] / SIZE_MB} MB.` + ); + } + } else { + switch (mediaType) { + case 'image': + if (file.size > maxSizeImage) { + throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeImage / SIZE_MB} MB.`); + } + break; + case 'audio': + if (file.size > maxSizeAudio) { + throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeAudio / SIZE_MB} MB.`); + } + break; + case 'video': + if (file.size > maxSizeVideo) { + throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeVideo / SIZE_MB} MB.`); + } + break; + case 'text': + if (file.size > maxSizeText) { + throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeText / SIZE_MB} MB.`); + } + break; + case 'model': + if (file.size > maxSizeModel) { + throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeModel / SIZE_MB} MB.`); + } + break; + case 'application': + if (file.size > maxSizeApplication) { + throw new Error( + `Sorry, type ${mediaType} is limited to ${maxSizeApplication / SIZE_MB} MB.` + ); + } + break; + default: + throw new Error(`Missing or unrecognized file type`); + } + return false; + } + return file; +}; -- 2.45.3 From 2ff0728db498c1d1cf49ad2f52c86f249355c428 Mon Sep 17 00:00:00 2001 From: jessop Date: Sat, 9 Feb 2019 00:03:41 -0500 Subject: [PATCH 2/8] updates wallet check in config to address_list --- cli/configure.js | 50 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/cli/configure.js b/cli/configure.js index 95922f17..b34ec760 100644 --- a/cli/configure.js +++ b/cli/configure.js @@ -13,7 +13,8 @@ let thumbnailChannelDefault = '@thumbnails'; let thumbnailChannel = ''; let thumbnailChannelId = ''; -const createConfigFile = (fileName, configObject, topSecret) => { // siteConfig.json , siteConfig +const createConfigFile = (fileName, configObject, topSecret) => { + // siteConfig.json , siteConfig const fileLocation = topSecret ? Path.resolve(__dirname, `../site/private/${fileName}`) : Path.resolve(__dirname, `../site/config/${fileName}`); @@ -39,15 +40,8 @@ try { siteConfig = require('./defaults/siteConfig.json'); } const { - details: { - port, - title, - host, - }, - publishing: { - uploadDirectory, - channelClaimBidAmount: channelBid, - }, + details: { port, title, host }, + publishing: { uploadDirectory, channelClaimBidAmount: channelBid }, } = siteConfig; let lbryConfig; @@ -80,12 +74,12 @@ try { // authConfig let randSessionKey = pwGenerator.generate({ - length : 20, + length: 20, numbers: true, }); let randMasterPass = pwGenerator.generate({ - length : 20, + length: 20, numbers: true, }); @@ -94,7 +88,7 @@ try { authConfig = require('../site/private/authConfig.json'); } catch (error) { authConfig = { - sessionKey : randSessionKey, + sessionKey: randSessionKey, masterPassword: randMasterPass, }; } @@ -111,7 +105,7 @@ inquirer console.log('\nRetrieving your primary claim address from LBRY...'); return axios .post('http://localhost:5279', { - method: 'wallet_list', + method: 'address_list', }) .then(response => { if (response.data) { @@ -125,7 +119,8 @@ inquirer return; } throw new Error('No data received from lbrynet'); - }).catch(error => { + }) + .catch(error => { throw error; }); }) @@ -159,7 +154,10 @@ inquirer } console.log('name:', foundChannel.name); console.log('claim_id:', foundChannel.claim_id); - if (foundChannel.name === thumbnailChannel && foundChannel.claim_id === thumbnailChannelId) { + if ( + foundChannel.name === thumbnailChannel && + foundChannel.claim_id === thumbnailChannelId + ) { console.log('No update to existing thumbnail config required\n'); } else { console.log(`Replacing thumbnail channel in config...`); @@ -174,11 +172,12 @@ inquirer return false; } throw new Error('No data received from lbrynet'); - }).catch(error => { + }) + .catch(error => { throw error; }); }) - .then((thumbnailChannelAlreadyExists) => { + .then(thumbnailChannelAlreadyExists => { // exit if a channel already exists, skip this step if (thumbnailChannelAlreadyExists) { return; @@ -190,7 +189,7 @@ inquirer method: 'channel_new', params: { channel_name: thumbnailChannelDefault, - amount : channelBid, + amount: channelBid, }, }) .then(response => { @@ -207,7 +206,8 @@ inquirer return; } throw new Error('No data received from lbrynet'); - }).catch(error => { + }) + .catch(error => { throw error; }); }) @@ -232,11 +232,15 @@ inquirer createConfigFile('authConfig.json', authConfig, true); }) .then(() => { - console.log('\nYou\'re all done!'); - console.log('\nIt\'s a good idea to BACK UP YOUR MASTER PASSWORD \nin "/site/private/authConfig.json" so that you don\'t lose \ncontrol of your channel.'); + console.log("\nYou're all done!"); + console.log( + '\nIt\'s a good idea to BACK UP YOUR MASTER PASSWORD \nin "/site/private/authConfig.json" so that you don\'t lose \ncontrol of your channel.' + ); console.log('\nNext step: run "npm run start" to build and start your server!'); - console.log('If you want to change any settings, you can edit the files in the "/site" folder.'); + console.log( + 'If you want to change any settings, you can edit the files in the "/site" folder.' + ); process.exit(0); }) .catch(error => { -- 2.45.3 From e364dc4024179e1374f3b8f6d24bda9b1b6ab589 Mon Sep 17 00:00:00 2001 From: jessop Date: Wed, 6 Feb 2019 12:56:28 -0500 Subject: [PATCH 3/8] publishes any file type, handles 99% for download --- cli/defaults/siteConfig.json | 14 +++- .../components/DropzonePreviewImage/index.jsx | 14 ++-- client/src/containers/Dropzone/view.jsx | 10 +-- client/src/utils/createAssetMetaTags.js | 2 - server/chainquery/models/ClaimModel.js | 10 ++- .../publish/parsePublishApiRequestFiles.js | 3 +- .../claim/publish/validateFileForPublish.js | 38 +++++++++++ utils/validateFileForPublish.js | 65 ++++--------------- 8 files changed, 87 insertions(+), 69 deletions(-) create mode 100644 server/controllers/api/claim/publish/validateFileForPublish.js diff --git a/cli/defaults/siteConfig.json b/cli/defaults/siteConfig.json index dd9f7066..67e4a79f 100644 --- a/cli/defaults/siteConfig.json +++ b/cli/defaults/siteConfig.json @@ -60,7 +60,19 @@ "publicDisallowedTypesMain": [] }, "customFileExtensions": { - "application/example-type": "example" + "application/x-troff-man": ".man", + "application/x-troff-me": ".me", + "application/x-mif": ".mif", + "application/x-troff-ms": ".ms", + "application/x-troff": ".roff", + "application/x-python-code": ".pyc", + "text/x-python": ".py", + "application/x-pn-realaudio": ".ram", + "application/x-sgml": ".sgm", + "model/stl": ".stl", + "image/pict": ".pct", + "text/xul": ".xul", + "text/x-go": "go" } }, "startup": { diff --git a/client/src/components/DropzonePreviewImage/index.jsx b/client/src/components/DropzonePreviewImage/index.jsx index d62abaeb..5aa6f730 100644 --- a/client/src/components/DropzonePreviewImage/index.jsx +++ b/client/src/components/DropzonePreviewImage/index.jsx @@ -5,8 +5,9 @@ class PublishPreview extends React.Component { constructor (props) { super(props); this.state = { - imgSource : '', - defaultThumbnail: '/assets/img/video_thumb_default.png', + imgSource : '', + defaultVideoThumbnail: '/assets/img/video_thumb_default.png', + defaultThumbnail : '/assets/img/Speech_Logo_Main@OG-02.jpg', }; } componentDidMount () { @@ -37,12 +38,13 @@ class PublishPreview extends React.Component { }; } setPreviewImageSource (file) { - if (file.type !== 'video/mp4') { + if (this.props.thumbnail) { + this.setPreviewImageSourceFromFile(this.props.thumbnail); + } else if (file.type.substr(0, file.type.indexOf('/')) === 'image'){ this.setPreviewImageSourceFromFile(file); + } else if (file.type === 'video'){ + this.setState({imgSource: this.state.defaultVideoThumbnail}); } else { - if (this.props.thumbnail) { - this.setPreviewImageSourceFromFile(this.props.thumbnail); - } this.setState({imgSource: this.state.defaultThumbnail}); } } diff --git a/client/src/containers/Dropzone/view.jsx b/client/src/containers/Dropzone/view.jsx index a4a6d00e..cba685f9 100644 --- a/client/src/containers/Dropzone/view.jsx +++ b/client/src/containers/Dropzone/view.jsx @@ -1,13 +1,13 @@ import React from 'react'; -import { validateFile } from '../../utils/file'; import Memeify from '@components/Memeify'; import DropzonePreviewImage from '@components/DropzonePreviewImage'; import DropzoneDropItDisplay from '@components/DropzoneDropItDisplay'; import DropzoneInstructionsDisplay from '@components/DropzoneInstructionsDisplay'; +import validateFileForPublish from '@globalutils/validateFileForPublish'; -import { library } from '@fortawesome/fontawesome-svg-core' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEdit } from '@fortawesome/free-solid-svg-icons'; const isFacebook = (() => { @@ -29,7 +29,7 @@ class Dropzone extends React.Component { memeify : false, }; - if(props.file) { + if (props.file) { // No side effects allowed with `getDerivedStateFromProps`, so // we must use `componentDidUpdate` and `constructor` routines. // Note: `FileReader` has an `onloadend` side-effect @@ -133,7 +133,7 @@ class Dropzone extends React.Component { chooseFile (file) { if (file) { try { - validateFile(file); // validate the file's name, type, and size + validateFileForPublish(file); // validate the file's name, type, and size } catch (error) { return this.props.setFileError(error.message); } diff --git a/client/src/utils/createAssetMetaTags.js b/client/src/utils/createAssetMetaTags.js index 70bead44..e2a0b5ef 100644 --- a/client/src/utils/createAssetMetaTags.js +++ b/client/src/utils/createAssetMetaTags.js @@ -47,8 +47,6 @@ const createAssetMetaTags = asset => { const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail); const ogThumbnail = claimData.thumbnail || defaultThumbnail; - console.log('asset.claimData', asset.claimData); - // {property: 'og:title'] = ogTitle}, const metaTags = { 'og:title': ogTitle, diff --git a/server/chainquery/models/ClaimModel.js b/server/chainquery/models/ClaimModel.js index 022656f9..4fcc1af0 100644 --- a/server/chainquery/models/ClaimModel.js +++ b/server/chainquery/models/ClaimModel.js @@ -1,9 +1,17 @@ const logger = require('winston'); const mime = require('mime-types'); +const { + serving: { customFileExtensions }, +} = require('@config/siteConfig'); const getterMethods = { generated_extension() { - return mime.extension(this.content_type) ? mime.extension(this.content_type) : 'jpg'; + logger.info('trying to generate extension', this.content_type); + if (customFileExtensions.hasOwnProperty(this.content_type)) { + return customFileExtensions[this.content_type]; + } else { + return mime.extension(this.content_type) ? mime.extension(this.content_type) : 'jpg'; + } }, }; diff --git a/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js b/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js index 3fdcdfc3..6979740e 100644 --- a/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js +++ b/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js @@ -1,5 +1,6 @@ const path = require('path'); const validateFileTypeAndSize = require('./validateFileTypeAndSize.js'); +const validateFileForPublish = require('./validateFileForPublish.js'); const parsePublishApiRequestFiles = ({ file, thumbnail }, isUpdate) => { // make sure a file was provided @@ -40,7 +41,7 @@ const parsePublishApiRequestFiles = ({ file, thumbnail }, isUpdate) => { } // validate the file - if (file) validateFileTypeAndSize(file); + if (file) validateFileForPublish(file); // return results const obj = { fileName: file.name, diff --git a/server/controllers/api/claim/publish/validateFileForPublish.js b/server/controllers/api/claim/publish/validateFileForPublish.js new file mode 100644 index 00000000..1047c048 --- /dev/null +++ b/server/controllers/api/claim/publish/validateFileForPublish.js @@ -0,0 +1,38 @@ +const logger = require('winston'); + +const { publishing } = require('@config/siteConfig.json'); + +const { fileSizeLimits } = publishing; + +const SIZE_MB = 1000000; + +const validateFileForPublish = file => { + let contentType = file.type; + let mediaType = contentType ? contentType.substr(0, contentType.indexOf('/')) : ''; + let mediaTypeLimit = fileSizeLimits[mediaType] || false; + let customLimits = fileSizeLimits['customByContentType']; + + if (!file) { + throw new Error('no file provided'); + } + + if (/'/.test(file.name)) { + throw new Error('apostrophes are not allowed in the file name'); + } + + if (Object.keys(customLimits).includes(contentType)) { + if (file.size > customLimits[contentType]) { + throw new Error( + `Sorry, type ${contentType} is limited to ${customLimits[contentType] / SIZE_MB} MB.` + ); + } + } + if (mediaTypeLimit) { + if (file.size > mediaTypeLimit) { + throw new Error(`Sorry, type ${mediaType} is limited to ${mediaTypeLimit / SIZE_MB} MB.`); + } + } + return file; +}; + +module.exports = validateFileForPublish; diff --git a/utils/validateFileForPublish.js b/utils/validateFileForPublish.js index b2e8f87d..ef129305 100644 --- a/utils/validateFileForPublish.js +++ b/utils/validateFileForPublish.js @@ -1,22 +1,14 @@ import { publishing } from '@config/siteConfig.json'; -const { - fileSizeLimits: { - image: maxSizeImage = 10000000, - video: maxSizeVideo = 50000000, - audio: maxSizeAudio = 50000000, - text: maxSizeText = 50000000, - model: maxSizeModel = 50000000, - application: maxSizeApplication = 50000000, - customByContentType, - }, -} = publishing; +const { fileSizeLimits } = publishing; const SIZE_MB = 1000000; -export const validateFileForPublish = file => { +export default function validateFileForPublish(file) { let contentType = file.type; let mediaType = contentType ? contentType.substr(0, contentType.indexOf('/')) : ''; + let mediaTypeLimit = fileSizeLimits[mediaType] || false; + let customLimits = fileSizeLimits['customByContentType']; if (!file) { throw new Error('no file provided'); @@ -26,50 +18,17 @@ export const validateFileForPublish = file => { throw new Error('apostrophes are not allowed in the file name'); } - if (Object.keys(customByContentType).includes(contentType)) { - if (file.size > customByContentType[contentType]) { + if (Object.keys(customLimits).includes(contentType)) { + if (file.size > customLimits[contentType]) { throw new Error( - `Sorry, type ${contentType} is limited to ${customByContentType[contentType] / SIZE_MB} MB.` + `Sorry, type ${contentType} is limited to ${customLimits[contentType] / SIZE_MB} MB.` ); } - } else { - switch (mediaType) { - case 'image': - if (file.size > maxSizeImage) { - throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeImage / SIZE_MB} MB.`); - } - break; - case 'audio': - if (file.size > maxSizeAudio) { - throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeAudio / SIZE_MB} MB.`); - } - break; - case 'video': - if (file.size > maxSizeVideo) { - throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeVideo / SIZE_MB} MB.`); - } - break; - case 'text': - if (file.size > maxSizeText) { - throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeText / SIZE_MB} MB.`); - } - break; - case 'model': - if (file.size > maxSizeModel) { - throw new Error(`Sorry, type ${mediaType} is limited to ${maxSizeModel / SIZE_MB} MB.`); - } - break; - case 'application': - if (file.size > maxSizeApplication) { - throw new Error( - `Sorry, type ${mediaType} is limited to ${maxSizeApplication / SIZE_MB} MB.` - ); - } - break; - default: - throw new Error(`Missing or unrecognized file type`); + } + if (mediaTypeLimit) { + if (file.size > mediaTypeLimit) { + throw new Error(`Sorry, type ${mediaType} is limited to ${mediaTypeLimit / SIZE_MB} MB.`); } - return false; } return file; -}; +} -- 2.45.3 From 230a862458fb90ade78c025d0b5a27779bbe69af Mon Sep 17 00:00:00 2001 From: jessop Date: Sat, 9 Feb 2019 16:18:09 -0500 Subject: [PATCH 4/8] updates wallet_balance to account_balance --- server/index.js | 110 ++++++++++++++++++++++------------------ server/lbrynet/index.js | 54 ++++++++++++++------ 2 files changed, 98 insertions(+), 66 deletions(-) diff --git a/server/index.js b/server/index.js index be209d33..d423ffac 100644 --- a/server/index.js +++ b/server/index.js @@ -13,7 +13,7 @@ const httpContext = require('express-http-context'); const db = require('./models'); const requestLogger = require('./middleware/requestLogger'); const createDatabaseIfNotExists = require('./models/utils/createDatabaseIfNotExists'); -const { getWalletBalance } = require('./lbrynet/index'); +const { getAccountBalance } = require('./lbrynet/index'); const configureLogging = require('./utils/configureLogging'); const configureSlack = require('./utils/configureSlack'); const { setupBlockList } = require('./utils/blockList'); @@ -27,10 +27,7 @@ const { const { details: { port: PORT, blockListEndpoint }, - startup: { - performChecks, - performUpdates, - }, + startup: { performChecks, performUpdates }, } = require('@config/siteConfig'); const { sessionKey } = require('@private/authConfig.json'); @@ -38,7 +35,7 @@ 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 finalBlockListEndpoint; -function Server () { +function Server() { this.initialize = () => { // configure logging configureLogging(); @@ -53,12 +50,16 @@ function Server () { const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); - const webpackClientConfig = require('../webpack/webpack.client.config')(null, { mode: 'development' }); + const webpackClientConfig = require('../webpack/webpack.client.config')(null, { + mode: 'development', + }); const clientCompiler = webpack(webpackClientConfig); - app.use(webpackDevMiddleware(clientCompiler, { - publicPath: webpackClientConfig.output.publicPath, - })); + app.use( + webpackDevMiddleware(clientCompiler, { + publicPath: webpackClientConfig.output.publicPath, + }) + ); app.use(require('webpack-hot-middleware')(clientCompiler)); } @@ -67,8 +68,15 @@ function Server () { app.enable('trust proxy'); app.use((req, res, next) => { - if (req.get('User-Agent') === 'Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1') { - res.status(403).send('

Forbidden

If you are seeing this by mistake, please contact us using https://chat.lbry.io/'); + if ( + req.get('User-Agent') === + 'Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1' + ) { + res + .status(403) + .send( + '

Forbidden

If you are seeing this by mistake, please contact us using https://chat.lbry.io/' + ); res.end(); } else { next(); @@ -101,38 +109,45 @@ function Server () { app.use(requestLogger); // initialize passport - app.use(cookieSession({ - name: 'session', - keys: [sessionKey], - })); + app.use( + cookieSession({ + name: 'session', + keys: [sessionKey], + }) + ); app.use(speechPassport.initialize()); app.use(speechPassport.session()); // configure handlebars & register it with express app const viewsPath = Path.resolve(process.cwd(), 'server/views'); - app.engine('handlebars', expressHandlebars({ - async : false, - dataType : 'text', - defaultLayout: 'embed', - partialsDir : Path.join(viewsPath, '/partials'), - layoutsDir : Path.join(viewsPath, '/layouts'), - })); + app.engine( + 'handlebars', + expressHandlebars({ + async: false, + dataType: 'text', + defaultLayout: 'embed', + partialsDir: Path.join(viewsPath, '/partials'), + layoutsDir: Path.join(viewsPath, '/layouts'), + }) + ); app.set('views', viewsPath); app.set('view engine', 'handlebars'); // set the routes on the app const routes = require('./routes'); - Object.keys(routes).map((routePath) => { + Object.keys(routes).map(routePath => { let routeData = routes[routePath]; let routeMethod = routeData.hasOwnProperty('method') ? routeData.method : 'get'; - let controllers = Array.isArray(routeData.controller) ? routeData.controller : [routeData.controller]; + let controllers = Array.isArray(routeData.controller) + ? routeData.controller + : [routeData.controller]; app[routeMethod]( routePath, logMetricsMiddleware, setRouteDataInContextMiddleware(routePath, routeData), - ...controllers, + ...controllers ); }); @@ -153,22 +168,18 @@ function Server () { }; this.syncDatabase = () => { logger.info(`Syncing database...`); - return createDatabaseIfNotExists() - .then(() => { - db.sequelize.sync(); - }); + return createDatabaseIfNotExists().then(() => { + db.sequelize.sync(); + }); }; this.performChecks = () => { if (!performChecks) { return; } logger.info(`Performing checks...`); - return Promise.all([ - getWalletBalance(), - ]) - .then(([walletBalance]) => { - logger.info('Starting LBC balance:', walletBalance); - }); + return Promise.all([getAccountBalance()]).then(([accountBalance]) => { + logger.info('Starting LBC balance:', accountBalance); + }); }; this.performUpdates = () => { @@ -178,27 +189,29 @@ function Server () { if (blockListEndpoint) { finalBlockListEndpoint = 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.'); + 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.' + ); finalBlockListEndpoint = 'https://api.lbry.io/file/list_blocked'; } } logger.info(`Peforming updates...`); if (!finalBlockListEndpoint) { logger.info('Configured for no Block List'); - db.Tor.refreshTable().then((updatedTorList) => { + db.Tor.refreshTable().then(updatedTorList => { logger.info('Tor list updated, length:', updatedTorList.length); }); } else { return Promise.all([ db.Blocked.refreshTable(finalBlockListEndpoint), - db.Tor.refreshTable()]) - .then(([updatedBlockedList, updatedTorList]) => { - logger.info('Blocked list updated, length:', updatedBlockedList.length); - logger.info('Tor list updated, length:', updatedTorList.length); - }); + db.Tor.refreshTable(), + ]).then(([updatedBlockedList, updatedTorList]) => { + logger.info('Blocked list updated, length:', updatedBlockedList.length); + logger.info('Tor list updated, length:', updatedTorList.length); + }); } }; this.start = () => { @@ -210,10 +223,7 @@ function Server () { return this.startServerListening(); }) .then(() => { - return Promise.all([ - this.performChecks(), - this.performUpdates(), - ]); + return Promise.all([this.performChecks(), this.performUpdates()]); }) .then(() => { return setupBlockList(); diff --git a/server/lbrynet/index.js b/server/lbrynet/index.js index 3b4eafcd..5d8c24c2 100644 --- a/server/lbrynet/index.js +++ b/server/lbrynet/index.js @@ -8,7 +8,7 @@ const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js'); const { publishing } = require('@config/siteConfig'); module.exports = { - publishClaim (publishParams) { + publishClaim(publishParams) { logger.debug(`lbryApi >> Publishing claim to "${publishParams.name}"`); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -18,7 +18,13 @@ module.exports = { params: publishParams, }) .then(response => { - sendGATimingEvent('lbrynet', 'publish', chooseGaLbrynetPublishLabel(publishParams), gaStartTime, Date.now()); + sendGATimingEvent( + 'lbrynet', + 'publish', + chooseGaLbrynetPublishLabel(publishParams), + gaStartTime, + Date.now() + ); handleLbrynetResponse(response, resolve, reject); }) .catch(error => { @@ -26,7 +32,7 @@ module.exports = { }); }); }, - getClaim (uri) { + getClaim(uri) { logger.debug(`lbryApi >> Getting Claim for "${uri}"`); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -47,7 +53,7 @@ module.exports = { }); }); }, - async abandonClaim ({claimId}) { + async abandonClaim({ claimId }) { logger.debug(`lbryApi >> Abandon claim "${claimId}"`); const gaStartTime = Date.now(); try { @@ -62,7 +68,7 @@ module.exports = { return error; } }, - getClaimList (claimName) { + getClaimList(claimName) { logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -80,7 +86,7 @@ module.exports = { }); }); }, - resolveUri (uri) { + resolveUri(uri) { logger.debug(`lbryApi >> Resolving URI for "${uri}"`); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -97,9 +103,11 @@ module.exports = { db.Claim.findOne({ where: { claimId: uri.split('#')[1] } }) .then(() => reject('This claim has not yet been confirmed on the LBRY blockchain')) .catch(() => reject(`Claim ${uri} does not exist`)); - } else if (data.result[uri].error) { // check for errors + } else if (data.result[uri].error) { + // check for errors reject(data.result[uri].error); - } else { // if no errors, resolve + } else { + // if no errors, resolve resolve(data.result[uri]); } }) @@ -108,7 +116,7 @@ module.exports = { }); }); }, - getDownloadDirectory () { + getDownloadDirectory() { logger.debug('lbryApi >> Retrieving the download directory path from lbry daemon...'); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -117,11 +125,19 @@ module.exports = { method: 'settings_get', }) .then(({ data }) => { - sendGATimingEvent('lbrynet', 'getDownloadDirectory', 'SETTINGS_GET', gaStartTime, Date.now()); + sendGATimingEvent( + 'lbrynet', + 'getDownloadDirectory', + 'SETTINGS_GET', + gaStartTime, + Date.now() + ); if (data.result) { resolve(data.result.download_directory); } else { - return new Error('Successfully connected to lbry daemon, but unable to retrieve the download directory.'); + return new Error( + 'Successfully connected to lbry daemon, but unable to retrieve the download directory.' + ); } }) .catch(error => { @@ -130,7 +146,7 @@ module.exports = { }); }); }, - createChannel (name) { + createChannel(name) { logger.debug(`lbryApi >> Creating channel for ${name}...`); const gaStartTime = Date.now(); return new Promise((resolve, reject) => { @@ -139,7 +155,7 @@ module.exports = { method: 'channel_new', params: { channel_name: name, - amount : publishing.channelClaimBidAmount, + amount: publishing.channelClaimBidAmount, }, }) .then(response => { @@ -151,15 +167,21 @@ module.exports = { }); }); }, - getWalletBalance () { + getAccountBalance() { const gaStartTime = Date.now(); return new Promise((resolve, reject) => { axios .post(lbrynetUri, { - method: 'wallet_balance', + method: 'account_balance', }) .then(response => { - sendGATimingEvent('lbrynet', 'getWalletBalance', 'SETTINGS_GET', gaStartTime, Date.now()); + sendGATimingEvent( + 'lbrynet', + 'getAccountBalance', + 'SETTINGS_GET', + gaStartTime, + Date.now() + ); handleLbrynetResponse(response, resolve, reject); }) .catch(error => { -- 2.45.3 From 3de2b3a5729b578383e56ada3b744da2db3cbfae Mon Sep 17 00:00:00 2001 From: jessop Date: Tue, 12 Feb 2019 12:32:55 -0500 Subject: [PATCH 5/8] updates lodash for vulnerabilities --- package-lock.json | 6 +++--- package.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfd34b73..a1413b3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8091,9 +8091,9 @@ } }, "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash.assign": { "version": "4.2.0", diff --git a/package.json b/package.json index 8698d893..19a168ba 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "image-size": "^0.6.3", "inquirer": "^5.2.0", "ip": "^1.1.5", + "lodash": "^4.17.11", "make-dir": "^1.3.0", "mime-types": "^2.1.21", "module-alias": "^2.1.0", -- 2.45.3 From 27cbe26936eb87667c3fc78b906018f59cb42cf4 Mon Sep 17 00:00:00 2001 From: Kenneth C Date: Wed, 13 Feb 2019 16:50:27 -0500 Subject: [PATCH 6/8] Updated the instructions to reflect building before starting Updated this in a previous pull request but the way I phrased it was a little bit off. This provides the instructions that you must BUILD before STARTING the server for it to actually reflect the changes made. --- customize.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/customize.md b/customize.md index faba1bf1..1e09f475 100644 --- a/customize.md +++ b/customize.md @@ -31,7 +31,11 @@ function Logo () { export default Logo; ``` -Restart the server, and you should see your site with a new Logo in the top left corner! +Rebuild and restart the server, and you should see your site with a new Logo in the top left corner! +``` +$ npm run build +``` +Then ``` $ npm run start ``` -- 2.45.3 From 5e8e22dcad6720bfb6004d622ff938a051537890 Mon Sep 17 00:00:00 2001 From: jessop Date: Tue, 12 Feb 2019 19:38:07 -0500 Subject: [PATCH 7/8] enables nsfw updates --- client/src/components/PublishNsfwInput/index.jsx | 2 +- server/utils/getClaimData.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/PublishNsfwInput/index.jsx b/client/src/components/PublishNsfwInput/index.jsx index 602f7b72..99562be0 100644 --- a/client/src/components/PublishNsfwInput/index.jsx +++ b/client/src/components/PublishNsfwInput/index.jsx @@ -14,7 +14,7 @@ const PublishNsfwInput = ({ nsfw, handleInput }) => { type='checkbox' id='publish-nsfw' name='nsfw' - value={nsfw} + checked={nsfw} onChange={handleInput} /> } diff --git a/server/utils/getClaimData.js b/server/utils/getClaimData.js index f725f94e..b95d0847 100644 --- a/server/utils/getClaimData.js +++ b/server/utils/getClaimData.js @@ -53,6 +53,7 @@ module.exports = async (data, chName = null, chShortId = null) => { claimId: dataVals.claim_id || data.claimId, fileExt: fileExt, description: dataVals.description, + nsfw: dataVals.is_nsfw, thumbnail: dataVals.thumbnail_url || data.thumbnail || thumbnail, outpoint, host, -- 2.45.3 From 181b3463b7c2e7fadc77ae18cc09b47176031865 Mon Sep 17 00:00:00 2001 From: jessop Date: Fri, 8 Feb 2019 23:45:18 -0500 Subject: [PATCH 8/8] styling, markdown settings tied to config --- README.md | 251 ++++++++++-------- cli/defaults/siteConfig.json | 42 ++- client/scss/_asset-display.scss | 16 +- client/scss/_markdown.scss | 51 +++- client/src/components/ErrorBoundary/index.jsx | 24 ++ client/src/components/FileViewer/index.jsx | 11 +- client/src/containers/AssetInfo/view.jsx | 15 +- docs/settings.md | 96 +++++++ 8 files changed, 367 insertions(+), 139 deletions(-) create mode 100644 client/src/components/ErrorBoundary/index.jsx create mode 100644 docs/settings.md diff --git a/README.md b/README.md index c3fe459a..f2f35b2a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spee.ch -spee.ch provides a user-friendly, custom-designed, image and video hosting site backed by a decentralized network and +spee.ch provides a user-friendly, custom-designed, image and video hosting site backed by a decentralized network and blockchain ([LBRY](https://lbry.tech/)). Via just a small set of config files, you can spin your an entire spee.ch site back up including assets. ![App GIF](https://spee.ch/e/speechgif.gif) @@ -18,56 +18,64 @@ For a closed, custom-hosted and branded example, check out https://lbry.theantim ### Full Instructions #### Get some information ready: - * mysqlusername - * mysqlpassword - * domainname or 'http://localhost:3000' - * speechport = 3000 + +- mysqlusername +- mysqlpassword +- domainname or 'http://localhost:3000' +- speechport = 3000 #### Install and Set Up Dependencies - * Firewall open ports - * 22 - * 80 - * 443 - * 3333 - * 4444 - * [NodeJS](https://nodejs.org) - * [MySQL version 5.7 or higher](https://dev.mysql.com/doc/refman/8.0/en/installing.html) - * mysqlusername or root - * mysqlpassword - * Requires mysql_native_password plugin - ``` - mysql> `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';` - ``` - * [lbrynet](https://github.com/lbryio/lbry) daemon - * run this as a service exposing ports 3333 and 4444 - * _note_: once the daemon is running, issue commands in another terminal session (tmux) to retrieve an address for your wallet to recieve 5+ LBC credits (or join us in the [#speech discord channel](https://discord.gg/YjYbwhS) and we will send you a few) - * `./lbrynet commands` gets a list of commands - * `./lbrynet account_balance` gets your balance (initially 0.0) - * `./lbrynet address_list` gets addresses you can use to recieve LBC - * [FFmpeg](https://www.ffmpeg.org/download.html) - * Spee.ch (below) - * pm2 (optional) process manager such as pm2 to run speech server.js - * http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000 - * _note: even running on http://localhost, you must redirect http or https to port 3000_ +- Firewall open ports + - 22 + - 80 + - 443 + - 3333 + - 4444 +- [NodeJS](https://nodejs.org) +- [MySQL version 5.7 or higher](https://dev.mysql.com/doc/refman/8.0/en/installing.html) + - mysqlusername or root + - mysqlpassword + - Requires mysql_native_password plugin + ``` + mysql> `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';` + ``` +- [lbrynet](https://github.com/lbryio/lbry) daemon + - run this as a service exposing ports 3333 and 4444 + - _note_: once the daemon is running, issue commands in another terminal session (tmux) to retrieve an address for your wallet to recieve 5+ LBC credits (or join us in the [#speech discord channel](https://discord.gg/YjYbwhS) and we will send you a few) + - `./lbrynet commands` gets a list of commands + - `./lbrynet account_balance` gets your balance (initially 0.0) + - `./lbrynet address_list` gets addresses you can use to recieve LBC +- [FFmpeg](https://www.ffmpeg.org/download.html) +- Spee.ch (below) +- pm2 (optional) process manager such as pm2 to run speech server.js +- http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000 + - _note: even running on http://localhost, you must redirect http or https to port 3000_ #### Clone spee.ch - * release version for stable production + +- release version for stable production + ``` $ git clone -b release https://github.com/lbryio/spee.ch.git ``` - * master version for development + +- master version for development + ``` $ git clone https://github.com/lbryio/spee.ch.git ``` - * your own fork for customization -#### Change directory into your project +- your own fork for customization + +#### Change directory into your project + ``` $ cd spee.ch ``` #### Install node dependencies + ``` $ npm install ``` @@ -91,7 +99,8 @@ $ npm run start ``` #### View in browser - * Visit [http://localhost:3000](http://localhost:3000) in your browser + +- Visit [http://localhost:3000](http://localhost:3000) in your browser #### Customize your app @@ -99,34 +108,44 @@ Check out the [customization guide](https://github.com/lbryio/spee.ch/blob/readm #### (optional) add custom components and update the styles - * Create custom components by creating React components in `site/custom/src/` - * Update or override the CSS by changing the files in `site/custom/scss` +- Create custom components by creating React components in `site/custom/src/` +- Update or override the CSS by changing the files in `site/custom/scss` #### (optional) install your own chainquery + Instructions are coming at [lbry-docker] to install your own chainquery instance using docker-compose. This will require 50GB of preferably SSD space and at least 10 minutes to download, possibly much longer. +## Settings + +There are a number of settings available for customizing the behavior of your installation. +_INSERT LINK TO SETTINGS.MD_ + ## API + #### /api/claim/publish method: `POST` example: + ``` curl -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim/publish ``` + Parameters: - * `name` (required, must be unique across the instance) - * `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png) - * `nsfw` (optional) - * `license` (optional) - * `title` (optional) - * `description` (optional) - * `thumbnail` URL to thumbnail image, for .mp4 uploads only (optional) - * `channelName` channel to publish too (optional) - * `channelPassword` password for channel to publish too (optional, but required if `channelName` is provided) +- `name` (required, must be unique across the instance) +- `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png) +- `nsfw` (optional) +- `license` (optional) +- `title` (optional) +- `description` (optional) +- `thumbnail` URL to thumbnail image, for .mp4 uploads only (optional) +- `channelName` channel to publish too (optional) +- `channelPassword` password for channel to publish too (optional, but required if `channelName` is provided) response: + ``` { "success": , @@ -150,13 +169,17 @@ response: ``` #### /api/claim/availability/:name + method: `GET` example: + ``` curl https://spee.ch/api/claim/availability/doitlive ``` + response: + ``` { "success": , // `true` if spee.ch successfully checked the claim availability @@ -168,90 +191,98 @@ response: ## Contribute ### Stack -The spee.ch stack is MySQL, Express.js, Node.js, and React.js. Spee.ch also runs `lbrynet` on its server, and it uses the `lbrynet` API to make requests -- such as `publish`, `create_channel`, and `get` -- on the `LBRY` network. -Spee.ch also runs a sync tool, which decodes blocks from the `LBRY` blockchain as they are mined, and stores the information in MySQL. It stores all claims in the `Claims` table, and all channel claims in the `Certificates` table. +The spee.ch stack is MySQL, Express.js, Node.js, and React.js. Spee.ch also runs `lbrynet` on its server, and it uses the `lbrynet` API to make requests -- such as `publish`, `create_channel`, and `get` -- on the `LBRY` network. -* server - * [MySQL](https://www.mysql.com/) - * [express](https://www.npmjs.com/package/express) - * [node](https://nodejs.org/) - * [lbry](https://github.com/lbryio/lbry) - * [FFmpeg](https://www.ffmpeg.org/) -* client - * [react](https://reactjs.org/) - * redux - * sagas - * scss - * handlebars +Spee.ch also runs a sync tool, which decodes blocks from the `LBRY` blockchain as they are mined, and stores the information in MySQL. It stores all claims in the `Claims` table, and all channel claims in the `Certificates` table. +- server + - [MySQL](https://www.mysql.com/) + - [express](https://www.npmjs.com/package/express) + - [node](https://nodejs.org/) + - [lbry](https://github.com/lbryio/lbry) + - [FFmpeg](https://www.ffmpeg.org/) +- client + - [react](https://reactjs.org/) + - redux + - sagas + - scss + - handlebars ### Architecture -* `cli/` contains the code for the CLI tool. Running the tool will create `.json` config files and place them in the `site/config/` folder - * `configure.js` is the entry point for the CLI tool - * `cli/defaults/` holds default config files - * `cli/questions/` holds the questions that the CLI tool asks to build the config files -* `client/` contains all of the client code - * The client side of spee.ch uses `React` and `Redux` - * `client/src/index.js` is the entry point for the client side js. It checks for preloaded state, creates the store, and places the `` component in the document. - * `client/src/app.js` holds the `` component, which contains the routes for `react-router-dom` - * `client/src/` contains all of the JSX code for the app. When the app is built, the content of this folder is transpiled into the `client/build/` folder. - * The Redux code is broken up into `actions/` `reducers/` and `selectors/` - * The React components are broken up into `containers/` (components that pull props directly from the Redux store), `components/` ('dumb' components), and `pages/` - * spee.ch also uses sagas which are in the `sagas/` folders and `channels/` - * `client/scss/` contains the CSS for the project - * +- `cli/` contains the code for the CLI tool. Running the tool will create `.json` config files and place them in the `site/config/` folder -* `site/custom` is a folder which can be used to override the default components in `client/` - * The folder structure mimics that of the `client/` folder - * to customize spee.ch, place your own components and scss in the `site/custom/src/` and `site/custom/scss` folders. + - `configure.js` is the entry point for the CLI tool + - `cli/defaults/` holds default config files + - `cli/questions/` holds the questions that the CLI tool asks to build the config files -* `server/` contains all of the server code - * `index.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config files. - * `server/routes/` contains all of the routes for the express app - * `server/controllers/` contains all of the controllers for all of the routes - * `server/models/` contains all of the models which the app uses to interact with the `MySQL` database. - * Spee.ch uses the [sequelize](http://docs.sequelizejs.com/) ORM for communicating with the database. +- `client/` contains all of the client code -* `tests/` holds the end-to-end tests for this project - * Spee.ch uses `mocha` with the `chai` assertion library - * unit tests are located inside the project in-line with the files being tested and are designated with a `xxxx.test.js` file name + - The client side of spee.ch uses `React` and `Redux` + - `client/src/index.js` is the entry point for the client side js. It checks for preloaded state, creates the store, and places the `` component in the document. + - `client/src/app.js` holds the `` component, which contains the routes for `react-router-dom` + - `client/src/` contains all of the JSX code for the app. When the app is built, the content of this folder is transpiled into the `client/build/` folder. + - The Redux code is broken up into `actions/` `reducers/` and `selectors/` + - The React components are broken up into `containers/` (components that pull props directly from the Redux store), `components/` ('dumb' components), and `pages/` + - spee.ch also uses sagas which are in the `sagas/` folders and `channels/` + - `client/scss/` contains the CSS for the project \* + +- `site/custom` is a folder which can be used to override the default components in `client/` + + - The folder structure mimics that of the `client/` folder + - to customize spee.ch, place your own components and scss in the `site/custom/src/` and `site/custom/scss` folders. + +- `server/` contains all of the server code + + - `index.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config files. + - `server/routes/` contains all of the routes for the express app + - `server/controllers/` contains all of the controllers for all of the routes + - `server/models/` contains all of the models which the app uses to interact with the `MySQL` database. + - Spee.ch uses the [sequelize](http://docs.sequelizejs.com/) ORM for communicating with the database. + +- `tests/` holds the end-to-end tests for this project + - Spee.ch uses `mocha` with the `chai` assertion library + - unit tests are located inside the project in-line with the files being tested and are designated with a `xxxx.test.js` file name ### Tests -* This package uses `mocha` with `chai` for testing. -* Before running tests, create a `testingConfig.js` file in `devConfig/` by copying `testingConfig.example.js` -* To run tests: - * To run all tests, including those that require LBC (like publishing), simply run `npm test` - * To run only tests that do not require LBC, run `npm run test:no-lbc` + +- This package uses `mocha` with `chai` for testing. +- Before running tests, create a `testingConfig.js` file in `devConfig/` by copying `testingConfig.example.js` +- To run tests: + - To run all tests, including those that require LBC (like publishing), simply run `npm test` + - To run only tests that do not require LBC, run `npm run test:no-lbc` ### URL formats -Spee.ch has a few types of URL formats that return different assets from the LBRY network. Below is a list of all possible URLs for the content on spee.ch. You can learn more about LBRY URLs [here](https://lbry.tech/resources/uri). -* retrieve the controlling `LBRY` claim: - * https://spee.ch/`claim` - * https://spee.ch/`claim`.`ext` (serve) -* retrieve a specific `LBRY` claim: - * https://spee.ch/`claim_id`/`claim` - * https://spee.ch/`claim_id`/`claim`.`ext` (serve) -* retrieve all contents for the controlling `LBRY` channel - * https://spee.ch/`@channel` -* a specific `LBRY` channel - * https://spee.ch/`@channel`:`channel_id` -* retrieve a specific claim within the controlling `LBRY` channel - * https://spee.ch/`@channel`/`claim` - * https://spee.ch/`@channel`/`claim`.`ext` (serve) -* retrieve a specific claim within a specific `LBRY` channel - * https://spee.ch/`@channel`:`channel_id`/`claim` - * https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve) +Spee.ch has a few types of URL formats that return different assets from the LBRY network. Below is a list of all possible URLs for the content on spee.ch. You can learn more about LBRY URLs [here](https://lbry.tech/resources/uri). + +- retrieve the controlling `LBRY` claim: + - https://spee.ch/`claim` + - https://spee.ch/`claim`.`ext` (serve) +- retrieve a specific `LBRY` claim: + - https://spee.ch/`claim_id`/`claim` + - https://spee.ch/`claim_id`/`claim`.`ext` (serve) +- retrieve all contents for the controlling `LBRY` channel + - https://spee.ch/`@channel` +- a specific `LBRY` channel + - https://spee.ch/`@channel`:`channel_id` +- retrieve a specific claim within the controlling `LBRY` channel + - https://spee.ch/`@channel`/`claim` + - https://spee.ch/`@channel`/`claim`.`ext` (serve) +- retrieve a specific claim within a specific `LBRY` channel + - https://spee.ch/`@channel`:`channel_id`/`claim` + - https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve) ### Dependencies Spee.ch depends on two other lbry technologies: - * [chainquery](https://github.com/lbryio/chainquery) - a normalized database of the blockchain data. We've provided credentials to use a public chainquery service. You can also install it on your own server to avoid being affected by the commons. - * [lbrynet](https://github.com/lbryio/lbry) - a daemon that handles your wallet and transactions. + +- [chainquery](https://github.com/lbryio/chainquery) - a normalized database of the blockchain data. We've provided credentials to use a public chainquery service. You can also install it on your own server to avoid being affected by the commons. +- [lbrynet](https://github.com/lbryio/lbry) - a daemon that handles your wallet and transactions. ### Bugs + If you find a bug or experience a problem, please report your issue here on GitHub and find us in the lbry discord! ## License diff --git a/cli/defaults/siteConfig.json b/cli/defaults/siteConfig.json index 15243fce..8d5b3b96 100644 --- a/cli/defaults/siteConfig.json +++ b/cli/defaults/siteConfig.json @@ -42,11 +42,43 @@ }, "serving": { "markdownSettings": { - "skipHtml": true, - "privilegedDisallowedTypesDescriptions": ["Image"], - "privilegedDisallowedTypesMain": [], - "publicDisallowedTypesDescriptions": ["Image"], - "publicDisallowedTypesMain": [] + "skipHtmlMain": true, + "escapeHtmlMain": true, + "skipHtmlDescriptions": true, + "escapeHtmlDescriptions": true, + "allowedTypesMain": [], + "allowedTypesDescriptions": [], + "allowedTypesExample": [ + "see react-markdown docs", + "root", + "text", + "break", + "paragraph", + "emphasis", + "strong", + "thematicBreak", + "blockquote", + "delete", + "link", + "image", + "linkReference", + "imageReference", + "table", + "tableHead", + "tableBody", + "tableRow", + "tableCell", + "list", + "listItem", + "heading", + "inlineCode", + "code", + "html", + "parsedHtml" + ], + "disallowedTypesMain": [], + "disallowedTypesDescriptions": ["image", "html"], + "disallowedTypesExample": ["image", "html"] }, "customFileExtensions": { "application/example-type": "example" diff --git a/client/scss/_asset-display.scss b/client/scss/_asset-display.scss index b6d8dae0..7121f452 100644 --- a/client/scss/_asset-display.scss +++ b/client/scss/_asset-display.scss @@ -5,7 +5,7 @@ } .asset-document { - width: 100%; + max-width: 1000px; padding: $thin-padding; height: fit-content; box-sizing: border-box; @@ -16,15 +16,17 @@ } .asset-title { + max-width: 1000px; padding-bottom: $thin-padding; text-align: center; } .asset-image, .asset-video { - max-height: 95vh; - max-width: 95vw; + max-height: 75vh; + max-width: 85vw; object-fit: contain; object-position: center; + background: black; } /*below must die if this is intended to be shareable component! it also probably doesn't need to be*/ @@ -111,12 +113,15 @@ margin: $primary-padding; width: 100%; + @media (min-width: $break-point-tablet) { + padding: $primary-padding; + } @media (max-width: $break-point-tablet) { - margin: $primary-padding $secondary-padding; + padding: $tertiary-padding; } @media (max-width: $break-point-mobile) { - margin: $primary-padding 0; + margin: $tertiary-padding; } } @@ -125,4 +130,5 @@ padding-top: $primary-padding; margin-top: $primary-padding; color: $grey; + text-align: center; } diff --git a/client/scss/_markdown.scss b/client/scss/_markdown.scss index ed972f03..b835407f 100644 --- a/client/scss/_markdown.scss +++ b/client/scss/_markdown.scss @@ -1,22 +1,26 @@ .markdown-preview { - // Headers + + margin: $tertiary-padding 0px; + h1, h2, - h3, - h4, - h5, - h6 { + h3 { font-size: inherit; + font-weight: inherit; + margin: $tertiary-padding 0px; + } + + h4, h5, h6 { + font-size: $text-large; font-weight: 600; - margin-bottom: $tertiary-padding; - padding-top: $tertiary-padding; + margin: $tertiary-padding 0px; } // Paragraphs p { font-size: 1.15rem; - margin-bottom: $tertiary-padding; white-space: pre-line; + margin: $tertiary-padding 0px; svg { width: 1rem; @@ -29,10 +33,20 @@ } blockquote { - border-radius: 8px; background: $blockquote-background; padding: $tertiary-padding; min-width: 60%; + margin: $tertiary-padding; + p:first-child{ + margin-top: 0px; + } + p:last-child { + margin-bottom: 0px; + } + + div { + display: none; + } } // Strikethrough text @@ -42,10 +56,10 @@ // Tables table { width: 100%; - margin-bottom: 1.2rem; background-color: $base-color; border-spacing: 0; border: .5px solid $chrome-color; + margin: $tertiary-padding 0px; tr { td, @@ -77,9 +91,20 @@ margin-top: $tertiary-padding; padding: $secondary-padding; object-fit: scale-down; - max-width: 100%; - border: $subtle-border; box-sizing: border-box; + display: block; + margin-left: auto; + margin-right: auto; + max-width: 90vw; + } + + iframe { + display: block; + margin-left: auto; + margin-right: auto; + max-width: 90vw; + margin-top: $tertiary-padding 0px; + margin-bottom: $tertiary-padding 0px; } // Horizontal Rule @@ -114,7 +139,7 @@ a { color: $primary-color; - display: inline-block; + display: inline; } // Lists diff --git a/client/src/components/ErrorBoundary/index.jsx b/client/src/components/ErrorBoundary/index.jsx new file mode 100644 index 00000000..a52e38fc --- /dev/null +++ b/client/src/components/ErrorBoundary/index.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + componentDidCatch(error, info) { + // Display fallback UI + this.setState({ hasError: true }); + // You can also log the error to an error reporting service + console.log('Error occurred while rendering markdown') + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return (

A component was prevented from crashing the App.

); + } + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/client/src/components/FileViewer/index.jsx b/client/src/components/FileViewer/index.jsx index 0d3d4866..84b75b5d 100644 --- a/client/src/components/FileViewer/index.jsx +++ b/client/src/components/FileViewer/index.jsx @@ -1,7 +1,8 @@ import React from 'react'; -import ReactMarkdown from 'react-markdown'; -// TODO: get markdown settings from siteConfig - +import ReactMarkdown from 'react-markdown/with-html'; +import { serving } from '@config/siteConfig.json'; +import ErrorBoundary from '@components/ErrorBoundary'; +const { markdownSettings: { escapeHtmlMain, skipHtmlMain, allowedTypesMain } } = serving; class FileViewer extends React.Component { constructor (props) { @@ -39,7 +40,9 @@ class FileViewer extends React.Component {
{ this.state.fileLoaded && - + + + } { !this.state.fileLoaded && diff --git a/client/src/containers/AssetInfo/view.jsx b/client/src/containers/AssetInfo/view.jsx index c1e6154d..ea095b0d 100644 --- a/client/src/containers/AssetInfo/view.jsx +++ b/client/src/containers/AssetInfo/view.jsx @@ -12,7 +12,8 @@ import { createPermanentURI } from '@clientutils/createPermanentURI'; import ReactMarkdown from 'react-markdown'; const { details: { host } } = siteConfig; - +const { serving } = siteConfig; +const { markdownSettings: { escapeHtmlDescriptions, skipHtmlDescriptions, allowedTypesDescriptions } } = serving; class AssetInfo extends React.Component { render () { const { editable, asset } = this.props; @@ -38,7 +39,17 @@ class AssetInfo extends React.Component { { description && ( } - content={
} + content={ +
+ +
+ } /> )} {editable && ( diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 00000000..981132a3 --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,96 @@ +Settings found in cli/defaults/siteConfig.json will be copied to /site/config/siteConfig.json by running npm run configure + +You are encouraged to dig into those settings to make your installation behave how you wish. Below is a description of settings available. + +ANALYTICS: + + "googleId": null + +ASSET DEFAULTS: _These are some default values for publishes_ + + "title": "Default Content Title", + "description": "Default Content Description", + "thumbnail": "https://spee.ch/0e5d4e8f4086e13f5b9ca3f9648f518e5f524402/speechflag.png" + +DETAILS: + + "port": 3000, - this is the internal server port for the application_ + "title": "My Site", + "ipAddress": "", + "host": "https://www.example.com", - must contain "http(s)://" and if localhost, "http://localhost:3000" + "description": "A decentralized hosting platform built on LBRY", + "twitter": false, + "blockListEndpoint": - the LBRY default endpoint is generally for the US. Empty string "" negates. + +PUBLISHING: + + "primaryClaimAddress": null, - generally supplied by your lbrynet sdk + "uploadDirectory": "/home/lbry/Uploads", - lbrynet sdk will know your uploads are here + "lbrynetHome": "/home/lbry", + "thumbnailChannel": null, - when publishing non-image content, thumbnails will go here. + "thumbnailChannelId": null, + "additionalClaimAddresses": [], + "disabled": false, + "disabledMessage": "Default publishing disabled message", + "closedRegistration": false, - true: prevent new channels from being registered + "serveOnlyApproved": false, - true: prevent your site from serving up unapproved channels + "publishOnlyApproved": false, - true: restrict + "approvedChannels": [], - If either of the above two are true, ['@MyKittens', '@BobsKittens'] + "publishingChannelWhitelist": [], + "channelClaimBidAmount": "0.1", - When creating a channel, how much you deposit to control the name + "fileClaimBidAmount": "0.01", - When publishing content, how much you deposit to control the name + "maxSizeImage": 10000000, - You may not want people uploading 50GB files. 1000000 = 1MB + "maxSizeGif": 50000000, + "maxSizeVideo": 50000000 + +SERVING: + + "markdownSettings": { + "skipHtmlMain": true, - false: render html, in a somewhat unpredictable way~ + "escapeHtmlMain": true, - true: rather than render html, escape it and print it visibly + "skipHtmlDescriptions": true, - as above, for descriptions + "escapeHtmlDescriptions": true, - as above, for descriptions + "allowedTypesMain": [], - markdown rendered as main content + "allowedTypesDescriptions": [], - markdown rendered in description in content details + "allowedTypesExample": [ - here are examples of allowed types + "see react-markdown docs", `https://github.com/rexxars/react-markdown` + "root", + "text", + "break", + "paragraph", + "emphasis", + "strong", + "thematicBreak", + "blockquote", + "delete", + "link", + "image", - you may not have a lot of control over how these are rendered + "linkReference", + "imageReference", + "table", + "tableHead", + "tableBody", + "tableRow", + "tableCell", + "list", + "listItem", + "heading", + "inlineCode", + "code", + "html", - potentially DANGEROUS, intended for `serveOnlyApproved = true` environments, includes iframes, divs. + "parsedHtml" + ], + "disallowedTypesMain": [], - not implemented + "disallowedTypesDescriptions": ["image", "html"], - not implemented + "disallowedTypesExample": ["image", "html"] - not implemented + }, + "customFileExtensions": { - suggest a file extension for experimental content types you may be publishing + "application/example-type": "example" + } + +STARTUP: + + "performChecks": true, + "performUpdates": true + +} -- 2.45.3