diff --git a/.gitignore b/.gitignore index b512c09d..63559f2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +ApiPublishTest.html \ No newline at end of file diff --git a/README.md b/README.md index ae330592..45b9701f 100644 --- a/README.md +++ b/README.md @@ -32,5 +32,26 @@ spee.ch is a single-serving site that reads and publishes images to and from the * To view a batch of files at a claim * E.g. spee.ch/doitlive/all +## API + +#### GET +* /api/resolve/:name + * a successfull request returns the resolve results for the claim at that name in JSON format +* /api/claim_list/:name + * a successfull request returns a list of claims at that claim name in JSON format + +#### POST +* /api/publish + * request parameters: + * body (form-data): + * claim: string (optional, defults to the file's name sans extension) + * license: string (optional, defaults to "No License Provided") + * nsfw: string ("on"/"off") or boolean (true/false). (optional, defaults `true`) + * files: + * (the `files` object submitted must use "file1" or "null" as the key for the file's value object) + * a successfull request will return the transaction details resulting from your published claim in JSON format + + + ## bugs If you find a bug or experience a problem, please report your issue here on github and find us in the lbry slack! diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index cb9b9aa3..be110732 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -1,6 +1,6 @@ { "WalletConfig": { - "lbryAddress": "LBRY_WALLET_ADDRESS" + "LbryAddress": "LBRY_WALLET_ADDRESS" }, "Database": { "MySqlConnectionUri": "MYSQL_CONNECTION_STRING" diff --git a/config/default.json b/config/default.json index 2b06f70a..546378c7 100644 --- a/config/default.json +++ b/config/default.json @@ -1,9 +1,9 @@ { "WalletConfig": { - "lbryAddress": "none" + "LbryAddress": "none" }, "AnalyticsConfig":{ - "googleId": "none" + "GoogleId": "none" }, "Database": { "MySqlConnectionUri": "none", diff --git a/config/development.json b/config/development.json index 2712bde3..38c7affe 100644 --- a/config/development.json +++ b/config/development.json @@ -1,9 +1,9 @@ { "WalletConfig": { - "lbryAddress": "none" + "LbryAddress": "none" }, "AnalyticsConfig":{ - "googleId": "UA-100747990-1" + "GoogleId": "UA-100747990-1" }, "Database": { "MySqlConnectionUri": "none", diff --git a/helpers/logging/loggerSetup.js b/config/loggerSetup.js similarity index 100% rename from helpers/logging/loggerSetup.js rename to config/loggerSetup.js diff --git a/config/production.json b/config/production.json index 7e7e70bb..56fde915 100644 --- a/config/production.json +++ b/config/production.json @@ -1,9 +1,9 @@ { "WalletConfig": { - "lbryAddress": "none" + "LbryAddress": "none" }, "AnalyticsConfig":{ - "googleId": "UA-60403362-2" + "GoogleId": "UA-60403362-2" }, "Database": { "MySqlConnectionUri": "none", diff --git a/config/test.json b/config/test.json index c20a8c16..b74d2997 100644 --- a/config/test.json +++ b/config/test.json @@ -1,9 +1,9 @@ { "WalletConfig": { - "lbryAddress": "none" + "LbryAddress": "none" }, "AnalyticsConfig":{ - "googleId": "UA-100747990-1" + "GoogleId": "UA-100747990-1" }, "Database": { "MySqlConnectionUri": "none", diff --git a/controllers/publishController.js b/controllers/publishController.js index abf3dca8..51d5ddfc 100644 --- a/controllers/publishController.js +++ b/controllers/publishController.js @@ -1,32 +1,8 @@ const fs = require('fs'); const logger = require('winston'); const lbryApi = require('../helpers/libraries/lbryApi.js'); -const config = require('config'); -const walledAddress = config.get('WalletConfig.lbryAddress'); -const errorHandlers = require('../helpers/libraries/errorHandlers.js'); const db = require('../models'); -function createPublishParams (claim, filePath, license, nsfw) { - logger.debug(`Creating Publish Parameters for "${claim}"`); - const publishParams = { - name : claim, - file_path: filePath, - bid : 0.01, - metadata : { - description: `${claim} published via spee.ch`, - title : claim, - author : 'spee.ch', - language : 'en', - license, - nsfw, - }, - claim_address : walledAddress, - change_address: walledAddress, - }; - logger.debug('publishParams:', publishParams); - return publishParams; -} - function deleteTemporaryFile (filePath) { fs.unlink(filePath, err => { if (err) throw err; @@ -49,51 +25,45 @@ function upsert (Model, values, condition) { } module.exports = { - publish (name, fileName, filePath, fileType, license, nsfw, socket, visitor) { - console.log('nsfw:', nsfw); - // validate nsfw - if (typeof nsfw === 'string') { - nsfw = (nsfw.toLowerCase() === 'true'); - } - // update the client - socket.emit('publish-status', 'Your image is being published (this might take a second)...'); - // send to analytics - visitor.event('Publish Route', 'Publish Request', filePath).send(); - // create the publish object - const publishParams = createPublishParams(name, filePath, license, nsfw); - // 1. publish the file - lbryApi - .publishClaim(publishParams, fileName, fileType) - .then(result => { - logger.info(`Successfully published ${fileName}`, result); - // google analytics - visitor.event('Publish Route', 'Publish Success', filePath).send(); - // 2. update old record of create new one (update is in case the claim has been published before by this daemon) - upsert( - db.File, - { - name, - claimId : result.claim_id, - outpoint: `${result.txid}:${result.nout}`, - height : 0, - fileName, - filePath, - fileType, - nsfw, - }, - { name, claimId: result.claim_id } - ).catch(error => { - logger.error('Sequelize findOne error', error); + publish: (publishParams, fileName, fileType) => { + const deferred = new Promise((resolve, reject) => { + // 1. publish the file + lbryApi + .publishClaim(publishParams) + .then(result => { + logger.info(`Successfully published ${fileName}`, result); + // 2. update old record of create new one (update is in case the claim has been published before by this daemon) + upsert( + db.File, + { + name : publishParams.name, + claimId : result.claim_id, + outpoint: `${result.txid}:${result.nout}`, + height : 0, + fileName, + filePath: publishParams.file_path, + fileType, + nsfw : publishParams.metadata.nsfw, + }, + { name: publishParams.name, claimId: result.claim_id } + ).then(() => { + // resolve the promise with the result from lbryApi.publishClaim; + resolve(result); + }) + .catch(error => { + logger.error('Sequelize findOne error', error); + // reject the promise + reject(error); + }); + }) + .catch(error => { + logger.error(`Error publishing ${fileName}`, error); + // delete the local file + deleteTemporaryFile(publishParams.file_path); + // reject the promise + reject(error); }); - // update client - socket.emit('publish-complete', { name, result }); - }) - .catch(error => { - logger.error(`Error publishing ${fileName}`, error); - // google analytics - visitor.event('Publish Route', 'Publish Failure', filePath).send(); - socket.emit('publish-failure', errorHandlers.handlePublishError(error)); - deleteTemporaryFile(filePath); - }); + }); + return deferred; }, }; diff --git a/helpers/libraries/errorHandlers.js b/helpers/libraries/errorHandlers.js index f23c5fa4..172ee912 100644 --- a/helpers/libraries/errorHandlers.js +++ b/helpers/libraries/errorHandlers.js @@ -8,7 +8,7 @@ module.exports = { } else if (error.response) { res.status(error.response.status).send(error.response.data.error.message); } else if (error.code === 'ECONNREFUSED') { - res.status(400).send('Connection refused. The daemon may not be running.'); + res.status(503).send('Connection refused. The daemon may not be running.'); } else { res.status(400).send(JSON.stringify(error)); } diff --git a/helpers/libraries/lbryApi.js b/helpers/libraries/lbryApi.js index 3ff86cfd..026e1eee 100644 --- a/helpers/libraries/lbryApi.js +++ b/helpers/libraries/lbryApi.js @@ -2,8 +2,8 @@ const axios = require('axios'); const logger = require('winston'); module.exports = { - publishClaim (publishParams, fileName, fileType) { - logger.debug(`Publishing claim for "${fileName}"`); + publishClaim (publishParams) { + logger.debug(`Publishing claim to "${publishParams.name}"`); const deferred = new Promise((resolve, reject) => { axios .post('http://localhost:5279/lbryapi', { diff --git a/helpers/libraries/publishHelpers.js b/helpers/libraries/publishHelpers.js new file mode 100644 index 00000000..9bf8e857 --- /dev/null +++ b/helpers/libraries/publishHelpers.js @@ -0,0 +1,35 @@ +const logger = require('winston'); + +const config = require('config'); +const walletAddress = config.get('WalletConfig.LbryAddress'); + +module.exports = { + createPublishParams (name, filePath, license, nsfw) { + logger.debug(`Creating Publish Parameters for "${name}"`); + // ensure nsfw is a boolean + if (nsfw.toLowerCase === 'true') { + nsfw = true; + } else if (nsfw.toLowerCase === 'on') { + nsfw = true; + } else { + nsfw = false; + } + const publishParams = { + name, + file_path: filePath, + bid : 0.01, + metadata : { + description: `${name} published via spee.ch`, + title : name, + author : 'spee.ch', + language : 'en', + license, + nsfw, + }, + claim_address : walletAddress, + change_address: walletAddress, + }; + logger.debug('publishParams:', publishParams); + return publishParams; + }, +}; diff --git a/package.json b/package.json index 1204d3e9..b4706565 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "axios": "^0.16.1", "body-parser": "^1.17.1", "config": "^1.26.1", + "connect-multiparty": "^2.0.0", "express": "^4.15.2", "express-handlebars": "^3.0.0", "mysql2": "^1.3.5", diff --git a/routes/api-routes.js b/routes/api-routes.js index 65e8056b..453325d8 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -1,6 +1,10 @@ -const errorHandlers = require('../helpers/libraries/errorHandlers.js'); -const lbryApi = require('../helpers/libraries/lbryApi.js'); const logger = require('winston'); +const multipart = require('connect-multiparty'); +const multipartMiddleware = multipart(); +const publishController = require('../controllers/publishController.js'); +const lbryApi = require('../helpers/libraries/lbryApi.js'); +const publishHelpers = require('../helpers/libraries/publishHelpers.js'); +const errorHandlers = require('../helpers/libraries/errorHandlers.js'); module.exports = app => { // route to run a claim_list request on the daemon @@ -27,4 +31,33 @@ module.exports = app => { errorHandlers.handleRequestError(error, res); }); }); + // route to run a publish request on the daemon + app.post('/api/publish', multipartMiddleware, ({ originalUrl, body, files }, res) => { + logger.debug(`POST request on ${originalUrl}`); + const file = files.thumbnail || files.null; + if (!file) { + res.status(400).send('error: No file was submitted or the key used was incorrect. Files posted through this route must use a key of "thumbnail" or null'); + return; + } + const name = body.claim || file.name.substring(0, file.name.indexOf('.')); + const license = body.license || 'No License Provided'; + const nsfw = body.nsfw || true; + const fileName = file.name; + const filePath = file.path; + const fileType = file.type; + /* + make sure it's not a harmful file type + */ + // prepare the publish parameters + const publishParams = publishHelpers.createPublishParams(name, filePath, license, nsfw); + // publish the file + publishController + .publish(publishParams, fileName, fileType) + .then(result => { + res.status(200).json(result); + }) + .catch(error => { + errorHandlers.handleRequestError(error, res); + }); + }); }; diff --git a/routes/sockets-routes.js b/routes/sockets-routes.js index 0589a58b..af4ecd41 100644 --- a/routes/sockets-routes.js +++ b/routes/sockets-routes.js @@ -1,5 +1,7 @@ -const publishController = require('../controllers/publishController.js'); const logger = require('winston'); +const publishController = require('../controllers/publishController.js'); +const publishHelpers = require('../helpers/libraries/publishHelpers.js'); +const errorHandlers = require('../helpers/libraries/errorHandlers.js'); module.exports = (app, siofu, hostedContentPath, ua, googleAnalyticsId) => { const http = require('http').Server(app); @@ -7,8 +9,9 @@ module.exports = (app, siofu, hostedContentPath, ua, googleAnalyticsId) => { io.on('connection', socket => { logger.silly('a user connected via sockets'); - // create visitor record + // google analytics const visitor = ua(googleAnalyticsId, { https: true }); + visitor.event('Publish Route', 'Publish Request').send(); // attach upload listeners const uploader = new siofu(); uploader.dir = hostedContentPath; @@ -27,8 +30,18 @@ module.exports = (app, siofu, hostedContentPath, ua, googleAnalyticsId) => { uploader.on('saved', ({ file }) => { if (file.success) { logger.debug(`Client successfully uploaded ${file.name}`); - socket.emit('publish-status', 'file upload successfully completed'); - publishController.publish(file.meta.name, file.name, file.pathName, file.meta.type, file.meta.license, file.meta.nsfw, socket, visitor); + socket.emit('publish-status', 'file upload successfully completed. Your image is being published (this might take a second)...'); + // prepare the publish parameters + const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.license, file.meta.nsfw); + // publish the file + publishController + .publish(publishParams, file.name, file.meta.type) + .then(result => { + socket.emit('publish-complete', { name: publishParams.name, result }); + }) + .catch(error => { + socket.emit('publish-failure', errorHandlers.handlePublishError(error)); + }); } else { logger.error(`An error occurred in uploading the client's file`); socket.emit('publish-failure', 'file uploaded, but with errors'); diff --git a/server.js b/server.js index c2aef521..8df35748 100644 --- a/server.js +++ b/server.js @@ -8,13 +8,13 @@ const config = require('config'); const ua = require('universal-analytics'); const winston = require('winston'); -const googleAnalyticsId = config.get('AnalyticsConfig.googleId'); +const googleAnalyticsId = config.get('AnalyticsConfig.GoogleId'); const hostedContentPath = config.get('Database.PublishUploadPath'); // configure logging const logLevel = config.get('Logging.LogLevel'); const logDir = config.get('Logging.LogDirectory'); -require('./helpers/logging/loggerSetup.js')(winston, logLevel, logDir); +require('./config/loggerSetup.js')(winston, logLevel, logDir); // set port const PORT = 3000; @@ -38,7 +38,7 @@ const hbs = expressHandlebars.create({ helpers : { // define any extra helpers you may need googleAnalytics () { - const googleApiKey = config.get('AnalyticsConfig.googleId'); + const googleApiKey = config.get('AnalyticsConfig.GoogleId'); return new Handlebars.SafeString( `