Publish api fix #191

Merged
bones7242 merged 4 commits from publish-api-fix into redesign-1 2017-10-07 02:47:59 +02:00
13 changed files with 299 additions and 209 deletions

View file

@ -28,28 +28,24 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
#### GET
* /api/resolve/:name
* a successfull request returns the resolve results for the claim at that name in JSON format
* example: `curl https://spee.ch/api/resolve/doitlive`
* /api/claim_list/:name
* a successfull request returns a list of claims at that claim name in JSON format
* /api/isClaimAvailable/:name
* a successfull request returns a boolean: `true` if the name is still available, `false` if the name has already been published to by spee.ch.
* example: `curl https://spee.ch/api/claim_list/doitlive`
* /api/isClaimAvailable/:name (returns `true`/`false` for whether a name is available through spee.ch)
* example: `curl https://spee.ch/api/isClaimAvailable/doitlive`
#### POST
* /api/publish
* request parameters:
* body (form-data):
* name: string (optional)
* defaults to the file's name, sans extension
* names can only contain the following characters: `A-Z`, `a-z`, `_`, or `-`
* license: string (optional)
* defaults to "No License Provided"
* only "Public Domain" or "Creative Commons" licenses are allowed
* nsfw: string, number, or boolean (optional)
* defaults `true`
* nsfw can be a string ("on"/"off"), number (0 or 1), or boolean (`true`/`false`)
* files:
* the `files` object submitted must use "speech" 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
* example: `curl -X POST -F 'name=MyPictureName' -F 'nsfw=false' -F 'file=@/path/to/my/picture.jpeg' https://spee.ch/api/publish`
* Parameters:
* name (string)
* nsfw (boolean)
* file (.mp4, .jpeg, .jpg, .gif, or .png)
* license (string, optional)
* title (string, optional)
* description (string, optional)
* channelName(string, optional)
* channelPassword (string, optional)
## bugs
If you find a bug or experience a problem, please report your issue here on github and find us in the lbry slack!

View file

