diff --git a/.gitignore b/.gitignore index 9f11b755..3c3629e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.idea/ +node_modules diff --git a/LBRY.class.php b/LBRY.class.php deleted file mode 100644 index 7fdc413a..00000000 --- a/LBRY.class.php +++ /dev/null @@ -1,85 +0,0 @@ - $function, 'params' => $params])); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $serverOutput = curl_exec($ch); - curl_close($ch); - - if ($serverOutput) - { - $responseData = json_decode($serverOutput, true); - if (isset($responseData['error'])) - { - throw new Exception($responseData['error']['message'] ?? 'Something unknown went wrong'); - } - if (isset($responseData['result'])) - { - return $responseData['result']; - } - throw new Exception('Received unknown response format.'); - } - } - - public static function publishPublicClaim($name, $tmpFileName) - { - $filePath = '/home/lbry/publishes/newupload-' . random_int(1, PHP_INT_MAX); - - move_uploaded_file($tmpFileName, $filePath); - - $apiResult = LBRY::api('publish', [ - 'name' => $name, - 'bid' => 1, - 'file_path' => $filePath, - 'description' => 'An image published from spee.ch', - 'author' => 'https://spee.ch', - 'language' => 'en', - 'license' => 'Public Domain', - 'nsfw' => 0, - 'title' => 'Image published from spee.ch' - ]); - - return isset($apiResult['claim_id']); - } - - public static function findTopPublicFreeClaim($name) - { - $claims = LBRY::api('claim_list', ['name' => $name]); - - if (!$claims || !isset($claims['claims'])) - { - return null; - } - - $freePublicClaims = array_filter($claims['claims'], function($claim) { - $metadata = json_decode($claim['value'], true); - return - //TODO: Expand these checks AND verify it is an image claim! - ($metadata['license'] == "Public Domain" || stripos($metadata['license'], 'Creative Commons') !== false) && - !isset($metadata['fee']); - }); - - if (count($freePublicClaims) > 1) - { - usort($freePublicClaims, function($claimA, $claimB) { - if ($claimA['amount'] == $claimB['amount']) - { - return $claimA['height'] < $claimB['height'] ? -1 : 1; - } - return $claimA['amount'] > $claimB['amount'] ? -1 : 1; - }); - } - - return reset($freePublicClaims); - } -} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 77909b45..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 LBRY - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index c92edc8d..f68d744c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,44 @@ -# spee.ch -spee.ch is a simple but powerful image hosting service on top of the LBRY protocol. +# spee.ch (js) +this is a clone of spee.ch with a javascript backend -It was built in real-time on March 29th, 2017. +## how to use this repository +* start lbry + * install the [`lbry`](https://github.com/lbryio/lbry) daemon + * start the `lbry` daemon +* start RabbitMQ (this will handle the queue for background processing) + * install & run [RabbitMQ](https://www.rabbitmq.com/#getstarted) +* clone this repo +* run `npm install` +* from your terminal, run `npm start` + * to run hot, run `nodemon server.js` +* start at least one worker by running `node worker.js` +* visit [localhost:3000](http://localhost:3000) and enjoy! -You can watch the video and learn more from [https://spee.ch](https://spee.ch) +## site navigation + +* spee.ch. + * To publish a file, navigate to the homepage. +* spee.ch/ + * To view the file with the largest bid at a claim. + * E.g. spee.ch/doitlive. +* spee.ch/< the name of the claim >/< the claim_id > + * To view a specific file at a claim + * E.g. spee.ch/doitlive/c496c8c55ed79816fec39e36a78645aa4458edb5 +* spee.ch//all + * To view a batch of files at a claim + * E.g. spee.ch/doitlive/all + +## development to-do's +* discover/explore functionality for home page +* display a list of claims at /:name/all +* fetching: a temporary page while the request is being made (with a loading bar?) +* publishing: a temporary page while the request is being handled by the server (with a loading bar?) +* publishing: after publishing, take the user to a temp page with the tx info and status of the tx (then redirect when the tx is complete) + +## API + +Note: these are being used for testing durring spee.ch development and may not be maintained + +* A GET request to spee.ch/claim_list/ + * Will return the claim_list for the claim in json format. + * E.g. spee.ch/claim_list/doitlive diff --git a/helpers/lbryApi.js b/helpers/lbryApi.js new file mode 100644 index 00000000..e1ca554e --- /dev/null +++ b/helpers/lbryApi.js @@ -0,0 +1,173 @@ +// load dependencies +var path = require('path'); +var axios = require('axios'); + +// helper function to filter an array of claims for only free, public claims +function filterForFreePublicClaims(claimsListArray){ + //console.log(">> filterForFreePublicClaims, claimsListArray:", claimsListArray); + if (!claimsListArray) { + return null; + }; + var freePublicClaims = claimsListArray.filter(function(claim){ + return (((claim.value.stream.metadata.license.indexOf('Public Domain') != -1) || (claim.value.stream.metadata.license.indexOf('Creative Commons') != -1)) && + (!claim.value.stream.metadata.fee || claim.value.stream.metadata.fee === 0)); + }); + return freePublicClaims; +} +// helper function to decide if a claim is free and public +function isFreePublicClaim(claim){ + console.log(">> isFreePublicClaim, claim:", claim); + if ((claim.value.stream.metadata.license === 'Public Domain' || claim.value.stream.metadata.license === 'Creative Commons') && + (!claim.value.stream.metadata.fee || claim.value.stream.metadata.fee.amount === 0)) { + return true; + } else { + return false; + } +} +// helper function to order a set of claims +function orderTopClaims(claimsListArray){ + console.log(">> orderTopClaims, claimsListArray:"); + claimsListArray.sort(function(claimA, claimB){ + if (claimA.amount === claimB.amount){ + return (claimA.height > claimB.height); + } else { + return (claimA.amount < claimB.amount); + } + }) + return claimsListArray; +} + +module.exports = { + publishClaim: function(publishObject){ + axios.post('http://localhost:5279/lbryapi', publishObject) + .then(function (response) { + // receive resonse from LBRY + // if successfull, (1) delete file (2) send response to the client + console.log(">> 'publish' success..."); + console.log(">> 'publish' response.data:", response.data); + console.log(" [x] Done"); + // return the claim we got + //res.status(200).send(JSON.stringify({msg: "you succsessfully published!", txData: response.data})); + }).catch(function(error){ + // receive response from LBRY + // if not successfull, (1) delete file and (2) send response to the client + console.log(">> 'publish' error.response.data:", error.response.data); + console.log(" [x] Done"); + //res.status(500).send(JSON.stringify({msg: "your file was not published", err: error.response.data.error.message})); + }) + }, + serveClaimBasedOnNameOnly: function(claimName, res){ + // make a call to the daemon to get the claims list + axios.post('http://localhost:5279/lbryapi', { + method: "claim_list", + params: { + name: claimName + } + } + ).then(function (response) { + console.log(">> Claim_list success"); + console.log(">> Number of claims:", response.data.result.claims.length) + // return early if no claims were found + if (response.data.result.claims.length === 0){ + res.status(200).sendFile(path.join(__dirname, '../public', 'noClaims.html')); + return; + } + // filter the claims to return free, public claims + var freePublicClaims = filterForFreePublicClaims(response.data.result.claims); + // return early if no free, public claims were found + if (!freePublicClaims || (freePublicClaims.length === 0)){ + res.status(200).sendFile(path.join(__dirname, '../public', 'noClaims.html')); + return; + } + // order the claims + var orderedPublcClaims = orderTopClaims(freePublicClaims); + // create the uri for the first (selected) claim + console.log(">> ordered free public claims", orderedPublcClaims); + var freePublicClaimUri = "lbry://" + orderedPublcClaims[0].name + "#" + orderedPublcClaims[0].claim_id; + console.log(">> your free public claim uri:", freePublicClaimUri); + // fetch the image to display + axios.post('http://localhost:5279/lbryapi', { + method: "get", + params: { + uri: freePublicClaimUri + } + } + ).then(function (getResponse) { + console.log(">> 'get claim' success..."); + console.log(">> response data:", getResponse.data); + console.log(">> dl path =", getResponse.data.result.download_path) + // return the claim we got + res.status(200).sendFile(getResponse.data.result.download_path); + }).catch(function(getError){ + console.log(">> /c/ 'get' error:", getError.response.data); + res.status(500).send(JSON.stringify({msg: "An error occurred while fetching the free, public claim by URI.", err: getError.response.data.error.message})); + }) + }).catch(function(error){ + console.log(">> /c/ error:", error.response.data); + res.status(500).send(JSON.stringify({msg: "An error occurred while getting the claim list.", err: error.response.data.error.message})); + }) + }, + serveClaimBasedOnUri: function(uri, res){ + /* + to do: need to pass the URI through a test (use 'resolve') to see if it is free and public. Right now it is jumping straight to 'get'ing and serving the asset. + */ + console.log(">> your uri:", uri); + // fetch the image to display + axios.post('http://localhost:5279/lbryapi', { // to do: abstract this code to a function that can be shared + method: "get", + params: { + uri: uri + } + } + ).then(function (getResponse) { + console.log(">> 'get claim' success..."); + console.log(">> response data:", getResponse.data); + console.log(">> dl path =", getResponse.data.result.download_path) + /* + to do: make sure the file has completed downloading before serving back the file + */ + // return the claim we got + res.status(200).sendFile(getResponse.data.result.download_path); + + /* delete the file after a certain amount of time? */ + + }).catch(function(error){ + console.log(">> /c/ 'get' error:", error.response.data); + res.status(500).send(JSON.stringify({msg: "an error occurred", err: error.response.data.error.message})); + }) + }, + serveAllClaims: function(claimName, res){ + // make a call to the daemon to get the claims list + axios.post('http://localhost:5279/lbryapi', { + method: "claim_list", + params: { + name: claimName + } + } + ).then(function (response) { + console.log(">> Claim_list success"); + console.log(">> Number of claims:", response.data.result.claims.length) + // return early if no claims were found + if (response.data.result.claims.length === 0){ + res.status(200).sendFile(path.join(__dirname, '../public', 'noClaims.html')); + return; + } + // filter the claims to return free, public claims + var freePublicClaims = filterForFreePublicClaims(response.data.result.claims); + // return early if no free, public claims were found + if (!freePublicClaims || (freePublicClaims.length === 0)){ + res.status(200).sendFile(path.join(__dirname, '../public', 'noClaims.html')); + return; + } + console.log(">> Number of free public claims:", freePublicClaims.length); + // order the claims + var orderedPublicClaims = orderTopClaims(freePublicClaims); + // serve the response + res.status(200).send(orderedPublicClaims); //to do: rather than returning json, serve a page of all these claims + }).catch(function(error){ + console.log(">> /c/ error:", error.response.data); + // serve the response + res.status(500).send(JSON.stringify({msg: "An error occurred while finding the claim list.", err: error.response.data.error.message})); + }) + } +} diff --git a/helpers/queueApi.js b/helpers/queueApi.js new file mode 100644 index 00000000..6f8c5fef --- /dev/null +++ b/helpers/queueApi.js @@ -0,0 +1,22 @@ +// require amqp library +var amqp = require('amqplib/callback_api'); + +module.exports = { + addNewTaskToQueue: function(task){ + // connect to RabbitMQ server + amqp.connect('amqp://localhost', function(err, conn) { + // create a channel + conn.createChannel(function(err, ch) { + var q = 'task_queue2'; // declaring a que is idempotent (it will only be created if it doesnt already exist) + var msg = task || "request received with no task!"; + // declare a queue + ch.assertQueue(q, {durable: true}); + // publish a message to the queue + ch.sendToQueue(q, new Buffer.from(msg), {persistent: true}); + console.log(` [x] Sent '${msg}' to ${q}`); + }); + // close the connection and exit + setTimeout(function() {conn.close() }, 500); + }); + } +} \ No newline at end of file diff --git a/image.php b/image.php deleted file mode 100644 index ca67139b..00000000 --- a/image.php +++ /dev/null @@ -1,39 +0,0 @@ - $name, 'claim_id' => $claim['claim_id']]); - - if (isset($getResult['completed']) && $getResult['completed'] && isset($getResult['download_path'])) - { - $path = $getResult['download_path']; -// $validType = isset($getResult['content_type']) && in_array($getResult['content_type'], ['image/jpeg', 'image/png']); - header('Content-type: image/jpeg'); - header('Content-length: ' . filesize($path)); - readfile($getResult['download_path']); - } - elseif (isset($getResult['written_bytes'])) - { - echo 'This image is on it\'s way...
'; - echo 'Received: ' . $getResult['written_bytes'] . " / " . $getResult['total_bytes'] . ' bytes'; - } - else - { - echo 'There seems to be a valid claim, but are having trouble retrieving the content.'; - } -} -elseif (isset($_GET['new']) && $_GET['new']) -{ - echo 'Your image is on the way. It can take a few minutes to reach the blockchain and be public. You can refresh this page to check the progress.'; -} -else -{ - echo 'No valid claim for this name. Make one!'; - include './publish.php'; -} - -exit(0); diff --git a/index.php b/index.php deleted file mode 100644 index 6446539d..00000000 --- a/index.php +++ /dev/null @@ -1,45 +0,0 @@ -Something went wrong publishing your content. We are only somewhat sorry.

'; - } - exit(0); -} -?> - -

spee.ch logospee.ch

-

spee.ch is a single-serving site that reads and publishes images to and from the LBRY blockchain.

-

Examples:

- -

Publish Your Own

- -

About This Site

-

It was built live in a little over 2 hours on March 29th, 2017. You can watch the video here:

- diff --git a/package.json b/package.json new file mode 100644 index 00000000..c5793764 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "spee.ch-backend", + "version": "0.0.1", + "description": "a back end for spee.ch", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/billbitt/spee.ch-backend.git" + }, + "keywords": [ + "spee.ch", + "lbry", + "blockchain" + ], + "author": "@billbitt @vxn", + "license": "MIT", + "bugs": { + "url": "https://github.com/billbitt/spee.ch-backend/issues" + }, + "homepage": "https://github.com/billbitt/spee.ch-backend#readme", + "dependencies": { + "amqplib": "^0.5.1", + "axios": "^0.16.1", + "body-parser": "^1.17.1", + "connect-multiparty": "^2.0.0", + "express": "^4.15.2", + "nodemon": "^1.11.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..315a811f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/fourOhfour.html b/public/fourOhfour.html new file mode 100644 index 00000000..982f8678 --- /dev/null +++ b/public/fourOhfour.html @@ -0,0 +1,14 @@ + + + + + + + Four Oh Four + + +

spee.ch

+

404: Not Found

+

That page does not exist. Return home.

+ + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..d6561367 --- /dev/null +++ b/public/index.html @@ -0,0 +1,106 @@ + + + + + + + Spee.ch + + +

spee.ch

+

spee.ch is a single-serving site that reads and publishes images to and from the LBRY blockchain.

+

Examples:

+ +

Publish Your Own

+
+ +
+ Image preview... +
+ Title: +
+ Description: +
+ Author: +
+ Language: +
+ License: +
+ NSFW: +
+ +
+ +

Help

+

Site Navigation

+ +

API

+

Note: these are being used for testing durring spee.ch development and may not be maintained

+
    +
  • A GET request to spee.ch/claim_list/<the name of the claim> + +
  • +
+ + + + + \ No newline at end of file diff --git a/public/noClaims.html b/public/noClaims.html new file mode 100644 index 00000000..5ec22a47 --- /dev/null +++ b/public/noClaims.html @@ -0,0 +1,14 @@ + + + + + + + No Claims + + +

spee.ch

+

No Claims

+

There are no free, public images at that claim. You should publish one at spee.ch.

+ + \ No newline at end of file diff --git a/public/publishingClaim.html b/public/publishingClaim.html new file mode 100644 index 00000000..f599e292 --- /dev/null +++ b/public/publishingClaim.html @@ -0,0 +1,14 @@ + + + + + + + Publishing Asset + + +

spee.ch

+

Publishing Asset

+

Your asset is being published by a handy background worker. You can return to spee.ch and your asset should be published to your claim shortly.

+ + \ No newline at end of file diff --git a/publish.php b/publish.php deleted file mode 100644 index c9cb46a8..00000000 --- a/publish.php +++ /dev/null @@ -1,14 +0,0 @@ -
-
- -
- - - -
- lbry:// -
- - -

Publishing can take a few moments. Please be patient.

-
\ No newline at end of file diff --git a/routes/api-routes.js b/routes/api-routes.js new file mode 100644 index 00000000..67bbed41 --- /dev/null +++ b/routes/api-routes.js @@ -0,0 +1,29 @@ +// require dependencies +var path = require('path'); +var axios = require('axios'); +var multipart = require('connect-multiparty'); +var multipartMiddleware = multipart(); +// import helpers +var lbryApi = require('../helpers/lbryApi.js'); +var queueApi = require('../helpers/queueApi.js'); + +module.exports = function(app){ + // route to return claim list in json + app.get("/claim_list/:claim", function(req, res){ + var claim = req.params.claim; + // make a call to the daemon + axios.post('http://localhost:5279/lbryapi', { + method: "claim_list", + params: { + name: claim + } + } + ).then(function (response) { + console.log("success"); + res.send(response.data); + }).catch(function(error){ + console.log(error.data); + res.send(error.data); + }) + }); +} \ No newline at end of file diff --git a/routes/html-routes.js b/routes/html-routes.js new file mode 100644 index 00000000..2f358a86 --- /dev/null +++ b/routes/html-routes.js @@ -0,0 +1,80 @@ +// load dependencies +var path = require('path'); +var multipart = require('connect-multiparty'); +var multipartMiddleware = multipart(); +// load helpers +var lbryApi = require('../helpers/lbryApi.js'); +var queueApi = require('../helpers/queueApi.js'); + +// routes to export +module.exports = function(app){ + // route to fetch one free public claim + app.get("/favicon.ico", function(req, res){ + console.log(" >> GET request on favicon.ico"); + res.sendFile(path.join(__dirname, '../public', 'favicon.ico')); + }); + // route to publish a new claim + app.post("/publish", multipartMiddleware, function(req, res){ + // receive the request + console.log(" >> POST request on /publish"); + //console.log(">> req.files:", req.files) + console.log(" >> req.body:", req.body) + + // build the data needed to publish the file + var publishObject = { + "method":"publish", + "params": { + "name": req.body.title, + "file_path": req.files.file.path, + "bid": 0.1, + "metadata": { + "description": req.body.description, + "title": req.body.title, + "author": req.body.author, + "language": req.body.language, + "license": req.body.license, + "nsfw": req.body.nsfw.value + } + } + }; + //console.log(">> publishObject:", publishObject) + + // post the task to the que + queueApi.addNewTaskToQueue(JSON.stringify({ + type: 'publish', + data: publishObject + })); + // respond to the client that the task has been queued + res.status(200).sendFile(path.join(__dirname, '../public', 'publishingClaim.html')); + + }); + // route to fetch one free public claim + app.get("/:name/all", function(req, res){ + var name = req.params.name; + console.log(">> GET request on /" + name + " (all)"); + lbryApi.serveAllClaims(name, res); + }); + // route to fetch one free public claim + app.get("/:name/:claim_id", function(req, res){ + var uri = "lbry://" + req.params.name + "#" + req.params.claim_id; + console.log(">> GET request on /" + uri); + lbryApi.serveClaimBasedOnUri(uri, res); + }); + // route to fetch one free public claim + app.get("/:name", function(req, res){ + var name = req.params.name; + console.log(">> GET request on /" + name) + // publish a message to the cue + // queueApi.addNewTaskToQueue("return claim for " + req.params.name + " ...") + // retrieve the claim + lbryApi.serveClaimBasedOnNameOnly(name, res); + }); + // route for the home page + app.get("/", function(req, res){ + res.sendFile(path.join(__dirname, '../public', 'index.html')); + }); + // a catch-all route if someone visits a page that does not exist + app.use("*", function(req, res){ + res.sendFile(path.join(__dirname, '../public', 'fourOhfour.html')); + }); +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 00000000..7111174b --- /dev/null +++ b/server.js @@ -0,0 +1,20 @@ +// load dependencies +var express = require('express'); +var bodyParser = require('body-parser'); +var path = require('path'); +// set port +var PORT = 80; +// initialize express +var app = express(); +// make express look in the public directory for assets (css/js/img) +app.use(express.static(__dirname + '/public')); +// configure epress +app.use(bodyParser.json()); // for parsing application/json +app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded +// require in routes +require("./routes/api-routes.js")(app); +require("./routes/html-routes.js")(app); +// start server +app.listen(PORT, function() { + console.log("Listening on PORT " + PORT); +}); diff --git a/worker.js b/worker.js new file mode 100644 index 00000000..dd7b179d --- /dev/null +++ b/worker.js @@ -0,0 +1,31 @@ +// load dependencies +var amqp = require('amqplib/callback_api'); +// load helpers +var lbryApi = require('./helpers/lbryApi'); +// open a connection and a channel +amqp.connect('amqp://localhost', function(err, conn) { + // open a channel + conn.createChannel(function(err, ch) { + var q = 'task_queue2'; + // declare the cue (in case the publisher hasn't made it yet) + ch.assertQueue(q, {durable: true}); + // tell the queue to only assign one task at a time to this worker + ch.prefetch(1); + // listen for messages & pass callback for what to do with the msgs + console.log(" [x] Waiting for messages in %s. To exit press ctrl+c", q); + ch.consume(q, function(msg) { + var task = JSON.parse(msg.content.toString()); + console.log(` [o] Received a ${task.type} task`); + // initiate the task + switch(task.type) { + case 'publish': + console.log(" [-] publishing:", task.data); + lbryApi.publishClaim(task.data); + break; + default: + console.log(" [-] that task type is not recognized"); + console.log(" [x] Done"); + } + }, {noAck: true}); + }); +}); \ No newline at end of file