diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..9160059
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1 @@
+service_name: travis-ci
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6b3066e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+- '8'
+services:
+ - mongodb
+ - redis-server
+after_script: istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
diff --git a/README.md b/README.md
index 945add7..2e1a532 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
decentralize
============
+[](https://travis-ci.org/FabricLabs/decentralize)
+[](https://coveralls.io/r/FabricLabs/decentralize)
+[](https://github.com/FabricLabs/decentralize/contributors)
podcast
diff --git a/config/index.js b/config/index.js
index cce2fd8..0f2948a 100644
--- a/config/index.js
+++ b/config/index.js
@@ -12,13 +12,14 @@ module.exports = {
mission: 'the podcast',
description: 'The best place for learning about what\'s going on with the latest decentralized tech.',
source: 'https://github.com/martindale/decentralize',
- points: []
+ points: [],
+ dependencies: ['decentral']
},
source: {
- sockets: 'ws://',
- proto: 'http',
- host: 'localhost',
- port: '15005'
+ sockets: 'wss://',
+ proto: 'https',
+ host: process.env.DECENTRAL_HOST || 'decentral.fm',
+ port: process.env.DECENTRAL_PORT || '443'
},
soundcloud: {
clientID: '98ce66124fabdff2f33beb2f810a0a25',
diff --git a/decentralize.js b/decentralize.js
index f51c8c7..fbaa31c 100644
--- a/decentralize.js
+++ b/decentralize.js
@@ -1,20 +1,25 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
var config = require('./config');
+var WebSocket = require('ws');
var procure = require('procure');
-
var Maki = require('maki');
+
var Soundcloud = require('./lib/Soundcloud');
var Engine = require('./lib/Engine');
+
var Remote = require('maki-remote');
-var WebSocket = require('ws');
+var Sessions = require('maki-sessions');
+var ProxAuth = require('maki-auth-proxy');
+var Policies = require('maki-auth-tokens');
-var jsonpatch = require('fast-json-patch');
+var rest = require('restler');
var home = 'https://' + config.service.authority;
var source = config.source;
source.authority = source.host;
+
if (!~[80, 443].indexOf( parseInt(source.port) )) {
source.authority += ':' + source.port;
}
@@ -22,34 +27,16 @@ source.base = source.proto + '://' + source.authority;
var decentralize = new Maki( config );
var soundcloud = new Soundcloud( config.soundcloud );
-
-var Sessions = require('maki-sessions');
var sessions = new Sessions();
+var policies = new Policies();
decentralize.use(sessions);
+//decentralize.use(policies);
// setup of various remotes, subservices on another Maki namespace
var MailPimpSubscription = new Remote('http://localhost:2525/subscriptions');
var MailPimpTask = new Remote('http://localhost:2525/tasks');
-Show = decentralize.define('Show', {
- attributes: {
- title: { type: String , slug: true },
- slug: { type: String },
- recorded: { type: Date },
- released: { type: Date , default: Date.now , required: true },
- description: { type: String },
- audio: { type: String },
- filename: { type: String },
- // TODO: replace with a sources list.
- youtube: { type: String },
- soundcloud: { type: String },
- },
- names: { get: 'item' },
- source: source.proto + '://' + source.authority + '/recordings',
- icon: 'sound'
-});
-
var Subscription = decentralize.define('Subscription', {
attributes: {
email: { type: String , required: true , validator: function(value) {
@@ -88,18 +75,8 @@ Subscription.post('create', function(done) {
});
});
-Index = decentralize.define('Index', {
- name: 'Index',
- routes: { query: '/' },
- templates: { query: 'index' },
- requires: {
- 'Show': {
- filter: {}
- }
- },
- static: true,
- internal: true
-});
+Show = decentralize.define('Show', require('./resources/Show') );
+Index = decentralize.define('Index', require('./resources/Index') );
procure( source.base + '/shows/decentralize' , function(err, show) {
if (err || !show) return console.error(err || 'no such show: decentralize');
@@ -157,6 +134,25 @@ procure( source.base + '/shows/decentralize' , function(err, show) {
return next();
}
});
+ decentralize.app.get('/shows/:showSlug/edit', function(req, res, next) {
+ Show.get({ slug: req.param('showSlug') }, function(err, show) {
+ return res.render('show-edit', {
+ item: show
+ });
+ });
+ });
+ decentralize.app.post('/shows/:showSlug', function(req, res, next) {
+ rest.patch( source.proto + '://' + source.authority + '/recordings/' + req.param('showSlug') , {
+ headers: {
+ Accept: 'application/json'
+ },
+ data: req.body
+ }).on('complete', function(data, request) {
+ if (request.statusCode !== 200) return res.error('Editing failed');
+ req.flash('success', 'Content edited successfully!');
+ return res.redirect('/shows/' + req.param('showSlug'));
+ });
+ });
decentralize.app.get('/rss', function(req, res, next) {
res.redirect( 301 , '/feed' );
});
@@ -178,6 +174,7 @@ procure( source.base + '/shows/decentralize' , function(err, show) {
engine.subscribe();
engine.sync();
+ engine.subscribe();
});
diff --git a/decentralize:web.js b/decentralize:web.js
new file mode 100644
index 0000000..c091707
--- /dev/null
+++ b/decentralize:web.js
@@ -0,0 +1,5 @@
+var config = require('./config');
+var DECENTRALIZE = require('./lib/decentralize');
+var decentralize = new DECENTRALIZE(config);
+
+decentralize.start();
diff --git a/lib/Engine.js b/lib/Engine.js
index 6f5a60c..a4015e1 100644
--- a/lib/Engine.js
+++ b/lib/Engine.js
@@ -56,6 +56,9 @@ Engine.prototype.subscribe = function( cb ) {
attemptReconnect();
});
+
+ // TODO: port to Fabric
+ // TODO: add Router
self.ws.on('message', function(data) {
console.log('DATAGRAM:' , data );
var msg = JSON.parse( data );
@@ -72,7 +75,6 @@ Engine.prototype.subscribe = function( cb ) {
}
}
}
-
}
});
};
@@ -171,6 +173,6 @@ Engine.prototype.sync = function( cb ) {
});
});
});
-}
+};
module.exports = Engine;
diff --git a/lib/decentralize.js b/lib/decentralize.js
new file mode 100644
index 0000000..824b60c
--- /dev/null
+++ b/lib/decentralize.js
@@ -0,0 +1,181 @@
+process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
+
+var procure = require('procure');
+
+var Maki = require('maki');
+var Soundcloud = require('./Soundcloud');
+var Engine = require('./Engine');
+var Remote = require('maki-remote');
+var WebSocket = require('ws');
+
+var jsonpatch = require('fast-json-patch');
+
+function DECENTRALIZE (config) {
+ if (!config) config = require('../config');
+
+ var self = this;
+
+ self.config = config;
+ self.home = 'https://' + config.service.authority;
+
+ self.source = config.source;
+ self.source.authority = self.source.host;
+ if (!~[80, 443].indexOf( parseInt(self.source.port) )) {
+ self.source.authority += ':' + self.source.port;
+ }
+ self.source.base = self.source.proto + '://' + self.source.authority;
+
+ self.app = new Maki( config );
+ var soundcloud = new Soundcloud( config.soundcloud );
+
+ var Sessions = require('maki-sessions');
+ var sessions = new Sessions();
+
+ self.app.use(sessions);
+
+ // setup of various remotes, subservices on another Maki namespace
+ var MailPimpSubscription = new Remote('http://localhost:2525/subscriptions');
+ var MailPimpTask = new Remote('http://localhost:2525/tasks');
+
+ var Subscription = self.app.define('Subscription', {
+ attributes: {
+ email: { type: String , required: true , validator: function(value) {
+ // see HTML spec: https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
+ // this should mirror HTML5's "type=email", as per the above link
+ return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test( value );
+ } },
+ status: { type: String , enum: ['requested', 'pending', 'validated'], default: 'requested' },
+ created: { type: Date , default: Date.now },
+ },
+ handlers: {
+ html: {
+ create: function(req, res, next) {
+ req.flash('info', 'Successfully subscribed! Check your inbox for a confirmation.');
+ res.redirect('/');
+ }
+ }
+ }
+ });
+
+ Subscription.post('create', function(done) {
+ var subscription = this;
+ MailPimpSubscription.create({
+ email: subscription.email,
+ // TODO: place in config, or auto-create-and-collect
+ _list: '55d490d83e5a3dcf5287129e'
+ }, function(err, data) {
+ // TODO: retry, error handling, etc.
+ if (!data._id) return done('no subscription created');
+ Subscription.patch({ _id: subscription._id }, [
+ { op: 'replace', path: '/status', value: 'pending' }
+ ], function(err) {
+ if (err) console.error(err);
+ done(err);
+ });
+ });
+ });
+
+ Show = self.app.define('Show', require('../resources/Show'));
+ Index = self.app.define('Index', require('../resources/Index'));
+
+}
+
+DECENTRALIZE.prototype.start = function (done) {
+ var self = this;
+ if (!done) done = Function();
+
+ procure( self.source.base + '/shows/decentralize' , function(err, show) {
+ if (err || !show) return console.error(err || 'no such show: decentralize');
+
+ try {
+ self.config.show = JSON.parse( show );
+ } catch (e) {
+ console.error(e);
+ return done(e);
+ }
+
+ self.config.show.source = self.source;
+ self.config.show.home = self.home;
+
+ console.log('maki starting...');
+
+ self.app.start(function() {
+ console.log('maki started!');
+
+ self.app.app.locals.show = self.config.show;
+
+ self.app.app.get('/about', function(req, res, next) {
+ res.render('about');
+ });
+ self.app.app.get('/contact', function(req, res, next) {
+ res.render('contact');
+ });
+ self.app.app.post('/contact', function(req, res, next) {
+ MailPimpTask.create({
+ subject: 'DECENTRALIZE Contact Form',
+ recipient: 'eric@self.app.fm',
+ sender: req.param('from'),
+ content: req.param('message')
+ }, function(err, task) {
+ req.flash('info', 'Mail sent successfully! We\'ll get in touch shortly.');
+ res.redirect('/');
+ });
+ });
+ self.app.app.get('/team', function(req, res, next) {
+ res.render('team');
+ });
+ self.app.app.get('/subscribe', function(req, res, next) {
+ res.render('subscribe');
+ });
+
+ // redirect an erroneous lengthy tag
+ self.app.app.get('/shows/episode-26-nick-sullivan-on-changetip-and-the-future-of-bitcoin-microtransactions', function(req, res, next) {
+ res.redirect( 301 , '/shows/episode-25-nick-sullivan');
+ });
+ self.app.app.get('/shows/episode-29-susanne-tempelhof-and-dan-metcalf-on-blockchain-applications-and-bitcoin-2-0', function(req, res, next) {
+ res.redirect( 301 , '/shows/episode-29-susanne-tempelhof-and-dan-metcalf');
+ });
+
+ self.app.app.get('/:somePath', function(req, res, next) {
+ Show.get({ slug: req.param('somePath') }, function(err, show) {
+ if (show) return res.redirect('/shows/' + show.slug );
+ next();
+ });
+ });
+ self.app.app.get('/shows', function(req, res, next) {
+ if (req.accepts('application/rss+xml')) {
+ return res.redirect('/feed');
+ } else {
+ return next();
+ }
+ });
+ self.app.app.get('/rss', function(req, res, next) {
+ res.redirect( 301 , '/feed' );
+ });
+ self.app.app.get('/feed', function(req, res, next) {
+ Show.query({}, function(err, shows) {
+ res.set('Content-Type', 'application/rss+xml');
+ res.render('feed', {
+ resource: Show,
+ collection: shows
+ });
+ });
+ });
+
+ var engine = new Engine( self.config , self.app );
+
+ setInterval(function() {
+ //engine.sync();
+ }, /*/ 2500 /*/ 1 * 3600 * 1000 /**/ );
+
+ engine.subscribe();
+ engine.sync();
+
+ return done();
+
+ });
+
+ });
+}
+
+module.exports = DECENTRALIZE;
diff --git a/package.json b/package.json
index e6ff217..6232113 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,12 @@
"name": "decentralize",
"version": "0.0.0",
"description": "decentralize the podcast",
- "main": "decentralize.js",
+ "main": "lib/decentralize.js",
+ "engine": "node >= 8.0.0",
"scripts": {
- "test": "mocha"
+ "test": "mocha",
+ "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive",
+ "start": "node decentralize:web.js"
},
"repository": {
"type": "git",
@@ -23,21 +26,23 @@
"dependencies": {
"async": "^0.9.0",
"content-disposition": "^0.5.0",
- "fast-json-patch": "^0.5.1",
+ "fast-json-patch": "^0.5.2",
"form-data": "^0.2.0",
"lodash": "^3.3.0",
- "maki": "git://github.com/martindale/maki#6be8e8d5949e00f79abf348a406e3369c883051d",
+ "maki": "github:martindale/maki#tatami",
"maki-remote": "0.0.1",
+ "maki-auth-proxy": "0.0.0",
"maki-sessions": "^0.1.0",
"node-torrent-stream": "git://github.com/unusualbob/node-torrent-stream",
- "procure": "^0.1.3",
+ "procure": "^0.1.4",
"request": "^2.53.0",
"restler": "^3.2.2",
"ws": "^0.7.1"
},
"devDependencies": {
- "coveralls": "^2.11.2",
- "istanbul": "^0.3.6",
+ "chai": "^4.1.2",
+ "coveralls": "^2.13.3",
+ "istanbul": "^0.3.22",
"mocha": "^2.1.0"
}
}
diff --git a/public/js/marked.min.js b/public/js/marked.min.js
new file mode 100644
index 0000000..a5164c4
--- /dev/null
+++ b/public/js/marked.min.js
@@ -0,0 +1,6 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ */
+(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/ "+text+"
"}return'"+(escaped?code:escape(code,true))+"\n
\n"};Renderer.prototype.blockquote=function(quote){return"'+(escaped?code:escape(code,true))+"\n\n"+quote+"
\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"
\n":"
\n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+""+type+">\n"};Renderer.prototype.listitem=function(text){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
\n"};Renderer.prototype.tablerow=function(content){return"\n"+content+" \n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+""+type+">\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
":"
"};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0){return""}}var out='"+text+"";return out};Renderer.prototype.image=function(href,title,text){var out='":">";return out};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i
"+escape(e.message+"",true)+""}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); \ No newline at end of file diff --git a/resources/Index.js b/resources/Index.js new file mode 100644 index 0000000..a56349b --- /dev/null +++ b/resources/Index.js @@ -0,0 +1,12 @@ +module.exports = { + name: 'Index', + routes: { query: '/' }, + templates: { query: 'index' }, + requires: { + 'Show': { + filter: {} + } + }, + static: true, + internal: true +}; diff --git a/resources/Show.js b/resources/Show.js new file mode 100644 index 0000000..5996214 --- /dev/null +++ b/resources/Show.js @@ -0,0 +1,17 @@ +var source = require('../config').source; + +module.exports = { + attributes: { + title: { type: String , slug: true }, + slug: { type: String }, + recorded: { type: Date }, + released: { type: Date , default: Date.now , required: true }, + description: { type: String , format: 'markdown' }, + audio: { type: String }, + // TODO: replace with a sources list. + youtube: { type: String } + }, + names: { get: 'item' }, + source: source.proto + '://' + source.authority + '/recordings', + icon: 'sound' +} diff --git a/test/decentralize.integration.js b/test/decentralize.integration.js new file mode 100644 index 0000000..14d11c7 --- /dev/null +++ b/test/decentralize.integration.js @@ -0,0 +1,24 @@ +var assert = require('assert'); +var expect = require('chai').expect; + +var DECENTRALIZE = require('../'); +var config = require('../config'); + +describe('DECENTRALIZE', function () { + this.timeout(20000); + + it('should expose a constructor', function () { + assert(typeof DECENTRALIZE, 'function'); + }); + + it('should start successfully', function (done) { + var app = new DECENTRALIZE(config); + app.start(function () { + app.destroy(done); + }); + }); + + it('can create a local recording', function (done) { + done(); + }); +}); diff --git a/views/layouts/default.jade b/views/layouts/default.jade index 2a65895..228bbf4 100644 --- a/views/layouts/default.jade +++ b/views/layouts/default.jade @@ -55,6 +55,7 @@ html.no-js script(src="/js/highlight.pack.js") script(src="/js/uuid.js") script(src="/js/jsonrpc.js") + script(src="/js/marked.min.js") script(src="/js/maki.js") diff --git a/views/show-edit.jade b/views/show-edit.jade new file mode 100644 index 0000000..2d2a1a1 --- /dev/null +++ b/views/show-edit.jade @@ -0,0 +1,41 @@ +extends layouts/default + +block content + + h1 #{item.title} + + h2 Preview + .preview !{ markdown( item.description ) } + + h2 Edit + + form.ui.form(action="/shows/#{item.slug}", method="POST") + input(type="hidden", name="method", value="PATCH") + + .field + label Description + textarea(name="description", style="height: 20em;") #{item.description} + + .field + label YouTube Video ID + input(type="text", name="youtube", value="#{item.youtube || ''}") + + .two.fields + .field + label decentral.fm Username + input(type="text", name="username") + .field + label decentral.fm Password + input(type="password", name="password") + + button.ui.submit.button(type="submit") Edit » + +block scripts + script. + $('textarea[name=description]').keyup( renderPreview ); + + function renderPreview() { + var value = this.value; + var rendered = marked( value ); + $('.preview').html( rendered ); + }