diff --git a/controllers/publishController.js b/controllers/publishController.js index fc632836..abf3dca8 100644 --- a/controllers/publishController.js +++ b/controllers/publishController.js @@ -4,12 +4,10 @@ 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}"`); - if (typeof nsfw === 'string') { - nsfw = (nsfw.toLowerCase() === 'on'); - } const publishParams = { name : claim, file_path: filePath, @@ -36,24 +34,63 @@ function deleteTemporaryFile (filePath) { }); } +function upsert (Model, values, condition) { + return Model + .findOne({ where: condition }) + .then(function (obj) { + if (obj) { // update + return obj.update(values); + } else { // insert + return Model.create(values); + } + }).catch(function (error) { + logger.error('Sequelize findOne error', error); + }); +} + module.exports = { - publish (claim, fileName, filePath, fileType, license, nsfw, socket, visitor) { + 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(claim, filePath, license, nsfw); - // get a promise to publish + const publishParams = createPublishParams(name, filePath, license, nsfw); + // 1. publish the file lbryApi .publishClaim(publishParams, fileName, fileType) .then(result => { - logger.info(`Successfully published ${fileName}`); + logger.info(`Successfully published ${fileName}`, result); + // google analytics visitor.event('Publish Route', 'Publish Success', filePath).send(); - socket.emit('publish-complete', { name: claim, 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, + 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); + }); + // 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); diff --git a/controllers/serveController.js b/controllers/serveController.js index e00daf46..4017bd33 100644 --- a/controllers/serveController.js +++ b/controllers/serveController.js @@ -4,10 +4,92 @@ const logger = require('winston'); const getAllFreePublicClaims = require('../helpers/functions/getAllFreePublicClaims.js'); const isFreePublicClaim = require('../helpers/functions/isFreePublicClaim.js'); -function getClaimAndHandleResponse (claimUri, resolve, reject) { +function updateFileIfNeeded (uri, claimName, claimId, localOutpoint, localHeight) { + logger.debug(`A mysql record was found for ${claimId}`); + logger.debug('initiating resolve on claim to check outpoint'); + // 1. resolve claim lbryApi - .getClaim(claimUri) - .then(({ file_name, download_path, mime_type }) => { + .resolveUri(uri) + .then(result => { + // logger.debug('resolved result:', result); + const resolvedOutpoint = `${result[uri].claim.txid}:${result[uri].claim.nout}`; + const resolvedHeight = result[uri].claim.height; + logger.debug('database outpoint:', localOutpoint); + logger.debug('resolved outpoint:', resolvedOutpoint); + // 2. if the outpoint's match, no further work needed + if (localOutpoint === resolvedOutpoint) { + logger.debug('local outpoint matched'); + // 2. if the outpoints don't match, check the height + } else if (localHeight > resolvedHeight) { + logger.debug('local height was greater than resolved height'); + // 2. get the resolved claim + } else { + logger.debug(`local outpoint did not match for ${uri}. Initiating update.`); + getClaimAndUpdate(uri, resolvedHeight); + } + }) + .catch(error => { + logger.error(`error resolving "${uri}" >> `, error); + }); +} + +function getClaimAndUpdate (uri, height) { + // 1. get the claim + lbryApi + .getClaim(uri) + .then(({ name, claim_id, outpoint, file_name, download_path, mime_type, metadata }) => { + logger.debug(' Get returned outpoint: ', outpoint); + // 2. update the entry in db + db.File + .update({ + outpoint, + height, // note: height is coming from 'resolve', not 'get'. + fileName: file_name, + filePath: download_path, + fileType: mime_type, + nsfw : metadata.stream.metadata.nsfw, + }, { + where: { + name, + claimId: claim_id, + }, + }) + .then(result => { + logger.debug('successfully updated mysql record', result); + }) + .catch(error => { + logger.error('sequelize error', error); + }); + }) + .catch(error => { + logger.error(`error while getting claim for ${uri} >> `, error); + }); +} + +function getClaimAndHandleResponse (uri, height, resolve, reject) { + lbryApi + .getClaim(uri) + .then(({ name, claim_id, outpoint, file_name, download_path, mime_type, metadata }) => { + // create entry in the db + logger.debug('creating new record in db'); + db.File + .create({ + name, + claimId : claim_id, + outpoint, + height, + fileName: file_name, + filePath: download_path, + fileType: mime_type, + nsfw : metadata.stream.metadata.nsfw, + }) + .then(result => { + logger.debug('successfully created mysql record'); + }) + .catch(error => { + logger.error('sequelize create error', error); + }); + // resolve the request resolve({ fileName: file_name, filePath: download_path, @@ -22,35 +104,31 @@ function getClaimAndHandleResponse (claimUri, resolve, reject) { module.exports = { getClaimByName (claimName) { const deferred = new Promise((resolve, reject) => { - // get all free public claims + // 1. get the top free, public claims getAllFreePublicClaims(claimName) .then(freePublicClaimList => { - const claimId = freePublicClaimList[0].claim_id; const name = freePublicClaimList[0].name; - const freePublicClaimOutpoint = `${freePublicClaimList[0].txid}:${freePublicClaimList[0].nout}`; - const freePublicClaimUri = `${name}#${claimId}`; - // check to see if the file is available locally + const claimId = freePublicClaimList[0].claim_id; + const uri = `${name}#${claimId}`; + const height = freePublicClaimList[0].height; + // 2. check to see if the file is available locally db.File - .findOne({ where: { name: name, claimId: claimId } }) + .findOne({ where: { name, claimId } }) .then(claim => { - // if a matching claim is found locally... + // 3. if a matching claim_id is found locally, serve it if (claim) { - logger.debug(`A mysql record was found for ${claimId}`); - // if the outpoint's match return it - if (claim.dataValues.outpoint === freePublicClaimOutpoint) { - logger.debug(`local outpoint matched for ${claimId}`); - resolve(claim.dataValues); - // if the outpoint's don't match, fetch updated claim - } else { - logger.debug(`local outpoint did not match for ${claimId}`); - getClaimAndHandleResponse(freePublicClaimUri, resolve, reject); - } - // ... otherwise use daemon to retrieve it + // serve the file + resolve(claim.dataValues); + // trigger update if needed + updateFileIfNeeded(uri, name, claimId, claim.dataValues.outpoint, claim.dataValues.height); + // 3. otherwise use daemon to retrieve it } else { - getClaimAndHandleResponse(freePublicClaimUri, resolve, reject); + // get the claim and serve it + getClaimAndHandleResponse(uri, height, resolve, reject); } }) .catch(error => { + logger.error('sequelize error', error); reject(error); }); }) @@ -60,42 +138,37 @@ module.exports = { }); return deferred; }, - getClaimByClaimId (claimName, claimId) { + getClaimByClaimId (name, claimId) { const deferred = new Promise((resolve, reject) => { - const uri = `${claimName}#${claimId}`; - // resolve the Uri - lbryApi - .resolveUri(uri) // note: use 'spread' and make parallel with db.File.findOne() - .then(result => { - const resolvedOutpoint = `${result[uri].claim.txid}:${result[uri].claim.nout}`; - // check locally for the claim - db.File - .findOne({ where: { name: claimName, claimId: claimId } }) - .then(claim => { - // if a found locally... - if (claim) { - logger.debug(`A mysql record was found for ${claimId}`); - // if the outpoint's match return it - if (claim.dataValues.outpoint === resolvedOutpoint) { - logger.debug(`local outpoint matched for ${claimId}`); - resolve(claim.dataValues); - // if the outpoint's don't match, fetch updated claim - } else { - logger.debug(`local outpoint did not match for ${claimId}`); - getClaimAndHandleResponse(uri, resolve, reject); - } - // ... otherwise use daemon to retrieve it - } else { + const uri = `${name}#${claimId}`; + // 1. check locally for the claim + db.File + .findOne({ where: { name, claimId } }) + .then(claim => { + // 2. if a match is found locally, serve it + if (claim) { + // serve the file + resolve(claim.dataValues); + // trigger an update if needed + updateFileIfNeeded(uri, name, claimId, claim.dataValues.outpoint, claim.dataValues.outpoint); + // 2. otherwise use daemon to retrieve it + } else { + // 3. resolve the Uri + lbryApi + .resolveUri(uri) + .then(result => { + // 4. check to see if the claim is free & public if (isFreePublicClaim(result[uri].claim)) { - getClaimAndHandleResponse(uri, resolve, reject); + // 5. get claim and serve + getClaimAndHandleResponse(uri, result[uri].claim.height, resolve, reject); } else { reject('NO_FREE_PUBLIC_CLAIMS'); } - } - }) - .catch(error => { - reject(error); - }); + }) + .catch(error => { + reject(error); + }); + } }) .catch(error => { reject(error); diff --git a/helpers/functions/getAllFreePublicClaims.js b/helpers/functions/getAllFreePublicClaims.js index 4d3e39ec..0f9c30a2 100644 --- a/helpers/functions/getAllFreePublicClaims.js +++ b/helpers/functions/getAllFreePublicClaims.js @@ -16,8 +16,8 @@ function filterForFreePublicClaims (claimsListArray) { return freePublicClaims; } -function orderTopClaims (claimsListArray) { - logger.debug('ordering the top claims'); +function orderClaims (claimsListArray) { + logger.debug('ordering the free public claims'); claimsListArray.sort((a, b) => { if (a.amount === b.amount) { return a.height < b.height; @@ -51,7 +51,7 @@ module.exports = claimName => { return; } // order the claims - const orderedPublicClaims = orderTopClaims(freePublicClaims); + const orderedPublicClaims = orderClaims(freePublicClaims); // resolve the promise resolve(orderedPublicClaims); }) diff --git a/helpers/libraries/errorHandlers.js b/helpers/libraries/errorHandlers.js index b0e8757f..f23c5fa4 100644 --- a/helpers/libraries/errorHandlers.js +++ b/helpers/libraries/errorHandlers.js @@ -2,7 +2,7 @@ const logger = require('winston'); module.exports = { handleRequestError (error, res) { - logger.error('Request Error,', error); + logger.error('Request Error >>', error); if (error === 'NO_CLAIMS' || error === 'NO_FREE_PUBLIC_CLAIMS') { res.status(307).render('noClaims'); } else if (error.response) { @@ -10,11 +10,11 @@ module.exports = { } else if (error.code === 'ECONNREFUSED') { res.status(400).send('Connection refused. The daemon may not be running.'); } else { - res.status(400).send(error.toString()); + res.status(400).send(JSON.stringify(error)); } }, handlePublishError (error) { - logger.error('Publish Error,', error); + logger.error('Publish Error >>', error); if (error.code === 'ECONNREFUSED') { return 'Connection refused. The daemon may not be running.'; } else if (error.response.data.error) { diff --git a/helpers/libraries/lbryApi.js b/helpers/libraries/lbryApi.js index 7eb95fcc..3ff86cfd 100644 --- a/helpers/libraries/lbryApi.js +++ b/helpers/libraries/lbryApi.js @@ -1,15 +1,6 @@ const axios = require('axios'); -const db = require('../../models'); const logger = require('winston'); -function createFilesRecord (name, claimId, outpoint, fileName, filePath, fileType, nsfw) { - db.File - .create({ name, claimId, outpoint, fileName, filePath, fileType, nsfw }) - .catch(error => { - logger.error(`Sequelize File.create error`, error); - }); -} - module.exports = { publishClaim (publishParams, fileName, fileType) { logger.debug(`Publishing claim for "${fileName}"`); @@ -21,8 +12,6 @@ module.exports = { }) .then(response => { const result = response.data.result; - createFilesRecord( - publishParams.name, result.claim_id, `${result.txid}:${result.nout}`, fileName, publishParams.file_path, fileType, publishParams.metadata.nsfw); resolve(result); }) .catch(error => { @@ -50,11 +39,7 @@ module.exports = { /* note: put in a check to make sure we do not resolve until the download is actually complete (response.data.completed === true) */ - // save a record of the file to the Files table - const result = data.result; - createFilesRecord( - result.name, result.claim_id, result.outpoint, result.file_name, result.download_path, result.mime_type, result.metadata.stream.metadata.nsfw); - resolve(result); + resolve(data.result); }) .catch(error => { reject(error); @@ -88,6 +73,11 @@ module.exports = { params: { uri }, }) .then(({ data }) => { + // check for errors + if (data.result[uri].error) { + reject(data.result[uri].error); + return; + } resolve(data.result); }) .catch(error => { diff --git a/models/file.js b/models/file.js index acadd0c9..cc6cd325 100644 --- a/models/file.js +++ b/models/file.js @@ -1,4 +1,4 @@ -module.exports = (sequelize, { STRING, BOOLEAN }) => { +module.exports = (sequelize, { STRING, BOOLEAN, INTEGER }) => { const File = sequelize.define( 'File', { @@ -14,6 +14,11 @@ module.exports = (sequelize, { STRING, BOOLEAN }) => { type : STRING, allowNull: false, }, + height: { + type : INTEGER, + allowNull: false, + default : 0, + }, fileName: { type : STRING, allowNull: false, diff --git a/models/index.js b/models/index.js index 74e31518..0b64bb69 100644 --- a/models/index.js +++ b/models/index.js @@ -14,10 +14,10 @@ const sequelize = new Sequelize(connectionUri, { sequelize .authenticate() .then(() => { - logger.info('Sequelize has has been established mysql connection successfully.'); + logger.info('Sequelize has established mysql connection successfully.'); }) .catch(err => { - logger.error('Sequelize was nable to connect to the database:', err); + logger.error('Sequelize was unable to connect to the database:', err); }); fs.readdirSync(__dirname).filter(file => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js').forEach(file => { diff --git a/public/assets/js/claimPublish.js b/public/assets/js/claimPublish.js new file mode 100644 index 00000000..97ffc829 --- /dev/null +++ b/public/assets/js/claimPublish.js @@ -0,0 +1,158 @@ +// define variables +var socket = io(); +var uploader = new SocketIOFileUpload(socket); +var stagedFiles = null; + +/* helper functions */ +// create a progress animation +function createProgressBar(element, size){ + var x = 1; + var adder = 1; + function addOne(){ + var bars = '
|'; + for (var i = 0; i < x; i++){ bars += ' | '; } + bars += '
'; + element.innerHTML = bars; + if (x === size){ + adder = -1; + } else if ( x === 0){ + adder = 1; + } + x += adder; + }; + setInterval(addOne, 300); +} +// preview file and stage the image for upload +function previewAndStageFile(selectedFile){ + var preview = document.getElementById('image-preview'); + var dropzone = document.getElementById('drop-zone'); + var previewReader = new FileReader(); + + preview.style.display = 'block'; + dropzone.style.display = 'none'; + + previewReader.onloadend = function () { + preview.src = previewReader.result; + }; + + if (selectedFile) { + console.log(selectedFile); + previewReader.readAsDataURL(selectedFile); // reads the data and sets the img src + document.getElementById('publish-name').value = selectedFile.name.substring(0, selectedFile.name.indexOf('.')); // updates metadata inputs + stagedFiles = [selectedFile]; // stores the selected file for upload + } else { + preview.src = ''; + } +} +// update the publish status +function updatePublishStatus(msg){ + document.getElementById('publish-status').innerHTML = msg; +} +// process the drop-zone drop +function drop_handler(ev) { + console.log("drop"); + ev.preventDefault(); + // if dropped items aren't files, reject them + var dt = ev.dataTransfer; + if (dt.items) { + if (dt.items[0].kind == 'file') { + var droppedFile = dt.items[0].getAsFile(); + previewAndStageFile(droppedFile); + } else { + console.log("no files were found") + } + } else { + console.log("no items were found") + } +} +// prevent the browser's default drag behavior +function dragover_handler(ev) { + ev.preventDefault(); +} +// remove all of the drag data +function dragend_handler(ev) { + var dt = ev.dataTransfer; + if (dt.items) { + for (var i = 0; i < dt.items.length; i++) { + dt.items.remove(i); + } + } else { + ev.dataTransfer.clearData(); + } +} + +/* configure the submit button */ +document.getElementById('publish-submit').addEventListener('click', function(event){ + event.preventDefault(); + // make sure a file was selected + if (stagedFiles) { + // make sure only 1 file was selected + if (stagedFiles.length > 1) { + alert("Only one file allowed at a time"); + return; + } + // make sure the content type is acceptable + switch (stagedFiles[0].type) { + case "image/png": + case "image/jpeg": + case "image/gif": + case "video/mp4": + uploader.submitFiles(stagedFiles); + break; + default: + alert("Only .png, .jpeg, .gif, and .mp4 files are currently supported"); + break; + } + } +}) + +/* socketio-file-upload listeners */ +uploader.addEventListener('start', function(event){ + var name = document.getElementById('publish-name').value; + var license = document.getElementById('publish-license').value; + var nsfw = document.getElementById('publish-nsfw').checked; + event.file.meta.name = name; + event.file.meta.license = license; + event.file.meta.nsfw = nsfw; + event.file.meta.type = stagedFiles[0].type; + // re-set the html in the publish area + document.getElementById('publish-active-area').innerHTML = ''; + // start a progress animation + createProgressBar(document.getElementById('progress-bar'), 12); +}); +uploader.addEventListener('progress', function(event){ + var percent = event.bytesLoaded / event.file.size * 100; + updatePublishStatus('File is ' + percent.toFixed(2) + '% loaded to the server'); +}); + +/* socket.io message listeners */ +socket.on('publish-status', function(msg){ + updatePublishStatus(msg); +}); +socket.on('publish-failure', function(msg){ + document.getElementById('publish-active-area').innerHTML = '' + JSON.stringify(msg) + '
--(✖╭╮✖)→
For help, post the above error text in the #speech channel on the lbry slack'; +}); + +socket.on('publish-complete', function(msg){ + var publishResults; + var directUrl = '/' + msg.name + '/' + msg.result.claim_id; + // build new publish area + publishResults = 'Your publish is complete! View it here!
'; + publishResults += ''; + publishResults += ''; + // update publish area + document.getElementById('publish-active-area').innerHTML = publishResults; + // add a tweet button + twttr.widgets + .createShareButton( + document.getElementById('tweet-meme-button'), + { + text: 'Check out my image, hosted for free on the distributed awesomeness that is the LBRY blockchain!', + url: 'https://spee.ch/' + directUrl, + hashtags: 'LBRY', + via: 'lbryio' + }) + .then( function( el ) { + console.log('Tweet button added.'); + }); +}); \ No newline at end of file diff --git a/public/assets/js/memeDraw.js b/public/assets/js/memeDraw.js index db1cf2c5..dbfb4f31 100644 --- a/public/assets/js/memeDraw.js +++ b/public/assets/js/memeDraw.js @@ -22,6 +22,18 @@ img.onload = function() { drawMeme() } +function newCanvas(image){ + // hide start image + img = image; + // get dimensions of the start img + canvasHeight = canvasWidth * (img.height / img.width); + // size the canvas + canvas.width = canvasWidth; + canvas.height = canvasHeight; + // draw the meme + drawMeme() +} + // if the text changes, re-draw the meme topText.addEventListener('keyup', drawMeme); bottomText.addEventListener('keyup', drawMeme); @@ -40,14 +52,14 @@ function drawMeme() { var text1 = topText.value; text1 = text1.toUpperCase(); x = canvasWidth / 2; - y = 0; + y = 5; wrapText(ctx, text1, x, y, canvasWidth, fontSize, false); ctx.textBaseline = 'bottom'; var text2 = bottomText.value; text2 = text2.toUpperCase(); - y = canvasHeight; + y = canvasHeight - 5; wrapText(ctx, text2, x, y, canvasHeight, fontSize, true); diff --git a/public/assets/js/memePublish.js b/public/assets/js/memePublish.js index 1d020aab..7f69cd6b 100644 --- a/public/assets/js/memePublish.js +++ b/public/assets/js/memePublish.js @@ -114,23 +114,20 @@ socket.on('publish-failure', function(msg){ }); socket.on('publish-complete', function(msg){ var publishResults; - var directUrl = 'https://spee.ch/' + msg.name + '/' + msg.result.claim_id; + var directUrl = '/' + msg.name + '/' + msg.result.claim_id; // build new publish area - publishResults = 'Your publish is complete! Go ahead, share it with the world!
'; - publishResults += 'NOTE: the blockchain will need a few minutes to process your amazing work. Please allow some time for your meme to appear at your link.
'; - publishResults += 'Your meme has been published to http://spee.ch/' + msg.name + '
'; - publishResults += 'Here is a direct link to where your meme will be stored: ' + directUrl + '
'; - publishResults += 'Your Transaction ID is: ' + msg.result.txid + '
'; - publishResults += ''; + publishResults = 'Your publish is complete! View it here!
'; + publishResults += ''; + publishResults += ''; // update publish area document.getElementById('publish-active-area').innerHTML = publishResults; // add a tweet button twttr.widgets .createShareButton( - directUrl, document.getElementById('tweet-meme-button'), { text: 'Check out my meme creation on the LBRY blockchain!', + url: 'https://spee.ch/' + directUrl, hashtags: 'LBRYMemeFodder', via: 'lbryio' }) diff --git a/views/allClaims.handlebars b/views/allClaims.handlebars index 75b5eb20..437ec081 100644 --- a/views/allClaims.handlebars +++ b/views/allClaims.handlebars @@ -5,7 +5,7 @@These are all the free, public assets at that claim. You can publish more at spee.ch.
{{#each claims}} - +claim_id: {{this.claim_id}}
direct link here
diff --git a/views/index.handlebars b/views/index.handlebars index 120edfff..86a27dc4 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -1,10 +1,10 @@Here's how it's played...
-Create a meme based on the current /meme-fodder claim using the tools below. Got a masterpiece? Share it with the community and see what they think!
-Note: /meme-fodder will always use the public, free image at the claim lbry://meme-fodder. Want to put a different image on the chopping block? Go publish it!
+Create a meme based on the current lbry://meme-fodder claims using the tool below. Got a masterpiece? Share it with the community and see what they think!
+Spee.ch/meme-fodder/play uses the free, public images at the claim lbry://meme-fodder. Want to put a different image on the chopping block? Go publish it!
Below are some of the most recent images that have been fuel for our enjoyment on /meme-fodder
+Below are the images published to /meme-fodder. Click one to choose it as your canvas.