{{title}}{{domain}}
+{{title}}{{domain}}
{{pluralize votes "Vote"}}, submitted by {{author}}, + {{#if clicks}}{{pluralize clicks "click"}},{{/if}} {{pluralize commentsCount "comment"}} {{#if ownPost}}Edit{{/if}}
diff --git a/client/templates/posts/post_item.js b/client/templates/posts/post_item.js index cfc626bf..7cbd060d 100644 --- a/client/templates/posts/post_item.js +++ b/client/templates/posts/post_item.js @@ -14,6 +14,9 @@ Template.postItem.helpers({ } else { return 'disabled'; } + }, + postUrl: function(){ + return this.shortUrl ? this.shortUrl : this.url; } }); @@ -22,4 +25,4 @@ Template.postItem.events({ e.preventDefault(); Meteor.call('upvote', this._id); } -}); \ No newline at end of file +}); diff --git a/lib/collections/posts.js b/lib/collections/posts.js index 3ac24415..b48a0dfc 100644 --- a/lib/collections/posts.js +++ b/lib/collections/posts.js @@ -61,6 +61,13 @@ Meteor.methods({ votes: 0 }); + // shorten link URL + if (Meteor.isServer) { + var shortUrl = Bitly.shortenURL(post.url); + if(post.url && shortUrl) + post.shortUrl = shortUrl; + } + var postId = Posts.insert(post); return { diff --git a/lib/router.js b/lib/router.js index 9513e11a..c46f8984 100644 --- a/lib/router.js +++ b/lib/router.js @@ -3,7 +3,10 @@ Router.configure({ loadingTemplate: 'loading', notFoundTemplate: 'notFound', waitOn: function() { - return [Meteor.subscribe('notifications')] + return [ + Meteor.subscribe('currentUser'), + Meteor.subscribe('notifications') + ] } }); @@ -49,6 +52,14 @@ BestPostsController = PostsListController.extend({ } }); +ClickedPostsController = PostsListController.extend({ + sort: {clicks: -1, submitted: -1, _id: -1}, + nextPath: function() { + return Router.routes.clickedPosts.path({postsLimit: this.postsLimit() + this.increment}) + } +}); + + Router.route('/', { name: 'home', controller: NewPostsController @@ -58,6 +69,7 @@ Router.route('/new/:postsLimit?', {name: 'newPosts'}); Router.route('/best/:postsLimit?', {name: 'bestPosts'}); +Router.route('/clicked/:postsLimit?', {name: 'clickedPosts'}); Router.route('/posts/:_id', { name: 'postPage', @@ -80,6 +92,59 @@ Router.route('/posts/:_id/edit', { Router.route('/submit', {name: 'postSubmit'}); +Router.route('/feed.xml', { + where: 'server', + name: 'rss', + action: function() { + var feed = new RSS({ + title: "New Microscope Posts", + description: "The latest posts from Microscope, the smallest news aggregator." + }); + + Posts.find({}, {sort: {submitted: -1}, limit: 20}).forEach(function(post) { + feed.item({ + title: post.title, + description: post.body, + author: post.author, + date: post.submitted, + url: '/posts/' + post._id + }) + }); + + this.response.write(feed.xml()); + this.response.end(); + } +}); + +Router.route('/api/posts', { + where: 'server', + name: 'apiPosts', + action: function() { + var parameters = this.request.query, + limit = !!parameters.limit ? parseInt(parameters.limit) : 20, + data = Posts.find({}, {limit: limit, fields: {title: 1, author: 1, url: 1, submitted: 1, }}).fetch(); + + this.response.write(JSON.stringify(data)); + this.response.end(); + } +}); + +Router.route('/api/posts/:_id', { + where: 'server', + name: 'apiPost', + action: function() { + var post = Posts.findOne(this.params._id); + + if(post){ + this.response.write(JSON.stringify(post)); + } else { + this.response.writeHead(404, {'Content-Type': 'text/html'}); + this.response.write("Post not found."); + } + this.response.end(); + } +}); + var requireLogin = function() { if (! Meteor.user()) { if (Meteor.loggingIn()) { @@ -92,5 +157,7 @@ var requireLogin = function() { } } -Router.onBeforeAction('dataNotFound', {only: 'postPage'}); -Router.onBeforeAction(requireLogin, {only: 'postSubmit'}); +if (Meteor.isClient) { + Router.onBeforeAction('dataNotFound', {only: 'postPage'}); + Router.onBeforeAction(requireLogin, {only: 'postSubmit'}); +} diff --git a/packages/bitly/.gitignore b/packages/bitly/.gitignore new file mode 100644 index 00000000..677a6fc2 --- /dev/null +++ b/packages/bitly/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/bitly/bitly.js b/packages/bitly/bitly.js new file mode 100644 index 00000000..2175ff3b --- /dev/null +++ b/packages/bitly/bitly.js @@ -0,0 +1,71 @@ +Bitly = {}; + +Bitly.shortenURL = function(url){ + if(!Meteor.settings.bitly) + throw new Meteor.Error(500, 'Please provide a Bitly token in Meteor.settings'); + + var shortenResponse = Meteor.http.get( + "https://api-ssl.bitly.com/v3/shorten?", + { + timeout: 5000, + params:{ + "format": "json", + "access_token": Meteor.settings.bitly, + "longUrl": url + } + } + ); + if(shortenResponse.statusCode == 200){ + return shortenResponse.data.data.url + }else{ + throw new Meteor.Error(500, "Bitly call failed with error: "+shortenResponse.status_txt); + } +} + +Bitly.getClicks = function(link){ + if(!Meteor.settings.bitly) + throw new Meteor.Error(500, 'Please provide a Bitly token in Meteor.settings'); + + var statsResponse = Meteor.http.get( + "https://api-ssl.bitly.com/v3/link/clicks?", + { + timeout: 5000, + params:{ + "format": "json", + "access_token": Meteor.settings.bitly, + "link": link + } + } + ); + if(statsResponse.data.status_code == 200) + return statsResponse.data.data.link_clicks +} + +Meteor.methods({ + 'getBitlyClicks': function(link){ + return Bitly.getClicks(link); + } +}) + +var callInterval = 10000; // 1000ms * 10 = 10s + +Meteor.setInterval(function(){ + // get all posts with the shortUrl property + var shortUrlPosts = Posts.find({shortUrl: { $exists: true }}); + var postsNumber = shortUrlPosts.count(); + + // initialize counter + var count = 0; + + shortUrlPosts.forEach(function(post){ + // calculate the right delay to distribute API calls evenly throughout the interval + var callTimeout = Math.round(callInterval/postsNumber*count); + + Meteor.setTimeout(function(){ + Posts.update(post._id, {$set: {clicks: Bitly.getClicks(post.shortUrl)}}); + }, callTimeout); + + count++; + }); + +}, callInterval); \ No newline at end of file diff --git a/packages/bitly/package.js b/packages/bitly/package.js new file mode 100644 index 00000000..a1f07c25 --- /dev/null +++ b/packages/bitly/package.js @@ -0,0 +1,11 @@ +Package.describe({ + name: 'bitly', + summary: "Bitly package", + version: '1.0.0' +}); + +Package.onUse(function (api) { + api.versionsFrom('0.9.4'); + api.addFiles('bitly.js', 'server'); + api.export('Bitly'); +}); \ No newline at end of file diff --git a/packages/bitly/versions.json b/packages/bitly/versions.json new file mode 100644 index 00000000..fca353a7 --- /dev/null +++ b/packages/bitly/versions.json @@ -0,0 +1,15 @@ +{ + "dependencies": [ + [ + "meteor", + "1.1.3" + ], + [ + "underscore", + "1.0.1" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.35", + "format": "1.0" +} \ No newline at end of file diff --git a/packages/intercom/.gitignore b/packages/intercom/.gitignore new file mode 100644 index 00000000..677a6fc2 --- /dev/null +++ b/packages/intercom/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/intercom/intercom_loader.js b/packages/intercom/intercom_loader.js new file mode 100644 index 00000000..7f433f34 --- /dev/null +++ b/packages/intercom/intercom_loader.js @@ -0,0 +1 @@ +(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}};})() \ No newline at end of file diff --git a/packages/intercom/intercom_server.js b/packages/intercom/intercom_server.js new file mode 100644 index 00000000..091fba0a --- /dev/null +++ b/packages/intercom/intercom_server.js @@ -0,0 +1,7 @@ +var crypto = Npm.require('crypto'); + +IntercomHash = function(user, secret) { + var secret = new Buffer(secret, 'utf8') + return crypto.createHmac('sha256', secret) + .update(user._id).digest('hex'); +} \ No newline at end of file diff --git a/packages/intercom/package.js b/packages/intercom/package.js new file mode 100644 index 00000000..c5126856 --- /dev/null +++ b/packages/intercom/package.js @@ -0,0 +1,13 @@ +Package.describe({ + name: 'intercom', + summary: "Intercom package", + version: '1.0.0' +}); + +Package.on_use(function (api) { + api.versionsFrom('0.9.4'); + api.add_files('intercom_loader.js', 'client'); + api.add_files('intercom_server.js', 'server'); + + api.export('IntercomHash', 'server'); +}); \ No newline at end of file diff --git a/packages/intercom/versions.json b/packages/intercom/versions.json new file mode 100644 index 00000000..fca353a7 --- /dev/null +++ b/packages/intercom/versions.json @@ -0,0 +1,15 @@ +{ + "dependencies": [ + [ + "meteor", + "1.1.3" + ], + [ + "underscore", + "1.0.1" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.35", + "format": "1.0" +} \ No newline at end of file diff --git a/packages/rss/.gitignore b/packages/rss/.gitignore new file mode 100644 index 00000000..677a6fc2 --- /dev/null +++ b/packages/rss/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/rss/.npm/package/.gitignore b/packages/rss/.npm/package/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/packages/rss/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rss/.npm/package/README b/packages/rss/.npm/package/README new file mode 100644 index 00000000..3d492553 --- /dev/null +++ b/packages/rss/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rss/.npm/package/npm-shrinkwrap.json b/packages/rss/.npm/package/npm-shrinkwrap.json new file mode 100644 index 00000000..dbd99a7d --- /dev/null +++ b/packages/rss/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "rss": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/rss/-/rss-0.0.4.tgz", + "from": "rss@0.0.4", + "dependencies": { + "xml": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/xml/-/xml-0.0.10.tgz", + "from": "xml@0.0.10" + } + } + } + } +} diff --git a/packages/rss/package.js b/packages/rss/package.js new file mode 100644 index 00000000..c2427168 --- /dev/null +++ b/packages/rss/package.js @@ -0,0 +1,13 @@ +Package.describe({ + name: 'rss', + summary: "RSS feed generator", + version: "1.0.0" +}); + +Npm.depends({rss: '0.0.4'}); + +Package.onUse(function (api) { + api.versionsFrom('0.9.4'); + api.addFiles('rss.js', 'server'); + api.export('RSS'); +}); \ No newline at end of file diff --git a/packages/rss/rss.js b/packages/rss/rss.js new file mode 100644 index 00000000..7be98168 --- /dev/null +++ b/packages/rss/rss.js @@ -0,0 +1 @@ +RSS = Npm.require('rss'); \ No newline at end of file diff --git a/packages/rss/versions.json b/packages/rss/versions.json new file mode 100644 index 00000000..fca353a7 --- /dev/null +++ b/packages/rss/versions.json @@ -0,0 +1,15 @@ +{ + "dependencies": [ + [ + "meteor", + "1.1.3" + ], + [ + "underscore", + "1.0.1" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.35", + "format": "1.0" +} \ No newline at end of file diff --git a/server/accounts.js b/server/accounts.js new file mode 100644 index 00000000..d791c212 --- /dev/null +++ b/server/accounts.js @@ -0,0 +1,8 @@ +Accounts.onCreateUser(function(options, user) { + user.intercomHash = IntercomHash(user, '12345678'); + + if (options.profile) + user.profile = options.profile; + + return user; +}); diff --git a/server/publications.js b/server/publications.js index 2a91210f..d3cf1048 100644 --- a/server/publications.js +++ b/server/publications.js @@ -11,12 +11,15 @@ Meteor.publish('singlePost', function(id) { return Posts.find(id); }); - Meteor.publish('comments', function(postId) { check(postId, String); return Comments.find({postId: postId}); }); Meteor.publish('notifications', function() { - return Notifications.find({userId: this.userId, read: false}); + return Notifications.find({userId: this.userId}); +}); + +Meteor.publish('currentUser', function() { + return Meteor.users.find(this.userId, {fields: {createdAt: 1, intercomHash: 1}}); });