@ -2,21 +2,23 @@ const db = require('../models');
const logger = require('winston');
module.exports = {
authenticateApiPublish (username, password) {
authenticateChannelCredentials (channelName, userPassword) {
return new Promise((resolve, reject) => {
if (username === 'none') {
if (!channelName) {
resolve(true);
return;
}
const userName = channelName.substring(1);
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
db.User
.findOne({where: {userName: username}})
.findOne({where: { userName }})
.then(user => {
if (!user) {
logger.debug('no user found');
resolve(false);
return;
}
if (!user.validPassword(password, user.password)) {
if (!user.validPassword(userPassword, user.password)) {
logger.debug('incorrect password');
resolve(false);
return;

View file

@ -7,24 +7,23 @@ module.exports = {
publish (publishParams, fileName, fileType) {
return new Promise((resolve, reject) => {
let publishResults = {};
// 1. make sure the name is available
publishHelpers.checkClaimNameAvailability(publishParams.name)
// 2. publish the file
.then(result => {
if (result === true) {
return lbryApi.publishClaim(publishParams);
} else {
return new Error('That name is already in use by spee.ch.');
}
})
// 3. upsert File record (update is in case the claim has been published before by this daemon)
// 1. publish the file
lbryApi.publishClaim(publishParams)
// 2. upsert File record (update is in case the claim has been published before by this daemon)
.then(tx => {
logger.info(`Successfully published ${fileName}`, tx);
publishResults = tx;
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
})
.then(user => {
if (user) { logger.debug('successfully found user in User table') } else { logger.error('user for publish not found in User table') };
let certificateId;
if (user) {
certificateId = user.channelClaimId;
logger.debug('successfully found user in User table');
} else {
certificateId = null;
logger.debug('user for publish not found in User table');
};
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
@ -48,7 +47,7 @@ module.exports = {
height : 0,
contentType: fileType,
nsfw : publishParams.metadata.nsfw,
certificateId: user.channelClaimId,
certificateId,
amount : publishParams.bid,
};
const upsertCriteria = {

View file

@ -7,7 +7,7 @@ const { postToStats, sendGoogleAnalytics } = require('../controllers/statsContro
const SERVE = 'SERVE';
const SHOW = 'SHOW';
const SHOWLITE = 'SHOWLITE';
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/content-freedom-large.png';
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png';
function checkForLocalAssetByClaimId (claimId, name) {
return new Promise((resolve, reject) => {

View file

@ -155,7 +155,7 @@ module.exports = {
element['directUrlLong'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
element['directUrlShort'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
element['contentType'] = element.fileType;
element['thumbnail'] = 'https://spee.ch/assets/img/content-freedom-large.png';
element['thumbnail'] = 'https://spee.ch/assets/img/video_thumb_default.png';
});
}
resolve(results);

View file

@ -26,6 +26,23 @@ module.exports = {
res.status(400).send(error);
}
},
handlePublishError (error) {
logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error));
if (error.code === 'ECONNREFUSED') {
return 'Connection refused. The daemon may not be running.';
} else if (error.response) {
if (error.response.data) {
if (error.response.data.message) {
return error.response.data.message;
} else if (error.response.data.error) {
return error.response.data.error.message;
}
return error.response.data;
}
return error.response;
} else {
return error;
}
useObjectPropertiesIfNoKeys (err) {
return useObjectPropertiesIfNoKeys(err);
},

View file

@ -1,78 +1,130 @@
const logger = require('winston');
const config = require('config');
const fs = require('fs');
const db = require('../models');
const config = require('config');
module.exports = {
validateFile (file, name, license, nsfw) {
validateApiPublishRequest (body, files) {
if (!body) {
throw new Error('no body found in request');
}
if (!body.name) {
throw new Error('no name field found in request');
}
if (!body.nsfw) {
throw new Error('no nsfw field found in request');
}
if (!files) {
throw new Error('no files found in request');
}
if (!files.file) {
throw new Error('no file with key of [file] found in request');
}
},
validatePublishSubmission (file, claimName, nsfw) {
try {
module.exports.validateFile(file);
module.exports.validateClaimName(claimName);
module.exports.validateNSFW(nsfw);
} catch (error) {
throw error;
}
},
validateFile (file) {
if (!file) {
throw new Error('No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null');
logger.debug('publish > file validation > no file found');
throw new Error('no file provided');
}
// check the file name
if (/'/.test(file.name)) {
logger.debug('publish > file validation > file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// check file type and size
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
logger.debug('publish > file validation > .jpeg/.jpg/.png was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
throw new Error('Your image exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .gif was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
throw new Error('Your video exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .mp4 was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
throw new Error('The ' + file.Type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
logger.debug('publish > file validation > unrecognized file type');
throw new Error('The ' + file.type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
}
// validate claim name
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(name);
return file;
},
validateClaimName (claimName) {
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(claimName);
if (invalidCharacters) {
throw new Error('The url name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
}
// validate license
},
validateLicense (license) {
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) {
throw new Error('Only posts with a "Public Domain" license, or one of the Creative Commons licenses are eligible for publishing through spee.ch');
throw new Error('Only posts with a "Public Domain" or "Creative Commons" license are eligible for publishing through spee.ch');
}
},
cleanseNSFW (nsfw) {
switch (nsfw) {
case true:
case false:
case 'true':
case 'false':
case 'on':
case 'true':
case 1:
case '1':
return true;
case false:
case 'false':
case 'off':
case 0:
case '0':
case 1:
case '1':
break;
return false;
default:
throw new Error('NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"');
return null;
}
},
createPublishParams (name, filePath, title, description, license, nsfw, channel) {
logger.debug(`Creating Publish Parameters for "${name}"`);
const claimAddress = config.get('WalletConfig.LbryClaimAddress');
const defaultChannel = config.get('WalletConfig.DefaultChannel');
// filter nsfw and ensure it is a boolean
if (nsfw === false) {
nsfw = false;
} else if (typeof nsfw === 'string') {
if (nsfw.toLowerCase === 'false' || nsfw.toLowerCase === 'off' || nsfw === '0') {
nsfw = false;
cleanseChannelName (channelName) {
if (channelName) {
if (channelName.indexOf('@') !== 0) {
channelName = `@${channelName}`;
}
} else if (nsfw === 0) {
nsfw = false;
} else {
nsfw = true;
}
// provide defaults for title & description
if (title === null || title === '') {
return channelName;
},
validateNSFW (nsfw) {
if (nsfw === true || nsfw === false) {
return;
}
throw new Error('NSFW must be set to either true or false');
},
createPublishParams (filePath, name, title, description, license, nsfw, channelName) {
logger.debug(`Creating Publish Parameters`);
// provide defaults for title
if (title === null || title.trim() === '') {
title = name;
}
// provide default for description
if (description === null || description.trim() === '') {
description = `${name} published via spee.ch`;
}
// provide default for license
if (license === null || license.trim() === '') {
license = 'All Rights Reserved';
}
// create the publish params
const publishParams = {
name,
@ -86,16 +138,12 @@ module.exports = {
license,
nsfw,
},
claim_address: claimAddress,
claim_address: config.get('WalletConfig.LbryClaimAddress'),
};
// add channel if applicable
if (channel !== 'none') {
publishParams['channel_name'] = channel;
} else {
publishParams['channel_name'] = defaultChannel;
// add channel to params, if applicable
if (channelName) {
publishParams['channel_name'] = channelName;
}
logger.debug('publishParams:', publishParams);
return publishParams;
},
deleteTemporaryFile (filePath) {

View file

@ -33,6 +33,7 @@
"express": "^4.15.2",
"express-handlebars": "^3.0.0",
"express-session": "^1.15.5",
"form-data": "^2.3.1",
"helmet": "^3.8.1",
"mysql2": "^1.3.5",
"nodemon": "^1.11.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -1,5 +1,3 @@
// validation function which checks the proposed file's type, size, and name
function validateFile(file) {
if (!file) {
@ -210,7 +208,7 @@ function validateNewChannelSubmission(userName, password){
console.log('channel is available');
resolve();
} else {
console.log('channel is not avaliable');
console.log('channel is not available');
reject(new ChannelNameError('that channel name has already been taken'));
}
})

View file

@ -4,10 +4,10 @@ const multipartMiddleware = multipart();
const db = require('../models');
const { publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { createPublishParams, validateFile, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseNSFW, cleanseChannelName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
const { authenticateApiPublish } = require('../auth/authentication.js');
const { authenticateChannelCredentials } = require('../auth/authentication.js');
module.exports = (app) => {
// route to run a claim_list request on the daemon
@ -71,52 +71,80 @@ module.exports = (app) => {
});
});
// route to run a publish request on the daemon
app.post('/api/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl }, res) => {
// google analytics
sendGoogleAnalytics('PUBLISH', headers, ip, originalUrl);
// validate that a file was provided
const file = files.speech || files.null;
const name = body.name || file.name.substring(0, file.name.indexOf('.'));
const title = body.title || null;
const description = body.description || null;
const license = body.license || 'No License Provided';
const nsfw = body.nsfw || null;
const channelName = body.channelName || 'none';
const channelPassword = body.channelPassword || null;
logger.debug(`name: ${name}, license: ${license}, nsfw: ${nsfw}`);
app.post('/api/publish', multipartMiddleware, (req, res) => {
logger.debug(req);
const body = req.body;
const files = req.files;
try {
validateFile(file, name, license, nsfw);
validateApiPublishRequest(body, files);
} catch (error) {
postToStats('publish', originalUrl, ip, null, null, error.message);
logger.debug('rejected >>', error.message);
res.status(400).send(error.message);
logger.debug('publish request rejected, insufficient request parameters');
res.status(400).json({success: false, message: error.message});
return;
}
// required inputs
const file = files.file;
const fileName = file.name;
const filePath = file.path;
const fileType = file.type;
// channel authorization
authenticateApiPublish(channelName, channelPassword)
const name = body.name;
let nsfw = body.nsfw;
// cleanse nsfw
nsfw = cleanseNSFW(nsfw);
// validate file, name, license, and nsfw
try {
validatePublishSubmission(file, name, nsfw);
} catch (error) {
logger.debug('publish request rejected');
res.status(400).json({success: false, message: error.message});
return;
}
logger.debug(`name: ${name}, nsfw: ${nsfw}`);
// optional inputs
const license = body.license || null;
const title = body.title || null;
const description = body.description || null;
let channelName = body.channelName || null;
channelName = cleanseChannelName(channelName);
const channelPassword = body.channelPassword || null;
logger.debug(`license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}"`);
// check channel authorization
authenticateChannelCredentials(channelName, channelPassword)
.then(result => {
if (!result) {
res.status(401).send('Authentication failed, you do not have access to that channel');
throw new Error('authentication failed');
throw new Error('Authentication failed, you do not have access to that channel');
}
return createPublishParams(name, filePath, title, description, license, nsfw, channelName);
// make sure the claim name is available
return checkClaimNameAvailability(name);
})
.then(result => {
if (!result) {
throw new Error('That name is already in use by spee.ch.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, channelName);
})
.then(publishParams => {
logger.debug('publishParams:', publishParams);
// publish the asset
return publish(publishParams, fileName, fileType);
})
// publish the asset
.then(result => {
postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json(result);
// postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json({
success: true,
message: {
url : `spee.ch/${result.claim_id}/${name}`,
lbryTx: result,
},
});
})
.catch(error => {
logger.error('publish api error', error);
res.status(400).json({success: false, message: error.message});
});
});
// route to get a short claim id from long claim Id
app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => {
// serve content

View file

@ -1,7 +1,7 @@
const logger = require('winston');
const publishController = require('../controllers/publishController.js');
const publishHelpers = require('../helpers/publishHelpers.js');
const { useObjectPropertiesIfNoKeys } = require('../helpers/errorHandlers.js');
const { publish } = require('../controllers/publishController.js');
const { createPublishParams } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats } = require('../controllers/statsController.js');
module.exports = (app, siofu, hostedContentPath) => {
@ -40,12 +40,13 @@ module.exports = (app, siofu, hostedContentPath) => {
NOTE: need to validate that client has the credentials to the channel they chose
otherwise they could circumvent security client side.
*/
let channelName = file.meta.channel;
if (channelName === 'none') channelName = null;
// prepare the publish parameters
const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, file.meta.channel);
const publishParams = createPublishParams(file.pathName, file.meta.name, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, channelName);
logger.debug(publishParams);
// publish the file
publishController.publish(publishParams, file.name, file.meta.type)
publish(publishParams, file.name, file.meta.type)
.then(result => {
socket.emit('publish-complete', { name: publishParams.name, result });
postToStats('PUBLISH', '/', null, null, null, 'success');

View file

@ -7,7 +7,7 @@
<div class="all-claims-item">
<a href="/{{this.claimId}}/{{this.name}}">
{{#ifConditional this.fileType '===' 'video/mp4'}}
<img class="all-claims-asset" src="/assets/img/content-freedom-large.png"/>
<img class="all-claims-asset" src="/assets/img/video_thumb_default.png"/>
{{else}}
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.ext" />
{{/ifConditional}}