Merge branch 'speech-as-a-package' of github.com:lbryio/spee.ch into folder-structure

This commit is contained in:
bill bittner 2018-03-19 13:48:26 -07:00
commit 149dca21d2
68 changed files with 567 additions and 383 deletions

View file

@ -1,3 +1,4 @@
node_modules/
public/
public/bundle
index.js
test

7
.gitignore vendored
View file

@ -1,7 +1,4 @@
node_modules
.idea
config/sequelizeCliConfig.js
config/speechConfig.js
public/bundle
server.js
webpack.config.js
/devConfig/sequelizeCliConfig.js
/devConfig/testingConfig.js

View file

@ -1,5 +1,5 @@
const path = require('path');
module.exports = {
'config': path.resolve('config', 'sequelizeCliConfig.js'),
}
'config': path.resolve('devConfig', 'sequelizeCliConfig.js'),
}

View file

@ -1,7 +1,7 @@
# Spee.ch
Spee.ch is a web app that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
## How to run this repository locally
##Installation
* start mysql
* install mysql
* create a database called `lbry`
@ -12,7 +12,8 @@ Spee.ch is a web app that reads and publishes images and videos to and from the
* start spee.ch
* clone this repo
* run `npm install`
* create your `speechConfig.js` file
* create your `
speechConfig.js` file
* copy `speechConfig.js.example` and name it `speechConfig.js`
* replace the `null` values in the config file with the appropriate values for your environment
* build the app by running `npm run build-prod`

8
config/lbryConfig.js Normal file
View file

@ -0,0 +1,8 @@
const lbryConfig = {
api: {
apiHost: 'localhost',
apiPort: '5279',
},
};
module.exports = lbryConfig;

View file

