├── lib ├── oui ├── couchdb.js ├── imagemagick.js ├── dropular │ ├── tag.js │ ├── config.js │ ├── legacy-user.js │ ├── sql.js │ └── drop.js ├── aws │ ├── utf8.js │ ├── base64.js │ ├── pool.js │ ├── s3.js │ ├── sha1.js │ └── httputil.js ├── imgdb.js └── base62.js ├── public ├── oui.js ├── favicon.ico ├── res │ ├── logo.png │ ├── offf.jpg │ ├── plus.png │ ├── close.png │ ├── no_icon.png │ ├── register.jpg │ ├── dear_visitor.jpg │ ├── loading-bg.png │ ├── loading-bg.psd │ ├── loading-view.gif │ ├── loading-view.psd │ ├── throbber-32.png │ └── dropular_logo_eps.zip ├── 404.html ├── 502.html └── drop.js ├── client ├── main-view.js ├── util │ ├── modal.html │ ├── modal.less │ ├── throbber.html │ └── notify.html ├── about │ ├── index.js │ ├── todo.html │ ├── flag.html │ ├── privacy.html │ ├── registrations-will-soon-open.html │ ├── about.html │ └── terms.html ├── users │ ├── index.js │ ├── listing.html │ ├── profile.less │ └── profile.html ├── header │ ├── userinfo.html │ ├── login-sheet.html │ └── userinfo.js ├── toolbox │ ├── subscriptions.html │ ├── subscriptions.less │ └── index.js ├── drops │ ├── drop.less │ ├── index.html │ ├── index.less │ ├── drop.html │ └── index.js ├── error.html ├── welcome.js ├── main-view-loading.html ├── header.less ├── _base.less ├── debug │ └── stats.js ├── jquery.continuum.js ├── index.less ├── index.html ├── index.js └── drop │ └── index.html ├── .gitignore ├── httpd.conf.js.in ├── .gitmodules ├── misc ├── lighttpd.conf ├── data │ ├── views │ │ ├── users-users.json │ │ ├── drops-replication.json.disabled │ │ ├── drops-offline.json.disabled │ │ ├── drops-tags.json │ │ └── drops-drops.json │ ├── import-docs.js │ ├── import-batch.js │ └── README.md ├── bin │ └── legacy-passcheck.js ├── nginx.conf ├── data-recipes │ └── auto-complete.js ├── docs │ └── setting-up-an-ec2-instance.md ├── init.d │ └── dropular-httpd └── offline │ └── drop-popularity.js └── README.md /lib/oui: -------------------------------------------------------------------------------- 1 | ../../oui/oui -------------------------------------------------------------------------------- /public/oui.js: -------------------------------------------------------------------------------- 1 | ../../oui/client/oui.js -------------------------------------------------------------------------------- /lib/couchdb.js: -------------------------------------------------------------------------------- 1 | ../../node-couchdb-min/couchdb.js -------------------------------------------------------------------------------- /lib/imagemagick.js: -------------------------------------------------------------------------------- 1 | ../../node-imagemagick/imagemagick.js -------------------------------------------------------------------------------- /client/main-view.js: -------------------------------------------------------------------------------- 1 | index.mixinViewControl(exports, '#main'); 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/logo.png -------------------------------------------------------------------------------- /public/res/offf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/offf.jpg -------------------------------------------------------------------------------- /public/res/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/plus.png -------------------------------------------------------------------------------- /public/res/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/close.png -------------------------------------------------------------------------------- /public/res/no_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/no_icon.png -------------------------------------------------------------------------------- /public/res/register.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/register.jpg -------------------------------------------------------------------------------- /public/res/dear_visitor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/dear_visitor.jpg -------------------------------------------------------------------------------- /public/res/loading-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/loading-bg.png -------------------------------------------------------------------------------- /public/res/loading-bg.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/loading-bg.psd -------------------------------------------------------------------------------- /public/res/loading-view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/loading-view.gif -------------------------------------------------------------------------------- /public/res/loading-view.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/loading-view.psd -------------------------------------------------------------------------------- /public/res/throbber-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/throbber-32.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public/index.css 2 | /public/index.html 3 | /public/index.js 4 | .build-cache 5 | /httpd.conf.js 6 | -------------------------------------------------------------------------------- /public/res/dropular_logo_eps.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/dropular-2010/HEAD/public/res/dropular_logo_eps.zip -------------------------------------------------------------------------------- /httpd.conf.js.in: -------------------------------------------------------------------------------- 1 | //exports.debug = true 2 | //exports.port = 8800 3 | //exports.quiet = true 4 | //exports.addr = '127.0.0.1' 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "node.dbslayer.js"] 2 | path = node.dbslayer.js 3 | url = git://github.com/Guille/node.dbslayer.js.git 4 | [submodule "oui"] 5 | path = oui 6 | url = git://github.com/rsms/oui.git 7 | -------------------------------------------------------------------------------- /client/util/modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Message

6 |

7 | Description 8 |

9 |
10 |

Details

