├── .gitignore ├── README.md ├── app ├── bower.json ├── index.html ├── styles.css └── scripts.js ├── package.json ├── ws.js ├── reply.js ├── LICENSE └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | app/bower_components 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | anarcho-autism 2 | ============== 3 | 4 | Latest BnW replies feed 5 | 6 | Installation 7 | ============ 8 | 9 | ``` 10 | git clone https://github.com/border-radius/anarcho-autism 11 | cd anarcho-autism 12 | npm install 13 | cd app 14 | bower install 15 | cd .. 16 | node index.js 17 | ``` 18 | -------------------------------------------------------------------------------- /app/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnw-talks", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "border-radius " 6 | ], 7 | "license": "Public Domain", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular": "~1.2.15", 17 | "moment": "~2.5.1", 18 | "angular-route": "~1.2.23" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnw-talks", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "express": "~3.5.1", 8 | "mongoose": "~3.8.8", 9 | "request": "^2.40.0", 10 | "ws": "~0.4.31", 11 | "rss": "~1.1.1" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "", 18 | "license": "Public Domain" 19 | } 20 | -------------------------------------------------------------------------------- /ws.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('ws'); 2 | 3 | function Socket (address, onmessage) { 4 | 5 | console.log('Connecting to', address, new Date()); 6 | 7 | var E = function (e) { 8 | console.log(e); 9 | 10 | setTimeout(function () { 11 | Socket(address, onmessage); 12 | }, 1000); 13 | } 14 | 15 | var ws = new WebSocket(address); 16 | 17 | ws.on('message', onmessage); 18 | 19 | ws.on('close', E); 20 | ws.on('error', E); 21 | }; 22 | 23 | module.exports = Socket; 24 | -------------------------------------------------------------------------------- /reply.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var request = require('request'); 3 | 4 | mongoose.connect('mongodb://localhost/bnw-talks'); 5 | 6 | var Reply = new mongoose.Schema({ 7 | anonymous: Boolean, 8 | date: Number, 9 | id: { 10 | type: String, 11 | index: { 12 | unique: true 13 | } 14 | }, 15 | message: String, 16 | num: Number, 17 | replyto: String, 18 | replytotext: String, 19 | text: String, 20 | user: String, 21 | mentions: [String] 22 | }); 23 | 24 | Reply.pre('save', function (next) { 25 | this.mentions = (this.text.match(/\@([\-0-9A-z]+)/ig) || []).map(function (mention) { 26 | return mention.replace(/^\@/, ''); 27 | }); 28 | 29 | var that = this; 30 | 31 | if (this.replyto) return next(); 32 | 33 | request('https://bnw.im/api/show?message=' + this.id.split('/')[0], function (e, res, body) { 34 | if (!e && res.statusCode !== 200) { 35 | e = new Error ('BNW returned status code ' + res.statusCode); 36 | e.res = res; 37 | } 38 | 39 | if (e) { 40 | console.log(e); 41 | return next(e); 42 | } 43 | 44 | that.mentions.push(JSON.parse(body).messages[0].user); 45 | 46 | next(); 47 | }); 48 | }); 49 | 50 | module.exports = mongoose.model('Reply', Reply); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bnw Replies 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Reply = require('./reply'); 2 | var express = require('express'); 3 | var ws = require('./ws'); 4 | var RSS = require('rss'); 5 | 6 | ws('wss://bnw.im/comments/ws', function (message, flags) { 7 | var reply = new Reply(JSON.parse(message)); 8 | reply.save(function (e) { 9 | if (e) console.log(e); 10 | }); 11 | }); 12 | 13 | var app = express(); 14 | 15 | app.use(express.static(__dirname + '/app')); 16 | 17 | (function (ctrl) { 18 | app.get('/for/:user', ctrl); 19 | app.get('/top', ctrl); 20 | })(function (req, res) { 21 | res.sendfile(__dirname + '/app/index.html'); 22 | }); 23 | 24 | function getComments(query, skip, next) { 25 | query.sort({ date: -1 }).limit(20).skip(skip|0).exec(next); 26 | } 27 | 28 | app.get('/comments', function (req, res) { 29 | getComments(Reply.find(), req.param('skip'), function (e, replies) { 30 | if (e) return res.status(500).send(); 31 | res.json(replies); 32 | }); 33 | }); 34 | 35 | app.get('/comments/:user', function (req, res) { 36 | getComments(Reply.find({ mentions: req.params.user }), req.param('skip'), function (e, replies) { 37 | if (e) return res.status(500).send(); 38 | res.json(replies); 39 | }); 40 | }); 41 | 42 | app.get('/for/:user/rss', function (req, res) { 43 | getComments(Reply.find({ mentions: req.params.user }), 0, function (e, replies) { 44 | if (e) return res.status(500).send(); 45 | var last = replies[0] || {}, 46 | feed = new RSS({ 47 | title: 'Аутизм для ' + req.params.user, 48 | feed_url: 'http://autism.anarchy.info/for/' + req.params.user + '.rss', 49 | site_url: 'http://autism.anarchy.info/for/' + req.params.user, 50 | pubDate: new Date(last.date * 1000) 51 | }); 52 | 53 | replies.forEach(function (reply) { 54 | feed.item({ 55 | title: 'Ответ от ' + reply.user, 56 | description: reply.text, 57 | url: 'https://meow.bnw.im/p/' + reply.id.replace('/', '#'), 58 | guid: reply.id, 59 | date: new Date(reply.date * 1000) 60 | }); 61 | }); 62 | 63 | res.set('Content-type', 'application/rss+xml').send(feed.xml()); 64 | }); 65 | }); 66 | 67 | app.get('/api/top', function (req, res) { 68 | Reply.aggregate([ 69 | { 70 | $match: { 71 | date: { 72 | $gt: new Date()/1000 - 24 * 3600 73 | }, 74 | replyto: { 75 | $ne: null 76 | } 77 | } 78 | }, 79 | { 80 | $group: { 81 | _id: "$replyto", 82 | count: { 83 | $sum: 1 84 | } 85 | } 86 | }, 87 | { 88 | $sort: { 89 | count: -1 90 | } 91 | }, 92 | { 93 | $limit: 10 94 | } 95 | ], function (e, replyto) { 96 | if (e) return res.status(500).send(e); 97 | Reply.find({ 98 | id: { 99 | $in: replyto.map(function (reply) { 100 | return reply._id; 101 | }) 102 | } 103 | }, function (e, replies) { 104 | if (e) return res.status(500).send(e); 105 | res.json(replies); 106 | }) 107 | }); 108 | }); 109 | 110 | app.listen(8000); 111 | 112 | console.log('Listening ', 8000, ' launched at ', new Date()); 113 | -------------------------------------------------------------------------------- /app/styles.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans&subset=latin,cyrillic); 2 | 3 | html, body, body > div { 4 | margin: 0; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | body, button, input { 10 | font: 14px/1.4 'Open Sans', Arial, sans-serif; 11 | color: #fff; 12 | } 13 | 14 | .container { 15 | height: 100%; 16 | overflow-y: scroll; 17 | background: #000; 18 | } 19 | 20 | header { 21 | text-align: center; 22 | } 23 | 24 | .replies { 25 | margin: 0 auto; 26 | padding: 0 10px; 27 | max-width: 560px; 28 | } 29 | 30 | div { 31 | -webkit-box-sizing: border-box; 32 | -moz-box-sizing: border-box; 33 | box-sizing: border-box; 34 | } 35 | 36 | .reply { 37 | margin: 40px 0 40px 60px; 38 | max-width: 550px; 39 | } 40 | 41 | .quote { 42 | max-width: 430px; 43 | padding: 1px 15px; 44 | border-radius: 8px 8px 0 0; 45 | color: #CCCCCC; 46 | border-bottom: 0 none; 47 | } 48 | 49 | .quote, .text p{ 50 | overflow: hidden; 51 | } 52 | 53 | .quote { 54 | border-bottom: none; 55 | } 56 | 57 | .quote a { 58 | color: #999999; 59 | text-decoration: none; 60 | } 61 | 62 | .quote a:hover { 63 | text-decoration: underline; 64 | } 65 | 66 | .text { 67 | padding: 7px 15px; 68 | color: #CCCCCC; 69 | border-radius: 8px; 70 | position: relative; 71 | background: #111; 72 | } 73 | 74 | .userpic { 75 | width: 48px; 76 | position: absolute; 77 | top: 6px; 78 | left: -62px; 79 | text-align: center; 80 | } 81 | 82 | .userpic img { 83 | max-width: 48px; 84 | max-height: 48px; 85 | border-radius: 4px; 86 | } 87 | 88 | .btn { 89 | padding: 3px 12px 5px; 90 | background: #000; 91 | color: #D79D35; 92 | border: 1px solid #D79D35; 93 | border-radius: 4px; 94 | } 95 | 96 | .center { 97 | text-align: center; 98 | } 99 | 100 | .nav { 101 | list-style-type: none; 102 | } 103 | 104 | h1 a { 105 | color: #D39C3A; 106 | } 107 | 108 | h1 a:hover { 109 | color: #B77C11; 110 | } 111 | 112 | .nav input { 113 | padding: 0 5px; 114 | width: 100px; 115 | border: 0 none; 116 | border-bottom: 1px solid #D39C3A; 117 | color: #D39C3A; 118 | background: none; 119 | } 120 | 121 | .nav button { 122 | padding: 5px 12px; 123 | background: none; 124 | border: 1px solid #D39C3A; 125 | border-radius: 4px; 126 | color: #D39C3A; 127 | cursor: pointer; 128 | } 129 | 130 | .nav input:focus, .nav button:focus { 131 | outline: 0 none; 132 | border-color: #B77C11; 133 | color: #B77C11; 134 | } 135 | 136 | .nav button:active { 137 | opacity: 0.5; 138 | } 139 | 140 | .webui { 141 | margin-top: 15px; 142 | } 143 | 144 | .webui select { 145 | padding: 5px 24px 5px 12px; 146 | background: none; 147 | border: 1px solid #D39C3A; 148 | border-radius: 4px; 149 | color: #D39C3A; 150 | cursor: pointer; 151 | -webkit-appearance: none; 152 | -moz-appearance: none; 153 | appearance: none; 154 | } 155 | 156 | .webui label { 157 | position: relative; 158 | } 159 | 160 | .webui label:after { 161 | content: '▼'; 162 | color: #D39C3A; 163 | right: 12px; 164 | top: 1px; 165 | position: absolute; 166 | pointer-events: none; 167 | } 168 | -------------------------------------------------------------------------------- /app/scripts.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('bnw-replies', ['ngRoute']); 2 | 3 | app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { 4 | var ctrl = { 5 | templateUrl: 'feed.html', 6 | controller: 'Replies' 7 | } 8 | $routeProvider.when('/', ctrl) 9 | .when('/for/:user', ctrl) 10 | .when('/top', { 11 | templateUrl: 'feed.html', 12 | controller: 'Top' 13 | }); 14 | 15 | $locationProvider.html5Mode(true); 16 | }]); 17 | 18 | app.directive('autoscroll', function ($timeout) { 19 | return function (scope, elem, attrs) { 20 | var height = elem[0].scrollHeight; 21 | 22 | scope.$watch(attrs.autoscroll, function (n, o) { 23 | $timeout(function () { 24 | var change = elem[0].scrollHeight - height; 25 | if ((n || []).length - (o || []).length == 1) { 26 | elem[0].scrollTop += change; 27 | } 28 | height = elem[0].scrollHeight; 29 | }); 30 | }, true); 31 | }; 32 | }); 33 | 34 | app.directive('infinitescroll', function () { 35 | return function (scope, elem, attrs) { 36 | elem.on('scroll', function () { 37 | if (elem[0].scrollHeight - elem[0].scrollTop < screen.height + 150) { 38 | scope.$apply(attrs.infinitescroll); 39 | } 40 | }); 41 | }; 42 | }); 43 | 44 | app.filter('moment', function () { 45 | return function (date) { 46 | return moment(date, 'X').fromNow(); 47 | }; 48 | }); 49 | 50 | app.filter('lines', function () { 51 | return function (text) { 52 | return text.replace(/\n+/g, '\n').replace(/(^\n|\n$)/g, '').split('\n'); 53 | }; 54 | }); 55 | 56 | app.controller('Top', function ($scope, $http) { 57 | $scope.load = function() {}; 58 | $scope.top = true; 59 | 60 | $http.get('/api/top').success(function (replies) { 61 | $scope.replies = replies; 62 | }); 63 | }); 64 | 65 | app.controller('Replies', function ($scope, $http, $routeParams, $timeout) { 66 | $scope.user = $routeParams.user; 67 | $scope.replies = []; 68 | 69 | var addReply = function (reply) { 70 | $timeout(function () { 71 | $scope.replies.unshift(reply); 72 | }); 73 | }; 74 | 75 | var initWS = function () { 76 | var ws = new WebSocket('wss://bnw.im/comments/ws'); 77 | ws.onmessage = function (event) { 78 | var reply = JSON.parse(event.data); 79 | if (!$scope.user) return addReply(reply); 80 | if ((reply.text.match(/\@([\-0-9A-z]+)/ig) || []).indexOf('@' + $scope.user) > -1) { 81 | addReply(reply); 82 | } else if (!reply.replyto) { 83 | $http.get('https://bnw.im/api/show?message=' + reply.id.split('/')[0]).success(function (res) { 84 | if (res.messages[0].user == $scope.user) addReply(reply); 85 | }); 86 | } 87 | }; 88 | ws.onclose = initWS; 89 | }; 90 | initWS(); 91 | 92 | $scope.load = function () { 93 | if ($scope.loading) return; 94 | 95 | $scope.loading = true; 96 | 97 | $http.get('/comments' + (($scope.user) ? '/' + $scope.user : '') + '?skip='+$scope.replies.length) 98 | .success(function (replies) { 99 | if (!replies.length) { 100 | $scope.finish = true; 101 | } else { 102 | $scope.replies = $scope.replies.concat(replies); 103 | } 104 | }).then(function () { 105 | $scope.loading = false; 106 | }); 107 | }; 108 | $scope.load(); 109 | }); 110 | 111 | app.controller('For', ['$scope', '$location', function($scope, $location) { 112 | $scope.name = localStorage['last'] || 'anonymous'; 113 | $scope.for = function (name) { 114 | localStorage['last'] = name; 115 | $location.url('/for/' + name); 116 | }; 117 | }]); 118 | 119 | app.controller('Webui', ['$scope', '$rootScope', function($scope, $rootScope) { 120 | $scope.webuilist = [ 121 | {name: 'meow', url: 'https://meow.bnw.im'}, 122 | {name: '6nw', url: 'https://6nw.im'}, 123 | {name: 'default', url: 'https://bnw.im'} 124 | ]; 125 | $scope.update = function () { 126 | localStorage['webui'] = $scope.webui; 127 | $rootScope.webuiurl = $scope.webuilist[$scope.webui].url; 128 | }; 129 | $scope.webui = localStorage['webui'] || '0'; 130 | $scope.update(); 131 | }]); 132 | --------------------------------------------------------------------------------