├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── deduplicator.js ├── github.js ├── gulpfile.js ├── package.json ├── public ├── chart.js ├── index.html ├── list.js ├── main.js └── screen.css ├── screenshot.gif └── server.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "curly": 2, 9 | "quotes": [2, "single"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.swp 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.11" 5 | - "0.10" 6 | before_install: 7 | - npm install -g gulp 8 | script: 9 | - gulp 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.12-onbuild 2 | EXPOSE 3000 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Lukas Martinelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Realtime Relay [](https://travis-ci.org/lukasmartinelli/ghrr) [](https://codeclimate.com/github/lukasmartinelli/ghrr) 2 | 3 | > :warning: This repository is no longer maintained by Lukas Martinelli. 4 | 5 | Receive all GitHub events in realtime with [socket.io](http://socket.io/) from the [GitHub Realtime Relay](http://ghrr.lukasmartinelli.ch) which polls all public events and then relays them directly via websockets. 6 | This is probably the simplest way to create a realtime application on top of GitHub. 7 | 8 | Below you see a statistics page of GitHub events built on top of [GHRR](http://ghrr.lukasmartinelli.ch). 9 | 10 | [](http://ghrr.lukasmartinelli.ch) 11 | 12 | For a short tutorial head over to 13 | [my blog post about GHRR](http://lukasmartinelli.ch/web/2015/07/29/github-realtime-relay.html) or continue reading. For a more sophisticated usage example checkout my other project [delptr](http://github.com/lukasmartinelli/delptr), which lints all C++ commits in realtime. 14 | 15 | ## Connect from Server (Node) 16 | 17 | Install the [socket.io-client](https://www.npmjs.org/package/socket.io-client) from npm. 18 | 19 | ```bash 20 | npm install socket.io-client 21 | ``` 22 | 23 | To receive all events you can hook onto the `/events` namespace 24 | and subscribe to a [specific GitHub Event](https://developer.github.com/v3/activity/events/types/). Please use lower case for subscribing to the event types. 25 | 26 | ```javascript 27 | var url = 'http://ghrr.lukasmartinelli.ch:80/events'; 28 | var socket = require('socket.io-client')(url); 29 | 30 | socket.on('pushevent', function(event){ 31 | console.log('Push: ' + event.repository.full_name); 32 | }); 33 | 34 | ``` 35 | 36 | There is also a `/statistics` namespace used by the GHRR web interface that 37 | sends usage statistics for the Event Types. 38 | 39 | ```javascript 40 | var url = 'http://ghrr.lukasmartinelli.ch:80'; 41 | var io = require('socket.io-client')(url); 42 | io('/statistics').on('types', function(typeCounts) { 43 | console.log('PushEvents: ' + typeCounts.pushevent); 44 | } 45 | ``` 46 | 47 | ## Connect from Web Application 48 | 49 | You need to add the socket.io-client to your web application. 50 | 51 | ```html 52 | 53 | ``` 54 | 55 | You can now connect directly to the public websocket. We support 56 | [CORS](http://www.html5rocks.com/en/tutorials/cors/) 57 | for all domains so you should not encounter any problems. 58 | 59 | ```javascript 60 | var url = 'http://ghrr.lukasmartinelli.ch:80/events'; 61 | var socket = io(url); 62 | 63 | socket.on('pushevent', function (event) { 64 | console.log('Push: ' + event.repository.full_name); 65 | }); 66 | ``` 67 | 68 | ## Host it yourself 69 | 70 | In order to poll all events you need an OAUTH access token. 71 | Run the github realtime relay with a poll rate of `1000` and on port `3000`. 72 | 73 | ```bash 74 | docker pull lukasmartinelli/ghrr 75 | docker run -e GITHUB_TOKEN="acbas3dfas.." -p 3000:3000 lukasmartinelli/ghrr 76 | ``` 77 | -------------------------------------------------------------------------------- /deduplicator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var CBuffer = require('CBuffer'); 3 | 4 | var buffer = new CBuffer(5); 5 | buffer.fill({}); 6 | 7 | module.exports = { 8 | isUnique: function(event) { 9 | var hasDuplicates = buffer.some(function(ids) { 10 | var exists = event.id in ids; 11 | ids[event.id] = true; 12 | return exists; 13 | }); 14 | return !hasDuplicates; 15 | }, 16 | discardOldest: function() { 17 | buffer.push({}); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /github.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var request = require('request'); 3 | 4 | module.exports = function(accessToken) { 5 | var options = { 6 | url: 'https://api.github.com/events', 7 | headers: { 8 | 'User-Agent': 'GHHR', 9 | 'Authorization': 'token ' + accessToken, 10 | 'If-None-Match': '""' 11 | } 12 | }; 13 | var log = { 14 | ratelimit: { 15 | limit: 5000, 16 | remaining: 0, 17 | reset: 0 18 | }, 19 | events: 0, 20 | requests: 0 21 | }; 22 | var parseInfo = function(headers, events) { 23 | options.headers['If-None-Match'] = headers.etag; 24 | log.ratelimit.limit = headers['x-ratelimit-limit']; 25 | log.ratelimit.remaining = headers['x-ratelimit-remaining']; 26 | log.ratelimit.reset = headers['x-ratelimit-reset'] * 1000; 27 | 28 | log.events += events.length; 29 | }; 30 | return { 31 | getEvents: function(callback) { 32 | log.requests += 1; 33 | request(options, function(error, response, body) { 34 | if(error) { 35 | console.error(error); 36 | } 37 | if(response.statusCode === 304) { 38 | callback([]); 39 | } 40 | if(response.statusCode === 200) { 41 | var events = JSON.parse(body); 42 | parseInfo(response.headers, events); 43 | callback(events); 44 | } 45 | }); 46 | }, 47 | getInfo: function() { 48 | return log; 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gulp = require('gulp'); 3 | var eslint = require('gulp-eslint'); 4 | 5 | gulp.task('lint', function () { 6 | return gulp.src(['*.js', 'tests/*.js']) 7 | .pipe(eslint()) 8 | .pipe(eslint.format()) 9 | .pipe(eslint.failOnError()); 10 | }); 11 | 12 | gulp.task('watch', function () { 13 | gulp.watch('*.js', ['lint']); 14 | }); 15 | 16 | gulp.task('default', ['lint']); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghrr", 3 | "version": "1.0.0", 4 | "description": "Github Realtime Relay", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/lukasmartinelli/ghrr.git" 13 | }, 14 | "keywords": [ 15 | "github", 16 | "realtime", 17 | "relay", 18 | "socket" 19 | ], 20 | "author": "Lukas Martinelli", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/lukasmartinelli/ghrr/issues" 24 | }, 25 | "homepage": "https://github.com/lukasmartinelli/ghrr", 26 | "dependencies": { 27 | "CBuffer": "^0.1.5", 28 | "baconjs": "^0.7.35", 29 | "express": "^4.10.2", 30 | "request": "^2.48.0", 31 | "socket.io": "^1.2.0" 32 | }, 33 | "devDependencies": { 34 | "gulp": "^3.8.10", 35 | "gulp-eslint": "^0.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/chart.js: -------------------------------------------------------------------------------- 1 | var EventTypeChart = function(el, eventTypes){ 2 | var chartData = _.chain(eventTypes) 3 | .filter(function(type) { return type.plottable; }) 4 | .map(function(type) { 5 | return { 6 | label: type.label, 7 | values: [{ time: new Date(), y: type.count }] 8 | }; 9 | }).value(); 10 | 11 | return $(el).epoch({ 12 | type: 'time.bar', 13 | data: chartData, 14 | axes: ['left', 'bottom'], 15 | ticks: { time: 10, left: 5}, 16 | tickFormats: { bottom: function(d) { return d.toLocaleTimeString(); } }, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
26 |
27 | var url = 'ghrr.lukasmartinelli.ch:80/events';
28 | var socket = require('socket.io-client')(url);
29 |
30 | socket.on('pushevent', function(event){
31 | console.log('Push: ' + event.repository.full_name);
32 | });
33 |
34 |
35 |
36 | Documentation for Node
37 |
38 |
42 |
43 | var url = 'http://ghrr.lukasmartinelli.ch:80/events';
44 | var socket = io(url);
45 |
46 | socket.on('pushevent', function (event) {
47 | console.log('Push: ' + event.repository.full_name);
48 | });
49 |
50 |
51 |
52 | Documentation for Web Apps
53 |
54 |