11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /misc/lighttpd.conf: -------------------------------------------------------------------------------- 1 | $HTTP["host"] == "dropular.hunch.se" { 2 | $HTTP["url"] =~ "^/(session|drop|user)" { 3 | #proxy.balance = "hash" # hash=per-URI, fair=default, round-robin=n-req 4 | #proxy.debug = 1 5 | proxy.server = (""=>( 6 | ( "host" => "127.0.0.1", "port" => 8100 ), 7 | ( "host" => "127.0.0.1", "port" => 8101 ) 8 | )) 9 | } 10 | } -------------------------------------------------------------------------------- /client/about/index.js: -------------------------------------------------------------------------------- 1 | oui.anchor.on(/^about\/([^\/]+)/, function(params){ 2 | var modname = oui.util.canonicalizeModuleName(params.page), 3 | mod = about[modname], 4 | view; 5 | if (!mod || !mod.__html || (view = mod.__html('div:first')).length === 0) { 6 | error.present({title:'Not found'}); 7 | } else { 8 | mainView.setView(view); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /client/users/index.js: -------------------------------------------------------------------------------- 1 | function User() {} 2 | exports.User = User; 3 | 4 | User.fromDocument = function(doc) { 5 | var u = new User(); 6 | oui.mixin(u, doc); 7 | return u; 8 | }; 9 | 10 | User.find = function(username, callback) { 11 | if (!callback) 12 | throw new Error(__name+'.User.find: callback parameter must be a function'); 13 | oui.app.session.get('users/'+oui.urlesc(username), callback); 14 | }; 15 | -------------------------------------------------------------------------------- /misc/data/views/users-users.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "_design/users", 3 | "language": "javascript", 4 | "views": { 5 | "by-email": { 6 | "map": "function(doc) {\n emit(doc.email.toLowerCase(), null);\n}" 7 | }, 8 | "followers": { 9 | "map": "function(doc) {\n if (!doc.following) return;\n doc.following.forEach(function(u) {\n emit(u, null);\n });\n}" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /misc/data/views/drops-replication.json.disabled: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "_design/replication", 3 | "language": "javascript", 4 | "filters": { 5 | "user-drops": "function(doc, req) { if (!req.query.usernames) return false; if (doc._id === '_design/user-drops') return true; if (doc && !doc.disabled && typeof doc.users === 'object' && req.query.usernames.some(function(username){ return doc.users[username]})) return true;\n return false;\n}" 6 | } 7 | } -------------------------------------------------------------------------------- /client/header/userinfo.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Hi, Username 4 | Logout 5 |
6 | 7 | 8 |
9 | 10 | Register 13 |
14 | -------------------------------------------------------------------------------- /client/toolbox/subscriptions.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Subscriptions

4 | 5 | 8 | 9 |
10 | 11 |
12 |

13 | 14 |
15 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 Not found — Dropular 6 | 13 | 14 | 15 |

404 Not found —

16 |

Oops. I'm afraid there's nothing here.

17 | 18 | 19 | -------------------------------------------------------------------------------- /public/502.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dropular 6 | 13 | 14 | 15 |

Dropular —

16 |

Temporarily down for maintenance. Check back in a few minutes.

17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/dropular/tag.js: -------------------------------------------------------------------------------- 1 | var strfold = require('../strfold'); 2 | exports.allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789_$()+-_/'; 3 | 4 | /** 5 | * Return canonical representation of tag which can be a string or an array of 6 | * strings. 7 | */ 8 | exports.canonicalize = function(tag) { 9 | if (Array.isArray(tag)) { 10 | for (var i=0,t; (t = tag[i]); ++i) { 11 | t = String(t).toLowerCase(); 12 | if (t.length) 13 | tag[i] = strfold.fold(t, exports.allowedChars); 14 | } 15 | tag = tag.unique(); 16 | tag.sort(); 17 | return tag; 18 | } else { 19 | return strfold.fold(String(tag).toLowerCase(), exports.allowedChars); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/drops/drop.less: -------------------------------------------------------------------------------- 1 | @import "../_base.less"; 2 | drop { 3 | color: #1a1a1a; 4 | 5 | h1 { 6 | overflow:hidden; text-overflow:ellipsis; 7 | } 8 | 9 | info { 10 | display: block; 11 | float: left; 12 | width: 550px; 13 | //.origin {} 14 | .redroppers { display: block; } 15 | } 16 | 17 | tools { 18 | display: block; 19 | float: right; 20 | margin-right: 12px; 21 | } 22 | 23 | .clear { 24 | display: block; 25 | clear: both; 26 | height: 50px; 27 | } 28 | 29 | tags { 30 | font-style: italic; 31 | a { color: #bbbbbb; } 32 | } 33 | 34 | img { 35 | display: block; 36 | max-width: 650px; 37 | height: auto; 38 | margin: 25px 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/error.html: -------------------------------------------------------------------------------- 1 | 12 | 22 | 23 |

An error occured

24 |

Message

25 | Message 26 |
-------------------------------------------------------------------------------- /misc/bin/legacy-passcheck.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -- 2 | var sys = require('sys'), fs = require('fs'), path = require('path'); 3 | require.paths.unshift(path.join(path.dirname(fs.realpathSync(__filename)), '/../../lib')); 4 | var legacy_user = require('dropular/legacy-user'), 5 | dbslayer = require('dbslayer'); 6 | 7 | legacy_user.db = new dbslayer.Server('hal.hunch.se', 9090, 15000); 8 | 9 | if (process.argv.length < 4) throw new Error('too few arguments'); 10 | 11 | legacy_user.passwdCheck(process.argv[2], process.argv[3], function(err, match) { 12 | if (err) throw err; 13 | if (match) { 14 | sys.error('success -- todo: re-encrypt password and store it'); 15 | } else { 16 | sys.error('bad password'); 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /misc/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream dropular_backends { 2 | ip_hash; 3 | server 127.0.0.1:8100; 4 | #server 127.0.0.1:8102; 5 | #server 127.0.0.1:8103; 6 | #server 127.0.0.1:8104; 7 | } 8 | 9 | server { 10 | listen 80; 11 | server_name dropular.net www.dropular.net; 12 | access_log /var/log/nginx/dropular.access.log; 13 | error_page 502 /502.html; 14 | error_page 404 /404.html; 15 | location ^~ /api/get_ { 16 | # send 404 for legacy api requests 17 | root /var/dropular/dropular/public/notfound; 18 | } 19 | location ^~ /api/ { 20 | proxy_pass http://dropular_backends; 21 | proxy_set_header X-Client-Addr $remote_addr; 22 | } 23 | location / { 24 | root /var/dropular/dropular/public; 25 | index index.html; 26 | } 27 | } -------------------------------------------------------------------------------- /lib/dropular/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | couchdb = require('../couchdb'), 4 | s3 = require("../aws/s3"); 5 | __dirname = path.dirname(fs.realpathSync(__filename)); 6 | 7 | 8 | // Load user config 9 | exports.config = {} 10 | try { 11 | exports.config = require(__dirname+'/../../config'); 12 | } catch (e) {} 13 | var cf = exports.config; 14 | 15 | // Databases 16 | exports.db = { 17 | //_auth: {type:'basic', username:'dropular', password:'abc'}, 18 | users: new couchdb.Db(cf.users_db || {db:'dropular-users', timeout:15000}), 19 | drops: new couchdb.Db(cf.drops_db || 'dropular-drops'), 20 | newslist: new couchdb.Db(cf.newslist_db || 'dropular-newslist'), 21 | } 22 | 23 | exports.s3 = { 24 | 'static': new s3.Bucket('static.dropular.net', 25 | 'API_KEY', 'PRIVATE_KEY') 26 | }; 27 | -------------------------------------------------------------------------------- /client/welcome.js: -------------------------------------------------------------------------------- 1 | // show welcome for non-logged in users 2 | /*oui.app.session.on('userchange', function(ev, prevUser){ 3 | if (!this.user) 4 | mainView.setView(exports.$view); 5 | });*/ 6 | 7 | oui.app.on('start', function(ev, willAuthUser){ 8 | // trick to keep a reference to the intro view 9 | exports.$view = mainView.$container.find('div.welcome-view'); 10 | 11 | //setTimeout(function(){ 12 | oui.anchor.on("", function(){ 13 | if (oui.app.session.user) 14 | document.location.hash = '#drops'; 15 | else 16 | mainView.setView(exports.$view); 17 | }); 18 | 19 | 20 | //},1); // next tick 21 | 22 | // if the app is trying to authenticate a returning user, hide the welcome 23 | // message (it will be shown again if the user turned out not to be logged in) 24 | //if (willAuthUser) 25 | // exports.$view.hide(); 26 | }); 27 | -------------------------------------------------------------------------------- /client/users/listing.html: -------------------------------------------------------------------------------- 1 | 16 | 29 |
30 | 33 |
-------------------------------------------------------------------------------- /client/drops/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 |
8 |

Recent drops

9 |

From everyone

10 |
11 |
12 |

Drops

13 |

From people you follow

14 |
15 |
16 |

Interesting drops

17 |

From everyone

18 |
19 |
20 |

Drops tagged

21 |

From everyone

22 |
23 | 24 | 25 |
26 |

From

27 |

28 |
29 | 30 |
31 |
32 | Load 18 more... 33 |
34 | -------------------------------------------------------------------------------- /client/main-view-loading.html: -------------------------------------------------------------------------------- 1 | 19 | 26 |
27 |
-------------------------------------------------------------------------------- /client/users/profile.less: -------------------------------------------------------------------------------- 1 | @import "../_base.less"; 2 | 3 | content.user-profile { 4 | 5 | sources { 6 | display:block; 7 | float:right; 8 | width: 250px; 9 | } 10 | 11 | box { 12 | float: right; 13 | margin-left: 15px; 14 | h3 { 15 | text-align: right; 16 | display: block; 17 | margin: 0; padding: 0; 18 | a { color: @titleColor; } 19 | a:hover { border-bottom: 1px solid @linkColor; } 20 | a.active { color: @linkColor; } 21 | } 22 | small { text-align: right; display: block; font-size: 9px; } 23 | } 24 | 25 | box.not-yet-implemented { 26 | a, small { color:#aaa; cursor:default; } 27 | a:hover { border:none; } 28 | } 29 | 30 | header { 31 | .about { width: 450px; } 32 | 33 | h1 { margin: 0; padding: 0; } 34 | 35 | img.avatar { 36 | .size(64px, 64px); 37 | margin-top: 3px; 38 | float: left; 39 | margin-right:25px; 40 | } 41 | } 42 | 43 | .clear { 44 | display: block; 45 | clear: both; 46 | margin-bottom: 25px; 47 | } 48 | } -------------------------------------------------------------------------------- /misc/data/views/drops-offline.json.disabled: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "_design/offline", 3 | "language": "javascript", 4 | "views": { 5 | "drop-popularity": { 6 | "options": {"collation": "raw"}, 7 | "map": "function(doc) {\n if (!doc || !doc._id || doc.disabled \n || typeof doc.users !== 'object') return;\n var user, timeAndScore;\n for (user in doc.users) {\n timeAndScore = doc.users[user];\n emit(doc._id, timeAndScore);\n }\n}", 8 | "reduce": "function(keys, values, rereduce) {\n var i, score = 0;\n if (!rereduce) {\n // oldest drop 2009-01-25T10:42:21.000Z\n var B = 1232883741000;\n var A = values[0][0];\n\n var t = A - B;\n var x = 0;\n for (i in values)\n x += values[i][1];\n\n if (x > 0) y = 1;\n else if (x == 0) y = 0;\n else y = -1;\n\n z = (Math.abs(x) >=1 && Math.abs(x) || 1);\n score = Math.log(z) + (y*t)/45000;\n } else {\n score = values[0];\n for (i=0; i score)\n score = values[i];\n }\n }\n\n return score;\n}" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /client/header.less: -------------------------------------------------------------------------------- 1 | @import "_base.less"; 2 | 3 | body > grid > header { 4 | .block; 5 | height: 75px; 6 | border-bottom: 10px solid #e1e1e1; 7 | background: transparent url(res/logo.png) no-repeat scroll 0 25px; 8 | 9 | // Login Register, etc 10 | .user-info { 11 | .block; 12 | float: right; 13 | margin-top: 32px; 14 | margin-top: @headerItemsTopMargin; 15 | width: @toolboxWidth; 16 | 17 | a { 18 | padding:4px 8px; 19 | margin:-4px -8px; 20 | .border-radius(8px); 21 | } 22 | a:hover { 23 | color:black; 24 | background-color:#ddd; 25 | background-color:rgba(0,0,0,0.13); 26 | } 27 | 28 | .user { 29 | color: #1a1a1a; 30 | font-weight: bold 31 | } 32 | 33 | .signin { 34 | color: #1a1a1a; 35 | margin-right: 7px; 36 | font-weight: bold; 37 | } 38 | 39 | .signout { 40 | color: #1a1a1a; 41 | margin-left: 7px; 42 | font-weight: bold; 43 | } 44 | 45 | .register { 46 | color: #1a1a1a; 47 | font-weight: normal 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /client/header/login-sheet.html: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |

Username

27 |

28 |

Password

29 |

30 |

31 |
32 | -------------------------------------------------------------------------------- /client/_base.less: -------------------------------------------------------------------------------- 1 | @toolboxWidth: 184px; 2 | @baseColor: #666; 3 | @baseFontSize: 11px; 4 | @titleColor: #1a1a1a; 5 | @linkColor: #3797d4; 6 | @pageWidth: 911px; 7 | 8 | @headerItemsTopMargin: 32px; 9 | 10 | .base-font { font-family: helvetica, arial, sans-serif } 11 | .small-font { font: 9px arial, verdana, sans-serif } 12 | .block { display: block; } 13 | 14 | // Position and size 15 | .min-size(@w:0, @h:0) { min-width: @w; min-height: @h; } 16 | .max-size(@w:0, @h:0) { max-width: @w; max-height: @h; } 17 | .origin(@x:0, @y:0) { left: @x; top: @y; } 18 | .size(@w:0, @h:0) { width: @w; height: @h; } 19 | .rect(@x:0, @y:0, @w:0, @h:0) { 20 | position: absolute; 21 | .origin(@x, @y); .size(@w, @h); 22 | } 23 | 24 | .border-radius(@r:8px) { 25 | border-radius: @r; -webkit-border-radius: @r; 26 | -moz-border-radius: @r; -khtml-border-radius: @r; 27 | } 28 | 29 | .box-shadow(@x:0, @y:1, @blur:0, @color:#000) { 30 | box-shadow: @x @y @blur @color; -khtml-box-shadow: @x @y @blur @color; 31 | -moz-box-shadow: @x @y @blur @color; -webkit-box-shadow: @x @y @blur @color; 32 | -o-box-shadow: @x @y @blur @color; -icab-box-shadow: @x @y @blur @color; 33 | } 34 | 35 | .opacity(@level:0.5) { 36 | opacity: @level; -moz-opacity: @level; 37 | // currently a bug in less.js: 38 | //@x: @level * 100; 39 | //filter:alpha(opacity=@x); 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/aws/utf8.js: -------------------------------------------------------------------------------- 1 | exports.encode = function (string) { 2 | string = string.replace(/\r\n/g,"\n"); 3 | var utftext = "", c, n = 0, L = string.length; 4 | for (; n < L; ++n) { 5 | c = string.charCodeAt(n); 6 | if (c < 128) { 7 | utftext += String.fromCharCode(c); 8 | } else if((c > 127) && (c < 2048)) { 9 | utftext += String.fromCharCode((c >> 6) | 192); 10 | utftext += String.fromCharCode((c & 63) | 128); 11 | } else { 12 | utftext += String.fromCharCode((c >> 12) | 224); 13 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 14 | utftext += String.fromCharCode((c & 63) | 128); 15 | } 16 | } 17 | return utftext; 18 | } 19 | 20 | exports.decode = function (utftext) { 21 | var string = "", i = 0, c, c2, c3, L = utftext.length; 22 | while (i < L) { 23 | c = utftext.charCodeAt(i); 24 | if (c < 128) { 25 | string += String.fromCharCode(c); 26 | ++i; 27 | } else if((c > 191) && (c < 224)) { 28 | c2 = utftext.charCodeAt(i+1); 29 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 30 | i += 2; 31 | } else { 32 | c2 = utftext.charCodeAt(i+1); 33 | c3 = utftext.charCodeAt(i+2); 34 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 35 | i += 3; 36 | } 37 | } 38 | return string; 39 | } 40 | -------------------------------------------------------------------------------- /misc/data-recipes/auto-complete.js: -------------------------------------------------------------------------------- 1 | // Auto-complete can be done by a combo of startkey, endkey and limit 2 | // - startkey should be the current prefix 3 | // - endkey should be the current prefix + high character (\u9999 in the examples below) 4 | // - limit should be whatever is reasonable (5 in the examples below) 5 | // 6 | // See for details. 7 | 8 | -> "r" 9 | "_design/users/_view/by_email?startkey=%22r%22&endkey=%22r\u9999%22&limit=5" 10 | {"id":"bbbeeccaa","key":"foo@bar.com","value":null}, 11 | {"id":"bug_barn","key":"foo@bar.com","value":null}, 12 | {"id":"cirone7","key":"foo@bar.com","value":null}, 13 | {"id":"rxmxa","key":"foo@bar.com","value":null}, 14 | {"id":"rachul","key":"foo@bar.com","value":null} 15 | 16 | -> "ra" 17 | "_design/users/_view/by_email?startkey=%22ra%22&endkey=%22ra\u9999%22&limit=5" 18 | {"id":"bbbeeccaa","key":"foo@bar.com","value":null}, 19 | {"id":"bug_barn","key":"foo@bar.com","value":null}, 20 | {"id":"cirone7","key":"foo@bar.com","value":null}, 21 | {"id":"rxmxa","key":"foo@bar.com","value":null}, 22 | {"id":"rachul","key":"foo@bar.com","value":null} 23 | 24 | -> "ras" 25 | "_design/users/_view/by_email?startkey=%22ras%22&endkey=%22ras\u9999%22&limit=5" 26 | {"id":"HUSKMELK","key":"foo@bar.com","value":null}, 27 | {"id":"rsms","key":"foo@bar.com","value":null} 28 | 29 | -> "rasm" 30 | "_design/users/_view/by_email?startkey=%22rasm%22&endkey=%22rasm\u9999%22&limit=5" 31 | {"id":"rsms","key":"foo@bar.com","value":null} 32 | -------------------------------------------------------------------------------- /client/util/modal.less: -------------------------------------------------------------------------------- 1 | @import "../_base.less"; 2 | 3 | modal { 4 | display: none; position: absolute; .min-size(300px, 400px); 5 | .base-font; font-size: 14px; color: #bbb; 6 | background-color: rgb(70,70,70); background-color: rgba(30,30,30,0.8); 7 | .border-radius(16px); 8 | .box-shadow(0, 4px, 14px, #888); 9 | padding: 20px; 10 | text-shadow: #000 0 1px 0; 11 | text-align: center; 12 | z-index: 91; 13 | li { list-style: none; } 14 | h2, h3 { font-size: 22px; color: #eee; margin-bottom: 10px; } 15 | h3 { font-size: 16px; margin-top: 10px; } 16 | .details { 17 | text-align: left; 18 | background-color: rgb(100,100,100); background-color: rgba(100,100,100,0.8); 19 | .border-radius(8px); 20 | margin-top: 10px; 21 | padding: 10px; 22 | text-shadow: #444 0 -1px 0; 23 | li { 24 | white-space: pre-wrap; 25 | color: #ddd; 26 | margin-top: 4px; 27 | padding-top: 4px; 28 | border-top: 1px solid #444; 29 | border-top-color: rgba(0,0,0,0.2); 30 | > strong { 31 | color: #fff; 32 | } 33 | } 34 | } 35 | input { 36 | /*float: right*/ 37 | margin-top: 10px; 38 | padding: 6px 12px; 39 | background-color: #ccc; 40 | border: none; 41 | height: 25px; 42 | .border-radius(25px); 43 | text-shadow: #eee 0 1px 0; 44 | .base-font; font-size: 13px; font-weight: bold; line-height: 13px; 45 | color: #222; 46 | } 47 | input:hover { background-color: #fff; } 48 | } 49 | 50 | modal-overlay { 51 | display: none; 52 | position: absolute; 53 | position: fixed; // trick! 54 | background-color: #fff; 55 | .opacity(0.8); 56 | z-index: 90; 57 | } 58 | -------------------------------------------------------------------------------- /client/util/throbber.html: -------------------------------------------------------------------------------- 1 | 47 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /misc/data/views/drops-tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "_design/tags", 3 | "language": "javascript", 4 | "views": { 5 | "all": { 6 | "options": {"collation": "raw"}, 7 | "map": "function(doc) {\n if (doc.disabled || typeof doc.tags !== 'object' || !doc.tags.forEach) return;\n var modified = 0; // find highest timestamp\n for (var user in doc.users) {\n modified = Math.max(modified, doc.users[user][0]);\n }\n if (modified === 0) return; // no users\n var RE = /^[\\w\\d_-]+$/;\n doc.tags.forEach(function(tag) {\n if (doc && tag.match(RE))\n emit([tag.toLowerCase(), modified], null);\n });\n}\n\n" 8 | }, 9 | "top": { 10 | "options": {"collation": "raw"}, 11 | "map": "function(doc) {\n if (doc.disabled || typeof doc.tags !== 'object' || !doc.tags.forEach) return;\n var RE = /^[\\w\\d_-]+$/;\n doc.tags.forEach(function(tag) {\n if (doc && tag.match(RE))\n emit(null, tag.toLowerCase());\n });\n}\n", 12 | "reduce": "function(key, values, rereduce){\n var hash = {}\n if (!rereduce){\n for (var i in values){\n var tag = values[i]\n hash[tag] = (hash[tag] || 0) + 1\n }\n }else{\n for (var i in values){\n var topN = values[i]\n for (var i in topN){\n var pair = topN[i]\n var tag = pair[0]\n hash[tag] = (hash[tag] || 0) + pair[1]\n }\n }\n }\n var all = []\n for (var key in hash)\n all.push([key, hash[key]])\n all = all.sort(function(one, other){\n return other[1] - one[1]\n })\n if (all.length > 50) all = all.slice(0, 50);\n return all;\n}" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /misc/data/import-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -- 2 | // This program imports documents into a couchdb 3 | // usage: import-docs.js .. 4 | var sys = require('sys'), 5 | fs = require('fs'), 6 | couchdb = require('../../lib/couchdb'); 7 | 8 | var status = 0, p, p2; 9 | var rawDocs = []; 10 | var database = process.argv[2]; 11 | if (!database) { 12 | sys.error('usage: import-docs.js ..'); 13 | process.exit(1); 14 | } else if (database.charAt(0) === '{') { 15 | database = JSON.parse(database); 16 | } else if ((p = database.indexOf(':')) !== -1) { 17 | var db = ''; 18 | if ((p2 = database.indexOf('/')) !== -1) { 19 | db = database.substr(p2+1); 20 | database = database.substr(0, p2); 21 | } 22 | database = { 23 | host: database.substr(0, p), 24 | port: parseInt(database.substr(p+1)), 25 | db: db 26 | } 27 | } 28 | var db = new couchdb.Db(database); 29 | 30 | db.get('', function(err, r) { 31 | if (err) { 32 | if (err.couchDbError === 'not_found') sys.error('no such database '+sys.inspect(database)); 33 | else sys.error('['+err.couchDbError+'] '+err); 34 | return process.exit(1); 35 | } 36 | 37 | // go on and load data 38 | process.argv.slice(3).forEach(function(filename){ 39 | try { 40 | db.post('', fs.readFileSync(filename), function(err, result){ 41 | if (err) { 42 | sys.error(err+'\n'+sys.inspect(result)); 43 | process.exit(1); 44 | } else { 45 | sys.log('successfully imported '+filename+'\n'+sys.inspect(result)); 46 | } 47 | }); 48 | } catch (e) { 49 | if (e.message === 'No such file or directory') sys.error(e.message+': '+filename); 50 | else sys.error(e.stack || e); 51 | process.exit(1); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /client/toolbox/subscriptions.less: -------------------------------------------------------------------------------- 1 | @import "../_base.less"; 2 | 3 | #toolbox { 4 | div.subscriptions { 5 | margin-bottom:20px; 6 | //display:none; // since it's untangled and not used as a template 7 | li { 8 | list-style: none; 9 | .active { color: #1a1a1a; font-weight: bold; } 10 | a { 11 | color: @baseColor; 12 | // todo: some kind of :hover feedback so users understand they can clicketyclick? 13 | } 14 | .active { color: #1a1a1a; font-weight: bold; } 15 | span.unseen-count { 16 | color: #3797D4; 17 | margin-left: 5px; 18 | .small-font; 19 | font-weight: bold; 20 | } 21 | } 22 | a.add { 23 | display:block; 24 | margin-top: 5px; 25 | margin-bottom: 10px; 26 | .size(14px, 14px); 27 | background: transparent url(res/plus.png) no-repeat scroll 0 0; 28 | .opacity(1);filter:alpha(opacity=100); 29 | cursor:default; 30 | span { display:none; } 31 | } 32 | a.add:hover { .opacity(0.7);filter:alpha(opacity=70); } 33 | form.add { 34 | .formbox { 35 | background:#fff; 36 | border: 1px solid #e5e5e5; 37 | width: 100px; 38 | color:#666; 39 | padding:3px 5px; 40 | -moz-border-radius:4px; 41 | -khtml-border-radius:4px; 42 | -webkit-border-radius:4px; 43 | font:11px Arial, sans-serif; 44 | } 45 | p { 46 | margin-top:0; 47 | margin-bottom: 1em; 48 | } 49 | } 50 | } 51 | } 52 | /* HTML look like this: 53 |
54 |

Subscriptions

55 | 60 | 61 |
62 | */ -------------------------------------------------------------------------------- /lib/dropular/legacy-user.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | hash = require('../oui/hash'), 3 | User = require('./user').User; 4 | 5 | function passwd_encode(password) { 6 | var salt = ""; 7 | // I know, it's a fucking joke... 8 | for (var i=0; i 15 | var http = require('http'); 16 | var conn = http.createClient(80, 'hunch.se'); 17 | var path = '/dropular/uglyhack2010/crypt.php?'+ 18 | 'value='+encodeURIComponent(value)+ 19 | '&key='+encodeURIComponent(key); 20 | var request = conn.request('GET', path, {'Host': 'hunch.se',}); 21 | request.addListener('response', function(response) { 22 | //sys.error('response => '+response); 23 | //var clen = parseInt(response.headers['content-length']); 24 | var buf = ''; 25 | response.addListener('data', function (chunk) { 26 | buf += chunk; 27 | /*if (buf.length >= clen) { 28 | callback(null, buf); 29 | sys.error('chunk => '+chunk); 30 | }*/ 31 | }); 32 | response.addListener('end', function(){ 33 | //sys.error('response end'); 34 | callback(null, buf); 35 | }) 36 | }); 37 | request.close(); 38 | return request; 39 | } 40 | 41 | exports.authenticate = function(username, password, callback) { 42 | User.find(username, function(err, user) { 43 | if (err) return callback(err); 44 | if (!user.legacy) return callback(); 45 | var passhash = passwd_encode(password); 46 | remotecrypt(passhash, user.legacy.passhash, function(err, result) { 47 | //sys.error('crypt result => '+result); 48 | callback(err, (result === user.legacy.passhash) ? user : null); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /client/about/todo.html: -------------------------------------------------------------------------------- 1 | 4 | 28 |
29 |

Future features

30 |

Listed in no particular order

31 |
    32 |
  • Dropping (creating drops)
  • 33 |
  • Re-dropping
  • 34 |
  • Pagination of drops (Apr 21)
  • 35 |
  • Resetting passwords (if your long-term memory betrayed you)
  • 36 |
  • Thumbnail-size images in overviews
  • 37 |
  • Profile editing
  • 38 |
  • Registering a new account
  • 39 |
  • 40 | Profile: 41 |
      42 |
    • Score
    • 43 |
    • Listing of followers
    • 44 |
    • Summaries (drop count, number of followers, etc)
    • 45 |
    • A big fat "follow" button
    • 46 |
    47 |
  • 48 |
  • Similar drops
  • 49 |
  • Search by tag (Apr 21)
  • 50 |
  • NSFW: 51 |
      52 |
    • Browse mode (filtering content which is or isn't NSFW)
    • 53 |
    • Mark drops as NSFW
    • 54 |
    55 |
  • 56 |
  • Real-time updates (like live count on subscriptions)
  • 57 |
  • RSS, JSON, and other funky kinds of data feeds
  • 58 |
  • Public API consolidation & documentation
  • 59 |
60 |
61 | -------------------------------------------------------------------------------- /lib/imgdb.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | http = require('http'); 3 | 4 | function Db(id) { 5 | this.id = parseInt(id); 6 | } 7 | exports.Db = Db; 8 | 9 | Db.prototype.rpcCall = function(method, args, callback){ 10 | var reqObject = { 11 | method: method, 12 | params: args 13 | }; 14 | var reqBody = JSON.stringify(reqObject); 15 | var headers = { 16 | 'Content-Length': reqBody.length, 17 | 'Content-Type': 'application/json' 18 | }; 19 | var conn = http.createClient(31128, '127.0.0.1'); 20 | var req = conn.request('POST', '/JSON-RPC', headers); 21 | req.write(reqBody, 'utf-8'); 22 | req.addListener('response', function (res) { 23 | var data = ''; 24 | res.addListener('data', function (chunk){ 25 | data += chunk; 26 | }); 27 | res.addListener('end', function(){ 28 | try { 29 | var e, r = JSON.parse(data); 30 | if (r.fault !== undefined) { 31 | var e = new Error('ImgDBError: '+(r.faultString || r.fault)); 32 | e.type = r.fault; 33 | e.code = r.faultCode; 34 | } else if (Array.isArray(r)) { 35 | if (r.length === 1) r = r[0]; 36 | else if (r.length === 0) r = null; 37 | } 38 | callback(e, r); 39 | } catch (err) { 40 | callback(err); 41 | } 42 | }); 43 | }) 44 | req.end(); 45 | } 46 | 47 | Db.prototype.queryImageId = function(imgId, limit, callback){ 48 | if (typeof limit === 'function') { callback = limit; limit = null; } 49 | if (!limit) limit = 25; 50 | this.rpcCall("queryImgID", [this.id, imgId, limit], callback); 51 | } 52 | 53 | Db.prototype.addImage = function(imgId, filename, callback){ 54 | this.rpcCall("addImg", [this.id, imgId, filename], callback); 55 | } 56 | 57 | Db.prototype.appendImg = function(filename, callback){ 58 | this.rpcCall("appendImg", [this.id, filename], callback); 59 | } 60 | 61 | /* // Test 62 | var db = new Db(1); 63 | db.queryImageId(30, 25, function(err, rsp) { 64 | sys.p(rsp); 65 | }); 66 | db.appendImg('/Users/rasmus/similar-images/numbered/30.jpg', function(err, rsp) { 67 | if (err) sys.error(err.stack || err); 68 | sys.p(rsp); 69 | }); 70 | */ -------------------------------------------------------------------------------- /client/util/notify.html: -------------------------------------------------------------------------------- 1 | 35 | 63 | 64 |
65 |
66 |

hej

67 |
68 |
69 |
70 |

71 | Welcome back! We are now transferring your old account to the new Dropular.
72 | Please allow a few minutes of processing. 73 |

74 |
75 | -------------------------------------------------------------------------------- /lib/dropular/sql.js: -------------------------------------------------------------------------------- 1 | // prefabs 2 | exports.FIELDS_USER = "u.id, u.username, u.real_name, u.description,"+ 3 | " CONVERT_TZ(u.created, @@global.time_zone, '+00:00') as date_created,"+ 4 | " CONVERT_TZ(u.modified, @@global.time_zone, '+00:00') as date_modified "; 5 | 6 | exports.FIELDS_DROP = "drops.id, drops.score, drops.media_url, drops.origin_url, "+ 7 | "drops.title, drops.description,"+ 8 | " CONVERT_TZ(drops.created, @@global.time_zone, '+00:00') as date_created,"+ 9 | " CONVERT_TZ(drops.modified, @@global.time_zone, '+00:00') as date_modified "; 10 | 11 | // fragments 12 | exports.FRAG_USERS_DROPS = "SELECT "+exports.FIELDS_DROP+","+ 13 | " CONVERT_TZ(user_drop.created, @@global.time_zone, '+00:00') as date_dropped "+ 14 | " FROM drops, user_drop"+ 15 | " WHERE drops.id = user_drop.drop_id "; 16 | 17 | exports.FRAG_USERS_DROPS1 = "SELECT d.id,\ 18 | d.score,\ 19 | d.media_url as murl,\ 20 | d.origin_url as ourl,\ 21 | d.title,\ 22 | d.description as `desc`,\ 23 | CONVERT_TZ(d.created, @@global.time_zone, '+00:00') as dcreated,\ 24 | CONVERT_TZ(d.modified, @@global.time_zone, '+00:00') as dmod,\ 25 | CONVERT_TZ(j.created, @@global.time_zone, '+00:00') as ddropped,\ 26 | GROUP_CONCAT(u.username ORDER BY j.created SEPARATOR ' ') AS users\ 27 | FROM drops AS d\ 28 | INNER JOIN user_drop AS j ON j.drop_id = d.id\ 29 | INNER JOIN users AS u ON u.id = j.user_id\ 30 | WHERE d.id IN((SELECT drop_id FROM user_drop WHERE user_id = ", 31 | // (SELECT id FROM users WHERE username = str) | N 32 | exports.FRAG_USERS_DROPS2 = "))\ 33 | GROUP BY d.id\ 34 | ORDER BY j.created DESC"; 35 | 36 | exports.FRAG_FIND_USER_BY_USERNAME = "SELECT * FROM users WHERE username="; 37 | 38 | // ---------------------------------------------------------------------------- 39 | // Utilities 40 | 41 | exports.partUserId = function(db, params, simple) { 42 | if (params.user_id) 43 | return db.escape(params.user_id) 44 | else if (params.username) 45 | return "(SELECT id FROM users WHERE username = "+db.escape(params.username)+")"; 46 | throw new Error('missing required parameter "username"') 47 | } 48 | 49 | exports.enrichUser = function(user) { 50 | user.created = new Date(user.created + ' +0000') 51 | user.modified = new Date(user.modified + ' +0000') 52 | return user 53 | } 54 | -------------------------------------------------------------------------------- /client/debug/stats.js: -------------------------------------------------------------------------------- 1 | if (window.oui && oui.debug && window.chrome && chrome.loadTimes) { 2 | 3 | oui.debugTraceEvents = function(){ 4 | if (oui.debugTraceEvents.on) { 5 | return 'already tracing events'; 6 | } 7 | oui.debugTraceEvents.on = true; 8 | var knownEvents = ('click mousedown mouseup mouseover mousemove mouseout '+ 9 | 'DOMSubtreeModified DOMNodeInserted DOMNodeRemoved DOMNodeRemovedFromDocument '+ 10 | 'DOMNodeInsertedIntoDocument DOMAttrModified DOMCharacterDataModified '+ 11 | 'load unload abort error select change submit reset focus blur resize scroll').split(/[ \t\n\r]+/); 12 | var logEv = {handleEvent: function(ev) { 13 | console.log('\u2605 '+ev.type, ev); 14 | }}; 15 | for (var i=0; i < knownEvents.length; ++i) 16 | document.addEventListener(knownEvents[i], logEv, true); 17 | return 'now tracing '+knownEvents.length+' types of events.'; 18 | }; 19 | 20 | $(window).load(function(){ 21 | var dumpStats = function(st){ 22 | /* 23 | startLoadTime: 1271714433.483945 24 | commitLoadTime: 1271714433.49696 25 | firstPaintTime: 1271714433.670031 26 | finishDocumentLoadTime: 1271714433.871984 27 | finishLoadTime: 1271714434.356144 28 | firstPaintAfterLoadTime: 1271714434.379138 29 | navigationType: "Reload" 30 | requestTime: 0 31 | wasFetchedViaSpdy: fals 32 | */ 33 | var fixed = function(f){ return Number(f).toFixed(3); }; 34 | console.group('Load statistics'); 35 | 36 | console.info('load --['+fixed(st.commitLoadTime - st.startLoadTime)+'ms]--> commit '+ 37 | '--['+fixed(st.finishLoadTime - st.commitLoadTime)+']--> finish --> '+ 38 | fixed(st.finishLoadTime - st.startLoadTime) + 'ms'); 39 | 40 | console.info(' firstPaint --['+fixed(st.firstPaintAfterLoadTime - st.firstPaintTime)+'ms]--> firstPaintAfterLoad'+ 41 | ' ('+fixed(st.firstPaintAfterLoadTime - st.startLoadTime)+'ms)'); 42 | 43 | console.groupEnd(); 44 | }; 45 | var timer, st; 46 | timer = setInterval(function(){ 47 | st = chrome.loadTimes(); 48 | if (st.firstPaintAfterLoadTime !== 0) { 49 | clearInterval(timer); 50 | dumpStats(st); 51 | } 52 | }, 10); 53 | }); 54 | } -------------------------------------------------------------------------------- /lib/aws/base64.js: -------------------------------------------------------------------------------- 1 | var utf8 = require('./utf8'); 2 | 3 | exports.encode = function (input) { 4 | var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0, 5 | input = utf8.encode(input), L = input.length; 6 | while (i < L) { 7 | chr1 = input.charCodeAt(i++); 8 | chr2 = input.charCodeAt(i++); 9 | chr3 = input.charCodeAt(i++); 10 | 11 | enc1 = chr1 >> 2; 12 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 13 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 14 | enc4 = chr3 & 63; 15 | 16 | if (isNaN(chr2)) { 17 | enc3 = enc4 = 64; 18 | } else if (isNaN(chr3)) { 19 | enc4 = 64; 20 | } 21 | 22 | output += MAP.charAt(enc1) + MAP.charAt(enc2)+ 23 | MAP.charAt(enc3) + MAP.charAt(enc4); 24 | } 25 | return output; 26 | } 27 | 28 | exports.decode = function (input) { 29 | var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0, 30 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 31 | var L = input.length; 32 | while (i < L) { 33 | enc1 = MAPi[input.charAt(i++)]; 34 | enc2 = MAPi[input.charAt(i++)]; 35 | enc3 = MAPi[input.charAt(i++)]; 36 | enc4 = MAPi[input.charAt(i++)]; 37 | 38 | chr1 = (enc1 << 2) | (enc2 >> 4); 39 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 40 | chr3 = ((enc3 & 3) << 6) | enc4; 41 | 42 | output += String.fromCharCode(chr1); 43 | if (enc3 != 64) output += String.fromCharCode(chr2); 44 | if (enc4 != 64) output += String.fromCharCode(chr3); 45 | } 46 | return utf8.decode(output); 47 | } 48 | 49 | const MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 50 | var MAPi = {}; 51 | (function(){ 52 | for (var i=0,L=MAP.length;i 2 | // fix for oui builder which does not create a js module for script-less html 3 | exports.on('load', function(ev, instance){ 4 | var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 5 | $('#item').html('http://dropular.net/#drops/'+hashes[0]); 6 | }); 7 | 8 | 39 |
40 |

Flag / Copyright infringement notification

41 |

We make no claim to hold any copyright to the images posted on Dropular. All of the content on this site (including graphics, sounds, videos and other files) copyright their respective owners.

42 |

Our goal is to promote great content and link back to the original source. If you are the owner of an item on Dropular and do not wish to have it promoted on Dropular.net, we will simply remove the item if you provide the proper details and information asked below.

43 | 44 |

To file a copyright infringement notification with us, please follow the steps below:

45 | 46 |
    47 |
  • 1. Copy the item URL: and paste it into an e-mail.
  • 48 |
  • 2. Add information about the original copyrighted work, including the full name of the person / company / party holding the copyright.
  • 49 |
  • 3. Your full name used as guarantor that the above is accurate and not deceptive or false.
  • 50 |
  • 4. Send the e-mail to dropular (at) gmail (dot) com
  • 51 |
52 | 53 |

That's it!

54 | 55 |
56 | -------------------------------------------------------------------------------- /client/jquery.continuum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Continuum provides a way of filling in content when reaching the end of 3 | * scroll in a document. 4 | * 5 | * Released under an MIT license: 6 | * 7 | * Copyright (c) 2010 Rasmus Andersson 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | */ 27 | (function($){ 28 | 29 | $.fn.continuum = function(options, callback) { 30 | var ctx = $.extend({ 31 | threshold: 400 32 | }, typeof options === 'object' || {filler: options}); 33 | 34 | // Keep single set of refrences and jQuery objects. No leakin' here bouy! 35 | if (!$.continuum) { 36 | $.continuum = { 37 | '$doc': $(document), 38 | '$win': $(window) 39 | }; 40 | } 41 | 42 | // Locals hanging out at the pub 43 | var $doc = $.continuum.$doc, 44 | $win = $.continuum.$win, 45 | self = this, 46 | winHeight = $win.height(); 47 | 48 | /* var onscroll = function(){ 49 | var distance = $doc.height() - window.pageYOffset - winHeight; 50 | // on MSIE, replace window.pageYOffset with document.body.scrollTop 51 | if (distance < ctx.threshold) { 52 | //console.log('distance < threshold -- ', distance, '<', ctx.threshold); 53 | ctx.waiting = true; 54 | $win.unbind('scroll.continuum'); 55 | ctx.filler.call(this, function(stop){ 56 | ctx.waiting = false; 57 | if (stop) { 58 | ctx.stopped = true; 59 | } else { 60 | $win.bind('scroll.continuum', onscroll); 61 | } 62 | }); 63 | } 64 | }; 65 | */ 66 | 67 | //console.log('distance < threshold -- ', distance, '<', ctx.threshold); 68 | ctx.waiting = true; 69 | ctx.filler.call(this, function(stop){ 70 | ctx.waiting = false; 71 | if (stop) { 72 | ctx.stopped = true; 73 | } 74 | }); 75 | 76 | // Adjust window height when needed 77 | // $win.bind('resize.continuum', function(){ winHeight = $win.height(); }); 78 | 79 | // Check distance when scrolling 80 | //$win.bind('scroll.continuum', onscroll); 81 | 82 | return this; 83 | }; 84 | 85 | })(jQuery); -------------------------------------------------------------------------------- /client/about/privacy.html: -------------------------------------------------------------------------------- 1 | 4 | 42 |
43 | 44 |

Privacy policy

45 | 46 |

47 | Respecting user privacy is important to Dropular.net. Read this to learn about personal information that Dropular.net collects and how it may be used. 48 |

49 | 50 |

Dropular.net database

51 |

52 | The e-mail address you register with Dropular.net and the logs of your activity is used to calculate personalized recommendations as part of our service. We use this information in aggregate to generate statistics and other usage information. We share this information anonymously with you, other end users, and other third parties. 53 |

54 | 55 |

Links to third party web sites

56 |

57 | Our service may contain links to other websites and software. We are not responsible for the privacy practices or the content of these websites or software. Please visit the privacy policies of these third party sites in order to understand their privacy and information collection practices. 58 | 59 |

60 | 61 |

Disclosures required by law

62 |

63 | We reserve the right to disclose your personally identifiable information when we believe in good faith that an applicable law, regulation, or legal process requires it, or when we believe disclosure is necessary to protect or enforce our rights or the rights of another user. 64 |

65 | 66 |

Cookies

67 |

68 | Cookies are small text files stored by your browser on your computer when you visit a website. We use cookies to improve our website and make it easier to use. Cookies permit us to recognize you and avoid repetitive requests for the same information. Most browsers will accept cookies until you change your browser settings to refuse them. You may change your browser's settings to refuse our cookies. 69 |

70 | 71 |
72 | -------------------------------------------------------------------------------- /lib/aws/pool.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | // ---------------------------------------------------------------------------- 4 | // Simple instance pool (aka free list) 5 | 6 | function Pool(keep, limit) { 7 | process.EventEmitter.call(this); 8 | this.keep = keep || 0; 9 | this.limit = limit || 128; 10 | this.free = []; // push used to end/right, shift new from front/left 11 | this.busy = 0; 12 | this.getQueue = []; 13 | } 14 | exports.Pool = Pool; 15 | sys.inherits(Pool, process.EventEmitter); 16 | Pool.prototype.create = function() { throw new Error('not implemeted'); } 17 | Pool.prototype.get = function(callback) { 18 | var instance = this.free.shift(); 19 | if (!instance) { 20 | if (this.busy < this.limit) { 21 | instance = this.create(); 22 | } else { 23 | if (callback) this.getQueue.push(callback); 24 | return; 25 | } 26 | } 27 | this.busy++; 28 | if (callback) callback(null, instance); 29 | return instance; 30 | } 31 | Pool.prototype.cancelGet = function(callbackToCancel) { 32 | var i = this.getQueue.indexOf(callbackToCancel), found = (i !== -1); 33 | if (found) this.getQueue.splice(i,1); 34 | return found; 35 | } 36 | Pool.prototype.put = function(instance) { 37 | if (this.getQueue.length) { 38 | this.getQueue.shift()(null, instance); 39 | } else { 40 | this.busy--; 41 | if (this.free.length < this.keep) this.free.push(instance); 42 | else this.destroy(instance); 43 | } 44 | } 45 | Pool.prototype.remove = function(item, noDestroy) { 46 | var i = this.free.indexOf(item), found = (i !== -1); 47 | if (found) this.free.splice(i,1); 48 | if (!noDestroy) this.destroy(item); 49 | return found; 50 | } 51 | Pool.prototype.removeAll = function(noDestroy) { 52 | if (!noDestroy) 53 | for (var i=0,item; (item = this.free[i]); i++) this.destroy(item); 54 | this.free = []; 55 | } 56 | Pool.prototype.destroy = function(item) { } 57 | 58 | // ---------------------------------------------------------------------------- 59 | // HTTP connection pool 60 | 61 | var http = require('http'); 62 | 63 | function HTTPConnectionPool(keep, limit, port, host, secure) { 64 | Pool.call(this, keep, limit); 65 | this.port = port; 66 | this.host = host; 67 | this.secure = secure; 68 | var self = this; 69 | process.addListener("exit", function (){ 70 | // avoid lingering FDs 71 | try { self.removeAll(); }catch(e){} 72 | try { delete self; }catch(e){} 73 | }); 74 | } 75 | exports.HTTPConnectionPool = HTTPConnectionPool; 76 | sys.inherits(HTTPConnectionPool, Pool); 77 | HTTPConnectionPool.prototype.create = function(){ 78 | var self = this, conn = http.createClient(this.port, this.host); 79 | if (this.secure) { 80 | if (typeof this.secure !== 'object') this.secure = {}; 81 | conn.setSecure('X509_PEM', this.secure.ca_certs, this.secure.crl_list, 82 | this.secure.private_key, this.secure.certificate); 83 | } 84 | conn._onclose = function(hadError, reason) { 85 | self.remove(conn); 86 | if (hadError) 87 | self.emit('error', new Error('Connection error'+(reason ? ': '+reason : ''))); 88 | try { conn.removeListener('close', conn._onclose); }catch(e){} 89 | } 90 | conn.addListener('close', conn._onclose); 91 | return conn; 92 | } 93 | HTTPConnectionPool.prototype.destroy = function(conn){ 94 | try { conn.removeListener('close', conn._onclose); }catch(e){} 95 | try { conn.end(); }catch(e){} 96 | } -------------------------------------------------------------------------------- /misc/data/import-batch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -- 2 | // This program imports a batch of documents into couchdb. 3 | // 4 | // usage: import-batch.js .. 5 | // - Each line of should be a document i.e. {"_id":"foo","field1":123}. 6 | // - A line not starting with a "{" is ignored. 7 | // - Any trailing "," and whitespace on a line is ignored and can thus exists. 8 | // 9 | var sys = require('sys'), 10 | fs = require('fs'), 11 | querystring = require('querystring'), 12 | http = require('http'), 13 | couchdb = require('../../lib/couchdb'); 14 | 15 | // Parse options and load data 16 | var lines = ''; 17 | var database = process.argv[2]; 18 | if (!database) { 19 | sys.error('usage: import-batch.js ..'); 20 | process.exit(1); 21 | } else if (database.charAt(0) === '{') { 22 | database = JSON.parse(database); 23 | } else if ((p = database.indexOf(':')) !== -1) { 24 | var db = ''; 25 | if ((p2 = database.indexOf('/')) !== -1) { 26 | db = database.substr(p2+1); 27 | database = database.substr(0, p2); 28 | } 29 | database = { 30 | host: database.substr(0, p), 31 | port: parseInt(database.substr(p+1)), 32 | db: db 33 | } 34 | } 35 | var db = new couchdb.Db(database); 36 | // check for db existance 37 | db.get('', function(err, r) { 38 | if (!err) return; 39 | if (err.couchDbError === 'not_found') sys.error('no such database '+sys.inspect(database)); 40 | else sys.error('['+err.couchDbError+'] '+err); 41 | process.exit(1); 42 | }); 43 | // go on and load data 44 | process.argv.slice(3).forEach(function(filename){ 45 | try { 46 | lines += fs.readFileSync(filename); 47 | } catch (e) { 48 | if (e.message === 'No such file or directory') { 49 | sys.error(e.message+': '+filename); 50 | process.exit(1); 51 | } 52 | throw e; 53 | } 54 | }); 55 | if (lines.length === 0) { 56 | sys.error('No input files or they are empty -- aborting'); 57 | process.exit(1); 58 | } 59 | lines = lines.trim().split('\n'); 60 | sys.log('loaded '+lines.length+' prepared documents'); 61 | 62 | // proceed with sequentially submitted batches of 100 63 | function postNextBatch(offset, length) { 64 | var body = lines.slice(offset, offset+length); 65 | var count = body.length; 66 | body = '{"docs":[' + body.filter(function(line){ 67 | return line.length && line.charAt(0) === '{'; 68 | }).map(function(line){ 69 | return line.replace(/[,\n ]+$/,''); 70 | }).join(',\n') + ']}'; 71 | db.post('_bulk_docs', body, function(err, result) { 72 | if (err) sys.error(err.stack); 73 | else { 74 | // ignore conflict errors, but abort on other errors 75 | if (Array.isArray(result)) { 76 | var hadError = false; 77 | result.forEach(function(status){ 78 | if (status.error && status.error !== 'conflict') { 79 | sys.error('couchdb ['+status.error+'] '+status.reason+' (key: '+status.id+')'); 80 | hadError = true; 81 | } 82 | }); 83 | if (hadError) return; 84 | } else if (typeof result === 'object' && result.error) { 85 | sys.error('couchdb ['+status.error+'] '+status.reason+' -- '+sys.inspect(result)); 86 | return; 87 | } 88 | sys.log('posted ['+offset+','+count+'] ('+(offset+length)+' of '+lines.length+')'); 89 | if (count === length) 90 | postNextBatch(offset+length, length); 91 | } 92 | }); 93 | } 94 | postNextBatch(0, 100); 95 | -------------------------------------------------------------------------------- /client/about/registrations-will-soon-open.html: -------------------------------------------------------------------------------- 1 | 42 | 84 |
85 |

Registrations will soon open

86 |

Let us know your email and we'll let you know when registrations are open.

87 |

88 | Thanks! We'll keep in touch. 89 |

90 |

91 | You're already listed — speak to you soon. 92 |

93 |
94 |

95 | 96 | 97 |

98 |

99 | We promise to keep your email to ourselves. 100 |

101 |
102 |
103 | -------------------------------------------------------------------------------- /client/drops/index.less: -------------------------------------------------------------------------------- 1 | @import "../_base.less"; 2 | 3 | @thumbsize: 209px; 4 | @itemBorderSize: 1px; 5 | @itemMargin: 25px; 6 | @itemMargin2: @itemMargin - (@itemBorderSize * 2); 7 | 8 | a.detail-btn { 9 | margin-left: 10px; 10 | display: inline; 11 | cursor: pointer; 12 | } 13 | 14 | a.hide { 15 | display: none; 16 | } 17 | 18 | a.show { 19 | display: inline; 20 | } 21 | 22 | 23 | content.drops { 24 | 25 | more { 26 | background:#f2f2f2; 27 | border: 1px solid #e5e5e5; 28 | width: 100%; 29 | display: block; 30 | text-align: center; 31 | color:#666; 32 | padding:5px 5px; 33 | margin-bottom: 25px; 34 | -moz-border-radius:4px; 35 | -khtml-border-radius:4px; 36 | -webkit-border-radius:4px; 37 | font:11px Arial, sans-serif; 38 | cursor: pointer; 39 | } 40 | 41 | more:hover { 42 | .opacity(0.8);filter:alpha(opacity=80); 43 | } 44 | 45 | sources { 46 | display:block; 47 | float:right; 48 | h3 { 49 | display:inline; 50 | margin-left:10px; 51 | a { color: @titleColor; } 52 | a:hover { border-bottom: 1px solid @linkColor; } 53 | a.active { color: @linkColor; } 54 | } 55 | } 56 | 57 | div.title { 58 | display:none; 59 | } 60 | 61 | div.title.tagged { 62 | h1 { 63 | .tags { background-color:#fffc00; } 64 | } 65 | } 66 | 67 | throbber { 68 | margin:20px auto; 69 | } 70 | 71 | drops { 72 | display:block; 73 | margin-top: 25px; 74 | 75 | // counter-margins 76 | margin-right: (@itemMargin - (@itemMargin * 2)); 77 | margin-left: (@itemBorderSize - (@itemBorderSize * 2)); 78 | 79 | drop { 80 | display:inline-block; 81 | .size(@thumbsize, @thumbsize); 82 | margin:0 @itemMargin2 @itemMargin2 0; 83 | 84 | img { 85 | vertical-align:middle; 86 | display:block; 87 | margin:auto; 88 | .opacity(0);filter:alpha(opacity=0); 89 | .max-size(@thumbsize, @thumbsize); 90 | } 91 | 92 | background: url('res/loading-bg.png') no-repeat scroll center center; 93 | 94 | .info-wrapper { 95 | visibility: hidden; 96 | width: @thumbsize; 97 | position: absolute; 98 | text-align: center; 99 | z-index: 999; 100 | color: #1a1a1a; 101 | margin-top: @thumbsize/2-12; 102 | 103 | a { 104 | color: #1a1a1a; 105 | } 106 | .from { 107 | background: yellow; 108 | padding: 5px; 109 | height: 15px; 110 | white-space:nowrap; 111 | display: inline; 112 | } 113 | .droppy { 114 | color: #1a1a1a !important; 115 | .opacity(1.0);filter:alpha(opacity=100); 116 | padding: 15px; 117 | padding-top: 0px; 118 | width: 15px; 119 | height: 15px; 120 | white-space:nowrap; 121 | display: inline; 122 | background: url('res/plus.png') no-repeat scroll center center; 123 | z-index: -1; 124 | cursor: pointer; 125 | } 126 | .droppy:hover { 127 | .opacity(0.8);filter:alpha(opacity=80); 128 | } 129 | } 130 | } 131 | 132 | drop.loaded { 133 | border-color:transparent; 134 | background:none; 135 | img { .opacity(1.0);filter:alpha(opacity=100); } 136 | } 137 | drop.loaded:hover { 138 | img { .opacity(0.8);filter:alpha(opacity=80); } 139 | } 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /client/about/about.html: -------------------------------------------------------------------------------- 1 | 4 | 45 |
46 |

Dropular

47 |

Dropular is a micro-blogging service which makes it possible for members to save and collect their favorite images found from various sources online. Dropular is currently an invite only service and mostly designed for fashion designers, photographers, architects, graphic designers and artists.

48 | 49 |

Copyright infringement notification

50 |

We make no claim to hold any copyright to the images posted on Dropular. All of the content on this site (including graphics, sounds, videos and other files) copyright their respective owners.

51 |

Original source of each item can be found next to each and every "drop".

52 | 53 |

Our goal is to promote great content and link back to the original source. If you are the owner of an item on Dropular and do not wish to have it promoted on Dropular.net, we will simply remove the item if you provide the proper details and information asked below.

54 | 55 |

To file a copyright infringement notification with us, please send an email containing the following requirements:

56 | 57 |
    58 |
  • 1. The complete URL on Dropular to the item http://dropular.net/#drops/DROP-ID
  • 59 |
  • 2. Information about the original copyrighted work, including the full name of the person / company / party holding the copyright.
  • 60 |
  • 3. Your full name used as guarantor that the above is accurate and not deceptive or false.
  • 61 |
62 | 63 |

Contact

64 |

dropular (at) gmail (dot) com

65 | 66 |

Dropular bookmarklet (Droplet)

67 |

Droplet

68 | 69 |

Resources

70 |

Download logotype (EPS)

71 |
72 | -------------------------------------------------------------------------------- /misc/data/views/drops-drops.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "_design/drops", 3 | "language": "javascript", 4 | "views": { 5 | "by-username-and-time": { 6 | "options": {"collation": "raw"}, 7 | "map": "function(doc) {\n if (!doc.users || doc.disabled) return;\n for (var user in doc.users) {\n emit([user.toLowerCase(), doc.users[user][0]], null);\n }\n}\n" 8 | }, 9 | "recently-created": { 10 | "options": {"collation": "raw"}, 11 | "map": "function(doc) {\n if (!doc.users || doc.disabled) return;\n var created; // find lowest timestamp\n for (var user in doc.users) {\n var t = doc.users[user][0];\n created = created ? min(created, t) : t;\n }\n emit(created, null);\n}" 12 | }, 13 | "popular": { 14 | "options": {"collation": "raw"}, 15 | "map": "function(doc) {\n if (!doc.users || doc.disabled) return;\n // oldest drop 2009-01-25T10:42:21.000Z\n var refTime = 1232883741000,\n nowDate = new Date(),\n timeDecayEffect = 0.999,\n loneDropPunishment = 4.0, // higher = lower score\n decayBase = 60*60*1000, // 1 hour\n nowTime = nowDate.getTime()+(nowDate.getTimezoneOffset()*60*1000);\n\n function applyTimeDecay(score, time, effect) {\n if (effect === 0) return score;\n var decay, nscore = score, delta = nowTime-time;\n if (delta > decayBase) {\n decay = delta/decayBase;\n nscore /= decay;\n //puts('score2: '+score+', D: '+delta+', decay: '+decay)\n } else if (delta > 1000) {\n decay = delta/decayBase;\n nscore /= 1.0+decay;\n //puts('score2: '+score+', D: '+delta+', decay: '+decay)\n } else {\n //puts('score2: '+score+', D: '+delta+', decay: -')\n }\n if (effect === 1.0)\n return nscore;\n return score + ((nscore - score) * effect);\n }\n\n function calculateDropScore(doc) {\n var user, timeCreated,\n userdrops = [],\n endscoreDivisor = 45000;\n // first, remap userdrops to ordered list\n for (user in doc.users) {\n tuple = doc.users[user];\n userdrops.push({username:user, time:tuple[0], score:tuple[1]});\n }\n \n // special case for empty or single drops\n if (userdrops.length === 0) {\n return 0.0;\n } else {\n userdrops.sort(function(a, b){ return b.time - a.time; });\n timeCreated = userdrops[0].time;\n if (userdrops.length === 1) {\n score = (timeCreated - refTime)/(endscoreDivisor*loneDropPunishment);\n score = applyTimeDecay(score, timeCreated, timeDecayEffect);\n return score / (loneDropPunishment/2);\n }\n }\n \n var t, x, y, i, td, score = 0,\n tdpowerPunish = 1.5, tdpowerPraise = 1000*30;\n\n t = timeCreated - refTime;\n d = nowTime - timeCreated;\n x = 0;\n td = 0;\n \n //puts(d+', '+(t/d) + ', '+ (d/t));\n t *= t/d;\n \n for (i in userdrops) {\n x += userdrops[i].score;\n if (i > 0)\n td = userdrops[i].time - userdrops[i-1].time;\n //sys.error('td '+td)\n if (td > 0) {\n if (td < userdrops.length) {\n // punish sequential drops\n t /= td*tdpowerPunish;\n } else {\n // praise\n t += td*tdpowerPraise;\n }\n }\n }\n\n if (x > 0) y = 1;\n else if (x === 0) y = 0;\n else y = -1;\n \n if (userdrops.length === 1) {\n t *= 0.5; // demote new drops to 50%\n } else if (userdrops.length) {\n t *= userdrops.length;\n }\n\n z = (Math.abs(x) >=1 && Math.abs(x) || 1);\n score = Math.log(z) + (y*t)/endscoreDivisor;\n \n score = applyTimeDecay(score, timeCreated, timeDecayEffect);\n \n return score;\n }\n var sc = calculateDropScore(doc);\n emit(sc, null);\n emit(doc._id, sc);\n}" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /public/drop.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // don't run this script if we're on a dropular site 4 | if (document.location.hostname.toLowerCase().indexOf('dropular') !== -1) 5 | return; 6 | 7 | var imgs = document.images; 8 | var currentUrl = document.location.href; 9 | var theTitle = document.title; 10 | var badWordsRE = /sex|dick|fuck|sex|anal|pron|porn|lesbians?/i; 11 | var allowedImageNamesRE = /\.(gif|jpe?g|png)/i; 12 | var knownImageNamesRE = /\.(gif|jpe?g|png|tiff?|bmp)/; 13 | 14 | // abort on bad words 15 | if (currentUrl.match(badWordsRE)) return; 16 | 17 | var imgcount = 0; 18 | var imgClickHandler = function(){ 19 | var imgUrl = this.src; 20 | if (!imgUrl.match(/^http.?:\/\//i)) { 21 | if (imgUrl.substr(0,1) === '/') { 22 | imgUrl = currentUrl.replace(/^(http.?:\/\/[^\/]+)\/.*$/i, '$1')+imgUrl; 23 | } else if (!currentUrl.match(/\/$/)) { 24 | imgUrl = currentUrl.replace(/\/[^\/]+$/i, '/') + imgUrl; 25 | } else { 26 | imgUrl = currentUrl + imgUrl; 27 | } 28 | } 29 | 30 | var title = this.title; 31 | if (title) { 32 | title = String(title).replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, ''); 33 | if (title.length === 0) title = null; 34 | } else if (this.alt) { 35 | title = String(this.alt).replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, ''); 36 | if (title.length === 0) title = null; 37 | } 38 | if (!title && currentUrl !== this.src) 39 | title = theTitle; 40 | 41 | myLink = "http://dropular.net/#drop/?"+ 42 | "origin="+encodeURIComponent(currentUrl)+ 43 | "&url="+encodeURIComponent(imgUrl); 44 | if (title) 45 | myLink += "&title="+encodeURIComponent(title); 46 | window.open(myLink, 'Droplet', 47 | 'width=455,height=110,status=yes,scrollbars=no'); 48 | return false; 49 | }; 50 | 51 | for(i=0;i= 128 53 | && img.height >= 128 54 | && img.src !== "" 55 | && img.src.match(allowedImageNamesRE) 56 | ) 57 | { 58 | img.style.border = '10px solid #f20606'; 59 | img.style.marginRight = '20px'; 60 | img.onmouseover = function() { 61 | this.style.border='10px solid #ffde00'; 62 | } 63 | img.onmouseout = function() { 64 | this.style.border='10px solid #f20606'; 65 | } 66 | imgcount++; 67 | img.onclick = imgClickHandler; 68 | } 69 | })(imgs[i]); } 70 | 71 | // add an info banner, unless we are looking directly at an image 72 | if (imgcount > 0 && !currentUrl.match(knownImageNamesRE)) { 73 | function addHTML (html) { 74 | if (document.all) { 75 | document.body.insertAdjacentHTML('beforeEnd', html); 76 | } else if (document.createRange) { 77 | var range = document.createRange(); 78 | range.setStartAfter(document.body.lastChild); 79 | var docFrag = range.createContextualFragment(html); 80 | document.body.appendChild(docFrag); 81 | } else if (document.layers) { 82 | var l = new Layer(window.innerWidth); 83 | l.document.open(); 84 | l.document.write(html); 85 | l.document.close(); 86 | l.top = document.height; 87 | document.height += l.document.height; 88 | l.visibility = 'show'; 89 | } 90 | } 91 | addHTML("
Click on any of the bordered images below to add to Dropular.
"); 92 | } 93 | 94 | })(); -------------------------------------------------------------------------------- /client/toolbox/index.js: -------------------------------------------------------------------------------- 1 | index.mixinViewControl(exports, '#toolbox'); 2 | 3 | var $subs; 4 | 5 | exports.rebuildSubscriptionsList = function(subscriptions) { 6 | var ul, li, a; 7 | ul = $subs.find('ul').empty(); 8 | if (!subscriptions && oui.app.session.user) 9 | subscriptions = oui.app.session.user.subscriptions; 10 | if (subscriptions) { 11 | for (var i=0,tag; (tag = subscriptions[i]); ++i) { 12 | a = document.createElement('a'); 13 | a.setAttribute('href', '#drops/tagged/'+encodeURIComponent(tag)); 14 | a.appendChild(document.createTextNode(tag)); 15 | 16 | /* Remove button ready... but not the delete call I guess? 17 | 18 | rmv = document.createElement('a'); 19 | rmv.setAttribute('class', 'rmv-btn'); 20 | rmv.setAttribute('id', encodeURIComponent(tag)); 21 | rmv.appendChild(document.createTextNode('-')); 22 | */ 23 | 24 | li = document.createElement('li'); 25 | li.appendChild(a); 26 | // li.appendChild(rmv); 27 | 28 | 29 | ul.append(li); 30 | } 31 | } 32 | }; 33 | 34 | // clear the toolbox when a user logs out 35 | oui.app.session.on('userchange', function(ev, prevUser){ 36 | /*if (!this.user) { 37 | console.log('clearing #toolbox') 38 | toolbox.clear(); 39 | }*/ 40 | }); 41 | 42 | // update subscriptions when a user's info changed 43 | oui.app.session.on('userinfo', function(ev, prevUser, added, updated){ 44 | if ( !prevUser 45 | || (added && added.b && added.b.subscriptions) 46 | || (updated && updated.b && updated.b.subscriptions) ) 47 | { 48 | exports.rebuildSubscriptionsList(this.user.subscriptions); 49 | toolbox.setView($subs); 50 | } 51 | }); 52 | 53 | // Bind UI actions 54 | $(function(){ 55 | 56 | 57 | // Subscriptions (+) button 58 | $subs = $('#toolbox-subscriptions'); 59 | var $addButton = $subs.find('a.add'), 60 | $addForm = $subs.find('form.add').hide(); 61 | 62 | /* Remove button ready... but not the delete call I guess? 63 | 64 | $('.rmv-btn').live('click', function(){ 65 | var tags = this.id, S = oui.app.session, 66 | uri = 'users/'+encodeURIComponent(S.user.username)+'/subscriptions/remove'; 67 | S.post(uri, {tags:tags}, function(err, res){ 68 | // Hide the form 69 | index.reloadUser(); // defer reloading of user's info 70 | }); 71 | }); 72 | 73 | */ 74 | 75 | var activeSubmitHandler = function(ev){ 76 | ev.preventDefault(); 77 | console.log('submit', this); 78 | // Disable inputs 79 | $addForm.find('input').each(function(){ this.disabled = true; }); 80 | var tagsTF = $addForm.find('input[type=text]').get(0); 81 | // TODO "saving..."? 82 | var tags = tagsTF.value.split(/\s+/), S = oui.app.session, 83 | uri = 'users/'+encodeURIComponent(S.user.username)+'/subscriptions'; 84 | S.post(uri, {tags:tags}, function(err, res){ 85 | // Hide the form 86 | tagsTF.value = ''; 87 | $addButton.click(); 88 | index.reloadUser(); // defer reloading of user's info 89 | }); 90 | }; 91 | var passiveSubmitHandler = function(ev){ ev.preventDefault(); }; 92 | 93 | $addButton.toggle(function(){ 94 | // When clicking the (+) in state A, fade it in and enable the form 95 | $addButton.after($addForm.fadeIn(200)); 96 | // Enable inputs 97 | $addForm.find('input').each(function(){ this.disabled = false; }); 98 | $addForm.unbind('submit'); 99 | $addForm.one('submit', activeSubmitHandler).find('input[type=text]').focus(); 100 | }, function(){ 101 | // When clicking the (+) in state B, fade out and disable the form 102 | $addButton.nextAll('form').fadeOut(200); 103 | // Disable accidental submission 104 | $addForm.unbind('submit'); 105 | $addForm.one('submit', passiveSubmitHandler); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /client/users/profile.html: -------------------------------------------------------------------------------- 1 | 77 | 103 |
-------------------------------------------------------------------------------- /client/drops/drop.html: -------------------------------------------------------------------------------- 1 | 87 | 88 |

89 | 90 | 91 | Pinched from by on . 92 | 93 | 94 | 95 | 96 | Drop 97 | Flag 98 | 100 | 101 | 102 |
103 | -------------------------------------------------------------------------------- /client/index.less: -------------------------------------------------------------------------------- 1 | @import "_base.less"; 2 | 3 | // Base 4 | * { margin: 0; padding: 0 } 5 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr, 6 | acronym,address,code,del,dfn,em,img,q,dl,dt,dd,ol,ul,li,fieldset,form,label, 7 | legend,table,caption,tbody,tfoot,thead,tr,th,td { 8 | border: 0; vertical-align: baseline; 9 | } 10 | body { font-size: @baseFontSize; } 11 | shelf, module { display:none; } 12 | 13 | .userlevel1, .userlevel-eq1, .userlevel2, .userlevel-eq2, .userlevel3, .userlevel-eq3 { display: none } 14 | 15 | 16 | // Etc 17 | a { text-decoration: none; color: @linkColor; } 18 | b,strong { color: #1a1a1a; } 19 | content { .block; } 20 | 21 | // Headers 22 | h1,h2,h3 { .base-font; color: @titleColor; font-weight: bold; } 23 | h1 { font-size: 26px; margin-bottom: 5px; margin-left: -2px; } 24 | h2 { font-weight: normal; line-height: 1.3; } 25 | h3 { margin-bottom:10px; font-size:12px; } 26 | 27 | .btn { 28 | background:#f2f2f2; 29 | border: 1px solid #e5e5e5; 30 | width: 100%; 31 | display: block; 32 | text-align: center; 33 | color:#666; 34 | padding:3px 5px; 35 | margin-bottom: 25px; 36 | -moz-border-radius:4px; 37 | -khtml-border-radius:4px; 38 | -webkit-border-radius:4px; 39 | font:11px Arial, sans-serif; 40 | cursor: pointer; 41 | } 42 | 43 | .following { 44 | display: none; 45 | } 46 | 47 | // Body 48 | body { 49 | .base-font; 50 | line-height: 1.3; 51 | color: @baseColor; 52 | background: #fff; 53 | 54 | grid > header { 55 | .block; 56 | height: 75px; 57 | border-bottom: 1px solid #e1e1e1; 58 | background: transparent url(res/logo.png) no-repeat scroll 0 25px; 59 | 60 | h1 { 61 | display:inline-block; 62 | position: relative; 63 | .origin(0px, 20px); .size(200px, 40px); 64 | a { 65 | display:block; 66 | .size(200px, 40px); 67 | //background:rgba(120,0,0,0.5); 68 | span { display:none; } 69 | } 70 | } 71 | 72 | menu { 73 | display:block; 74 | margin-top:@headerItemsTopMargin - 1px; 75 | position:absolute; 76 | margin-left: 555px; 77 | h3 { 78 | display:inline; 79 | margin-left:10px; 80 | a { color: @titleColor; } 81 | a:hover { border-bottom: 1px solid @linkColor; } 82 | a.active { color: @linkColor; } 83 | } 84 | } 85 | } 86 | 87 | // main grid/view 88 | grid { 89 | .block; 90 | width: @pageWidth; 91 | margin: 0 auto; 92 | 93 | .welcome-view { margin-bottom: 50px;} 94 | 95 | start { 96 | display: block; float: left; width: 500px; 97 | font: 26px Helvetica, Arial, sans-serif; 98 | color: #1a1a1a; line-height: 1.1; margin: 25px 0 30px 0; 99 | a { 100 | color: #1a1a1a; border-bottom: 1px solid #1a1a1a; 101 | } 102 | } 103 | 104 | badges { 105 | display: block; float: right; margin-top: 15px; 106 | offf { 107 | display: block; margin-top: 25px; text-align: right; 108 | margin-right: 10px; 109 | } 110 | } 111 | 112 | dropularinfo { 113 | display: block; clear: both; 114 | font: 16px Helvetica, Arial, sans-serif; 115 | color: #1a1a1a; line-height: 1.4; margin-bottom: 25px; 116 | } 117 | 118 | #main { margin-top: 25px; width: 677px; float: left; } 119 | #toolbox-wrap { 120 | float: right; 121 | padding-left: 25px; 122 | margin-top: 25px; 123 | height: 100%; 124 | width: @toolboxWidth; 125 | border-left: 1px solid #e1e1e1; 126 | 127 | .list { margin-bottom: 20px; } 128 | 129 | li { 130 | list-style: none; 131 | a { color: @baseColor; } 132 | a:hover { color: @baseColor - #444; } 133 | .active { color: #1a1a1a; font-weight: bold; } 134 | } 135 | 136 | .lookup { 137 | .formbox { 138 | background:#fff; 139 | border: 1px solid #e5e5e5; 140 | width: 100px; 141 | color:#666; 142 | padding:3px 5px; 143 | -moz-border-radius:4px; 144 | -khtml-border-radius:4px; 145 | -webkit-border-radius:4px; 146 | font:11px Arial, sans-serif; 147 | } 148 | margin-bottom: 25px 149 | } 150 | 151 | disclaimer { 152 | display: block; 153 | margin-top:25px; 154 | a:hover { color: @baseColor - #444; } 155 | } 156 | } 157 | } 158 | } 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /client/header/userinfo.js: -------------------------------------------------------------------------------- 1 | var repositionFunc; // stored here so we can unbind it later 2 | 3 | exports.resetFormSubmitEvent = function(){ 4 | $('#login-sheet').unbind('submit').bind('submit', function(){ 5 | // NEVER enable the browser to bubble the event 6 | // http://twitter.com/joshuabaker/status/12535803672 7 | ev.preventDefault(); 8 | }); 9 | }; 10 | 11 | exports.on('load', function(){ 12 | exports.resetFormSubmitEvent(); 13 | }); 14 | 15 | function onUserchange(){ 16 | // top-right corner "Hi, username" 17 | var self = this, 18 | placeholder = $('header .user-info'), 19 | template, sheet, didChangeAnchor; 20 | if (!this.user) { 21 | // Setup the view for "signed-out" 22 | template = __html('.user-info.signed-out'); 23 | sheet = $('#login-sheet'); 24 | // "Sign in" link 25 | template.find('a.signin').toggle(function(){ 26 | var signInLink = $(this); 27 | // make the login sheet stick when resizing the window 28 | repositionFunc = function(){ 29 | var position = signInLink.offset(); 30 | position.top += signInLink.height(); 31 | sheet.css(position); 32 | }; 33 | $(window).resize(repositionFunc); 34 | repositionFunc(); 35 | 36 | // Show the sheet with an animation 37 | sheet.show().css({opacity:0.0}).animate({opacity: 1.0}, 100); 38 | // Give username focus 39 | var prevUsername = oui.cookie.get('dr_username'); 40 | var usernameField = sheet.find('input[name=username]'); 41 | if (prevUsername && prevUsername.length && usernameField.get(0)) { 42 | usernameField.get(0).value = prevUsername; 43 | sheet.find('input[name=password]').focus(); 44 | } else { 45 | usernameField.focus(); 46 | } 47 | // Rebind submit action 48 | sheet.bind('submit', function(ev){ 49 | ev.preventDefault(); 50 | ev.stopPropagation(); 51 | var $password = sheet.find('input[name=password]'), 52 | username = usernameField.get(0).value.trim(), 53 | password = $password.get(0).value, 54 | submitButton = sheet.find('input[type=submit]'); 55 | oui.cookie.set('dr_username', username, 60*60*24*9000); // 9k days TTL 56 | submitButton.attr('value', 'Signing in...').attr('disabled', 'true'); 57 | oui.app.session.signIn(username, password, function(err){ 58 | submitButton.attr('value', 'Sign in').removeAttr('disabled'); 59 | if (err) { 60 | util.notify.show(err, 3000); 61 | $password.focus(); 62 | $password.get(0).select(); 63 | } else { 64 | exports.resetFormSubmitEvent(); 65 | $password.get(0).value = ''; 66 | template.find('a.signin').trigger('click'); 67 | oui.app.session.on('userchange', true, function(){ 68 | console.log('oui.app.session.userMeta =>', oui.app.session.userMeta); 69 | if (oui.app.session.userMeta && oui.app.session.userMeta.legacy) { 70 | var msg = util.notify.__html('.messages .legacy-welcome').html(); 71 | util.notify.show(msg); 72 | } 73 | }); 74 | } 75 | }); 76 | return false; 77 | }); 78 | }, function(){ 79 | $(window).unbind('resize', repositionFunc); 80 | exports.resetFormSubmitEvent(); 81 | sheet.animate({opacity: 0.0}, 100, function(){ sheet.hide(); }); 82 | }); 83 | // TODO: make ESC key hide the login sheet while it's active. 84 | 85 | placeholder.replaceWith(template); 86 | } 87 | else { 88 | // logged in 89 | template = __html('.user-info.signed-in'); 90 | var usera = template.find('a.user'); 91 | usera.attr('href', '#users/'+self.user.username).text(self.user.username); 92 | usera.click(function(){ 93 | console.warn('TODO: show profile for user '+self.user.username); 94 | }); 95 | template.find('a.signout').click(function(ev){ 96 | $(this).removeAttr('href').unbind(ev).text('Signing out...'); 97 | oui.app.session.signOut(); 98 | 99 | return false; 100 | }); 101 | placeholder.replaceWith(template); 102 | // set default anchor if none set 103 | if (document.location.hash.substr(1) === '') { 104 | document.location.hash = '#drops'; 105 | didChangeAnchor = true; 106 | } 107 | } 108 | /*if (!didChangeAnchor) { 109 | oui.anchor.reload(); 110 | }*/ 111 | } 112 | 113 | oui.app.on('start', function(){ 114 | onUserchange.call(oui.app.session); 115 | oui.app.session.on('userchange', onUserchange); 116 | }); 117 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dropular 6 | 11 | 12 | 13 | 14 | 15 |
16 |

Dropular

17 | 18 |
19 | 20 | 21 | 22 |
23 |

24 | 25 | 26 | Welcome to Dropular version (3).002.
27 | Things are still being developed and
28 | we guarantee that you will stumble
29 | upon bugs and errors during your visit.
30 |
31 | We constantly working on new
32 | features
that can extend our service
33 | and make Dropular much more
convenient 34 | and ease to use.
35 |
36 | For updates,
37 | read our blog 38 | and add us on Twitter. 39 |
40 | 41 | 42 | 43 | 44 | 45 | Dropular is a micro-blogging service which makes it possible for members to save and collect their favorite images found from various sources online. Dropular is currently an invite only service and mostly designed for fashion designers, photographers, architects, graphic designers and artists. 46 | 47 |

© 09-2010.

48 |
49 |
50 | 51 | 52 |

Drops

53 | 58 | 59 | 60 |
61 |

Explore

62 |
63 |

64 |
65 |
66 |
67 |

Dropular

68 | 75 | 76 | 80 | © 2010 81 | 82 |
83 |
84 |
85 | 86 | 87 | 92 | 93 | 94 | 95 | 99 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /lib/base62.js: -------------------------------------------------------------------------------- 1 | var Base62 = function(pattern, encoder, ignore) { 2 | this.parser = new Parser(ignore); 3 | if (pattern) this.parser.put(pattern, ""); 4 | this.encoder = encoder; 5 | }; 6 | 7 | Base62.WORDS = /\b[\da-zA-Z]\b|\w{2,}/g; 8 | 9 | Base62.ENCODE10 = "String"; 10 | Base62.ENCODE36 = "function(c){return c.toString(36)}"; 11 | Base62.ENCODE62 = "function(c){return(c<62?'':e(parseInt(c/62)))+((c=c%62)>35?String.fromCharCode(c+29):c.toString(36))}"; 12 | 13 | Base62.UNPACK = "eval(function(p,a,c,k,e,r){e=%5;if('0'.replace(0,e)==0){while(c--)r[e(c)]=k[c];" + 14 | "k=[function(e){return r[e]||e}];e=function(){return'%6'};c=1};while(c--)if(k[c])p=p." + 15 | "replace(new RegExp('\\\\b'+e(c)+'\\\\b','g'),k[c]);return p}('%1',%2,%3,'%4'.split('|'),0,{}))"; 16 | 17 | mixin(Base62.prototype, { 18 | parser: null, 19 | encoder: undefined, 20 | 21 | search: function(script) { 22 | var words = new Words; 23 | this.parser.putAt(-1, function(word) { 24 | words.add(word); 25 | }); 26 | this.parser.exec(script); 27 | return words; 28 | }, 29 | 30 | encode: function(script) { 31 | var words = this.search(script); 32 | 33 | words.sort(); 34 | 35 | var encoded = new Collection; // a dictionary of base62 -> base10 36 | var size = words.size(); 37 | for (var i = 0; i < size; i++) { 38 | encoded.put(Packer.encode62(i), i); 39 | } 40 | 41 | function replacement(word) { 42 | return words["#" + word].replacement; 43 | }; 44 | 45 | var empty = K(""); 46 | var index = 0; 47 | forEach (words, function(word) { 48 | if (encoded.has(word)) { 49 | word.index = encoded.get(word); 50 | word.toString = empty; 51 | } else { 52 | while (words.has(Packer.encode62(index))) index++; 53 | word.index = index++; 54 | if (word.count == 1) { 55 | word.toString = empty; 56 | } 57 | } 58 | word.replacement = Packer.encode62(word.index); 59 | if (word.replacement.length == word.toString().length) { 60 | word.toString = empty; 61 | } 62 | }); 63 | 64 | // sort by encoding 65 | words.sort(function(word1, word2) { 66 | return word1.index - word2.index; 67 | }); 68 | 69 | // trim unencoded words 70 | words = words.slice(0, this.getKeyWords(words).split("|").length); 71 | 72 | script = script.replace(this.getPattern(words), replacement); 73 | 74 | /* build the packed script */ 75 | 76 | var p = this.escape(script); 77 | var a = "[]"; 78 | var c = this.getCount(words); 79 | var k = this.getKeyWords(words); 80 | var e = this.getEncoder(words); 81 | var d = this.getDecoder(words); 82 | 83 | // the whole thing 84 | return format(Base62.UNPACK, p,a,c,k,e,d); 85 | }, 86 | 87 | search: function(script) { 88 | var words = new Words; 89 | forEach (script.match(Base62.WORDS), words.add, words); 90 | return words; 91 | }, 92 | 93 | escape: function(script) { 94 | // Single quotes wrap the final string so escape them. 95 | // Also, escape new lines (required by conditional comments). 96 | return script.replace(/([\\'])/g, "\\$1").replace(/[\r\n]+/g, "\\n"); 97 | }, 98 | 99 | getCount: function(words) { 100 | return words.size() || 1; 101 | }, 102 | 103 | getDecoder: function(words) { 104 | // returns a pattern used for fast decoding of the packed script 105 | var trim = new RegGrp({ 106 | "(\\d)(\\|\\d)+\\|(\\d)": "$1-$3", 107 | "([a-z])(\\|[a-z])+\\|([a-z])": "$1-$3", 108 | "([A-Z])(\\|[A-Z])+\\|([A-Z])": "$1-$3", 109 | "\\|": "" 110 | }); 111 | var pattern = trim.exec(words.map(function(word) { 112 | if (word.toString()) return word.replacement; 113 | return ""; 114 | }).slice(0, 62).join("|")); 115 | 116 | if (!pattern) return "^$"; 117 | 118 | pattern = "[" + pattern + "]"; 119 | 120 | var size = words.size(); 121 | if (size > 62) { 122 | pattern = "(" + pattern + "|"; 123 | var c = Packer.encode62(size).charAt(0); 124 | if (c > "9") { 125 | pattern += "[\\\\d"; 126 | if (c >= "a") { 127 | pattern += "a"; 128 | if (c >= "z") { 129 | pattern += "-z"; 130 | if (c >= "A") { 131 | pattern += "A"; 132 | if (c > "A") pattern += "-" + c; 133 | } 134 | } else if (c == "b") { 135 | pattern += "-" + c; 136 | } 137 | } 138 | pattern += "]"; 139 | } else if (c == 9) { 140 | pattern += "\\\\d"; 141 | } else if (c == 2) { 142 | pattern += "[12]"; 143 | } else if (c == 1) { 144 | pattern += "1"; 145 | } else { 146 | pattern += "[1-" + c + "]"; 147 | } 148 | 149 | pattern += "\\\\w)"; 150 | } 151 | return pattern; 152 | }, 153 | 154 | getEncoder: function(words) { 155 | var size = words.size(); 156 | return Base62["ENCODE" + (size > 10 ? size > 36 ? 62 : 36 : 10)]; 157 | }, 158 | 159 | getKeyWords: function(words) { 160 | return words.map(String).join("|").replace(/\|+$/, ""); 161 | }, 162 | 163 | getPattern: function(words) { 164 | var words = words.map(String).join("|").replace(/\|{2,}/g, "|").replace(/^\|+|\|+$/g, "") || "\\x0"; 165 | return new RegExp("\\b(" + words + ")\\b", "g"); 166 | } 167 | }); 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dropular.net version 2010 2 | 3 | Note: This is an archived snapshot of the dropular.net source, May 2010. Some parts has been removed or altered for the sake of security and readability. 4 | 5 | ## Development 6 | 7 | ### Starting daily development 8 | 9 | Terminal 1: 10 | 11 | ssh -L5984:127.0.0.1:5984 dropular-ec2-1 12 | 13 | Terminal 2: 14 | 15 | cd dropular/dropular 16 | ./build.sh 17 | 18 | Terminal 3: 19 | 20 | cd dropular/dropular 21 | ./httpd.js 22 | 23 | ### Setting up a local installation 24 | 25 | **1. Install node** 26 | 27 | You'll need [Node](http://nodejs.org/) v0.1.32 or later. 28 | 29 | $ git clone git://github.com/ry/node.git 30 | $ cd node 31 | $ ./configure 32 | $ make 33 | $ sudo make install 34 | 35 | **2. Install oui and the dropular site.** 36 | 37 | $ mkdir dropular && cd dropular 38 | $ git clone git://github.com/rsms/oui.git 39 | $ git clone git://github.com/rsms/node-couchdb-min.git 40 | $ git clone git@github.com:rsms/dropular.git 41 | 42 | You might need to build the oui library: 43 | 44 | $ cd dropular/oui/client 45 | $ ./build.sh -s 46 | 47 | **3. Install CouchDB** 48 | 49 | First, install dependencies. 50 | OS X: 51 | 52 | $ sudo port install icu erlang spidermonkey curl 53 | 54 | Debian: 55 | 56 | $ sudo apt-get install build-essential erlang libicu-dev libmozjs-dev\ 57 | libcurl4-openssl-dev 58 | 59 | Suck down and build CouchDB: 60 | 61 | $ svn co http://svn.apache.org/repos/asf/couchdb/trunk couchdb 62 | $ cd couchdb 63 | $ ./bootstrap && ./configure 64 | $ make && sudo make install 65 | 66 | 67 | ---- 68 | 69 | [--deprecated--] 70 | 71 | In production, you should configure for prefix "" (that's "/"): 72 | 73 | $ ./configure --prefix '' 74 | 75 | And perform the following after `make install`: 76 | 77 | $ sudo adduser --system --home /var/lib/couchdb --no-create-home\ 78 | --shell /bin/bash --group --gecos 'CouchDB Administrator' couchdb 79 | $ sudo chown -R couchdb.couchdb /var/{lib,log,run}/couchdb /etc/couchdb 80 | $ sudo chmod -R 0770 /var/{lib,log,run}/couchdb /etc/couchdb 81 | 82 | Uninstall is partially possible by running `make uninstall` and then `sudo find /usr/local -iname '*couch*' | sudo xargs rm -rf`. 83 | 84 | Create a dropular user and group: 85 | 86 | $ sudo adduser --system --home /var/dropular --shell /bin/bash --group\ 87 | --gecos 'Dropular system user' dropular 88 | 89 | Generate an SSH key for the dropular user: 90 | 91 | $ sudo su dropular 92 | $ cd 93 | $ ssh-keygen -t rsa 94 | $ cat .ssh/id_rsa.pub 95 | 96 | Copy the output from the last command and add a new "deploy key" in https://github.com/rsms/dropular/edit 97 | 98 | Check out the dropular repo, logged in as `dropular`: 99 | 100 | $ git clone git@github.com:rsms/dropular.git /var/dropular/dropular 101 | 102 | Create a symlink in `/var/www`: 103 | 104 | $ ln -s /var/dropular/dropular /var/www/dropular.net/www 105 | 106 | #### Deploying a new version of dropular-httpd 107 | 108 | First thing, start a server instance in debug mode on an unused port: 109 | 110 | $ sudo -u dropular /var/dropular/dropular/httpd.js -d -p 9000 111 | 112 | Then, test with a client directly: 113 | 114 | $ open 'http://dropular.net:9000/' 115 | 116 | Then test with a client partially: 117 | 118 | $ open 'http://dropular.net/#OUI_DEBUG_BACKEND=dropular.net:9000' 119 | 120 | If everything look jolly good, restart all live server instances: 121 | 122 | $ sudo invoke-rc.d dropular-httpd restart 123 | 124 | Aaand test the live client: 125 | 126 | $ open 'http://dropular.net/' 127 | 128 | 129 | ### Client development 130 | 131 | When developing the client, you need to build it. The `build.sh` file does this for you automatically. In a terminal: 132 | 133 | $ ./build.sh -fO 0 134 | 135 | > `-fO 0` -- The `f` flag causes the first build to be "complete" (forced). The `O` flag sets the optimization level to zero, making debugging possible. 136 | 137 | Keep this running in a terminal -- when a file has been changed, the client will automatically be rebuilt. 138 | 139 | 140 | ### Server development 141 | 142 | When developing the server, you need to (re)start `httpd.js` when something changes in the server code. 143 | 144 | $ ./httpd.js -d 145 | 146 | > `-d` -- The `d` flag causes debugging to be enabled. 147 | 148 | 149 | ## MIT license 150 | 151 | Copyright (c) 2009-2010 Rasmus Andersson , Andreas Pihlström 152 | 153 | Permission is hereby granted, free of charge, to any person obtaining a copy 154 | of this software and associated documentation files (the "Software"), to deal 155 | in the Software without restriction, including without limitation the rights 156 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 157 | copies of the Software, and to permit persons to whom the Software is 158 | furnished to do so, subject to the following conditions: 159 | 160 | The above copyright notice and this permission notice shall be included in 161 | all copies or substantial portions of the Software. 162 | 163 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 164 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 165 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 166 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 167 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 168 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 169 | THE SOFTWARE. 170 | -------------------------------------------------------------------------------- /lib/aws/s3.js: -------------------------------------------------------------------------------- 1 | var http = require("http"), 2 | sys = require("sys"), 3 | sha1 = require('./sha1'), 4 | httputil = require('./httputil'); 5 | 6 | function Bucket(id, key, secret){ 7 | this.id = id; 8 | this.key = key; 9 | this.secret = secret; 10 | } 11 | exports.Bucket = Bucket; 12 | 13 | Bucket.prototype.urlTo = function(path){ 14 | return 'http://'+ 15 | (this.id ? this.id+'.' : '') + 's3.amazonaws.com'+ 16 | path; 17 | } 18 | 19 | Bucket.prototype.request = function(options, callback){ 20 | if (typeof options === 'function') { callback = options; options = undefined; } 21 | 22 | // Default options 23 | var opt = { 24 | host: (this.id ? this.id+'.' : '') + 's3.amazonaws.com', 25 | //debug: true, 26 | headers: {}, 27 | ctxid: module.id 28 | }; 29 | 30 | // mixin options 31 | if (typeof options === 'object') { 32 | // method, path, query, body, contentType, headers 33 | for (var k in options) opt[k] = options[k]; 34 | } 35 | 36 | // create request 37 | var ev = httputil.request(opt, function(err, data, res){ 38 | callback(err, data, res); 39 | }); 40 | 41 | // prepare just before the request is actually sent 42 | var self = this; 43 | ev.addListener('connection', function(opt, conn){ 44 | // user agent 45 | if (!('user-agent' in opt.headers)) 46 | opt.headers['user-agent'] = 'node-aws'; 47 | // sign? 48 | if (self.secret) { 49 | self.signRequest(opt); 50 | } 51 | }); 52 | 53 | return ev; 54 | } 55 | 56 | 57 | Bucket.prototype.get = function(path, query, headers, callback){ 58 | if (typeof headers === 'function') { callback = headers; headers = undefined; } 59 | else if (typeof query === 'function') { callback = query; query = undefined; } 60 | return this.request({ 61 | path: path, 62 | query: query, 63 | headers: headers 64 | }, callback); 65 | } 66 | 67 | Bucket.prototype.put = function(path, data, contentType, options, callback){ 68 | if (typeof options === 'function') { callback = options; options = undefined; } 69 | else if (typeof contentType === 'function') { callback = contentType; contentType = undefined; } 70 | 71 | var opt = { 72 | method: 'PUT', 73 | path: path, 74 | body: data, 75 | headers: {} 76 | }; 77 | 78 | if (typeof options === 'object') 79 | for (var k in options) opt[k] = options[k]; 80 | 81 | if (typeof opt.headers !== 'object') 82 | opt.headers = {}; 83 | 84 | if (!('x-amz-acl' in opt.headers)) 85 | opt.headers['x-amz-acl'] = 'public-read'; 86 | 87 | if (contentType) { 88 | if (typeof opt.headers !== 'object') opt.headers = {}; 89 | opt.headers['content-type'] = contentType; 90 | } 91 | 92 | return this.request(opt, callback); 93 | } 94 | 95 | Bucket.prototype.del = function(path, options, callback){ 96 | if (typeof options === 'function') { callback = options; options = undefined; } 97 | var opt = { 98 | method: 'DELETE', 99 | path: path 100 | }; 101 | if (typeof options === 'object') 102 | for (var k in options) opt[k] = options[k]; 103 | return this.request(opt, callback); 104 | } 105 | 106 | // Sign a request 107 | Bucket.prototype.signRequest = function(opt, date){ 108 | var date = new Date(), 109 | resource = (opt.path || '/'); 110 | if (this.id) resource = "/"+this.id + resource; 111 | opt.headers.date = date.toUTCString(); 112 | opt.headers.authorization = 'AWS '+this.key+':'+this.sign(opt.method, 113 | opt.headers['content-md5'], opt.headers['content-type'], 114 | date, opt.headers, resource); 115 | } 116 | 117 | // Create signature 118 | Bucket.prototype.sign = function(verb, contentMD5, contentType, date, amzHeaders, resource){ 119 | /* 120 | Example: 121 | Authorization: AWS 0PN5J17HBGZHT7JJ3X82:frJIUN8DYpKDtOLCwo//yllqDzg= 122 | 123 | BNF: 124 | Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; 125 | 126 | Signature = Base64( HMAC-SHA1( UTF-8-Encoding-Of( YourSecretAccessKeyID, StringToSign ) ) ); 127 | 128 | StringToSign = HTTP-Verb + "\n" + 129 | Content-MD5 + "\n" + 130 | Content-Type + "\n" + 131 | Date + "\n" + 132 | CanonicalizedAmzHeaders + 133 | CanonicalizedResource; 134 | 135 | CanonicalizedResource = [ "/" + Bucket ] + 136 | + 137 | [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 138 | 139 | CanonicalizedAmzHeaders = 140 | */ 141 | var s, amzHeadersIsObj = (typeof amzHeaders === 'object'); 142 | 143 | // Format date 144 | if (!date) { 145 | if (amzHeadersIsObj && amzHeaders['x-amz-date']) date = amzHeaders['x-amz-date']; 146 | else if (amzHeadersIsObj && amzHeaders['date']) date = amzHeaders['date']; 147 | else date = (new Date()).toUTCString(); 148 | } else if (typeof date === 'object') { 149 | date = date.toUTCString(); 150 | } 151 | 152 | // Fix headers 153 | if (amzHeadersIsObj) { 154 | s = ''; 155 | for (var k in amzHeaders) { 156 | if (k.indexOf('x-amz-') === 0) s += k+':'+amzHeaders[k]+'\n'; 157 | } 158 | amzHeaders = s; 159 | } else { 160 | amzHeaders = '\n'; 161 | } 162 | 163 | s = verb+"\n"+ 164 | (contentMD5 || '')+"\n"+ 165 | (contentType || '')+"\n"+ 166 | date+"\n"+ 167 | amzHeaders+ 168 | resource; 169 | //sys.error(s.replace(/\n/gm, '\\n\n')); 170 | return sha1.b64_hmac_sha1(this.secret, s); 171 | } 172 | 173 | /*var b = new Bucket('static.dropular.net', 174 | 'key', 'secret'); 175 | 176 | b.put('/test1.txt', 'hello test1', 'text/plain', function(err, data, res){ 177 | if (err) throw err; 178 | b.get('/test1.txt', function(err, data, res){ 179 | if (err) throw err; 180 | b.del('/test1.txt', function(err, data, res){ 181 | if (err) throw err; 182 | }) 183 | }) 184 | }) 185 | */ 186 | -------------------------------------------------------------------------------- /lib/aws/sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS PUB 180-1 4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | exports.hex_sha1 = function (s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} 23 | exports.b64_sha1 = function (s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} 24 | exports.str_sha1 = function (s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} 25 | exports.hex_hmac_sha1 = function (key, data){ return binb2hex(core_hmac_sha1(key, data));} 26 | exports.b64_hmac_sha1 = function (key, data){ return binb2b64(core_hmac_sha1(key, data));} 27 | exports.str_hmac_sha1 = function (key, data){ return binb2str(core_hmac_sha1(key, data));} 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function sha1_vm_test() 33 | { 34 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; 35 | } 36 | 37 | /* 38 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 39 | */ 40 | function core_sha1(x, len) 41 | { 42 | /* append padding */ 43 | x[len >> 5] |= 0x80 << (24 - len % 32); 44 | x[((len + 64 >> 9) << 4) + 15] = len; 45 | 46 | var w = Array(80); 47 | var a = 1732584193; 48 | var b = -271733879; 49 | var c = -1732584194; 50 | var d = 271733878; 51 | var e = -1009589776; 52 | 53 | for(var i = 0; i < x.length; i += 16) 54 | { 55 | var olda = a; 56 | var oldb = b; 57 | var oldc = c; 58 | var oldd = d; 59 | var olde = e; 60 | 61 | for(var j = 0; j < 80; j++) 62 | { 63 | if(j < 16) w[j] = x[i + j]; 64 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 65 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 66 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 67 | e = d; 68 | d = c; 69 | c = rol(b, 30); 70 | b = a; 71 | a = t; 72 | } 73 | 74 | a = safe_add(a, olda); 75 | b = safe_add(b, oldb); 76 | c = safe_add(c, oldc); 77 | d = safe_add(d, oldd); 78 | e = safe_add(e, olde); 79 | } 80 | return Array(a, b, c, d, e); 81 | 82 | } 83 | 84 | /* 85 | * Perform the appropriate triplet combination function for the current 86 | * iteration 87 | */ 88 | function sha1_ft(t, b, c, d) 89 | { 90 | if(t < 20) return (b & c) | ((~b) & d); 91 | if(t < 40) return b ^ c ^ d; 92 | if(t < 60) return (b & c) | (b & d) | (c & d); 93 | return b ^ c ^ d; 94 | } 95 | 96 | /* 97 | * Determine the appropriate additive constant for the current iteration 98 | */ 99 | function sha1_kt(t) 100 | { 101 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 102 | (t < 60) ? -1894007588 : -899497514; 103 | } 104 | 105 | /* 106 | * Calculate the HMAC-SHA1 of a key and some data 107 | */ 108 | function core_hmac_sha1(key, data) 109 | { 110 | var bkey = str2binb(key); 111 | if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); 112 | 113 | var ipad = Array(16), opad = Array(16); 114 | for(var i = 0; i < 16; i++) 115 | { 116 | ipad[i] = bkey[i] ^ 0x36363636; 117 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 118 | } 119 | 120 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 121 | return core_sha1(opad.concat(hash), 512 + 160); 122 | } 123 | 124 | /* 125 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 126 | * to work around bugs in some JS interpreters. 127 | */ 128 | function safe_add(x, y) 129 | { 130 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 131 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 132 | return (msw << 16) | (lsw & 0xFFFF); 133 | } 134 | 135 | /* 136 | * Bitwise rotate a 32-bit number to the left. 137 | */ 138 | function rol(num, cnt) 139 | { 140 | return (num << cnt) | (num >>> (32 - cnt)); 141 | } 142 | 143 | /* 144 | * Convert an 8-bit or 16-bit string to an array of big-endian words 145 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 146 | */ 147 | function str2binb(str) 148 | { 149 | var bin = new Array(); 150 | var mask = (1 << chrsz) - 1; 151 | for(var i = 0; i < str.length * chrsz; i += chrsz) { 152 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 153 | } 154 | return bin; 155 | } 156 | 157 | /* 158 | * Convert an array of big-endian words to a string 159 | */ 160 | function binb2str(bin) 161 | { 162 | var str = ""; 163 | var mask = (1 << chrsz) - 1; 164 | for(var i = 0; i < bin.length * 32; i += chrsz) 165 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 166 | return str; 167 | } 168 | 169 | /* 170 | * Convert an array of big-endian words to a hex string. 171 | */ 172 | function binb2hex(binarray) 173 | { 174 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 175 | var str = ""; 176 | for(var i = 0; i < binarray.length * 4; i++) 177 | { 178 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 179 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 180 | } 181 | return str; 182 | } 183 | 184 | /* 185 | * Convert an array of big-endian words to a base-64 string 186 | */ 187 | function binb2b64(binarray) 188 | { 189 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 190 | var str = ""; 191 | for(var i = 0; i < binarray.length * 4; i += 3) 192 | { 193 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 194 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 195 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 196 | for(var j = 0; j < 4; j++) 197 | { 198 | if((i * 8 + j * 6) > (binarray.length * 32)) str += b64pad; 199 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 200 | } 201 | } 202 | return str; 203 | } 204 | -------------------------------------------------------------------------------- /misc/docs/setting-up-an-ec2-instance.md: -------------------------------------------------------------------------------- 1 | # Setting up an EC2 instance 2 | 3 | ## Prerequisites 4 | 5 | - [Amazon EC2 command line tools](http://developer.amazonwebservices.com/connect/entry.jspa?externalID=351&categoryID=88) 6 | 7 | It is recommended to put the contents of the zip file referenced in the above web page in `~/.ec2`. First, unzip the file, then: 8 | 9 | mv ec2-api-tools-1.3-46266 ~/.ec2 10 | chmod 0700 ~/.ec2 11 | 12 | Now, you need to add the tools to your `PATH`. Edit your `~/.bashrc` file and add these lines somewhere: 13 | 14 | if [ -d ~/.ec2 ]; then 15 | export EC2_HOME="$HOME/.ec2" 16 | export PATH=$PATH:$EC2_HOME/bin 17 | export EC2_PRIVATE_KEY=`ls $EC2_HOME/pk-*.pem` 18 | export EC2_CERT=`ls $EC2_HOME/cert-*.pem` 19 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Home/ 20 | fi 21 | 22 | Download the X.509 certificates from the Amazon AWS accounts page and put them into `~/.ec2` and secure permissions (`chmod 0400 ~/.ec2/*.pem`). 23 | 24 | Now, listing the contents of `~/.ec2` should now yield something like this: 25 | 26 | $ ls -l ~/.ec2 27 | drwxrwxr-x 352 rasmus staff 11968 13 Dec 22:25 bin 28 | -r-------- 1 rasmus staff 916 17 Apr 13:56 cert-xxxxxxxxx.pem 29 | -r-------- 1 rasmus staff 1693 17 Apr 13:31 dropular-ec2-1.pem 30 | drwxrwxr-x 26 rasmus staff 884 13 Dec 22:25 lib 31 | -r-------- 1 rasmus staff 922 17 Apr 13:56 pk-xxxxxxxxx.pem 32 | 33 | > **IMPORTANT --** THE `pem` FILES ARE CONFIDENTIAL AND SHALL NEVER FALL INTO THE HANDS OF OTHER PEOPLE, as they are the key to controlling anything. 34 | 35 | ## Creating and configuring a new EC2 instance 36 | 37 | Choose an instance id. This should be in the form "dropular-ec2-N" where "N" is the next natural number: 38 | 39 | INSTANCEID='dropular-ec2-2' 40 | 41 | Now, create a key pair and launch a new instance with that key pair: 42 | 43 | mkdir -p ~/.ec2 && chmod 0700 ~/.ec2 44 | ec2-add-keypair $INSTANCEID > ~/.ec2/$INSTANCEID.pem 45 | chmod 0400 ~/.ec2/$INSTANCEID.pem 46 | ec2-run-instances ami-19a34270 -k $INSTANCEID 47 | 48 | > `ami-19a34270` is a 32bit Ubuntu 9.10 "karmic" image from Alestic for the Virigina site. `ami-2fc2e95b` is an alternative 32bit Ubuntu 9.10 "karmic" image for the EU site. 49 | 50 | Create a SSH short-hand alias: 51 | 52 | HOSTNAME=$(ec2-describe-instances | grep $INSTANCEID | awk '{print $4}') 53 | echo "Host $INSTANCEID" >> ~/.ssh/config 54 | echo " HostName $HOSTNAME" >> ~/.ssh/config 55 | echo " User root" >> ~/.ssh/config 56 | echo " IdentityFile $HOME/.ec2/$INSTANCEID.pem" >> ~/.ssh/config 57 | chmod 0600 ~/.ssh/config 58 | 59 | Also, make sure the security group in which the instance is operating in (`default` by default) have the appropriate ports opened: 60 | 61 | ec2-authorize default -p 22 62 | ec2-authorize default -p 80 63 | ec2-authorize default -p 8100-8199 64 | 65 | Now, you can log in to the server by referencing the `$INSTANCEID`. E.g: 66 | 67 | ssh $INSTANCEID 68 | 69 | > If the instance was started in a special region, include the `--region` option when calling the ec2 commands. E.g. `ec2-authorize --region eu-west-1 default -p 22` 70 | 71 | ## Setting up the system 72 | 73 | > If you see lines like this `E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem.` -- simply run `dpkg --configure -a` and re-run `apt-get install x` until completed. This is a EC2 specific issue. 74 | 75 | apt-get update 76 | apt-get install build-essential libc6-dev libstdc++6 git-core 77 | mkdir -p ~/src 78 | 79 | **Node:** 80 | 81 | cd ~/src 82 | git clone git://github.com/ry/node.git 83 | cd node && git checkout v0.1.94 84 | ./configure && make && make install 85 | 86 | **CouchDB:** 87 | 88 | apt-get install erlang libicu-dev libmozjs-dev libcurl4-openssl-dev 89 | cd ~/src 90 | wget http://www.apache.org/dist/couchdb/0.11.0/apache-couchdb-0.11.0.tar.gz 91 | tar xfz apache-couchdb-0.11.0.tar.gz && cd apache-couchdb-0.11.0 92 | adduser --system --home /var/lib/couchdb --no-create-home\ 93 | --shell /bin/bash --group --gecos 'CouchDB Administrator' couchdb 94 | ./configure --prefix '' 95 | make && make install 96 | update-rc.d couchdb defaults 97 | invoke-rc.d couchdb start 98 | 99 | **Dropular user:** 100 | 101 | adduser --system --home /var/dropular --shell /bin/bash --group\ 102 | --gecos 'Dropular system user' dropular 103 | 104 | **Git deploy key:** 105 | 106 | Chose all default answers asked by `ssh-keygen` 107 | 108 | sudo su dropular 109 | ssh-keygen 110 | less ~/.ssh/id_rsa.pub 111 | 112 | Copy the output from less and create a new deploy key at [https://github.com/rsms/dropular/edit](https://github.com/rsms/dropular/edit) -- name it `$INSTANCEID` (e.g. "dropular-ec2-2"). 113 | 114 | **`/var/dropular`:** 115 | 116 | apt-get install daemon imagemagick 117 | sudo su dropular 118 | cd 119 | git clone git://github.com/rsms/oui.git && oui/client/build.sh -s 120 | git clone git://github.com/rsms/node-couchdb-min.git 121 | git clone git://github.com/rsms/node-imagemagick.git 122 | git clone git@github.com:rsms/dropular.git && dropular/build.sh -s 123 | # ^D -- log out dropular and be logged in as root 124 | ln -s /var/dropular/dropular/misc/init.d/dropular-httpd /etc/init.d/ 125 | update-rc.d dropular-httpd defaults 126 | $EDITOR /etc/default/dropular-httpd 127 | # enter something like: DR_HTTPD_PORTS="8100 8101" 128 | invoke-rc.d dropular-httpd start 129 | 130 | **nginx:** 131 | 132 | apt-get install nginx 133 | cd /etc/nginx/sites-enabled 134 | cp /var/dropular/dropular/misc/nginx.conf ../sites-available/dropular 135 | ln -s ../sites-available/dropular 136 | $EDITOR dropular 137 | # edit dropular_backends to match DR_HTTPD_PORTS in /etc/defaults/dropular-httpd 138 | invoke-rc.d nginx restart 139 | 140 | ## Setting up the data 141 | 142 | First, create a temporary tunnel to the instance from which you want to replicate (while being logged in on the EC2 instance): 143 | 144 | ssh -L5984:127.0.0.1:5984 rasmus@hunch.se 145 | 146 | In a new terminal (logged in to the EC2 instance): 147 | 148 | curl -X PUT http://127.0.0.1:5984/dropular-{users,drops,newslist} 149 | curl -vX POST http://127.0.0.1:5984/_replicate -d\ 150 | '{"source":"http://127.0.0.1:5985/dropular-drops","target":"dropular-drops"}' 151 | curl -vX POST http://127.0.0.1:5984/_replicate -d\ 152 | '{"source":"http://127.0.0.1:5985/dropular-users","target":"dropular-users"}' 153 | 154 | This will take some time, depending on the load of the source instance and the amount of data (normally about 10 minutes). 155 | 156 | ## Testing the dropular server 157 | 158 | cd /var/dropular/dropular 159 | ./build.sh -s 160 | ./httpd.js -d 161 | 162 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | if (window.OUI_HELP) { 2 | // Extend the message from help 3 | window.OUI_HELP.sections.Examples += 4 | "\n"+ 5 | " Creating or updating a drop:\n"+ 6 | " oui.app.session.post('drop', {'url':'http://www.com/some/image.jpg'},\n"+ 7 | " function(err, result, resp) { console.log(err, result, resp); });\n"; 8 | } 9 | 10 | // BEGIN TEMPORARY FIX 11 | if (!oui.cookie.get('amazonfuckedupourserver')) { 12 | oui.cookie.clear('auth_token'); 13 | oui.cookie.set('amazonfuckedupourserver', 'ohhowsad', 60*60*24*365*10); 14 | } 15 | // END TEMPORARY FIX 16 | 17 | exports.reloadUser = function(callback){ 18 | if (oui.app.session.user) { 19 | console.log(__name+': reloading user '+oui.app.session.user.username); 20 | users.User.find(oui.app.session.user.username, function(err, user){ 21 | if (err) { 22 | util.notify.show(err); 23 | } else { 24 | console.log(__name+': reloaded user '+(user ? user.username : '')); 25 | oui.app.session.setUser(user); 26 | } 27 | if (callback) callback(err); 28 | }); 29 | } else { 30 | if (callback) callback(); 31 | } 32 | }; 33 | 34 | exports.updateUserLevelStyle = function(user) { 35 | user = user || oui.app.session.user; 36 | // lazily create the style tag, controlling visibility 37 | if (!exports.$userLevelStyle) { 38 | var $head = $('html > head'); 39 | $head.append(''); 40 | exports.$userLevelStyle = $head.find('style:last'); 41 | } 42 | // Hide/show any elements according to user's level 43 | var s = [], i, levels = 3, level = levels; 44 | if (user && typeof user.level === 'number') 45 | level = user.level; 46 | // refresh trick -- might be needed for legacy browsers. Let's keep it for now. 47 | for (i=levels; --i >= level; ) { 48 | s.push('.userlevel'+i+' { display:block; }'); 49 | } 50 | for (i=level; --i > -1; ) { 51 | s.push('.userlevel'+i+' { display:none; }'); 52 | } 53 | for (i=0;i*{outline:none;}'); 69 | 70 | // update universal UI stuff on userchange 71 | oui.app.session.on('userchange', function(ev, prevUser){ 72 | exports.updateUserLevelStyle(this.user); 73 | 74 | if (this.user) { 75 | $('.following').css('display','block'); 76 | } else{ 77 | $('.following').css('display','none'); 78 | } 79 | 80 | if (!this.user && prevUser && document.location.hash === '#drops') { 81 | // when a user logs out while at a user-restricted view, send her to home: 82 | $('.following').css('display','none'); 83 | document.location.hash = '#'; 84 | } 85 | $('grid').delay(200).show(); 86 | }); 87 | }); 88 | 89 | // Scroll state persistence 90 | var getScroll; 91 | if (document.all) { 92 | getScroll = function() { 93 | return { x: document.scrollLeft, y: document.scrollTop }; 94 | }; 95 | } else { 96 | getScroll = function() { 97 | return { x: window.pageXOffset, y: window.pageYOffset }; 98 | }; 99 | } 100 | // keyed by document.location.hash 101 | exports.scrollStates = {}; 102 | oui.anchor.events.addListener('change', function(ev, path, prevPath, routes){ 103 | exports.scrollStates[prevPath] = getScroll(); 104 | }); 105 | oui.anchor.events.addListener('changed', function(ev, path, prevPath, routes){ 106 | var scrollState = exports.scrollStates[path]; 107 | if (scrollState) { 108 | exports.scrollStates[path] = undefined; 109 | oui.app.session.on('idle', true, function(){ 110 | setTimeout(function(){ 111 | console.debug('restoring scroll to', scrollState.x, scrollState.y); 112 | window.scrollTo(scrollState.x, scrollState.y); 113 | },500); // todo: this is a shaky solution. must be some event we can listen for... 114 | }); 115 | } 116 | }); 117 | 118 | 119 | exports.mixinViewControl = function (exports, selector) { 120 | $(document).ready(function(){ 121 | exports.$container = $(selector); 122 | 123 | $('#taglookup input').focus(function() { 124 | $("#taglookup input").val(""); 125 | }); 126 | 127 | $('#taglookup').submit(function() { 128 | if ($("#taglookup input").val()) { 129 | to_url = window.location.href.replace( /#.*/, "")+'#drops/tagged/'+encodeURIComponent($("#taglookup input").val()); 130 | document.location = to_url; 131 | return false; 132 | } 133 | return false; 134 | }); 135 | }); 136 | 137 | 138 | exports.events = new oui.EventEmitter(); 139 | 140 | exports.visible = function(view){ 141 | if (view) { 142 | return (exports.$container.has(view).length !== 0 143 | && view.css('display') !== 'none' 144 | && view.css('opacity') !== '0'); 145 | } else { 146 | return (exports.$container.children().length !== 0 147 | && exports.$container.css('display') !== 'none' 148 | && exports.$container.css('opacity') !== '0'); 149 | } 150 | }; 151 | 152 | exports.clear = function(callback) { 153 | if (exports.visible) { 154 | exports.events.emit('viewclear'); 155 | exports.events.emit('viewchange'); 156 | exports.$container.fadeOut(200, callback); 157 | } 158 | }; 159 | 160 | exports.title = function(title) { 161 | var $h1 = exports.$container.find('h1'), currTitle = $h1.text(); 162 | if (title) 163 | $h1.text(title); 164 | return currTitle; 165 | }; 166 | 167 | exports.viewQueuedForDisplay = null; 168 | 169 | exports.setView = function(view, callback, title){ 170 | if (typeof view === 'function') { 171 | callback = view; 172 | view = null; 173 | } 174 | 175 | exports.events.emit('viewchange'); 176 | if (!view) { 177 | exports.$container.fadeIn(200, callback); 178 | } else if (!exports.visible(view)) { 179 | exports.viewQueuedForDisplay = view; 180 | exports.$container.fadeOut(100, function(){ 181 | if (view === exports.viewQueuedForDisplay) { 182 | exports.viewQueuedForDisplay = null; 183 | // defer to next tick, since otherwise the object is display:none 184 | // and things like input focus will not work. 185 | setTimeout(function(){ 186 | (view.ouiModule ? view.ouiModule.$html : view).triggerHandler('load', view); 187 | if (view.ouiModule) view.ouiModule.emit('load', view); 188 | },1); 189 | exports.$container.empty().append(view.show()).fadeIn(200, callback); 190 | } 191 | }); 192 | } else if (callback) { 193 | callback(); 194 | } 195 | }; 196 | }; 197 | 198 | -------------------------------------------------------------------------------- /misc/init.d/dropular-httpd: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: dropular-httpd 4 | # Required-Start: $remote_fs 5 | # Required-Stop: $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Dropular HTTP server 9 | # Description: Dropular HTTP server 10 | ### END INIT INFO 11 | 12 | # Author: Rasmus Andersson 13 | # Do NOT "set -e" 14 | 15 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 16 | PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin 17 | NAME=dropular-httpd 18 | DESC="Dropular HTTP server" 19 | RUNDIR=/var/run/dropular 20 | LOGDIR=/var/log/dropular 21 | SCRIPTNAME=/etc/init.d/$NAME 22 | USER=dropular 23 | GROUP=dropular 24 | 25 | # Default configuration for dropular-httpd 26 | DR_HTTPD_BIN=/var/dropular/dropular/httpd.js 27 | # Start one instance per port 28 | #DR_HTTPD_PORTS="8100 8101 8102 8103" 29 | DR_HTTPD_PORTS=8100 30 | #DR_HTTPD_ARGS='--debug' 31 | # How long to sleep in between instance restarts 32 | DR_HTTPD_RESTART_SLEEP=1 33 | 34 | # Daemon supervisor config 35 | SUPERVISOR=/usr/bin/daemon 36 | SUPERVISOR_ARGS="--respawn --chdir=$(dirname $DR_HTTPD_BIN)" # do NOT set --user here 37 | SUPERVISOR_PIDDIR="$RUNDIR" 38 | 39 | # Exit if the package is not available 40 | test -f "$DR_HTTPD_BIN" || exit 0 41 | 42 | # Read configuration variable file if it is present 43 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 44 | 45 | # The following are for internal use 46 | DR_HTTPD_NPORTS=$(echo $DR_HTTPD_PORTS | wc -w) 47 | 48 | # Load the VERBOSE setting and other rcS variables 49 | . /lib/init/vars.sh 50 | 51 | # Define LSB log_* functions. 52 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 53 | . /lib/lsb/init-functions 54 | 55 | # Make sure run and log directories exist 56 | mkdir -p $RUNDIR > /dev/null 2> /dev/null 57 | chown -R $USER:$GROUP $RUNDIR 58 | chmod 0750 $RUNDIR 59 | mkdir -p $LOGDIR > /dev/null 2> /dev/null 60 | chown -R $USER:$GROUP $LOGDIR 61 | chmod 0750 $LOGDIR 62 | 63 | VERBOSE=yes 64 | 65 | # start_instance PORT 66 | start_instance() { 67 | DR_HTTPD_PORT=$1 68 | INSTANCE_ID="$NAME-$DR_HTTPD_PORT" 69 | PIDFILE="$SUPERVISOR_PIDDIR/$INSTANCE_ID.pid" 70 | [ "$VERBOSE" != no ] && log_daemon_msg " starting $INSTANCE_ID" 71 | start-stop-daemon --start --quiet --pidfile $PIDFILE \ 72 | --exec $SUPERVISOR --test > /dev/null 73 | STATUS="$?" 74 | if [ "$STATUS" = "0" ]; then 75 | start-stop-daemon --start --quiet --pidfile $PIDFILE \ 76 | --exec $SUPERVISOR --chuid $USER:$GROUP -- \ 77 | $SUPERVISOR_ARGS \ 78 | --name=$INSTANCE_ID --pidfile=$PIDFILE \ 79 | --stdout=$LOGDIR/$INSTANCE_ID.out \ 80 | --errlog=$LOGDIR/$INSTANCE_ID.err \ 81 | -- "$DR_HTTPD_BIN" $DR_HTTPD_ARGS --port $DR_HTTPD_PORT 82 | STATUS="$?" 83 | else 84 | STATUS=2 85 | fi 86 | case "$STATUS" in 87 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 88 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 89 | esac 90 | return $STATUS 91 | } 92 | 93 | # stop_instance PORT 94 | stop_instance() { 95 | DR_HTTPD_PORT=$1 96 | INSTANCE_ID="$NAME-$DR_HTTPD_PORT" 97 | PIDFILE="$SUPERVISOR_PIDDIR/$INSTANCE_ID.pid" 98 | [ "$VERBOSE" != no ] && log_daemon_msg " stopping $INSTANCE_ID" 99 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 100 | STATUS="$?" 101 | case "$STATUS" in 102 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 103 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 104 | esac 105 | if [ "$STATUS" != "2" ]; then 106 | rm -f $PIDFILE 107 | fi 108 | return $STATUS 109 | } 110 | 111 | # signal_instance PORT SIGNAL 112 | signal_instance() { 113 | DR_HTTPD_PORT=$1 114 | SIGNAL=$2 115 | INSTANCE_ID="$NAME-$DR_HTTPD_PORT" 116 | PIDFILE="$SUPERVISOR_PIDDIR/$INSTANCE_ID.pid" 117 | [ "$VERBOSE" != no ] && log_daemon_msg " sending signal $SIGNAL to $INSTANCE_ID" 118 | kill -s $SIGNAL $(cat $PIDFILE) 119 | case "$?" in 120 | 0) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 121 | *) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 122 | esac 123 | return $? 124 | } 125 | 126 | do_start() { 127 | # Return 128 | # 0 if daemon has been started 129 | # 1 if daemon was already running 130 | # 2 if daemon could not be started 131 | for DR_HTTPD_PORT in $DR_HTTPD_PORTS; do 132 | start_instance $DR_HTTPD_PORT 133 | done 134 | return 0 135 | } 136 | 137 | do_stop() { 138 | # Return 139 | # 0 if daemon has been stopped 140 | # 1 if daemon was already stopped 141 | # 2 if daemon could not be stopped 142 | # other if a failure occurred 143 | RETVAL="0" 144 | for DR_HTTPD_PORT in $DR_HTTPD_PORTS; do 145 | stop_instance $DR_HTTPD_PORT 146 | if [ "$?" = "2" ] ; then RETVAL="2" ; fi 147 | done 148 | return "$RETVAL" 149 | } 150 | 151 | do_restart() { 152 | NPORT=0 153 | for DR_HTTPD_PORT in $DR_HTTPD_PORTS; do 154 | NPORT=$(expr $NPORT + 1) 155 | stop_instance $DR_HTTPD_PORT 156 | start_instance $DR_HTTPD_PORT 157 | [ "$?" = 2 ] && return 2 158 | if [ $DR_HTTPD_RESTART_SLEEP -gt 0 ] && [ $NPORT -lt $DR_HTTPD_NPORTS ]; then 159 | [ "$VERBOSE" != no ] && log_action_msg " waiting $DR_HTTPD_RESTART_SLEEP second(s) until restarting next instance..." 160 | sleep $DR_HTTPD_RESTART_SLEEP 161 | fi 162 | done 163 | return 0 164 | } 165 | 166 | do_reload() { 167 | for DR_HTTPD_PORT in $DR_HTTPD_PORTS; do 168 | signal_instance $DR_HTTPD_PORT 1 169 | [ "$?" = 2 ] && return 2 170 | done 171 | return 0 172 | } 173 | 174 | case "$1" in 175 | start) 176 | [ "$VERBOSE" != no ] && log_action_msg "Starting $DESC" 177 | do_start 178 | case "$?" in 179 | 0|1) [ "$VERBOSE" != no ] && log_action_msg "Started $DR_HTTPD_NPORTS instances" ;; 180 | 2) [ "$VERBOSE" != no ] && log_action_msg "Failed to start all $DR_HTTPD_NPORTS instances" ;; 181 | esac 182 | ;; 183 | stop) 184 | [ "$VERBOSE" != no ] && log_action_msg "Stopping $DESC" 185 | do_stop 186 | case "$?" in 187 | 0|1) [ "$VERBOSE" != no ] && log_action_msg "Stopped $DR_HTTPD_NPORTS instances" ;; 188 | 2) [ "$VERBOSE" != no ] && log_action_msg "Failed to stop all $DR_HTTPD_NPORTS instances" ;; 189 | esac 190 | ;; 191 | restart|force-reload) 192 | [ "$VERBOSE" != no ] && log_action_msg "Restarting $DESC" 193 | do_restart 194 | case "$?" in 195 | 0|1) [ "$VERBOSE" != no ] && log_action_msg "Restarted $DR_HTTPD_NPORTS instances" ;; 196 | 2) [ "$VERBOSE" != no ] && log_action_msg "Failed to restart all $DR_HTTPD_NPORTS instances" ;; 197 | esac 198 | ;; 199 | reload) 200 | [ "$VERBOSE" != no ] && log_action_msg "Reloading $DESC" 201 | do_reload 202 | case "$?" in 203 | 0|1) [ "$VERBOSE" != no ] && log_action_msg "Reloaded $DR_HTTPD_NPORTS instances" ;; 204 | 2) [ "$VERBOSE" != no ] && log_action_msg "Failed to reload all $DR_HTTPD_NPORTS instances" ;; 205 | esac 206 | ;; 207 | *) 208 | echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 209 | exit 3 210 | ;; 211 | esac 212 | 213 | : 214 | -------------------------------------------------------------------------------- /client/about/terms.html: -------------------------------------------------------------------------------- 1 | 4 | 41 |
42 | 43 |

Terms of service

44 | 45 |

46 | By accessing the Dropular.net website ("Site") or using the services offered by Dropular.net ("Services") you agree and acknowledge to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms or to our Privacy Policy, please do not access the Site or use the Services. Dropular.net reserves the right to change these Terms at any time. We recommend that you periodically check this Site for changes. 47 |

48 | 49 |

User content posted on the site

50 |

51 | You own all of the content and information you post on Dropular.net. In addition, for content that is covered by intellectual property rights, like photos, you specifically give us the following permission: you grant us a non-exclusive, transferable, sub-licensable, royalty-free, worldwide license to use any IP content that you post on or in connection with Dropular.net ("IP License"). This IP License ends when you delete your IP content or your account. 52 |

53 | 54 |

Prohibited uses

55 |

56 | You may not use the Dropular.net site and or its services to transmit any content which: 57 |

58 | 59 |
    60 |
  1. harasses, threatens, embarrasses or causes distress, unwanted attention or discomfort upon any other person,
  2. 61 |
  3. includes sexually explicit images or other content which is offensive or harmful to minors,
  4. 62 |
  5. includes any unlawful, harmful, threatening, abusive, harassing, defamatory, vulgar, obscene, or hateful material, including but not limited to material based on a person's race, national origin, ethnicity, religion, gender, sexual orientation, disablement or other such affiliation,
  6. 63 |
  7. impersonates any person or the appearance or voice of any person,
  8. 64 |
  9. utilizes a false name or identity or a name or identity that you are not entitled or authorized to use,
  10. 65 |
  11. contains any unsolicited advertising, promotional materials, or other forms of solicitation,
  12. 66 | 67 |
  13. contravenes any application law or government regulation,
  14. 68 |
  15. violates any operating rule, regulation, procedure, policy or guideline of Dropular.net as published on the Dropular.net website,
  16. 69 |
  17. may infringe the intellectual property rights or other rights of third parties, including trademark, copyright, trade secret, patent, publicity right, or privacy right, or
  18. 70 |
  19. distributes software or other Content in violation of any license agreement.
  20. 71 |
72 | 73 |

Use of content on external sites

74 |

75 | 76 | You understand that if you use a Dropular.net photo on an external web site, then the photo must link back to its look detail page on Dropular.net. 77 |

78 | 79 | 80 |

No warranty and limitation of liability

81 |

82 | Dropular.net PROVIDES THE SITE AND SERVICES "AS IS" AND WITHOUT ANY WARRANTY OR CONDITION, EXPRESS, IMPLIED OR STATUTORY. Dropular.net SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, INFORMATION ACCURACY, INTEGRATION, INTEROPERABILITY OR QUIET ENJOYMENT. Some states do not allow the disclaimer of implied warranties, so the foregoing disclaimer may not apply to you. 83 |

84 |

85 | You understand and agree that you use the Site and Services at your own discretion and risk and that you will be solely responsible for any damages that arise from such use. UNDER NO CIRCUMSTANCES SHALL Dropular.net BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES OF ANY KIND, OR ANY OTHER DAMAGES WHATSOEVER (HOWEVER ARISING, INCLUDING BY NEGLIGENCE), INCLUDING WITHOUT LIMITATION, DAMAGES RELATED TO USE, MISUSE, RELIANCE ON, INABILITY TO USE AND INTERRUPTION, SUSPENSION, OR TERMINATION OF THE SITE OR SERVICES, DAMAGES INCURRED THROUGH ANY LINKS PROVIDED ON THE SITE AND THE NONPERFORMANCE THEREOF AND DAMAGES RESULTING FROM LOSS OF USE, SALES, DATA, GOODWILL OR PROFITS, WHETHER OR NOT Dropular.net HAS BEEN ADVISED OF SUCH POSSIBILITY. YOUR ONLY RIGHT WITH RESPECT TO ANY DISSATISFACTION WITH THIS SITE OR SERVICES OR WITH Dropular.net SHALL BE TO TERMINATE USE OF THIS SITE AND SERVICES. Some states do not allow the exclusion of liability for incidental or consequential damages, so the above exclusions may not apply to you. 86 |

87 | 88 |

Other

89 | 90 |

91 | Dropular.net, in its sole discretion, may terminate your membership and remove and discard any information associated with the membership with or without notice. Dropular.net will not be liable to you for termination of your membership to the Service. 92 |

93 |

94 | Dropular.net, in its sole discretion, may delete any of the content posted to the Site and remove and discard any information associated with the content with or without notice. Dropular.net will not be liable to you for deletion of the images. 95 |

96 |

97 | Dropular.net reserves the right at any time and from time to time to modify or discontinue, temporarily or permanently, the Service (or any part thereof) with or without notice. You agree that Dropular.net shall not be liable to you or to any third party for any modification, suspension or discontinuance of the Service. 98 |

99 | Dropular.net Terms of Service as noted above may be updated by us from time to time without notice to you. In addition, when using particular Dropular.net owned or operated services, you and Dropular.net shall be subject to any posted guidelines or rules applicable to such services, which may be posted from time to time. 100 |

101 | 102 |
103 | -------------------------------------------------------------------------------- /client/drop/index.html: -------------------------------------------------------------------------------- 1 | 141 | 173 |
174 |
175 |

Drop image

176 |
177 |

178 |

179 |
180 | 181 | 182 | 183 | 184 | 185 |

186 |
187 |
188 |

Dropped

189 |

Go to the drop

190 |
191 |
192 |

Thanks, but you've already dropped this

193 |

Go to the drop

194 |
195 |
196 | 197 |
198 |

Not logged in

199 |

200 | You need to log in to drop something. 201 |

202 |
203 | 204 |
205 |

Sorry

206 |

207 | You can not create drops. Dropping is by invitation only. 208 |

209 |
210 |
-------------------------------------------------------------------------------- /lib/aws/httputil.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | http = require('http'), 3 | querystring = require('querystring'), 4 | base64 = require('./base64'), 5 | pool = require('./pool'); 6 | 7 | // ----------------------------------------------------------------------------- 8 | // connection pools keyed by "host:port:secure?" 9 | 10 | var connectionPools = {}, 11 | connectionPoolDefaults = { 12 | limit: 250, 13 | keepalive: 1 14 | }; 15 | 16 | exports.getConnectionPool = function(host, port, secure) { 17 | var key = host+':'+port+(secure ? ':1' : ':0'), 18 | p = connectionPools[key]; 19 | if (!p) { 20 | p = new pool.HTTPConnectionPool( 21 | connectionPoolDefaults.keepalive, 22 | connectionPoolDefaults.limit, 23 | port, host, secure); 24 | connectionPools[key] = p; 25 | } 26 | return p; 27 | } 28 | 29 | // ----------------------------------------------------------------------------- 30 | // Make a request 31 | 32 | /** 33 | * Make a request 34 | * 35 | * Returns an object emitting the following events: 36 | * 37 | * - connection(options, connection) -- when a connection has been made. 38 | * 39 | * - request(options, request) -- when a request is just about to be sent. 40 | * 41 | * - timeout(options, request) -- when a request timed out (only possible if 42 | * options.timeout is a number and > 0). 43 | * 44 | * - response(options, request, response) -- when a response has started. 45 | * 46 | */ 47 | exports.request = function(options, callback) { 48 | var timeoutId, req, res, cbFired, k, x, ev = new process.EventEmitter(); 49 | // Options 50 | var opt = { 51 | method: 'GET', 52 | host: '127.0.0.1', 53 | port: 80, 54 | path: '/', 55 | headers: {}, 56 | // Optional: 57 | // ctxid: 'myapp' 58 | // path: "/some/path" 59 | // query: {param1:"value1"} 60 | // body: "hello" 61 | // timeout: 5000 62 | // debug: true 63 | // auth: {type:"basic", username:"jdoe", password:"secret"} 64 | // secure: {ca_certs:x, crl_list:x, private_key:x, certificate:x} 65 | // connectionPool: 66 | } 67 | 68 | // parse arguments 69 | if (typeof options === 'string') opt.path = options; 70 | else if (typeof options !== 'object') throw new Error('options must be a string or an object'); 71 | else for (k in options) opt[k] = options[k]; 72 | if (callback && typeof callback !== 'function') 73 | throw new Error('callback must be a function'); 74 | opt.method = opt.method.toUpperCase(); 75 | 76 | // Context id? 77 | if (!opt.ctxid) opt.ctxid = module.id; 78 | 79 | // Connection 80 | if (!opt.connectionPool) 81 | opt.connectionPool = exports.getConnectionPool(opt.host, opt.port || 80, opt.secure); 82 | 83 | // Normalize headers (so we can easily override them) 84 | if (typeof opt.headers === 'object') { 85 | x = {}; 86 | for (k in opt.headers) 87 | x[k.toLowerCase()] = opt.headers[k]; 88 | opt.headers = x; 89 | } else { 90 | opt.headers = {}; 91 | } 92 | 93 | // Auth? 94 | if (opt.auth) { 95 | var authType = opt.auth.type || 'basic'; 96 | if (authType !== 'basic') 97 | throw new Error('only "basic" auth.type is supported'); 98 | opt.headers.authorization = 'Basic '+ 99 | base64.encode(opt.auth.username+":"+opt.auth.password); 100 | } 101 | 102 | // Setup headers 103 | if (!opt.headers.host) 104 | opt.headers.host = opt.host; 105 | if (!opt.headers.connection) 106 | opt.headers.connection = (opt.connectionPool.keep < 1) ? 'Close' : 'Keep-Alive'; 107 | 108 | // Setup path 109 | if (typeof opt.path !== 'string') throw new Error('path option must be a string'); 110 | opt.path = '/'+String(opt.path).replace(/^\/+/, ''); 111 | if (opt.query) { 112 | if (typeof opt.query === 'object') { 113 | k = querystring.stringify(opt.query); 114 | if (k.length) 115 | opt.path += '?' + k; 116 | } else if (typeof opt.query === 'string' && opt.query.length !== 0) { 117 | opt.path += '?' + opt.query; 118 | } 119 | } 120 | 121 | // Setup body 122 | if (opt.body && (opt.method === 'PUT' || opt.method === 'POST')) { 123 | if (typeof opt.body !== 'string') { 124 | opt.body = JSON.stringify(opt.body); 125 | opt.headers['content-type'] = 'application/json'; 126 | opt.bodyEncoding = 'utf-8'; 127 | } 128 | opt.headers['content-length'] = opt.body.length; 129 | } else if (opt.body) { 130 | opt.body = undefined; 131 | } 132 | if (!opt.bodyEncoding) 133 | opt.bodyEncoding = 'binary'; 134 | 135 | // Defer until we have a connection 136 | var onconn = function(err, conn) { 137 | ev.emit('connection', opt, conn); 138 | if (err) return (!cbFired) && (cbFired = 1) && callback && callback(err); 139 | var onConnClose = function(hadError, reason) { 140 | if (hadError && callback && !cbFired) { 141 | cbFired = true; callback(new Error(reason || 'Connection error')); 142 | } 143 | conn.removeListener('close', onConnClose); 144 | } 145 | conn.addListener('close', onConnClose); 146 | req = conn.request(opt.method, opt.path, opt.headers); 147 | if (opt.debug) { 148 | sys.log('['+opt.ctxid+'] --> '+opt.method+' '+opt.path+'\n '+ 149 | Object.keys(opt.headers).map(function(k){ return k+': '+opt.headers[k]; }).join('\n ')+ 150 | (opt.body ? '\n\n '+opt.body : '')); 151 | } 152 | if (opt.body) { 153 | req.write(opt.body, opt.bodyEncoding); 154 | } 155 | req.addListener('response', function (_res) { 156 | res = _res; 157 | var data = ''; 158 | ev.emit('response', opt, req, res); 159 | //res.setBodyEncoding(opt.bodyEncoding); // why does this fail? 160 | res.addListener('data', function (chunk){ 161 | data += chunk; 162 | }); 163 | res.addListener('end', function(){ 164 | if (timeoutId !== undefined) clearTimeout(timeoutId); 165 | conn.removeListener('close', onConnClose); 166 | opt.connectionPool.put(conn); 167 | // log 168 | if (opt.debug) { 169 | sys.log('['+opt.ctxid+'] <-- '+opt.method+' '+opt.path+' ['+res.statusCode+']\n '+ 170 | Object.keys(res.headers).map(function(k){ return k+': '+res.headers[k]; }).join('\n ')+ 171 | (data.length ? '\n\n '+data : '')); 172 | } 173 | // parse body if it's json 174 | var contentType = res.headers['content-type']; 175 | if (contentType && contentType.indexOf('application/json') !== -1) { 176 | try { 177 | data = JSON.parse(data); 178 | } catch (err) { 179 | if (!cbFired) { 180 | err.message = 'JSON parse error: '+err.message+'. Input was: '+sys.inspect(data); 181 | cbFired = 1; 182 | if (callback) callback(err, undefined, res); 183 | } 184 | } 185 | } 186 | // check and handle result 187 | if (!cbFired) { 188 | cbFired = 1; 189 | if (callback) callback(null, data, res); 190 | } 191 | }); 192 | }); 193 | ev.emit('request', opt, req); 194 | req.end(); 195 | } 196 | 197 | // Timeout 198 | opt.timeout = opt.timeout; 199 | if (typeof opt.timeout === 'number' && opt.timeout > 0) { 200 | timeoutId = setTimeout(function(){ 201 | opt.connectionPool.cancelGet(onconn); 202 | if (req) req.removeAllListeners('response'); 203 | if (res) { 204 | res.removeAllListeners('data'); 205 | res.removeAllListeners('end'); 206 | } 207 | ev.emit('timeout', opt, req); 208 | if (opt.debug) { 209 | sys.log('['+opt.ctxid+'] --X '+opt.method+' '+opt.path+' timed out after '+ 210 | (opt.timeout/1000.0)+' seconds'); 211 | } 212 | if (!cbFired) { 213 | cbFired = 1; 214 | if (callback) callback(new Error( 215 | req ? 'CounchDB connection timeout' : 'CouchDB connection pool timeout')); 216 | } 217 | }, opt.timeout); 218 | } 219 | 220 | // Request a connection from the pool 221 | process.nextTick(function(){ 222 | // next tick so the caller is guaranteed to be able to add listeners to ev 223 | opt.connectionPool.get(onconn); 224 | }); 225 | 226 | // return the EventEmitter 227 | return ev; 228 | } 229 | -------------------------------------------------------------------------------- /lib/dropular/drop.js: -------------------------------------------------------------------------------- 1 | // drop entitiy 2 | var sys = require('sys'), 3 | querystring = require("querystring"), 4 | path = require('path'), 5 | http = require('http'), 6 | hash = require('oui/hash'), 7 | ouiutil = require('oui/util'), 8 | mimetypes = require('oui/mimetypes'), 9 | _tag = require('./tag'), 10 | User = require('./user').User, 11 | config = require('./config'); 12 | 13 | 14 | function mkerr(message, statusCode, type) { 15 | var err = new Error(message); 16 | err.statusCode = statusCode; 17 | if (type) err.type = type; 18 | return err; 19 | } 20 | 21 | var Drop = exports.Drop = function(){}; 22 | 23 | const imageExts = {'.jpg':'image/jpeg', '.jpeg':'image/jpeg', '.png':'image/png', '.gif':'image/gif'}; 24 | const s3baseURL = 'http://BUCKET.s3.amazonaws.com'; 25 | const s3baseKey = '/drops/images/'; 26 | 27 | mixin(Drop.prototype, { 28 | get documentRepresentation() { 29 | var key, doc = {}; 30 | for (key in this) { 31 | if (!(key in Drop.prototype)) doc[key] = this[key]; 32 | } 33 | return doc; 34 | }, 35 | 36 | get publicRepresentation() { 37 | return this.documentRepresentation; 38 | }, 39 | 40 | save: function(callback) { 41 | return Drop.put(this.documentRepresentation, callback); 42 | }, 43 | 44 | get originalImageURL() { 45 | return Drop.largeImageURLFromId(this._id || this.id, this.url); 46 | }, 47 | 48 | get originalImageKey() { 49 | return Drop.imageKeyFromId(this._id || this.id, this.url); 50 | } 51 | }); 52 | 53 | 54 | mixin(Drop, { 55 | // Mixin Drop.prototype into object doc (e.g. which have been returned from the 56 | // database). 57 | fromDocument: function(doc) { 58 | mixin(doc, Drop.prototype); 59 | return doc; 60 | }, 61 | 62 | /** 63 | * @param creatorUser Can be either a user object or a username String. 64 | */ 65 | fromUserInput: function(params, creatorUser, callback) { 66 | /* 67 | A drop document looks like this: 68 | 69 | "f4M5oRKeBveKONIRlU1uPezaBmX" => { 70 | "url": "http://bar.com/finger/fashion/fingertip-shoes.jpg", 71 | "origin": "http://www.foundshit.com/finger-tip-shoe-fashion/", 72 | "tags": ["shoes", "fashion"], 73 | "title": "Finger Tip Fashion \u00bb Funny, Bizarre, Amazing Pictures", 74 | "desc": "Crazy little shoe for fingers", 75 | "disabled": true, 76 | "nsfw": true, 77 | "users": { 78 | "foo": [1258195680123, 2], 79 | "someuser": [1258195681123, 1], 80 | "mrtroll": [1258195682123, 1] 81 | } 82 | } 83 | */ 84 | 85 | // username 86 | var canonicalUsername = User.canonicalUsername(creatorUser); 87 | if (!canonicalUsername) { 88 | return callback(mkerr( 89 | 'Bad argument "creatorUser" passed to Drop.fromUserInput', 400)); 90 | } 91 | 92 | // the new drop object 93 | var drop = new Drop(); 94 | 95 | // sanitize input 96 | var err = ouiutil.sanitizeInput(params, drop, { 97 | url: {type:'url', required:true}, 98 | origin: {type:'url'}, 99 | tags: {type:'array', filter:_tag.canonicalize}, 100 | title: {type:'string'}, 101 | desc: {type:'string'}, 102 | nsfw: {type:'boolean'} 103 | }); 104 | if (err) return callback(err); 105 | 106 | // calculate id 107 | drop._id = hash.sha1(drop.url, 62); 108 | 109 | // fetch any previous version 110 | Drop.find(drop._id, function(err, prevDrop, cdres) { 111 | // TODO: repeat this if there are conflicts when PUT-ing (should be 112 | // implemented in the couchdb module using Object.merge3) 113 | var modified = true, isnew = false, err2; 114 | if (err) { 115 | sys.p(cdres); 116 | return callback(err); 117 | } 118 | drop.users = {}; 119 | if (prevDrop) { 120 | // merge 121 | if (!prevDrop.users || !(canonicalUsername in prevDrop.users)) 122 | drop.users[canonicalUsername] = [Date.currentUTCTimestamp, 1]; 123 | var a = prevDrop.documentRepresentation, 124 | b = drop.documentRepresentation, 125 | m3; 126 | // make sure input can not override some existing, already set properties 127 | (['origin', 'title', 'desc', 'nsfw']).forEach(function(n){ 128 | if (n in a && n in b) delete b[n]; 129 | }); 130 | // 3-way merge 131 | m3 = Object.merge3(a, b, a); 132 | //sys.debug('merge3 => '+sys.inspect(m3, false, 10)); 133 | // error on conflicts 134 | if (m3.conflicts) { 135 | err2 = new Error('Conflicting edits. Please retry.'); 136 | err2.statusCode = 409; 137 | drop = undefined; 138 | } else { 139 | drop = Drop.fromDocument(m3.merged); 140 | modified = (m3.added || m3.updated); 141 | } 142 | } else { 143 | // new drop 144 | // TODO: apply user-specific score when we have such 145 | drop.users[canonicalUsername] = [Date.currentUTCTimestamp, 2]; 146 | isnew = true; 147 | } 148 | // the drop is complete 149 | callback(err2, drop, modified, isnew); 150 | }); 151 | }, 152 | 153 | imageKeyFromId: function(id, deduceFileExtFromURLOrFileExt) { 154 | var ext = '.jpg'; 155 | if (deduceFileExtFromURLOrFileExt) { 156 | if (deduceFileExtFromURLOrFileExt.charAt(0) === '.') { 157 | ext = deduceFileExtFromURLOrFileExt; 158 | } else { 159 | // TODO: parse url, or at least parse away ?xyz=123 etc 160 | ext = path.extname(deduceFileExtFromURLOrFileExt, '.jpg'); 161 | } 162 | } 163 | ext = ext.toLowerCase(); 164 | if (!(ext in imageExts)) ext = '.jpg'; 165 | return s3baseKey + id.charAt(0)+'/'+id.substr(1,2)+'/'+id.substr(3) + ext; 166 | }, 167 | 168 | largeImageURLFromId: function(id, deduceFileExtFromURLOrFileExt) { 169 | return s3baseURL + Drop.imageKeyFromId(id, deduceFileExtFromURLOrFileExt); 170 | }, 171 | 172 | uploadImageFromURL: function(url, id, callback) { 173 | sys.log('[drop s3] fetching '+url); 174 | http.cat(url, "binary", function (err, contentBody) { 175 | if (err) return callback(err); 176 | var p, key = Drop.imageKeyFromId(id, url), 177 | fileext = path.extname(key), 178 | contentType = imageExts[fileext], 179 | s3bucket = config.s3.static, 180 | s3url = s3bucket.urlTo(key); 181 | sys.log('[drop s3] uploading '+s3url); 182 | //var b = new require('buffer').Buffer(contentBody.length); 183 | //b.write(contentBody, 'binary', 0); 184 | s3bucket.put(key, contentBody, contentType, function(err, data, res){ 185 | if (!err && (res.statusCode < 200 || res.statusCode >= 300)) { 186 | // TODO parse error message 187 | err = new Error('AWS responded with '+res.statusCode); 188 | } 189 | if (!err) { 190 | sys.log("[drop s3] upload to s3 finished: "+s3url+ 191 | ' (http status '+res.statusCode+')'); 192 | } else { 193 | sys.log("[drop s3] upload to s3 FAILED: "+(err.stack || err)+'\n'+data); 194 | } 195 | if (callback) callback(err, key, contentBody, fileext.substr(1)); 196 | }); 197 | }); 198 | }, 199 | 200 | // Find a drop by its id 201 | find: function(id, asDropObject, callback) { 202 | if (typeof asDropObject === 'function') { 203 | callback = asDropObject; 204 | asDropObject = undefined; 205 | } 206 | if (asDropObject === undefined) asDropObject = true; 207 | config.db.drops.get(querystring.escape(id), function(err, doc, res) { 208 | var drop; 209 | if (err) { 210 | if (res.statusCode === 404) err = undefined; 211 | } else if (doc) { 212 | drop = asDropObject ? Drop.fromDocument(doc) : doc; 213 | } 214 | callback(err, drop, res); 215 | }); 216 | }, 217 | 218 | put: function(drop, callback) { 219 | config.db.drops.put(drop._id, drop, callback); 220 | } 221 | }); 222 | 223 | mixin(Drop.prototype, { 224 | toJSON: function() { 225 | var key, obj = {}; 226 | for (key in this) { 227 | if (!(key in Drop.prototype)) obj[key] = this[key]; 228 | } 229 | return obj; 230 | }, 231 | }); 232 | 233 | // Used by Drop.prototype.publicRepresentation 234 | Drop.hiddenMembers = Object.keys(Drop.prototype); 235 | -------------------------------------------------------------------------------- /misc/offline/drop-popularity.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), puts = sys.puts, 2 | couchdb = require('../../lib/couchdb'); 3 | 4 | // 'http://127.0.0.1:5984/dropular-drops/_design/offline/_view/drop-popularity?group=true' 5 | 6 | var doc = { 7 | "_id": "zuogmAnKkYukKptwCQoIExYsksT", 8 | "_rev": "1-e7ce58de20b859b98c91651b1c46c9d7", 9 | "url": "http://retrospace.tumblr.com/page/23", 10 | "users": { 11 | "chrismeisner": [ 12 | 1251418913002, 13 | 1 14 | ], 15 | "jamesplankton": [ 16 | 1251418913013, 17 | 1 18 | ], 19 | "joshuabrantley": [ 20 | 1251418913003, 21 | 1 22 | ], 23 | "mildenstein": [ 24 | 1251418913020, 25 | 1 26 | ], 27 | "nitzan": [ 28 | 1251418913010, 29 | 1 30 | ], 31 | "elnitro": [ 32 | 1251418913025, 33 | 1 34 | ], 35 | "hallisar": [ 36 | 1251418913016, 37 | 1 38 | ], 39 | "zeroversion": [ 40 | 1251418913000, 41 | 2 42 | ], 43 | "landock": [ 44 | 1251418913019, 45 | 1 46 | ], 47 | "deucedly": [ 48 | 1251418913005, 49 | 1 50 | ], 51 | "mortenmarius": [ 52 | 1251418913026, 53 | 1 54 | ], 55 | "ericson": [ 56 | 1251418913023, 57 | 1 58 | ], 59 | "thonk": [ 60 | 1251418913006, 61 | 1 62 | ], 63 | "digz": [ 64 | 1251418913017, 65 | 1 66 | ], 67 | "okapi": [ 68 | 1251418913012, 69 | 1 70 | ], 71 | "stefan": [ 72 | 1251518913007, 73 | 1 74 | ], 75 | "dr_kab": [ 76 | 1251418913024, 77 | 1 78 | ], 79 | "karollorak": [ 80 | 1251418913004, 81 | 1 82 | ], 83 | "connaisseur": [ 84 | 1251418513022, 85 | 1 86 | ], 87 | "sydeffex": [ 88 | 1251418913018, 89 | 1 90 | ], 91 | "horizonfire": [ 92 | 1251418913015, 93 | 1 94 | ], 95 | "zerospace1984": [ 96 | 1251418913021, 97 | 1 98 | ], 99 | "falcadia": [ 100 | 1251418913001, 101 | 1 102 | ], 103 | "goreki": [ 104 | 1251419913009, 105 | 1 106 | ], 107 | "imadesigner": [ 108 | 1251428913011, 109 | 1 110 | ], 111 | "ttwice": [ 112 | 1251448913028, 113 | 1 114 | ], 115 | "proclaim": [ 116 | 1251418913029, 117 | 1 118 | ], 119 | "serf": [ 120 | 1251418918008, 121 | 1 122 | ], 123 | "henry": [ 124 | 1251418913014, 125 | 1 126 | ], 127 | "restate": [ 128 | 1251418913027, 129 | 1 130 | ] 131 | }, 132 | "origin": "http://retrospace.tumblr.com/page/23", 133 | "tags": [ 134 | "celebrity", 135 | "yearbook", 136 | "photography" 137 | ], 138 | "title": "Retrospace Zeta" 139 | }; 140 | 141 | var lowRankingDoc = {_id: "2CEMEKQ6C8o9nwFLy2wJg36lz0q", 142 | _rev: "1-a6b4339dda56e2979b1fccceac4ad3a0", 143 | url: "http://www.modcloth.com/store/ModCloth/Apartment/Decor/Kitchen+Bath/Bear+Bottle+Opener", users: { 144 | igotsmagicpants: [1251474121000, 2], rwquigley: [1251474121001, 1], rwquiglex: [1251474121002, 1] 145 | }, origin: "http://www.modcloth.com/store/ModCloth/Apartment/Decor/Kitchen+Bath/Bear+Bottle+Opener", tags: ["bear", "animals", "bottles", "bottle opener", "modcloth", "wants", "home", "decor", "kitsch", "wants"], title: "Bear Bottle Opener-Mod Retro Indie Clothing & Vintage Clothes"} 146 | 147 | var mediumRankingDoc = { 148 | "_id": "zwFSuj8V7vznBHfsY4YuE0Sv6Ux", 149 | "_rev": "1-80524382c0b05a5963ff6e8444d50da1", 150 | "url": "http://www.boston.com/bigpicture/2009/01/more_of_london_from_above_at_n.html", 151 | "users": { 152 | "markr": [1270012955001, 2], 153 | "abc": [1251512945000, 1] 154 | }, 155 | "origin": "http://www.boston.com/bigpicture/2009/01/more_of_london_from_above_at_n.html", 156 | "tags": [ 157 | "ferris", 158 | "eye", 159 | "london" 160 | ], 161 | "title": "More of London from above, at night - The Big Picture - Boston.com" 162 | } 163 | 164 | var newDrop = {_id: "yPHfe47t0QyIP2EQrzzcr3Yvju2", _rev: "1-4edc72543958ec0ae9af22e62368be2e", 165 | url: "http://farm3.static.flickr.com/2735/4486171859_3da7759a55_t.jpg", users: { 166 | rsms: [1270983496533, 2]}}; 167 | 168 | /* 169 | timeDecayEffect = 170 | ---- 171 | 1.0: 172 | 2199 173 | 234165 174 | 217 175 | 0.9999: 176 | 3389 177 | 240455 178 | 335 179 | 0.999: 180 | 14091 181 | 297061 182 | 1398 183 | 0.99: 184 | 121118 185 | 863122 186 | 12025 187 | 0.9: 188 | 1191387 189 | 6523735 190 | 118294 191 | 0: 192 | 11894071 193 | 63129867 194 | 1180979 195 | */ 196 | 197 | //nowTime = 1270983496533; 198 | var emit = function(key, value){ puts(key+' => '+JSON.stringify(value)+'\n-------------'); }; 199 | var mapfun = 200 | 201 | // --------- BEGIN ----------- 202 | function(doc) { 203 | // oldest drop 2009-01-25T10:42:21.000Z 204 | var refTime = 1232883741000, 205 | nowDate = new Date(), 206 | timeDecayEffect = 0.999, 207 | loneDropPunishment = 4.0, // higher = lower score 208 | decayBase = 60*60*1000, // 1 hour 209 | nowTime = nowDate.getTime()+(nowDate.getTimezoneOffset()*60*1000); 210 | 211 | function applyTimeDecay(score, time, effect) { 212 | if (effect === 0) return score; 213 | var decay, nscore = score, delta = nowTime-time; 214 | if (delta > decayBase) { 215 | decay = delta/decayBase; 216 | nscore /= decay; 217 | //puts('score2: '+score+', D: '+delta+', decay: '+decay) 218 | } else if (delta > 1000) { 219 | decay = delta/decayBase; 220 | nscore /= 1.0+decay; 221 | //puts('score2: '+score+', D: '+delta+', decay: '+decay) 222 | } else { 223 | //puts('score2: '+score+', D: '+delta+', decay: -') 224 | } 225 | if (effect === 1.0) 226 | return nscore; 227 | return score + ((nscore - score) * effect); 228 | } 229 | 230 | function calculateDropScore(doc) { 231 | var user, timeCreated, 232 | userdrops = [], 233 | endscoreDivisor = 45000; 234 | // first, remap userdrops to ordered list 235 | for (user in doc.users) { 236 | tuple = doc.users[user]; 237 | userdrops.push({username:user, time:tuple[0], score:tuple[1]}); 238 | } 239 | 240 | // special case for empty or single drops 241 | if (userdrops.length === 0) { 242 | return 0.0; 243 | } else { 244 | userdrops.sort(function(a, b){ return b.time - a.time; }); 245 | timeCreated = userdrops[0].time; 246 | if (userdrops.length === 1) { 247 | score = (timeCreated - refTime)/(endscoreDivisor*loneDropPunishment); 248 | score = applyTimeDecay(score, timeCreated, timeDecayEffect); 249 | return score / (loneDropPunishment/2); 250 | } 251 | } 252 | 253 | var t, x, y, i, td, score = 0, 254 | tdpowerPunish = 1.5, tdpowerPraise = 1000*30; 255 | 256 | t = timeCreated - refTime; 257 | d = nowTime - timeCreated; 258 | x = 0; 259 | td = 0; 260 | 261 | //puts(d+', '+(t/d) + ', '+ (d/t)); 262 | t *= t/d; 263 | 264 | for (i in userdrops) { 265 | x += userdrops[i].score; 266 | if (i > 0) 267 | td = userdrops[i].time - userdrops[i-1].time; 268 | //sys.error('td '+td) 269 | if (td > 0) { 270 | if (td < userdrops.length) { 271 | // punish sequential drops 272 | t /= td*tdpowerPunish; 273 | } else { 274 | // praise 275 | t += td*tdpowerPraise; 276 | } 277 | } 278 | } 279 | 280 | if (x > 0) y = 1; 281 | else if (x === 0) y = 0; 282 | else y = -1; 283 | 284 | if (userdrops.length === 1) { 285 | t *= 0.5; // demote new drops to 50% 286 | } else if (userdrops.length) { 287 | t *= userdrops.length; 288 | } 289 | 290 | z = (Math.abs(x) >=1 && Math.abs(x) || 1); 291 | score = Math.log(z) + (y*t)/endscoreDivisor; 292 | 293 | score = applyTimeDecay(score, timeCreated, timeDecayEffect); 294 | 295 | return score; 296 | } 297 | emit(calculateDropScore(doc), doc); 298 | } 299 | // --------- END ----------- 300 | 301 | mapfun(doc); 302 | mapfun(mediumRankingDoc); 303 | mapfun(lowRankingDoc); 304 | mapfun(newDrop); 305 | 306 | 307 | 308 | var 309 | tuple, score = 0, 310 | prevTime, timeDelta = 0, 311 | massCounterWeight = 0, 312 | refTime = 1232883741000, 313 | set = [] 314 | 315 | for (user in doc.users) { 316 | tuple = doc.users[user]; 317 | set.push({username:user, time:tuple[0], score:tuple[1]}); 318 | } 319 | 320 | set.sort(function(a, b){ return a.time - b.time; }); 321 | massCounterWeight = set.length * 0.5; 322 | 323 | /*set.forEach(function(o) { 324 | if (prevTime) 325 | timeDelta = o.time - prevTime; 326 | prevTime = o.time; 327 | timeDelta = timeDelta - refTime; 328 | sys.p(timeDelta); 329 | score += o.score; 330 | // slightly demote mass droppings 331 | if (massCounterWeight > 0) 332 | score /= massCounterWeight; 333 | sys.puts('score -> '+score); 334 | });*/ 335 | 336 | process.exit(0); 337 | 338 | var db = new couchdb.Db('dropular-drops'); 339 | db.get('_design/offline/_view/drop-popularity?group=true', function(err, r, res) { 340 | if (err) return sys.error(err.stack || err); 341 | r=r.rows; 342 | r.sort(function(a,b){ return b.value - a.value; }); 343 | r = r.map(function(x){ return [x.value, x.key]; }); 344 | sys.p(r); 345 | }) 346 | 347 | -------------------------------------------------------------------------------- /client/drops/index.js: -------------------------------------------------------------------------------- 1 | exports.on('load', function(ev, view){ 2 | console.log('drops loaded', view); 3 | }); 4 | 5 | function basename(fn) { 6 | var p = fn.lastIndexOf('/'); 7 | if (p !== -1) fn = fn.substr(p+1); 8 | return fn; 9 | } 10 | function extname(fn, _default) { 11 | if (fn && (fn = String(fn)) && fn.length) { 12 | fn = basename(fn); 13 | var p = fn.lastIndexOf('.'); 14 | if (p !== -1) return fn.substr(p); 15 | } 16 | return _default; 17 | } 18 | exports.dropURLfromId = function(id, url, requestedSize, originalSize) { 19 | var base = 'http://static.dropular.net.s3.amazonaws.com/drops/images/'; 20 | var ext = extname(url, '.jpg'); 21 | var imageExts = {'.jpg':1, '.jpeg':1, '.png':1, '.gif':1}; 22 | if (!imageExts[ext.toLowerCase()]) ext = '.jpg'; 23 | 24 | var orgsize; 25 | if (typeof originalSize === 'object') { 26 | orgsize = Math.max(originalSize.width, originalSize.height); 27 | } else if (typeof originalSize === 'number') { 28 | orgsize = originalSize; 29 | } 30 | 31 | if (requestedSize && orgsize) { 32 | requestedSize = requestedSize.substr(0,1).toLowerCase(); 33 | if (requestedSize === 's') { 34 | if (orgsize > 256) ext = '.256.jpg'; 35 | } else if (requestedSize === 'm') { 36 | if (orgsize > 720) ext = '.720.jpg'; 37 | } 38 | } 39 | // otherwise the URL for the original is returned 40 | 41 | url = base + id.charAt(0)+'/'+id.substr(1,2)+'/'+id.substr(3)+ext; 42 | 43 | return url; 44 | }; 45 | 46 | function img_adjustVerticalAlignment() { 47 | var q = $(this); 48 | var thumbsize = 209;//parseInt(q.closest('drop').css('width'));//broken 49 | var w = this.width, h = this.height, r = w/h, z; 50 | console.log(h, w, r, 'loaded'); 51 | if (w > h) { 52 | z = w/thumbsize; 53 | w /= z; 54 | h /= z; 55 | q.css('marginTop', Math.round((thumbsize-h)/2)+'px'); 56 | } else { 57 | // no need for h-alignment as that is taken care of by CSS 58 | } 59 | } 60 | 61 | exports.createView = function(drops){ 62 | var i, drop, img, 63 | view = __html('content'), 64 | items = view.find('drops'); 65 | 66 | exports.appendDrops(items.empty(), drops); 67 | return view; 68 | 69 | }; 70 | 71 | exports.appendDrops = function(appendToElement, drops) { 72 | var i, drop, img, imgURL; 73 | for (i=0,drop; (drop = drops[i]); i++) { (function(){ 74 | 75 | if (drop.doc.disabled) return; 76 | var item = __html('drop'); 77 | 78 | $.each(drop.doc.users, function(index, value) { 79 | 80 | if (! oui.app.session.user || oui.app.session.user.username === index) { 81 | item.find('.droppy').css('display','none'); 82 | } 83 | }); 84 | 85 | /* 86 | var createdBy, created, user; 87 | for (user in drop.doc.users) { 88 | var t = drop.doc.users[user][0]; 89 | if (!created || t < created) { 90 | created = t; 91 | createdBy = user; 92 | } 93 | } 94 | */ 95 | 96 | imgURL = exports.dropURLfromId(drop.id, drop.doc.url, 'small', drop.doc.image); 97 | img = item.find('img').attr('src', imgURL); 98 | // align image vertically on load 99 | img.one('load', function(){ 100 | img_adjustVerticalAlignment.call(this); 101 | item.addClass('loaded'); 102 | 103 | }); 104 | 105 | item.find('a').attr('href', '#drops/'+drop.id); 106 | item.find('.droppy').attr('id', drop.doc.url); 107 | item.find('.droppy').attr('title', drop.doc.origin); 108 | 109 | var title; 110 | 111 | if (drop.doc.title) { 112 | title = drop.doc.title.replace( /http:\/\//, "").replace( /www./, "").substring(0,35); 113 | } else if (!title) { 114 | if (drop.doc.origin) { 115 | title = drop.doc.origin.replace( /http:\/\//, "").replace( /www./, "").substring(0,35); 116 | } else if (drop.doc.url) { 117 | title = drop.doc.url.replace( /http:\/\//, "").replace( /www./, "").substring(0,35); 118 | } else { 119 | title = "Untitled"; 120 | } 121 | } 122 | item.find('.info-wrapper').find('.from').text(title); 123 | 124 | if (drop.doc.origin) 125 | item.find('.info-wrapper').find('a.from').attr('href', drop.doc.origin); 126 | 127 | appendToElement.append(item); 128 | 129 | })(); } 130 | 131 | }; 132 | 133 | exports.build = function(url, params, callback) { 134 | if (typeof params === 'function') { 135 | callback = params; 136 | params = undefined; 137 | } 138 | var request, continuumFiller, continuumState = {}; 139 | var start = true; 140 | 141 | params.skip = params.skip ? parseInt(params.skip) : 0; 142 | params.limit = params.limit !== undefined ? parseInt(params.limit) : 10; 143 | 144 | continuumFiller = function(continuumCallback) { 145 | var $drops = continuumState.view.find('drops'); 146 | var throbber = new util.throbber.Throbber(); 147 | $drops.after(throbber.$html); 148 | 149 | throbber.show(); 150 | params.skip += params.limit; 151 | oui.app.session.get(url, params, function(err, result){ 152 | 153 | throbber.remove(); 154 | if (err) { 155 | util.notify.show(err); 156 | continuumCallback(true); // stop 157 | } else { 158 | if (!result || !result.drops || result.drops.length === 0) { 159 | continuumCallback(true); // stop 160 | } else { 161 | if (!start) { 162 | exports.appendDrops($drops, result.drops); 163 | continuumCallback(); 164 | // 165 | } 166 | } 167 | } 168 | }); 169 | }; 170 | 171 | $('more').live('click', function(err, result){ 172 | $(continuumState.view).continuum(continuumFiller); 173 | start = false; 174 | }); 175 | 176 | oui.app.session.get(url, params, function(err, result){ 177 | 178 | if (result.drops.length !== 18) { 179 | $('more').hide(); 180 | } else { 181 | $('more').show(); 182 | } 183 | 184 | console.log(url+' -->', err, result); 185 | if (!err) 186 | continuumState.view = exports.createView(result.drops); 187 | if (callback) { 188 | callback(err, continuumState.view, continuumFiller); 189 | if (!err && continuumFiller) 190 | $(continuumState.view).continuum(continuumFiller); 191 | } 192 | }); 193 | }; 194 | 195 | exports.display_ = function(url, params, callback){ 196 | exports.build(url, params, function(err, view, filler){ 197 | if (err) { 198 | error.present(err); 199 | } else { 200 | mainView.setView(view); 201 | } 202 | if (callback) callback(err); 203 | }); 204 | }; 205 | 206 | exports.displayRecent = function(callback){ 207 | exports.display_('drops/recent', {complete:1, limit:18}, callback); 208 | }; 209 | 210 | exports.displayInteresting = function(callback){ 211 | exports.display_('drops/interesting', {complete:1, limit:18}, callback); 212 | }; 213 | 214 | exports.displayTagged = function(tags, callback){ 215 | if (!tags) return callback && callback(new Error('bad tags param')); 216 | exports.display_('drops/tagged/'+oui.urlesc(tags.join('|')), 217 | {complete:1, limit:18}, callback); 218 | }; 219 | 220 | exports.displayFromUser_ = function(path, username, callback){ 221 | exports.display_('users/'+oui.urlesc(username)+'/'+path, 222 | {complete:1, limit:18}, callback); 223 | }; 224 | 225 | exports.displayFromFollowing = function(username, callback){ 226 | exports.displayFromUser_('following/drops', username, callback); 227 | }; 228 | 229 | exports.displayFromUser = function(username, callback){ 230 | exports.displayFromUser_('drops', username, callback); 231 | }; 232 | 233 | // handlers 234 | 235 | oui.app.on('start', function(){ 236 | 237 | var oldhit; 238 | 239 | $('drop').live('mouseenter', function(){ 240 | $(this).find('.info-wrapper').css('visibility','visible'); 241 | }).live('mouseleave', function(){ 242 | $(this).find('.info-wrapper').css('visibility','hidden'); 243 | }); 244 | 245 | $('#toolbox-wrap a').live('click', function(){ 246 | $(oldhit).removeClass('active'); 247 | $(this).addClass('active'); 248 | oldhit = this; 249 | }); 250 | 251 | 252 | 253 | $('.droppy').live('click', function(){ 254 | redropit(this.id, $(this).attr('title')); 255 | $(this).hide(); 256 | }); 257 | 258 | var redropit = function(u,org) { 259 | var msg = { 'url' : u, 'origin' : org }; 260 | oui.app.session.post('drop', msg, function(err, r, res){ 261 | }); 262 | }; 263 | 264 | function prepare(source) { 265 | var sourceClass = source || 'home'; 266 | // set active source "tab" 267 | __html().find('sources a').removeClass('active') 268 | .filter('a[href=#drops'+(source ? '/'+source : '')+']').addClass('active'); 269 | // show relevant title 270 | __html().find('div.title').hide().filter('.'+sourceClass).show(); 271 | } 272 | 273 | oui.anchor.on('drops', function(params, path, prevPath) { 274 | prepare(); 275 | if (oui.app.session.user) { 276 | console.log('drops: displaying recent'); 277 | exports.displayRecent(); 278 | } else { 279 | setTimeout(function(){ 280 | if (oui.app.session.user) { 281 | exports.displayRecent(); 282 | } else { 283 | exports.displayRecent(); 284 | //error.present({title:'Not found', message:'Drops...what?!'}); 285 | } 286 | }, 500); 287 | } 288 | }); 289 | 290 | oui.anchor.on('drops/follow', function(params, path, prevPath) { 291 | prepare('recent'); 292 | console.log('drops: displaying from followers'); 293 | exports.displayFromFollowing(oui.app.session.user.username); 294 | }); 295 | 296 | oui.anchor.on('drops/interesting', function(params, path, prevPath) { 297 | prepare('interesting'); 298 | console.log('drops: displaying interesting'); 299 | exports.displayInteresting(); 300 | }); 301 | 302 | oui.anchor.on('drops/tagged/:tags', function(params, path, prevPath) { 303 | prepare('tagged'); 304 | var tags = params.tags.split(/[+\/,]+/); 305 | console.log('drops: displaying tagged', tags); 306 | __html().find('div.title.tagged h1 .tags').text(tags.join(', ')); 307 | exports.displayTagged(tags); 308 | }); 309 | }); -------------------------------------------------------------------------------- /misc/data/README.md: -------------------------------------------------------------------------------- 1 | > **Note:** The following instructions assume you are running a local "out-of-the-box" couchdb server. 2 | 3 | # Design 4 | 5 | Dropular consists of two key-value databases: 6 | 7 | - **users** -- houses all info about users (username, email, following, etc) 8 | - **drops** -- houses all drops 9 | 10 | As a database can be atomically snapshotted, we need to keep any data which might cause problems when out of sync to live inside the same database. This poses some obvious problems (i.e. a user creating a drop), but by listing the databases in a prioritized list, we can agree on where to expect relations to break or data to be partial: 11 | 12 | 1. users 13 | 2. drops 14 | 15 | Now, when taking snapshots (replicating), just walk the list from **bottom up** -- the later the replication happens, the more complete the data of that database will be. 16 | 17 | > **Discussion:** To further explain why this is importnat, let's imagine we do the reverse -- snapshot users before we snapshot drops. First this happens: we take a snapshot of users and start waiting for the server to build our snapshot, during this time a new user registers and creates a new drop. The user will not be part of the replication, as the snapshot has already been taken, *but the drop will*. Later, we also snapshot the drops database and we have one drop which has been created by a non-existing user. However, the reverse would be fine -- a user existing but have not created a drop. That's why we replicate the *weakest data* (or "most distant") data first. 18 | 19 | When taking snapshots (replicating) you should make sure to perform replications in parallel rather than in a sequence. That will minimize the risk of broken relations between snapshots. 20 | 21 | Each database exists of *schema-free entities* which can be though of as regular documents on a computer. They might be empty or contain some set of information. However, these "documents" are structured data (JSON) so we know where to look for a certain hunk of information. 22 | 23 | 24 | ## Design of the users database 25 | 26 | The user database houses *users* including related information. Keys in this database are prefixed in order to allow future expansion (having sidecar data synchronized with user data, providing consistency in snapshots, during backup and between reads and writes). 27 | 28 | A *user* entity is prefixed with `"user-"` and consists of the following data: 29 | 30 | "user-foo" => { 31 | "username": "Foo", 32 | "email": "foo@bar.com", 33 | "created": 1232854416321, 34 | "modified": 1267348334563, 35 | "passhash": "d2115e39d4fe47007a03c3f6a02812e5bf4497c5", 36 | "following": ["johndoe", "zorro"], 37 | "invite_quota": 958, 38 | "real_name": "Foo Barson", 39 | "url": "http://bar.com", 40 | "muted_users": ["goopymart", "thonk", "greatsuccess"], 41 | "about": "I'm a test dummy and I like it." 42 | } 43 | 44 | There should not be any empty (false, null or undefined) fields in a user struct as a missing field means the same as "empty", thus not all of the fields above might exist for a given user. 45 | 46 | The `passhash` is calculated as follows and is never sent in plain text: 47 | 48 | BASE-16( SHA-1( username ":" password ) ) 49 | 50 | ### Authentication 51 | 52 | Authentication is done over the standard oui client-server communication. 53 | 54 | 1. Client sends `GET /session/sign-in` to the server. 55 | 2. Server responds with a one-time valid nonce `auth_nonce`, actual username (might be unicode) and a session id. 56 | 3. Client calculates an appropriate response based on the `auth_nonce`, the actual username and password. See Figure 1. below. 57 | 4. Client sends the calculated response to `POST /session/sign-in`. 58 | 5. Server responds with `200 OK` containing a complete user structure and an `auth_token`, if credentials checked out. Otherwise `401 Unauthorized` is returned together with an explanation in the body. In this case of auth failure, steps 1 to 5 can be repeated. 59 | 60 | **Figure 1.** Client's calculation of auth challenge response: 61 | 62 | passhash = BASE16( SHA1( username ":" password ) ) 63 | auth_response = BASE16( SHA1_HMAC( auth_nonce, passhash ) ) 64 | 65 | The `auth_token` returned in step 5 will be saved by the client (persistently) and sent with every consecutive request together with `auth_user` and `sid`. The `auth_user` and `auth_token` information can be used to transparently re-authenticate a user on any backend (thus enabling seamless backend failover). The `auth_token` is generated by the server and is an opaque piece of data constructed as illustrated by Figure 2. 66 | 67 | **Figure 2.** `auth_token` construction: 68 | 69 | authToken = ts ":" token 70 | ts = BASE-36( TIME-NOW ) 71 | token = BASE-62( SHA1-HMAC( userSecret, serverSecret ":" ts ) ) 72 | userSecret = 73 | serverSecret = 74 | 75 | 76 | ### Legacy data 77 | 78 | Any user which was migrated from the old site will have the key "legacy" with a structure like this: 79 | 80 | "user-foo" => { 81 | "email": "foo@bar.com", 82 | ... 83 | "legacy": { 84 | "id": 1234, 85 | "passhash": "$1$jdj20dru$mBpFtOCxUNYpWh1n.bUhR0", 86 | "icon": "foo-bar_no-semantics.here.jpg" 87 | }, 88 | } 89 | 90 | This data is used when transitioning a user into the new system (like password transfer). 91 | 92 | As soon as a user has transitioned into a regular user the "legacy" structure can be removed as it does no longer serve any purpose. 93 | 94 | 95 | ## Design of the drops database 96 | 97 | The drops database consists only of drops, keyed by their id. 98 | 99 | "iAXTWJkVlxW2ozFAwW83bN3oil0" => { 100 | "url": "http://bar.com/finger/fashion/fingertip-shoes.jpg", 101 | "origin": "http://www.foundshit.com/finger-tip-shoe-fashion/", 102 | "tags": ["shoes", "fashion"], 103 | "title": "Finger Tip Fashion \u00bb Funny, Bizarre, Amazing Pictures", 104 | "desc": "Crazy little shoe for fingers", 105 | "disabled": true, 106 | "nsfw": true, 107 | "users": { 108 | "foo": [1258195680123, 2], 109 | "someuser": [1258195681123, 1], 110 | "mrtroll": [1258195682123, 1] 111 | }, 112 | "image": { 113 | "format": "JPEG", 114 | "width": 489, 115 | "height": 720, 116 | "depth": 8 117 | } 118 | } 119 | 120 | Just as with user entities, drop entities might not contain all the fields illustrated above. 121 | 122 | Description of fields in a "drop" entitity: 123 | 124 | - **url** -- url to the image this drop represents. Should always exist. 125 | - **origin** -- url to the document which contained the image. Might be missing if same as **url**. 126 | - **tags** -- a simple list of unique tags without any apparent order. 127 | - **title** -- an optional title. 128 | - **desc** -- optional description. 129 | - **disabled** -- if set and true, this drop has been disabled and should not be presented to users. 130 | - **nsfw** -- if set and true, this drop has been classed as "not safe for work", probably containing something nasty. These drops should not be displayed to users who only want "safe" content. 131 | - **users** -- see detailed description further down this document. 132 | 133 | The `id` is calculated like this: 134 | 135 | id = BASE-62( SHA-1( url ) ) 136 | 137 | 138 | ### "users" struct 139 | 140 | The `users` key points to a structure mapping users to time and score. Each entry in the `users` struct is itself a list: 141 | 142 | string username => [ int timestamp, int score ] 143 | 144 | - **timestamp** -- when the user dropped this drop. All timestamps are in UTC with millisecond precision. 145 | - **score** -- the score this drop is worth (calculated from the users own score at the time of dropping). 146 | 147 | Creation time of a drop can be inferred from this struct by finding the lowest timestamp value. 148 | 149 | ### Drop identifiers 150 | 151 | Drop ids are constructed as follows: 152 | 153 | BASE-62( SHA-1( url ) ) 154 | 155 | - Drops are unique per absolute URL (to the image). 156 | - We can easily perform a lookup from a URL. 157 | - No need for UUID generation or sequential counters. 158 | 159 | Drop data is stored in the Amazon S3 bucket `static.dropular.net` with a prefix and hiearchy namespacing: 160 | 161 | "drops/" type "/" "/" "/" "." original-suffix 162 | 163 | Examples: 164 | 165 | url => http://therewasrain.com/gvi7bukvlr_402_hedi_slimane.jpg 166 | id => 8TcklMBAFlTkQNzszPm6HfFhDaY 167 | data => drops/images/8/Tc/klMBAFlTkQNzszPm6HfFhDaY.jpg 168 | 169 | url => http://www.codeproject.com/KB/web-image/ASCIIArt/ASCIIArt2.gif 170 | id => trISBhp3gwu9eCrbkGZMHLYHj3i.gif 171 | data => drops/images/t/rI/SBhp3gwu9eCrbkGZMHLYHj3i.gif 172 | 173 | # Setting up a database 174 | ## Creating a new database for development 175 | 176 | 1) Create the databases 177 | 178 | $ curl -X PUT http://127.0.0.1:5984/dropular-{users,drops} 179 | 180 | 2) Create some test data 181 | 182 | $ head -n 100 users.json | tail -n 10 > sample-users.json 183 | $ head -n 12000 drops.json | tail -n 100 > sample-drops.json 184 | 185 | 3) Load views 186 | 187 | $ node import-docs.js 127.0.0.1:5984/dropular-users views/users-*.json 188 | $ node import-docs.js 127.0.0.1:5984/dropular-drops views/drops-*.json 189 | 190 | 4) Load test data 191 | 192 | $ node import-batch.js 127.0.0.1:5984/dropular-users sample-users.json 193 | $ node import-batch.js 127.0.0.1:5984/dropular-drops sample-drops.json 194 | 195 | You should now have a setup which you can play around with. 196 | 197 | To setup a production instance, you should only perform step 1 and then replicate from a live CouchDB instance. Alternatively, create a SSH tunnel: 198 | 199 | $ ssh -L5985:127.0.0.1:5984 dropular.net 200 | $ curl -X PUT http://127.0.0.1:5985/dropular-{users,drops} 201 | $ curl -iX POST -u dropular:uDxCLqiig1Nk -d @views/drops-drops.json \ 202 | 127.0.0.1:5985/dropular-drops 203 | $ ... 204 | 205 | ## Copying an existing database 206 | 207 | In most cases you probably want to pull down a copy of the live database. Doing so is simple. 208 | 209 | First, make sure you have created the local databases (see "Creating a development database", step 1), then perform a *replication*: 210 | 211 | $ curl -vX POST http://127.0.0.1:5984/_replicate -d\ 212 | '{"source":"http://remote.server:5984/dropular-drops",\ 213 | "target":"dropular-drops"}' & 214 | 215 | $ curl -vX POST http://127.0.0.1:5984/_replicate -d\ 216 | '{"source":"http://remote.server:5984/dropular-users",\ 217 | "target":"dropular-users"}' 218 | 219 | > **Discussion:** It is important you replicate data in the above order (first drops, then users) since the act of replication takes an "atomic snapshot" of the database (also make sure to perform the two queries in parallel if possible). There is a possibility a new user regiestered and created a drop while we are waiting for the "drops" replication to complete. The local replica would be broken, since there would exist drops made by users who does not exist in the users database. 220 | 221 | --------------------------------------------------------------------------------