@ -1,21 +1,5 @@
module.exports = (winston, logLevel) => {
winston.configure({
transports: [
new (winston.transports.Console)({
level : logLevel,
timestamp : false,
colorize : true,
prettyPrint : true,
handleExceptions : true,
humanReadableUnhandledException: true,
}),
],
});
winston.error('Level 0');
winston.warn('Level 1');
winston.info('Level 2');
winston.verbose('Level 3');
winston.debug('Level 4');
winston.silly('Level 5');
const loggerConfig = {
logLevel: 'debug', // options: silly, debug, verbose, info
};
module.exports = loggerConfig;

16
config/mysqlConfig.js Normal file
View file

@ -0,0 +1,16 @@
function MysqlConfig () {
this.database = 'default';
this.username = 'default';
this.password = 'default';
this.configure = (config) => {
if (!config) {
return console.log('No MySQL config received.');
}
const {database, username, password} = config;
this.database = database;
this.username = username;
this.password = password;
};
};
module.exports = new MysqlConfig();

42
config/siteConfig.js Normal file
View file

@ -0,0 +1,42 @@
function SiteConfig () {
this.analytics = {
googleId: 'default',
};
this.assetDefaults = {
description: 'An asset published on Spee.ch',
thumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
title : 'Spee.ch',
};
this.auth = {
sessionKey: 'default',
};
this.details = {
description: 'Open-source, decentralized image and video sharing.',
host : 'default',
port : 3000,
title : 'Spee.ch',
twitter : '@spee_ch',
};
this.publishing = {
additionalClaimAddresses: [],
disabled : false,
disabledMessage : 'Please check back soon.',
primaryClaimAddress : 'default',
thumbnailChannel : 'default',
thumbnailChannelId : 'default',
uploadDirectory : '/home/lbry/Uploads',
};
this.configure = (config) => {
if (!config) {
return console.log('No site config received.');
}
const { analytics, assetDefaults, auth, details, publishing } = config;
this.analytics = analytics;
this.assetDefaults = assetDefaults;
this.auth = auth;
this.details = details;
this.publishing = publishing;
};
};
module.exports = new SiteConfig();

View file

@ -1,34 +1,16 @@
const { logging } = require('./speechConfig.js');
const { slackWebHook, slackErrorChannel, slackInfoChannel } = logging;
const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook;
module.exports = (winston) => {
if (slackWebHook) {
// add a transport for errors to slack
if (slackErrorChannel) {
winston.add(winstonSlackWebHook, {
name : 'slack-errors-transport',
level : 'warn',
webhookUrl: slackWebHook,
channel : slackErrorChannel,
username : 'spee.ch',
iconEmoji : ':face_with_head_bandage:',
});
};
if (slackInfoChannel) {
winston.add(winstonSlackWebHook, {
name : 'slack-info-transport',
level : 'info',
webhookUrl: slackWebHook,
channel : slackInfoChannel,
username : 'spee.ch',
iconEmoji : ':nerd_face:',
});
};
// send test message
winston.error('Slack "error" logging is online.');
winston.info('Slack "info" logging is online.');
} else {
winston.warn('Slack logging is not enabled because no slackWebHook config var provided.');
}
function SlackConfig () {
this.slackWebHook = 'default';
this.slackErrorChannel = 'default';
this.slackInfoChannel = 'default';
this.configure = (config) => {
if (!config) {
return console.log('No slack config received.');
}
const {slackWebHook, slackErrorChannel, slackInfoChannel} = config;
this.slackWebHook = slackWebHook;
this.slackErrorChannel = slackErrorChannel;
this.slackInfoChannel = slackInfoChannel;
};
};
module.exports = new SlackConfig();

View file

@ -1,49 +0,0 @@
module.exports = {
analytics: {
googleId: null, // google id for analytics tracking; leave `null` if not applicable
},
sql: {
database: 'lbry',
username: null, // username for mysql
password: null, // password for mysql
},
logging: {
logLevel : 'debug', // options: silly, debug, verbose, info
slackWebHook : null, // enter a webhook if you wish to push logs to slack; otherwise leave as `null`
slackErrorChannel: null, // enter a slack channel (#example) for errors to be sent to; otherwise leave null
slackInfoChannel : null, // enter a slack channel (#info) for info level logs to be sent to otherwise leave null
},
session: {
sessionKey: null, // enter a secret key to be used for session encryption
},
files: {
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
},
site: {
title : 'Spee.ch',
name : 'Spee.ch',
host : 'https://spee.ch',
description: 'Open-source, decentralized image and video sharing.',
},
publish: {
additionalClaimAddresses: [], // // optional: add previously used claim addresses
disabled : false,
primaryClaimAddress : null, // choose any address from your lbry wallet
thumbnailChannel : '@channelName', // create a channel to use for thumbnail images
thumbnailChannelId : 'xyz123...', // the channel_id (claim id) for the channel above
},
claim: {
defaultTitle : 'Spee.ch',
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
defaultDescription: 'Open-source, decentralized image and video sharing.',
},
testing: {
testChannel : '@testpublishchannel', // a channel to make test publishes in
testChannelId : 'xyz123...', // the claim id for the test channel
testChannelPassword: 'password', // password for the test channel
},
api: {
apiHost: 'localhost',
apiPort: '5279',
},
};

View file

@ -2,7 +2,7 @@ const logger = require('winston');
const db = require('../models');
const lbryApi = require('../helpers/lbryApi.js');
const publishHelpers = require('../helpers/publishHelpers.js');
const { publish : { primaryClaimAddress, additionalClaimAddresses } } = require('../config/speechConfig.js');
const { publishing: { primaryClaimAddress, additionalClaimAddresses } } = require('../config/siteConfig.js');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;

View file

@ -2,28 +2,6 @@ const logger = require('winston');
const db = require('../models');
module.exports = {
getTrendingClaims (startDate) {
logger.debug('retrieving trending');
return new Promise((resolve, reject) => {
// get the raw requests data
db.getTrendingFiles(startDate)
.then(fileArray => {
let claimsPromiseArray = [];
if (fileArray) {
fileArray.forEach(file => {
claimsPromiseArray.push(db.Claim.resolveClaim(file.name, file.claimId));
});
return Promise.all(claimsPromiseArray);
}
})
.then(claimsArray => {
resolve(claimsArray);
})
.catch(error => {
reject(error);
});
});
},
getRecentClaims () {
logger.debug('retrieving most recent claims');
return new Promise((resolve, reject) => {

View file

@ -0,0 +1,25 @@
const sequelizeCliConfig = {
development: {
username: 'lbry',
password: 'yYa5B6f7WuGq1613q9D7UWP3HT',
database: 'lbry',
host : '127.0.0.1',
dialect : 'mysql',
},
test: {
username: 'lbry',
password: 'yYa5B6f7WuGq1613q9D7UWP3HT',
database: 'lbry',
host : '127.0.0.1',
dialect : 'mysql',
},
production: {
username: 'lbry',
password: 'yYa5B6f7WuGq1613q9D7UWP3HT',
database: 'lbry',
host : '127.0.0.1',
dialect : 'mysql',
},
};
module.exports = sequelizeCliConfig;

View file

@ -0,0 +1,5 @@
module.exports = {
testChannel : null, // a channel to make test publishes in
testChannelId : null, // the claim id for the test channel
testChannelPassword: null, // password for the test channel
};

View file

@ -1,4 +1,3 @@
// const db = require('../models'); // require our models for syncing
const logger = require('winston');
module.exports = {

View file

@ -1,9 +1,7 @@
const config = require('../config/speechConfig.js');
const logger = require('winston');
module.exports = function () {
module.exports = (config) => {
// get the config file
for (let configCategoryKey in config) {
if (config.hasOwnProperty(configCategoryKey)) {
// get the final variables for each config category

View file

@ -0,0 +1,24 @@
const { logLevel } = require('../config/loggerConfig');
module.exports = (winston) => {
// configure
winston.configure({
transports: [
new (winston.transports.Console)({
level : logLevel,
timestamp : false,
colorize : true,
prettyPrint : true,
handleExceptions : true,
humanReadableUnhandledException: true,
}),
],
});
// test all the log levels
winston.error('Level 0');
winston.warn('Level 1');
winston.info('Level 2');
winston.verbose('Level 3');
winston.debug('Level 4');
winston.silly('Level 5');
};

34
helpers/configureSlack.js Normal file
View file

@ -0,0 +1,34 @@
const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook;
const slackConfig = require('../config/slackConfig.js');
module.exports = (winston) => {
const {slackWebHook, slackErrorChannel, slackInfoChannel} = slackConfig;
if (slackWebHook) {
// add a transport for errors to slack
if (slackErrorChannel) {
winston.add(winstonSlackWebHook, {
name : 'slack-errors-transport',
level : 'warn',
webhookUrl: slackWebHook,
channel : slackErrorChannel,
username : 'spee.ch',
iconEmoji : ':face_with_head_bandage:',
});
};
if (slackInfoChannel) {
winston.add(winstonSlackWebHook, {
name : 'slack-info-transport',
level : 'info',
webhookUrl: slackWebHook,
channel : slackInfoChannel,
username : 'spee.ch',
iconEmoji : ':nerd_face:',
});
};
// send test message
winston.error('Slack "error" logging is online.');
winston.info('Slack "info" logging is online.');
} else {
winston.warn('Slack logging is not enabled because no slackWebHook config var provided.');
}
};

View file

@ -1,6 +1,6 @@
const logger = require('winston');
const ua = require('universal-analytics');
const { analytics : { googleId }, site: { name: siteName } } = require('../config/speechConfig.js');
const { analytics : { googleId }, details: { title } } = require('../config/siteConfig.js');
function createServeEventParams (headers, ip, originalUrl) {
return {
@ -49,7 +49,7 @@ module.exports = {
},
sendGATimingEvent (category, variable, label, startTime, endTime) {
const params = createPublishTimingEventParams(category, variable, label, startTime, endTime);
sendGoogleAnalyticsTiming(siteName, params);
sendGoogleAnalyticsTiming(title, params);
},
chooseGaLbrynetPublishLabel ({ channel_name: channelName, channel_id: channelId }) {
return (channelName || channelId ? 'PUBLISH_IN_CHANNEL_CLAIM' : 'PUBLISH_ANONYMOUS_CLAIM');

View file

@ -1,7 +1,6 @@
const axios = require('axios');
const logger = require('winston');
const config = require('../config/speechConfig.js');
const { apiHost, apiPort } = config.api;
const { api: { apiHost, apiPort } } = require('../config/lbryConfig.js');
const lbryApiUri = 'http://' + apiHost + ':' + apiPort;
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('./googleAnalytics.js');

View file

@ -1,6 +1,7 @@
const logger = require('winston');
const fs = require('fs');
const { site, publish } = require('../config/speechConfig.js');
const { details, publishing } = require('../config/siteConfig.js');
module.exports = {
parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) {
@ -109,12 +110,12 @@ module.exports = {
metadata : {
description,
title,
author : site.title,
author : details.title,
language: 'en',
license,
nsfw,
},
claim_address: publish.primaryClaimAddress,
claim_address: publishing.primaryClaimAddress,
};
// add thumbnail to channel if video
if (thumbnail) {
@ -135,14 +136,14 @@ module.exports = {
metadata : {
title : `${claimName} thumbnail`,
description: `a thumbnail for ${claimName}`,
author : site.title,
author : details.title,
language : 'en',
license,
nsfw,
},
claim_address: publish.primaryClaimAddress,
channel_name : publish.thumbnailChannel,
channel_id : publish.thumbnailChannelId,
claim_address: publishing.primaryClaimAddress,
channel_name : publishing.thumbnailChannel,
channel_id : publishing.thumbnailChannelId,
};
},
deleteTemporaryFile (filePath) {

File diff suppressed because one or more lines are too long

1
index.js.map Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
const logger = require('winston');
const { returnShortId } = require('../helpers/sequelizeHelpers.js');
const { claim, site } = require('../config/speechConfig.js');
const { defaultThumbnail } = claim;
const { assetDefaults: { thumbnail: defaultThumbnail }, details: { host } } = require('../config/siteConfig.js');
function determineFileExtensionFromContentType (contentType) {
switch (contentType) {
@ -31,7 +30,7 @@ function prepareClaimData (claim) {
// logger.debug('preparing claim data based on resolved data:', claim);
claim['thumbnail'] = determineThumbnail(claim.thumbnail, defaultThumbnail);
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
claim['host'] = site.host;
claim['host'] = host;
return claim;
};

View file

@ -1,9 +1,9 @@
const Sequelize = require('sequelize');
const logger = require('winston');
const config = require('../config/speechConfig.js');
const { database, username, password } = config.sql;
const db = {};
console.log('exporting sequelize models');
const { database, username, password } = require('../config/mysqlConfig');
const db = {};
// set sequelize options
const sequelize = new Sequelize(database, username, password, {
host : 'localhost',
@ -18,6 +18,7 @@ const sequelize = new Sequelize(database, username, password, {
},
});
// establish mysql connection
sequelize
.authenticate()
.then(() => {
@ -27,7 +28,7 @@ sequelize
logger.error('Sequelize was unable to connect to the database:', err);
});
// manually add each model to the db
// manually add each model to the db object
const Certificate = require('./certificate.js');
const Channel = require('./channel.js');
const Claim = require('./claim.js');
@ -55,7 +56,9 @@ db.Sequelize = Sequelize;
// add an 'upsert' method to the db object
db.upsert = (Model, values, condition, tableName) => {
return Model
.findOne({ where: condition })
.findOne({
where: condition,
})
.then(obj => {
if (obj) { // update
logger.debug(`updating record in db.${tableName}`);
@ -71,9 +74,4 @@ db.upsert = (Model, values, condition, tableName) => {
});
};
// add a 'getTrendingFiles' method to the db object. note: change this to get claims directly. might need new association between Request and Claim
db.getTrendingFiles = (startDate) => {
return db.sequelize.query(`SELECT COUNT(*), File.* FROM Request LEFT JOIN File ON Request.FileId = File.id WHERE FileId IS NOT NULL AND nsfw != 1 AND trendingEligible = 1 AND Request.createdAt > "${startDate}" GROUP BY FileId ORDER BY COUNT(*) DESC LIMIT 25;`, { type: db.sequelize.QueryTypes.SELECT });
};
module.exports = db;

View file

@ -1,19 +1,19 @@
{
"name": "spee.ch",
"version": "0.0.1",
"description": "a single-serving site that reads and publishes images to and from the LBRY blockchain",
"main": "speech.js",
"description": "a web application that reads and publishes images to and from the LBRY blockchain",
"main": "index.js",
"scripts": {
"test": "mocha --recursive",
"test-all": "mocha --recursive",
"start": "node server.js",
"start-dev": "nodemon server.js",
"start": "node index.js",
"start-dev": "nodemon index.js",
"lint": "eslint .",
"fix": "eslint . --fix",
"precommit": "eslint .",
"babel": "babel",
"build-dev": "webpack --config webpack.dev.js",
"build-prod": "webpack --config webpack.prod.js"
"build": "webpack --config webpack.prod.js"
},
"repository": {
"type": "git",
@ -34,6 +34,7 @@
"axios": "^0.16.1",
"bcrypt": "^1.0.3",
"body-parser": "^1.17.1",
"babel-polyfill": "^6.26.0",
"config": "^1.26.1",
"connect-multiparty": "^2.0.0",
"cookie-session": "^2.0.0-beta.3",
@ -59,7 +60,6 @@
"request-promise": "^4.2.2",
"sequelize": "^4.1.0",
"sequelize-cli": "^3.0.0-3",
"sleep": "^5.1.1",
"universal-analytics": "^0.4.13",
"webpack-node-externals": "^1.6.0",
"whatwg-fetch": "^2.0.3",
@ -70,7 +70,6 @@
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",

View file

@ -1,8 +1,8 @@
const PassportLocalStrategy = require('passport-local').Strategy;
const db = require('../models');
const logger = require('winston');
const db = require('../models');
function returnUserAndChannelInfo (userInstance) {
const returnUserAndChannelInfo = (userInstance) => {
return new Promise((resolve, reject) => {
let userInfo = {};
userInfo['id'] = userInstance.id;
@ -22,7 +22,7 @@ function returnUserAndChannelInfo (userInstance) {
reject(error);
});
});
}
};
module.exports = new PassportLocalStrategy(
{
@ -61,5 +61,5 @@ module.exports = new PassportLocalStrategy(
.catch(error => {
return done(error);
});
}
},
);

View file

@ -1,7 +1,7 @@
const db = require('../models');
const PassportLocalStrategy = require('passport-local').Strategy;
const lbryApi = require('../helpers/lbryApi.js');
const logger = require('winston');
const db = require('../models');
module.exports = new PassportLocalStrategy(
{

28
public/bundle/bundle.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function getLongClaimId (name, modifier) {
export function getLongClaimId (host, name, modifier) {
let body = {};
// create request params
if (modifier) {
@ -24,12 +23,12 @@ export function getLongClaimId (name, modifier) {
return Request(url, params);
};
export function getShortId (name, claimId) {
export function getShortId (host, name, claimId) {
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
return Request(url);
};
export function getClaimData (name, claimId) {
export function getClaimData (host, name, claimId) {
const url = `${host}/api/claim/data/${name}/${claimId}`;
return Request(url);
};

View file

@ -1,13 +1,12 @@
import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function getChannelData (name, id) {
export function getChannelData (host, id, name) {
if (!id) id = 'none';
const url = `${host}/api/channel/data/${name}/${id}`;
return Request(url);
};
export function getChannelClaims (name, longId, page) {
export function getChannelClaims (host, longId, name, page) {
if (!page) page = 1;
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
return Request(url);

View file

@ -1,12 +1,11 @@
import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function checkFileAvailability (name, claimId) {
export function checkFileAvailability (claimId, host, name) {
const url = `${host}/api/file/availability/${name}/${claimId}`;
return Request(url);
}
export function triggerClaimGet (name, claimId) {
export function triggerClaimGet (claimId, host, name) {
const url = `${host}/api/claim/get/${name}/${claimId}`;
return Request(url);
}

View file

@ -4,7 +4,7 @@ import HomePage from 'components/HomePage';
import AboutPage from 'components/AboutPage';
import LoginPage from 'containers/LoginPage';
import ShowPage from 'containers/ShowPage';
import FourOhFourPage from 'components/FourOhFourPage';
import FourOhFourPage from 'containers/FourOhFourPage';
const App = () => {
return (

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({site: {defaults: { defaultThumbnail }}}) => {
return {
defaultThumbnail,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,8 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
const { claim: { defaultThumbnail } } = require('../../../config/speechConfig.js');
const AssetPreview = ({ claimData: { name, claimId, fileExt, contentType, thumbnail } }) => {
const AssetPreview = ({ defaultThumbnail, claimData: { name, claimId, fileExt, contentType, thumbnail } }) => {
const directSourceLink = `${claimId}/${name}.${fileExt}`;
const showUrlLink = `/${claimId}/${name}`;
return (

View file

@ -1,10 +1,9 @@
import React from 'react';
import GoogleAnalytics from 'react-ga';
import { withRouter } from 'react-router-dom';
const config = require('../../../config/speechConfig.js');
const googleApiKey = config.analytics.googleId;
const { analytics: { googleId } } = require('../../../config/siteConfig.js');
GoogleAnalytics.initialize(googleApiKey);
GoogleAnalytics.initialize(googleId);
class GAListener extends React.Component {
componentDidMount () {

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
class Preview extends React.Component {
class PublishPreview extends React.Component {
constructor (props) {
super(props);
this.state = {
@ -53,10 +53,10 @@ class Preview extends React.Component {
}
};
Preview.propTypes = {
PublishPreview.propTypes = {
dimPreview: PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
thumbnail : PropTypes.object,
};
export default Preview;
export default PublishPreview;

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ site }) => {
const { defaultDescription, defaultThumbnail, description: siteDescription, host: siteHost, title: siteTitle, twitter: siteTwitter } = site;
return {
defaultDescription,
defaultThumbnail,
siteDescription,
siteHost,
siteTitle,
siteTwitter,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -8,10 +8,16 @@ import { createCanonicalLink } from 'utils/canonicalLink';
class SEO extends React.Component {
render () {
let { pageTitle, asset, channel, pageUri } = this.props;
pageTitle = createPageTitle(pageTitle);
const metaTags = createMetaTags(asset, channel);
const canonicalLink = createCanonicalLink(asset, channel, pageUri);
// props from state
const { defaultDescription, defaultThumbnail, siteDescription, siteHost, siteTitle, siteTwitter } = this.props;
// props from parent
const { asset, channel, pageUri } = this.props;
let { pageTitle } = this.props;
// create page title, tags, and canonical link
pageTitle = createPageTitle(siteTitle, pageTitle);
const metaTags = createMetaTags(siteDescription, siteHost, siteTitle, siteTwitter, asset, channel, defaultDescription, defaultThumbnail);
const canonicalLink = createCanonicalLink(asset, channel, pageUri, siteHost);
// render results
return (
<Helmet
title={pageTitle}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { validateFile } from 'utils/file';
import Preview from 'components/Preview';
import PublishPreview from 'components/PublishPreview';
class Dropzone extends React.Component {
constructor (props) {
@ -87,7 +87,7 @@ class Dropzone extends React.Component {
<div id='preview-dropzone' className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
{this.props.file ? (
<div>
<Preview
<PublishPreview
dimPreview={this.state.dimPreview}
file={this.props.file}
thumbnail={this.props.thumbnail}

View file

@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ site: { host, title } }) => {
return {
host,
title,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,10 +1,10 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import Helmet from 'react-helmet';
const { site: { title, host } } = require('../../../config/speechConfig.js');
class FourOhForPage extends React.Component {
render () {
const {title, host} = this.props;
return (
<div>
<Helmet>

View file

@ -1,13 +1,14 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import {updateSelectedChannel} from 'actions/publish';
import View from './view';
import {updateSelectedChannel} from '../../actions/publish';
const mapStateToProps = ({ channel }) => {
const mapStateToProps = ({ channel, site }) => {
return {
channelName : channel.loggedInChannel.name,
channelShortId: channel.loggedInChannel.shortId,
channelLongId : channel.loggedInChannel.longId,
siteDescription: site.description,
};
};

View file

@ -53,12 +53,13 @@ class NavBar extends React.Component {
}
}
render () {
const { siteDescription } = this.props;
return (
<div className='row row--wide nav-bar'>
<div className='row row--padded row--short flex-container--row flex-container--space-between-center'>
<Logo />
<div className='nav-bar--center'>
<span className='nav-bar-tagline'>Open-source, decentralized image and video sharing.</span>
<span className='nav-bar-tagline'>{siteDescription}</span>
</div>
<div className='nav-bar--right'>
<NavLink className='nav-bar-link link--nav' activeClassName='link--nav-active' to='/' exact>Publish</NavLink>

View file

@ -0,0 +1,10 @@
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
message: publish.disabledMessage,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -0,0 +1,16 @@
import React from 'react';
class PublishDisabledMessage extends React.Component {
render () {
const message = this.props.message;
console.log('this.props.message:', message);
return (
<div className='row dropzone--disabled row--tall flex-container--column flex-container--center-center'>
<p className='text--disabled'>Publishing is currently disabled.</p>
<p className='text--disabled'>{message}</p>
</div>
);
}
}
export default PublishDisabledMessage;

View file

@ -2,27 +2,28 @@ import React from 'react';
import Dropzone from 'containers/Dropzone';
import PublishDetails from 'containers/PublishDetails';
import PublishStatus from 'containers/PublishStatus';
import PublishDisabledMessage from 'containers/PublishDisabledMessage';
class PublishTool extends React.Component {
render () {
if (this.props.disabled) {
console.log('publish is disabled');
return (
<div className='row dropzone--disabled row--tall flex-container--column flex-container--center-center'>
<p className='text--disabled'>Publishing is temporarily disabled.</p>
<p className='text--disabled'>Please check back soon or join our <a className='link--disabled-text' href='https://discord.gg/YjYbwhS'>discord channel</a> for updates.</p>
</div>
<PublishDisabledMessage />
);
}
if (this.props.file) {
if (this.props.status) {
return (
<PublishStatus />
);
} else {
return <PublishDetails />;
} else {
console.log('publish is not disabled');
if (this.props.file) {
if (this.props.status) {
return (
<PublishStatus />
);
} else {
return <PublishDetails />;
}
}
return <Dropzone />;
}
return <Dropzone />;
}
};

View file

@ -1,9 +1,10 @@
import * as actions from 'constants/publish_action_types';
import { LOGIN } from 'constants/publish_channel_select_states';
const { publish } = require('../../config/speechConfig.js');
const { publishing } = require('../../config/siteConfig.js');
const initialState = {
disabled : publish.disabled,
disabled : publishing.disabled,
disabledMessage : publishing.disabledMessage,
publishInChannel : false,
selectedChannel : LOGIN,
showMetadataInputs: false,
@ -25,9 +26,7 @@ const initialState = {
license : '',
nsfw : false,
},
thumbnailChannel : publish.thumbnailChannel,
thumbnailChannelId: publish.thumbnailChannelId,
thumbnail : null,
thumbnail: null,
};
export default function (state = initialState, action) {

View file

@ -1,7 +1,29 @@
const { site } = require('../../config/speechConfig.js');
const siteConfig = require('../../config/siteConfig.js');
const {
analytics: {
googleId: googleAnalyticsId,
},
assetDefaults: {
thumbnail: defaultThumbnail,
description: defaultDescription,
},
details: {
description,
host,
title,
twitter,
},
} = siteConfig;
const initialState = {
host: site.host,
description,
googleAnalyticsId,
host,
title,
twitter,
defaultDescription,
defaultThumbnail,
};
export default function (state = initialState, action) {

View file

@ -1,16 +1,18 @@
import { call, put, takeLatest } from 'redux-saga/effects';
import {call, put, select, takeLatest} from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { updateFileAvailability, updateDisplayAssetError } from 'actions/show';
import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
import { checkFileAvailability, triggerClaimGet } from 'api/fileApi';
import { selectSiteHost } from 'selectors/site';
function * retrieveFile (action) {
const name = action.data.name;
const claimId = action.data.claimId;
const host = yield select(selectSiteHost);
// see if the file is available
let isAvailable;
try {
({ data: isAvailable } = yield call(checkFileAvailability, name, claimId));
({ data: isAvailable } = yield call(checkFileAvailability, claimId, host, name));
} catch (error) {
return yield put(updateDisplayAssetError(error.message));
};
@ -21,7 +23,7 @@ function * retrieveFile (action) {
yield put(updateFileAvailability(UNAVAILABLE));
// initiate get request for the file
try {
yield call(triggerClaimGet, name, claimId);
yield call(triggerClaimGet, claimId, host, name);
} catch (error) {
return yield put(updateDisplayAssetError(error.message));
};

View file

@ -3,6 +3,7 @@ import * as actions from 'constants/show_action_types';
import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
import { selectShowState } from 'selectors/show';
import { selectSiteHost } from 'selectors/site';
export function * newAssetRequest (action) {
const { requestType, requestId, name, modifier } = action.data;
@ -11,13 +12,14 @@ export function * newAssetRequest (action) {
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get long id && add request to request list
let longId;
try {
({data: longId} = yield call(getLongClaimId, name, modifier));
({data: longId} = yield call(getLongClaimId, host, name, modifier));
} catch (error) {
return yield put(onRequestError(error.message));
}
@ -31,14 +33,14 @@ export function * newAssetRequest (action) {
// get short Id
let shortId;
try {
({data: shortId} = yield call(getShortId, name, longId));
({data: shortId} = yield call(getShortId, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// get asset claim data
let claimData;
try {
({data: claimData} = yield call(getClaimData, name, longId));
({data: claimData} = yield call(getClaimData, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}

View file

@ -3,6 +3,7 @@ import * as actions from 'constants/show_action_types';
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
import { getChannelClaims, getChannelData } from 'api/channelApi';
import { selectShowState } from 'selectors/show';
import { selectSiteHost } from 'selectors/site';
export function * newChannelRequest (action) {
const { requestType, requestId, channelName, channelId } = action.data;
@ -11,13 +12,14 @@ export function * newChannelRequest (action) {
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get channel long id
let longId, shortId;
try {
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId));
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, host, channelName, channelId));
} catch (error) {
return yield put(onRequestError(error.message));
}
@ -32,7 +34,7 @@ export function * newChannelRequest (action) {
// get channel claims data
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1));
({ data: claimsData } = yield call(getChannelClaims, host, longId, channelName, 1));
} catch (error) {
return yield put(onRequestError(error.message));
}
@ -48,9 +50,10 @@ export function * watchNewChannelRequest () {
function * getNewClaimsAndUpdateChannel (action) {
const { channelKey, name, longId, page } = action.data;
const host = yield select(selectSiteHost);
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, name, longId, page));
({ data: claimsData } = yield call(getChannelClaims, host, longId, name, page));
} catch (error) {
return yield put(onRequestError(error.message));
}

View file

@ -1,3 +1,7 @@
export const selectSiteState = (state) => {
return state.site;
};
export const selectSiteHost = (state) => {
return state.site.host;
};

View file

@ -1,37 +1,29 @@
const { site: { host } } = require('../../config/speechConfig.js');
const createBasicCanonicalLink = (page) => {
if (!page) {
return `${host}`;
};
return `${host}/${page}`;
const createBasicCanonicalLink = (page, siteHost) => {
return `${siteHost}/${page}`;
};
const createAssetCanonicalLink = (asset) => {
const createAssetCanonicalLink = (asset, siteHost) => {
let channelName, certificateId, name, claimId;
if (asset.claimData) {
({ channelName, certificateId, name, claimId } = asset.claimData);
};
if (channelName) {
return `${host}/${channelName}:${certificateId}/${name}`;
return `${siteHost}/${channelName}:${certificateId}/${name}`;
};
return `${host}/${claimId}/${name}`;
return `${siteHost}/${claimId}/${name}`;
};
const createChannelCanonicalLink = (channel) => {
const createChannelCanonicalLink = (channel, siteHost) => {
const { name, longId } = channel;
return `${host}/${name}:${longId}`;
return `${siteHost}/${name}:${longId}`;
};
export const createCanonicalLink = (asset, channel, page) => {
export const createCanonicalLink = (asset, channel, page, siteHost) => {
if (asset) {
return createAssetCanonicalLink(asset);
return createAssetCanonicalLink(asset, siteHost);
}
if (channel) {
return createChannelCanonicalLink(channel);
return createChannelCanonicalLink(channel, siteHost);
}
if (page) {
return createBasicCanonicalLink(page);
}
return createBasicCanonicalLink();
return createBasicCanonicalLink(page, siteHost);
};

View file

@ -1,5 +1,3 @@
const { site: { title, host, description }, claim: { defaultThumbnail, defaultDescription } } = require('../../config/speechConfig.js');
const determineOgThumbnailContentType = (thumbnail) => {
if (thumbnail) {
const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.'));
@ -20,35 +18,35 @@ const determineOgThumbnailContentType = (thumbnail) => {
return '';
};
const createBasicMetaTags = () => {
const createBasicMetaTags = (siteHost, siteDescription, siteTitle, siteTwitter) => {
return [
{property: 'og:title', content: title},
{property: 'og:url', content: host},
{property: 'og:site_name', content: title},
{property: 'og:description', content: description},
{property: 'twitter:site', content: '@spee_ch'},
{property: 'og:title', content: siteTitle},
{property: 'og:url', content: siteHost},
{property: 'og:site_name', content: siteTitle},
{property: 'og:description', content: siteDescription},
{property: 'twitter:site', content: siteTwitter},
{property: 'twitter:card', content: 'summary'},
];
};
const createChannelMetaTags = (channel) => {
const createChannelMetaTags = (siteTitle, siteHost, siteTwitter, channel) => {
const { name, longId } = channel;
return [
{property: 'og:title', content: `${name} on ${title}`},
{property: 'og:url', content: `${host}/${name}:${longId}`},
{property: 'og:site_name', content: title},
{property: 'og:description', content: `${name}, a channel on ${title}`},
{property: 'twitter:site', content: '@spee_ch'},
{property: 'og:title', content: `${name} on ${siteTitle}`},
{property: 'og:url', content: `${siteHost}/${name}:${longId}`},
{property: 'og:site_name', content: siteTitle},
{property: 'og:description', content: `${name}, a channel on ${siteTitle}`},
{property: 'twitter:site', content: siteTwitter},
{property: 'twitter:card', content: 'summary'},
];
};
const createAssetMetaTags = (asset) => {
const createAssetMetaTags = (siteHost, siteTitle, siteTwitter, asset, defaultDescription, defaultThumbnail) => {
const { claimData } = asset;
const { contentType } = claimData;
const embedUrl = `${host}/${claimData.claimId}/${claimData.name}`;
const showUrl = `${host}/${claimData.claimId}/${claimData.name}`;
const source = `${host}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`;
const embedUrl = `${siteHost}/${claimData.claimId}/${claimData.name}`;
const showUrl = `${siteHost}/${claimData.claimId}/${claimData.name}`;
const source = `${siteHost}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`;
const ogTitle = claimData.title || claimData.name;
const ogDescription = claimData.description || defaultDescription;
const ogThumbnailContentType = determineOgThumbnailContentType(claimData.thumbnail);
@ -56,11 +54,11 @@ const createAssetMetaTags = (asset) => {
const metaTags = [
{property: 'og:title', content: ogTitle},
{property: 'og:url', content: showUrl},
{property: 'og:site_name', content: title},
{property: 'og:site_name', content: siteTitle},
{property: 'og:description', content: ogDescription},
{property: 'og:image:width', content: 600},
{property: 'og:image:height', content: 315},
{property: 'twitter:site', content: '@spee_ch'},
{property: 'twitter:site', content: siteTwitter},
];
if (contentType === 'video/mp4' || contentType === 'video/webm') {
metaTags.push({property: 'og:video', content: source});
@ -85,12 +83,12 @@ const createAssetMetaTags = (asset) => {
return metaTags;
};
export const createMetaTags = (asset, channel) => {
export const createMetaTags = (siteDescription, siteHost, siteTitle, siteTwitter, asset, channel, defaultDescription, defaultThumbnail) => {
if (asset) {
return createAssetMetaTags(asset);
return createAssetMetaTags(siteHost, siteTitle, siteTwitter, asset, defaultDescription, defaultThumbnail);
};
if (channel) {
return createChannelMetaTags(channel);
return createChannelMetaTags(siteHost, siteTitle, siteTwitter, channel);
};
return createBasicMetaTags();
return createBasicMetaTags(siteDescription, siteHost, siteTitle, siteTwitter);
};

View file

@ -1,6 +1,4 @@
const { site: { title: siteTitle } } = require('../../config/speechConfig.js');
export const createPageTitle = (pageTitle) => {
export const createPageTitle = (siteTitle, pageTitle) => {
if (!pageTitle) {
return `${siteTitle}`;
}

View file

@ -1,7 +1,7 @@
const logger = require('winston');
const multipart = require('connect-multiparty');
const { files, site } = require('../config/speechConfig.js');
const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
const { publishing: { uploadDirectory }, details: { host } } = require('../config/siteConfig.js');
const multipartMiddleware = multipart({uploadDir: uploadDirectory});
const db = require('../models');
const { claimNameIsAvailable, checkChannelAvailability, publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
@ -168,7 +168,7 @@ module.exports = (app) => {
data : {
name,
claimId: result.claim_id,
url : `${site.host}/${result.claim_id}/${name}`,
url : `${host}/${result.claim_id}/${name}`,
lbryTx : result,
},
});

View file

@ -15,7 +15,6 @@ module.exports = (app) => {
// route for log in
app.post('/login', (req, res, next) => {
passport.authenticate('local-login', (err, user, info) => {
logger.debug('info:', info);
if (err) {
return next(err);
}

View file

@ -1,4 +1,4 @@
const { site } = require('../config/speechConfig.js');
const { details: host } = require('../config/siteConfig.js');
const handlePageRender = require('../helpers/handlePageRender.jsx');
module.exports = (app) => {
@ -29,7 +29,6 @@ module.exports = (app) => {
app.get('/embed/:claimId/:name', ({ params }, res) => {
const claimId = params.claimId;
const name = params.name;
const host = site.host;
// get and render the content
res.status(200).render('embed', { layout: 'embed', host, claimId, name });
});

106
server.js Normal file
View file

@ -0,0 +1,106 @@
// app dependencies
const express = require('express');
const bodyParser = require('body-parser');
const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars');
const helmet = require('helmet');
const passport = require('passport');
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
const cookieSession = require('cookie-session');
const http = require('http');
// logging dependencies
const logger = require('winston');
function SpeechServer ({ mysqlConfig, siteConfig, slackConfig }) {
this.start = () => {
this.configureConfigFiles();
this.configureLogging();
this.configureApp();
this.configureServer();
this.startServer();
};
this.configureConfigFiles = () => {
const mysqlAppConfig = require('./config/mysqlConfig.js');
mysqlAppConfig.configure(mysqlConfig);
const siteAppConfig = require('./config/siteConfig.js');
siteAppConfig.configure(siteConfig);
this.PORT = siteAppConfig.details.port;
const slackAppConfig = require('./config/slackConfig.js');
slackAppConfig.configure(slackConfig);
};
this.configureLogging = () => {
require('./helpers/configureLogger.js')(logger);
require('./helpers/configureSlack.js')(logger);
};
this.configureApp = () => {
const app = express(); // create an Express application
// trust the proxy to get ip address for us
app.enable('trust proxy');
// add middleware
app.use(helmet()); // set HTTP headers to protect against well-known web vulnerabilties
app.use(express.static(`${__dirname}/public`)); // 'express.static' to serve static files from public directory
app.use(bodyParser.json()); // 'body parser' for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing application/x-www-form-urlencoded
app.use((req, res, next) => { // custom logging middleware to log all incoming http requests
logger.verbose(`Request on ${req.originalUrl} from ${req.ip}`);
next();
});
// configure passport
passport.serializeUser(serializeSpeechUser);
passport.deserializeUser(deserializeSpeechUser);
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
passport.use('local-login', localLoginStrategy);
// initialize passport
app.use(cookieSession({
name : 'session',
keys : [siteConfig.auth.sessionKey],
maxAge: 24 * 60 * 60 * 1000, // i.e. 24 hours
}));
app.use(passport.initialize());
app.use(passport.session());
// configure handlebars & register it with express app
const hbs = expressHandlebars.create({
defaultLayout: 'embed',
handlebars : Handlebars,
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// middleware to pass user info back to client (for handlebars access), if user is logged in
app.use(populateLocalsDotUser); // note: I don't think I need this any more?
// set the routes on the app
require('./routes/auth-routes.js')(app);
require('./routes/api-routes.js')(app);
require('./routes/page-routes.js')(app);
require('./routes/serve-routes.js')(app);
require('./routes/fallback-routes.js')(app);
this.app = app;
};
this.configureServer = () => {
this.server = http.Server(this.app);
};
this.startServer = () => {
const db = require('./models');
// sync sequelize
db.sequelize.sync()
// start the server
.then(() => {
this.server.listen(this.PORT, () => {
logger.info(`Server is listening on PORT ${this.PORT}`);
});
})
.catch((error) => {
logger.error(`Startup Error:`, error);
});
};
};
module.exports = SpeechServer;

View file

@ -2,9 +2,7 @@
const logger = require('winston');
const db = require('../models'); // require our models for syncing
// configure logging
const config = require('../config/speechConfig.js');
const { logLevel } = config.logging;
require('../config/loggerConfig.js')(logger, logLevel);
require('../helpers/configureLogger.js')(logger);
let totalClaims = 0;
let totalClaimsNoCertificate = 0;

View file

@ -1,10 +1,8 @@
// load dependencies
const logger = require('winston');
const db = require('../models'); // require our models for syncing
const db = require('../models');
// configure logging
const config = require('../config/speechConfig.js');
const { logLevel } = config.logging;
require('../config/loggerConfig.js')(logger, logLevel);
require('../helpers/configureLogger.js')(logger);
const userName = process.argv[2];
logger.debug('user name:', userName);

View file

@ -1,9 +1,8 @@
const chai = require('chai');
const expect = chai.expect;
const chaiHttp = require('chai-http');
const { site, testing } = require('../../config/speechConfig.js');
const { host } = site;
const { testChannel, testChannelId, testChannelPassword } = testing;
const { details: { host } } = require('../../config/siteConfig.js');
const { testChannel, testChannelId, testChannelPassword } = require('../../devConfig/testingConfig.js');
const requestTimeout = 20000;
const publishTimeout = 120000;
const fs = require('fs');

7
webpack.config.js Normal file
View file

@ -0,0 +1,7 @@
const serverBaseConfig = require('./webpack.server.common.js');
const clientBaseConfig = require('./webpack.client.common.js');
module.exports = [
serverBaseConfig,
clientBaseConfig,
];

View file

@ -8,11 +8,13 @@ module.exports = {
__dirname: false,
},
externals: [nodeExternals()],
entry : ['babel-polyfill', 'whatwg-fetch', './index.js'],
entry : ['babel-polyfill', 'whatwg-fetch', './server.js'],
output : {
path : Path.join(__dirname, '/'),
publicPath: '/',
filename : 'server.js',
path : Path.join(__dirname, '/'),
publicPath : '/',
filename : 'index.js',
library : '',
libraryTarget: 'commonjs-module',
},
module: {
rules: [