├── .dockerignore ├── .gitignore ├── Capfile ├── Dockerfile ├── Dockerfile_enqueuer ├── Dockerfile_nginx ├── Dockerfile_puma ├── Dockerfile_sidekiq ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app.rb ├── bower.json ├── circle.yml ├── config.ru ├── config.yml.example ├── config ├── .gitkeep ├── docker.env.example ├── newrelic.example.yml ├── puma.rb └── unicorn.rb ├── docker-compose.yml ├── docker ├── enqueuer │ └── entrypoint.sh ├── nginx │ ├── entrypoint.sh │ ├── gitnotifier │ └── nginx.conf ├── puma │ └── entrypoint.sh └── sidekiq │ └── entrypoint.sh ├── public ├── css │ ├── bootstrap-social.css │ ├── bootstrap.min.css │ ├── font-awesome.min.css │ ├── main.css │ └── octicons ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── screenshot1.png │ └── stack │ │ ├── redis.png │ │ ├── ruby.png │ │ ├── sidekiq.png │ │ └── sinatra.png └── js │ ├── app.js │ ├── bootstrap.min.js │ ├── html5shiv.min.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.spin.js │ ├── mailcheck.min.js │ ├── respond.min.js │ └── spin.js ├── scripts ├── add_users.rb ├── delete_user_by_email.rb ├── delete_user_by_token.rb ├── job_enqueuer.rb ├── migrations │ └── 0_add_last_queued_on.rb ├── send_email.rb └── send_newsletter.rb ├── sidekiq-web ├── README.md └── config.ru ├── test └── casper │ ├── authenticated.js │ ├── signup.js │ └── unauthenticated.js ├── tmp ├── .gitkeep ├── pids │ └── .gitignore └── sockets │ └── .gitignore ├── views ├── email │ ├── confirm.erb │ ├── confirm.txt │ ├── empty.txt │ ├── newsletter.erb │ ├── newsletter.txt │ ├── notification.erb │ ├── notification.txt │ └── src │ │ ├── confirm.erb │ │ ├── css │ │ └── styles.css │ │ ├── newsletter.erb │ │ └── notification.erb ├── events.haml ├── faq.haml ├── index.haml ├── layout.haml ├── preferences.haml ├── signup.haml └── unsubscribe.haml └── workers ├── email_builder.rb ├── init.rb ├── notifications_checker.rb └── send_email.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config.yml 3 | config/newrelic.yml 4 | config/docker.env 5 | log/* 6 | #bower_components/ 7 | tmp/pids/* 8 | tmp/sockets/* 9 | scripts/notes.txt 10 | .capistrano/ 11 | node_modules/ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config.yml 3 | config/newrelic.yml 4 | config/docker.env 5 | log/* 6 | bower_components/ 7 | .git_old* 8 | scripts/notes.txt 9 | .capistrano/ 10 | node_modules/ 11 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | require 'capistrano/puma' 8 | require 'capistrano/puma/monit' #if you need the monit tasks 9 | 10 | require 'capistrano/sidekiq' 11 | require 'capistrano/sidekiq/monit' #to require monit tasks # Only for capistrano3 12 | 13 | require 'new_relic/recipes' 14 | 15 | # Include tasks from other gems included in your Gemfile 16 | # 17 | # For documentation on these, see for example: 18 | # 19 | # https://github.com/capistrano/rvm 20 | # https://github.com/capistrano/rbenv 21 | # https://github.com/capistrano/chruby 22 | # https://github.com/capistrano/bundler 23 | # https://github.com/capistrano/rails 24 | # https://github.com/capistrano/passenger 25 | # 26 | # require 'capistrano/rvm' 27 | # require 'capistrano/rbenv' 28 | # require 'capistrano/chruby' 29 | # require 'capistrano/bundler' 30 | # require 'capistrano/rails/assets' 31 | # require 'capistrano/rails/migrations' 32 | # require 'capistrano/passenger' 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4.0-slim 2 | 3 | # Set proper locale 4 | ENV LANG=C.UTF-8 5 | 6 | # throw errors if Gemfile has been modified since Gemfile.lock 7 | RUN bundle config --global frozen 1 && \ 8 | buildDeps=' \ 9 | make \ 10 | gcc \ 11 | patch \ 12 | ' && \ 13 | apt-get update && apt-get install -y --no-install-recommends $buildDeps && \ 14 | rm -rf /var/lib/apt/lists/* 15 | 16 | ONBUILD ADD . /usr/src/app 17 | ONBUILD WORKDIR /usr/src/app 18 | ONBUILD RUN bundle install 19 | -------------------------------------------------------------------------------- /Dockerfile_enqueuer: -------------------------------------------------------------------------------- 1 | FROM gitnotifier/ruby:2.4.0 2 | 3 | # Set proper locale 4 | ENV APP_ENV=development \ 5 | APP_CONFIG_GITHUB_CLIENT_ID=someid \ 6 | APP_CONFIG_GITHUB_CLIENT_SECRET=somesecret \ 7 | 8 | APP_CONFIG_REDIS_HOST=localhost \ 9 | APP_CONFIG_REDIS_PORT=6379 \ 10 | APP_CONFIG_REDIS_DB=1 \ 11 | APP_CONFIG_REDIS_NAMESPACE=ghntfr \ 12 | 13 | APP_CONFIG_STATSD_HOST=localhost \ 14 | APP_CONFIG_STATSD_PORT=8125 \ 15 | 16 | APP_CONFIG_DOMAIN=gitnotifier.local \ 17 | 18 | APP_CONFIG_SECRET=somesecret \ 19 | 20 | APP_ENQUEUER_SLEEP_TIME=300 \ 21 | 22 | APP_CONFIG_DEPLOY_ID=1 \ 23 | 24 | APP_CONFIG_EMAIL_DEV_ON_SIGNUP=false \ 25 | APP_CONFIG_DEV_EMAIL_ADDRESS=some@email.com 26 | 27 | ENTRYPOINT [ "/usr/src/app/docker/enqueuer/entrypoint.sh" ] 28 | -------------------------------------------------------------------------------- /Dockerfile_nginx: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | ENV APP_CONFIG_DOMAIN=gitnotifier.local \ 4 | APP_UPSTREAM_PUMA=localhost 5 | 6 | ADD ./docker/nginx/nginx.conf /etc/nginx/nginx.conf 7 | ADD ./docker/nginx/gitnotifier /etc/nginx/conf.d/gitnotifier 8 | 9 | ADD ./docker/nginx/entrypoint.sh /entrypoint.sh 10 | 11 | ADD . /var/www/github-notifier/current 12 | 13 | ENTRYPOINT ["/entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /Dockerfile_puma: -------------------------------------------------------------------------------- 1 | FROM gitnotifier/ruby:2.4.0 2 | 3 | # Set proper locale 4 | ENV APP_ENV=development \ 5 | APP_CONFIG_GITHUB_CLIENT_ID=someid \ 6 | APP_CONFIG_GITHUB_CLIENT_SECRET=somesecret \ 7 | 8 | APP_CONFIG_REDIS_HOST=localhost \ 9 | APP_CONFIG_REDIS_PORT=6379 \ 10 | APP_CONFIG_REDIS_DB=1 \ 11 | APP_CONFIG_REDIS_NAMESPACE=ghntfr \ 12 | 13 | APP_CONFIG_STATSD_HOST=localhost \ 14 | APP_CONFIG_STATSD_PORT=8125 \ 15 | 16 | APP_CONFIG_DOMAIN=gitnotifier.local \ 17 | 18 | APP_CONFIG_SECRET=somesecret \ 19 | 20 | APP_CONFIG_DEPLOY_ID=1 \ 21 | 22 | APP_CONFIG_EMAIL_DEV_ON_SIGNUP=false \ 23 | APP_CONFIG_DEV_EMAIL_ADDRESS=some@email.com 24 | 25 | EXPOSE 9292 26 | 27 | ENTRYPOINT [ "/usr/src/app/docker/puma/entrypoint.sh" ] 28 | -------------------------------------------------------------------------------- /Dockerfile_sidekiq: -------------------------------------------------------------------------------- 1 | FROM gitnotifier/ruby:2.4.0 2 | 3 | # Set proper locale 4 | ENV APP_ENV=development \ 5 | APP_CONFIG_GITHUB_CLIENT_ID=someid \ 6 | APP_CONFIG_GITHUB_CLIENT_SECRET=somesecret \ 7 | 8 | APP_CONFIG_REDIS_HOST=localhost \ 9 | APP_CONFIG_REDIS_PORT=6379 \ 10 | APP_CONFIG_REDIS_DB=1 \ 11 | APP_CONFIG_REDIS_NAMESPACE=ghntfr \ 12 | 13 | APP_CONFIG_STATSD_HOST=localhost \ 14 | APP_CONFIG_STATSD_PORT=8125 \ 15 | 16 | APP_CONFIG_DOMAIN=gitnotifier.local \ 17 | 18 | APP_CONFIG_SECRET=somesecret \ 19 | 20 | APP_CONFIG_MAIL_ENABLE=true \ 21 | APP_CONFIG_MAIL_METHOD=smtp \ 22 | APP_CONFIG_MAIL_FROM="Git Notifier " \ 23 | APP_CONFIG_MAIL_HOST=smtp.example.org \ 24 | APP_CONFIG_MAIL_PORT=587 \ 25 | APP_CONFIG_MAIL_USER=someuser@somedomain.com \ 26 | APP_CONFIG_MAIL_PASSWORD=somepass \ 27 | 28 | APP_CONFIG_DEPLOY_ID=1 \ 29 | 30 | APP_CONFIG_EMAIL_DEV_ON_SIGNUP=false \ 31 | APP_CONFIG_DEV_EMAIL_ADDRESS=some@email.com 32 | 33 | ENTRYPOINT [ "/usr/src/app/docker/sidekiq/entrypoint.sh" ] 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra', '~> 1.4.5' 4 | gem 'github_api', '~> 0.13.0' 5 | gem "redis", "~> 3.3.3" 6 | gem 'hiredis', '~> 0.6.0' 7 | gem 'haml', '~> 4.0.6' 8 | gem 'json', '~> 2.1' 9 | gem 'sidekiq', '~> 5.0.4' 10 | gem 'mail', '~> 2.6.6' 11 | gem 'puma', '~> 3.9.1' 12 | gem 'newrelic_rpm', '~> 3.18.1' 13 | gem 'rack_csrf', '~> 2.6.0' 14 | gem 'rack-flash3', '~> 1.0.5' 15 | gem 'newrelic-redis', '~> 2.0.0' 16 | gem 'sinatra-contrib', '~> 1.4.7' 17 | gem 'dogstatsd-ruby', '~> 3.0.0' 18 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | backports (3.8.0) 6 | concurrent-ruby (1.0.5) 7 | connection_pool (2.2.1) 8 | descendants_tracker (0.0.4) 9 | thread_safe (~> 0.3, >= 0.3.1) 10 | dogstatsd-ruby (3.0.0) 11 | faraday (0.9.2) 12 | multipart-post (>= 1.2, < 3) 13 | github_api (0.13.1) 14 | addressable (~> 2.4.0) 15 | descendants_tracker (~> 0.0.4) 16 | faraday (~> 0.8, < 0.10) 17 | hashie (>= 3.4) 18 | multi_json (>= 1.7.5, < 2.0) 19 | oauth2 20 | haml (4.0.7) 21 | tilt 22 | hashie (3.4.3) 23 | hiredis (0.6.1) 24 | json (2.1.0) 25 | jwt (1.5.2) 26 | mail (2.6.6) 27 | mime-types (>= 1.16, < 4) 28 | mime-types (3.1) 29 | mime-types-data (~> 3.2015) 30 | mime-types-data (3.2016.0521) 31 | multi_json (1.12.1) 32 | multi_xml (0.5.5) 33 | multipart-post (2.0.0) 34 | newrelic-redis (2.0.2) 35 | newrelic_rpm (~> 3.11) 36 | redis (< 4.0) 37 | newrelic_rpm (3.18.1.330) 38 | oauth2 (1.0.0) 39 | faraday (>= 0.8, < 0.10) 40 | jwt (~> 1.0) 41 | multi_json (~> 1.3) 42 | multi_xml (~> 0.5) 43 | rack (~> 1.2) 44 | puma (3.9.1) 45 | rack (1.6.8) 46 | rack-flash3 (1.0.5) 47 | rack 48 | rack-protection (1.5.3) 49 | rack 50 | rack-test (0.6.3) 51 | rack (>= 1.0) 52 | rack_csrf (2.6.0) 53 | rack (>= 1.1.0) 54 | redis (3.3.3) 55 | sidekiq (5.0.4) 56 | concurrent-ruby (~> 1.0) 57 | connection_pool (~> 2.2, >= 2.2.0) 58 | rack-protection (>= 1.5.0) 59 | redis (~> 3.3, >= 3.3.3) 60 | sinatra (1.4.8) 61 | rack (~> 1.5) 62 | rack-protection (~> 1.4) 63 | tilt (>= 1.3, < 3) 64 | sinatra-contrib (1.4.7) 65 | backports (>= 2.0) 66 | multi_json 67 | rack-protection 68 | rack-test 69 | sinatra (~> 1.4.0) 70 | tilt (>= 1.3, < 3) 71 | thread_safe (0.3.5) 72 | tilt (2.0.7) 73 | 74 | PLATFORMS 75 | ruby 76 | 77 | DEPENDENCIES 78 | dogstatsd-ruby (~> 3.0.0) 79 | github_api (~> 0.13.0) 80 | haml (~> 4.0.6) 81 | hiredis (~> 0.6.0) 82 | json (~> 2.1) 83 | mail (~> 2.6.6) 84 | newrelic-redis (~> 2.0.0) 85 | newrelic_rpm (~> 3.18.1) 86 | puma (~> 3.9.1) 87 | rack-flash3 (~> 1.0.5) 88 | rack_csrf (~> 2.6.0) 89 | redis (~> 3.3.3) 90 | sidekiq (~> 5.0.4) 91 | sinatra (~> 1.4.5) 92 | sinatra-contrib (~> 1.4.7) 93 | 94 | BUNDLED WITH 95 | 1.12.5 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andrea Usuelli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Git Notifier 2 | ============================== 3 | 4 | [![Circle CI](https://circleci.com/gh/andreausu/git-notifier.svg?style=svg)](https://circleci.com/gh/andreausu/git-notifier) [![Dependency Status](https://gemnasium.com/andreausu/git-notifier.svg)](https://gemnasium.com/andreausu/git-notifier) 5 | 6 | Git Notifier is a Sinatra app that makes possible to receive email notifications for interesting GitHub events. 7 | 8 | The supported events are: 9 | - A user stars one of your repositories. 10 | - A user forks one of your repositories. 11 | - A user starts following you. 12 | - A user unfollows you. 13 | - A user that was following you was deleted. 14 | 15 | Git Notifier lets a GitHub user signup via OAuth and choose which type of notifications the user wishes to receive and at which frequency (asap, or in a nice daily or weekly report). 16 | 17 | You can take a look and use this project in production here: https://gitnotifier.io. 18 | 19 | Weekly report example 20 | ------------ 21 | 22 | ![Weekly report example](https://gitnotifier.io/img/screenshot1.png) 23 | 24 | Installation 25 | ------------ 26 | 27 | This app is fully dockerized, using the same containers for dev, CI and production, on you local env you can use docker-compose like this: 28 | 29 | Copy config/docker.env.example to config/docker.env and change the variables, then run: 30 | 31 | ``` bash 32 | $ docker-compose up # you can use the -d option to run it in background 33 | ``` 34 | 35 | At this point: 36 | - nginx should be listening on port 80 on the host where the docker daemon is running. 37 | - sidekiq will be running. 38 | - puma will be running. 39 | - redis will be running. 40 | 41 | After registering a user using the web ui, you can initiate a one off check for new notifications or emails to be sent like this: 42 | 43 | ``` 44 | docker exec githubnotifier_sidekiq_1 bundle exec ruby /usr/src/app/scripts/job_enqueuer.rb 45 | ``` 46 | 47 | There's also a docker file you can use that does this in a while loop, it's `Dockerfile_enqueuer` in the project root. 48 | 49 | Deployment 50 | ------- 51 | 52 | See here: https://github.com/andreausu/gitnotifier-provisioning. 53 | 54 | Testing 55 | ------- 56 | 57 | This project includes casperjs functional tests. 58 | 59 | On your local machine, you can run the tests like this: 60 | 61 | ``` bash 62 | $ casperjs test test/casper/unauthenticated.js 63 | $ casperjs test test/casper/authenticated.js --cookie=the-value-of-the-rack.session-cookie 64 | $ casperjs test test/casper/signup.js --username=githubuser --password=githubpassword # make sure you start with a clean redis db 65 | ``` 66 | 67 | The ultimate goal is to mock the GitHub API calls and Redis calls in order to build a proper unit tests suite, but at least for now those tests make sure that the basic functionality isn't impaired by a broken commit. 68 | 69 | License 70 | ------- 71 | 72 | This project is released under the MIT license. 73 | See the complete license: 74 | 75 | [LICENSE](LICENSE) 76 | 77 | Code contributions 78 | ---------------- 79 | 80 | If it's a feature that you think would need to be discussed please open an issue first, otherwise, you can follow this process: 81 | 82 | 1. Fork the project ( http://help.github.com/fork-a-repo/ ). 83 | 2. Create a feature branch ( `git checkout -b my_branch` ). 84 | 3. Push your changes to your new branch ( `git push origin my_branch` ). 85 | 4. Initiate a pull request on github ( http://help.github.com/send-pull-requests/ ). 86 | 5. Your pull request will be reviewed and hopefully merged :). 87 | 88 | Thanks! 89 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'haml' 4 | require 'json' 5 | require 'github_api' 6 | require 'datadog/statsd' 7 | 8 | class GitNotifier < Sinatra::Base 9 | 10 | @additional_js = nil 11 | 12 | configure :development do 13 | register Sinatra::Reloader 14 | end 15 | 16 | configure :production do 17 | set :production, true 18 | end 19 | 20 | configure do 21 | config_file = File.dirname(__FILE__) + '/config.yml' 22 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 23 | config = YAML.load_file(config_file) 24 | 25 | statsd = Datadog::Statsd.new(config['statsd']['host'], config['statsd']['port']) 26 | 27 | redis_conn = proc { 28 | Redis.new( 29 | :driver => :hiredis, 30 | :host => config['redis']['host'], 31 | :port => config['redis']['port'], 32 | :db => config['redis']['db'], 33 | network_timeout: 5 34 | ) 35 | } 36 | 37 | Sidekiq.configure_client do |cfg| 38 | cfg.redis = ConnectionPool.new(size: 27, &redis_conn) 39 | end 40 | 41 | set :CONFIG, config 42 | set :STATSD, statsd 43 | 44 | use Rack::Session::Cookie, :expire_after => 2592000, :secret => config['secret'] # 30 days 45 | 46 | use Rack::Csrf, :raise => true, :header => 'x-csrf-token' 47 | use Rack::Flash 48 | end 49 | 50 | before do 51 | @token = session['session_id'] 52 | @session = session 53 | @additional_js = [] 54 | @deploy_id = settings.CONFIG['deploy_id'] 55 | @page_title = 'Notifications for stars, forks, follow and unfollow' 56 | @custom_url = nil 57 | end 58 | 59 | get '/' do 60 | if session[:github_token] 61 | email = Sidekiq.redis do |conn| 62 | conn.hget("#{settings.CONFIG['redis']['namespace']}:users:#{session[:github_id]}", 'email') 63 | end 64 | if !email 65 | github = Github.new( 66 | client_id: settings.CONFIG['github']['client_id'], 67 | client_secret: settings.CONFIG['github']['client_secret'], 68 | oauth_token: session[:github_token] 69 | ) 70 | 71 | email_addresses = github.users.emails.list.to_a 72 | email_addresses.map! { |e| e.is_a?(String) ? e : e.email} 73 | @additional_js = ['mailcheck.min.js'] 74 | @custom_url = 'signup/homepage' 75 | NewRelic::Agent.set_transaction_name("GitNotifier/GET #{@custom_url}") 76 | haml :signup, :locals => {:email_addresses => email_addresses} 77 | else 78 | session[:email] = email 79 | @additional_js = ['spin.js', 'jquery.spin.js'] 80 | @custom_url = 'timeline' 81 | NewRelic::Agent.set_transaction_name("GitNotifier/GET #{@custom_url}") 82 | @page_title = 'Profile' 83 | haml :events, :locals => {:github_id => session[:github_id]} 84 | end 85 | else 86 | haml :index, :locals => {:index => true} 87 | end 88 | end 89 | 90 | get '/authorize' do 91 | begin 92 | github = Github.new(client_id: settings.CONFIG['github']['client_id'], client_secret: settings.CONFIG['github']['client_secret']) 93 | redirect github.authorize_url scope: 'user:email' 94 | rescue Exception => e 95 | NewRelic::Agent.notice_error(e) 96 | flash[:danger] = 'We experienced an error while connecting to GitHub, please try again.' 97 | redirect '/', 302 98 | end 99 | end 100 | 101 | get '/authorize/callback' do 102 | if !params[:code] 103 | flash[:danger] = 'Something went wrong, please try again.' 104 | redirect '/', 302 105 | end 106 | 107 | begin 108 | github = Github.new(client_id: settings.CONFIG['github']['client_id'], client_secret: settings.CONFIG['github']['client_secret']) 109 | token = github.get_token(params[:code]) 110 | rescue Exception => e 111 | NewRelic::Agent.notice_error(e) 112 | flash[:danger] = 'We experienced an error while connecting to GitHub, please try again.' 113 | redirect '/', 302 114 | end 115 | 116 | session[:github_token] = token.token 117 | 118 | user = nil 119 | Sidekiq.redis do |conn| 120 | user = conn.get("#{settings.CONFIG['redis']['namespace']}:tokens:#{token.token}") 121 | user = conn.hmget("#{settings.CONFIG['redis']['namespace']}:users:#{user}", 'login', 'github_id', 'email') if user 122 | end 123 | 124 | if user && user[0] && user[2] 125 | session[:github_login] = user[0] 126 | session[:github_id] = user[1] 127 | redirect '/' 128 | else 129 | 130 | begin 131 | github = Github.new( 132 | client_id: settings.CONFIG['github']['client_id'], 133 | client_secret: settings.CONFIG['github']['client_secret'], 134 | oauth_token: session[:github_token] 135 | ) 136 | 137 | user = github.users.get 138 | rescue Exception => e 139 | NewRelic::Agent.notice_error(e) 140 | flash[:danger] = 'We experienced an error while connecting to GitHub, please try again.' 141 | redirect '/', 302 142 | end 143 | 144 | session[:github_login] = user[:login] 145 | session[:github_id] = user[:id] 146 | 147 | current_timestamp = Time.now.to_i 148 | 149 | userExists = nil 150 | 151 | Sidekiq.redis do |conn| 152 | 153 | userExists = conn.exists("#{settings.CONFIG['redis']['namespace']}:users:#{user[:id]}") 154 | 155 | conn.multi do 156 | conn.set("#{settings.CONFIG['redis']['namespace']}:tokens:#{token.token}", user[:id]) 157 | if userExists 158 | # The user already exists, just update the token 159 | conn.hset( 160 | "#{settings.CONFIG['redis']['namespace']}:users:#{user[:id]}", 161 | :token, 162 | token.token 163 | ) 164 | else 165 | conn.hmset( 166 | "#{settings.CONFIG['redis']['namespace']}:users:#{user[:id]}", 167 | :login, user[:login], 168 | :last_event_id, 0, 169 | :token, token.token, 170 | :github_id, user[:id], 171 | :registered_on, current_timestamp, 172 | :notifications_frequency, 'daily', 173 | :last_email_sent_on, current_timestamp, 174 | :last_email_queued_on, current_timestamp, 175 | :first_check_completed, 0, 176 | :email_confirmed, 0 177 | ) 178 | end 179 | end 180 | end 181 | 182 | if userExists 183 | redirect '/', 302 184 | end 185 | 186 | NotificationsChecker.perform_async( 187 | "#{settings.CONFIG['redis']['namespace']}:users:#{user[:id]}", 188 | true 189 | ) 190 | 191 | email_addresses = github.users.emails.list.to_a 192 | email_addresses.map! { |em| em.is_a?(String) ? em : em.email} 193 | @additional_js = ['mailcheck.min.js'] 194 | haml :signup, :locals => {:email_addresses => email_addresses} 195 | end 196 | end 197 | 198 | post '/signup' do 199 | email = nil 200 | email_confirmed = 0 201 | 202 | if params[:email] == 'other_email' 203 | email = params[:other_email] 204 | else 205 | email = params[:email] 206 | email_confirmed = 1 207 | end 208 | 209 | if !email.match(/.+@.+\..+/) 210 | flash[:danger] = 'Please enter a valid email address' 211 | redirect "/", 302 212 | end 213 | 214 | Sidekiq.redis do |conn| 215 | conn.hmset( 216 | "#{settings.CONFIG['redis']['namespace']}:users:#{session[:github_id]}", 217 | :email, email, 218 | :email_confirmed, email_confirmed 219 | ) 220 | end 221 | session[:email] = email 222 | 223 | if params[:email] == 'other_email' 224 | 225 | expiry = (Time.now + 31536000).to_i.to_s 226 | 227 | digest = OpenSSL::Digest.new('sha512') 228 | hmac = OpenSSL::HMAC.hexdigest(digest, settings.CONFIG['secret'], session[:github_id].to_s + params[:other_email] + expiry) 229 | 230 | link = "#{request.scheme}://#{request.host_with_port}/signup/confirm?id=#{session[:github_id]}&email=#{CGI.escape(params[:other_email])}&expiry=#{expiry}&v=#{hmac}" 231 | 232 | Sidekiq::Client.push( 233 | 'queue' => 'send_email_signup', 234 | 'class' => SendEmail, 235 | 'args' => [email, 'Confirm your Git Notifier email address!', 'html', 'confirm', {:confirm_link => link, :username => user['login']}] 236 | ) 237 | settigs.STATSD.increment('ghntfr.business.signup') 238 | 239 | flash.now[:success] = "We have sent an email to #{email}, please open it and click on the link inside to activate your account." 240 | end 241 | 242 | if settings.CONFIG['email_dev_on_signup'] 243 | Sidekiq::Client.push( 244 | 'queue' => 'send_email', 245 | 'class' => SendEmail, 246 | 'args' => [settings.CONFIG['dev_email_address'], 'New user signup!', 'text', 'empty', {:content => "A new user just signed up! https://github.com/#{session[:github_login]}"}] 247 | ) 248 | end 249 | 250 | haml :preferences, :locals => { 251 | :disabled_notifications_type => [], 252 | :current_frequency => 'daily', 253 | :notifications_type => settings.CONFIG['notifications_type'], 254 | :notifications_frequency => settings.CONFIG['notifications_frequency'] 255 | } 256 | end 257 | 258 | get '/signup/confirm' do 259 | redirect "/", 302 if !params[:id] || !params[:email] || !params[:expiry] || !params[:v] 260 | 261 | github_id = params[:id] 262 | email = params[:email] 263 | expiry = params[:expiry] 264 | hmac = params[:v] 265 | 266 | digest = OpenSSL::Digest.new('sha512') 267 | redirect "/", 302 if hmac != OpenSSL::HMAC.hexdigest(digest, settings.CONFIG['secret'], github_id + email + expiry) 268 | 269 | if expiry.to_i < Time.now.to_i 270 | flash[:warning] = 'Expired link' 271 | redirect "/", 302 272 | end 273 | 274 | user_email = Sidekiq.redis do |conn| 275 | conn.hget("#{settings.CONFIG['redis']['namespace']}:users:#{github_id}", :email) 276 | end 277 | redirect "/", 302 if !user_email or email != user_email 278 | 279 | Sidekiq.redis do |conn| 280 | conn.hset("#{settings.CONFIG['redis']['namespace']}:users:#{github_id}", :email_confirmed, 1) 281 | end 282 | 283 | flash[:success] = "Email address #{email} successfully verified!" 284 | redirect "/", 302 285 | end 286 | 287 | get '/api/events' do 288 | 289 | return 403 if !session[:github_id] 290 | 291 | github_id = session[:github_id] 292 | events = [] 293 | eof = true 294 | 295 | user = Sidekiq.redis do |conn| 296 | conn.hgetall("#{settings.CONFIG['redis']['namespace']}:users:#{github_id}") 297 | end 298 | 299 | if user['first_check_completed'] == "0" 300 | 301 | page = params[:page] ||= 1 302 | 303 | begin 304 | github = Github.new( 305 | client_id: settings.CONFIG['github']['client_id'], 306 | client_secret: settings.CONFIG['github']['client_secret'], 307 | oauth_token: user['token'] 308 | ) 309 | response = github.activity.events.received(user['login'], page: page) 310 | rescue Exception => e 311 | NewRelic::Agent.notice_error(e) 312 | return 503 313 | end 314 | 315 | response = response.to_a 316 | response.each do |event| 317 | case event['type'] 318 | when 'WatchEvent' 319 | events << {'type' => 'star', 'entity' => event} if event['repo']['name'].include? user['login'] 320 | when 'ForkEvent' 321 | events << {'type' => 'fork', 'entity' => event} if event['repo']['name'].include? user['login'] 322 | end 323 | end 324 | 325 | eof = false if response.length >= 30 326 | 327 | else 328 | events = Sidekiq.redis do |conn| 329 | conn.lrange("#{settings.CONFIG['redis']['namespace']}:events:#{github_id}", 0, 99) 330 | end 331 | events ||= [] 332 | events.map! {|event| event = JSON.parse event} 333 | end 334 | 335 | events.map! do |event| 336 | email_body = '' 337 | type = event['type'] 338 | entity = event['entity'] 339 | case type 340 | when 'star' 341 | email_body += "#{entity['actor']['login']} starred your project #{entity['repo']['name']}" 342 | when 'fork' 343 | email_body += "#{entity['actor']['login']} forked your project #{entity['repo']['name']} to #{entity['payload']['forkee']['full_name']}" 344 | when 'follow' 345 | email_body += "#{entity['login']} started following you" 346 | when 'unfollow' 347 | email_body += "#{entity['login']} is not following you anymore" 348 | when 'deleted' 349 | email_body += "#{entity} that was following you has been deleted" 350 | end 351 | {:body => email_body, :timestamp => (defined?(event['timestamp']) && !event['timestamp'].nil? ? event['timestamp'] : ''), :type => type} 352 | end 353 | 354 | JSON.generate({:meta => {:eof => eof}, :objects => events}) 355 | 356 | end 357 | 358 | patch '/api/user/preferences' do 359 | 360 | return 403 unless session[:github_id] 361 | 362 | request.body.rewind 363 | body = request.body.read 364 | begin 365 | body = JSON.parse(body) 366 | rescue Exception => e 367 | NewRelic::Agent.notice_error(e) 368 | return 403 369 | end 370 | 371 | disabled_notifications_type = nil 372 | notifications_frequency = nil 373 | 374 | if body['disabled_notifications_type'] 375 | disabled_notifications_type = body['disabled_notifications_type'] 376 | disabled_notifications_type.each do |dnt| 377 | return 403 unless settings.CONFIG['notifications_type'].include? dnt 378 | end 379 | Sidekiq.redis do |conn| 380 | conn.hset( 381 | "#{settings.CONFIG['redis']['namespace']}:users:#{session[:github_id]}", 382 | 'disabled_notifications_type', 383 | JSON.generate(disabled_notifications_type) 384 | ) 385 | end 386 | end 387 | 388 | if body['notifications_frequency'] 389 | notifications_frequency = body['notifications_frequency'] 390 | if settings.CONFIG['notifications_frequency'].include? notifications_frequency 391 | Sidekiq.redis do |conn| 392 | conn.hset( 393 | "#{settings.CONFIG['redis']['namespace']}:users:#{session[:github_id]}", 394 | 'notifications_frequency', notifications_frequency 395 | ) 396 | end 397 | else 398 | return 403 399 | end 400 | end 401 | 402 | return [200, '{}'] 403 | end 404 | 405 | get '/unsubscribe' do 406 | 407 | redirect "/", 302 if !params[:id] || !params[:expiry] || !params[:v] 408 | 409 | @page_title = 'Unsubscribe' 410 | 411 | github_id = params[:id] 412 | expiry = params[:expiry] 413 | hmac = params[:v] 414 | 415 | digest = OpenSSL::Digest.new('sha512') 416 | redirect "/", 302 if hmac != OpenSSL::HMAC.hexdigest(digest, settings.CONFIG['secret'], github_id + expiry) 417 | if expiry.to_i < Time.now.to_i 418 | flash[:warning] = 'Expired link.' 419 | redirect "/", 302 420 | end 421 | redirect "/", 302 if Sidekiq.redis { |conn| !conn.exists("#{settings.CONFIG['redis']['namespace']}:users:#{github_id}") } 422 | 423 | res = Sidekiq.redis do |conn| 424 | conn.hmget( 425 | "#{settings.CONFIG['redis']['namespace']}:users:#{github_id}", 426 | 'disabled_notifications_type', 427 | 'notifications_frequency' 428 | ) 429 | end 430 | notifications_frequency = res[1] 431 | disabled_notifications = res[0] 432 | if disabled_notifications 433 | disabled_notifications = JSON.parse(disabled_notifications) 434 | else 435 | disabled_notifications = [] 436 | end 437 | 438 | timestamp = Time.now.to_i.to_s 439 | hmac = OpenSSL::HMAC.hexdigest(digest, settings.CONFIG['secret'], github_id + timestamp) 440 | 441 | haml :unsubscribe, :locals => { 442 | :disabled_notifications_type => disabled_notifications, 443 | :github_id => github_id, 444 | :current_frequency => notifications_frequency, 445 | :notifications_type => settings.CONFIG['notifications_type'], 446 | :notifications_frequency => settings.CONFIG['notifications_frequency'], 447 | :hmac => hmac, 448 | :timestamp => timestamp 449 | } 450 | end 451 | 452 | post '/unsubscribe' do 453 | 454 | redirect "/", 302 if !params[:id] 455 | redirect "/", 302 if !params[:timestamp] 456 | redirect "/", 302 if !params[:v] 457 | 458 | digest = OpenSSL::Digest.new('sha512') 459 | redirect "/", 302 if params[:v] != OpenSSL::HMAC.hexdigest(digest, settings.CONFIG['secret'], params[:id] + params[:timestamp]) 460 | 461 | disabled_notifications_type = nil 462 | notifications_frequency = nil 463 | 464 | if params[:change_frequency] 465 | notifications_frequency = params[:notifications_frequency] 466 | elsif params[:unsubscribe] 467 | disabled_notifications_type = settings.CONFIG['notifications_type'] - params[:notifications] 468 | elsif params[:unsubscribe_all] 469 | disabled_notifications_type = settings.CONFIG['notifications_type'] 470 | else 471 | redirect "/", 302 472 | end 473 | 474 | if disabled_notifications_type 475 | Sidekiq.redis do |conn| 476 | conn.hset( 477 | "#{settings.CONFIG['redis']['namespace']}:users:#{params[:id]}", 478 | 'disabled_notifications_type', JSON.generate(disabled_notifications_type) 479 | ) 480 | end 481 | else 482 | Sidekiq.redis do |conn| 483 | conn.hset( 484 | "#{settings.CONFIG['redis']['namespace']}:users:#{params[:id]}", 485 | 'notifications_frequency', notifications_frequency 486 | ) 487 | end 488 | end 489 | 490 | flash[:success] = 'Sucessfully unsubscribed!' 491 | redirect '/', 302 492 | end 493 | 494 | get '/user/preferences' do 495 | 496 | @page_title = 'Preferences' 497 | 498 | github_id = session[:github_id] 499 | res = Sidekiq.redis do |conn| 500 | conn.hmget( 501 | "#{settings.CONFIG['redis']['namespace']}:users:#{github_id}", 502 | 'disabled_notifications_type', 503 | 'notifications_frequency' 504 | ) 505 | end 506 | notifications_frequency = res[1] 507 | disabled_notifications_type = res[0] 508 | if disabled_notifications_type 509 | disabled_notifications_type = JSON.parse(disabled_notifications_type) 510 | else 511 | disabled_notifications_type = [] 512 | end 513 | 514 | haml :preferences, :locals => { 515 | :disabled_notifications_type => disabled_notifications_type, 516 | :current_frequency => notifications_frequency, 517 | :notifications_type => settings.CONFIG['notifications_type'], 518 | :notifications_frequency => settings.CONFIG['notifications_frequency'], 519 | :signup => "false" 520 | } 521 | end 522 | 523 | get '/logout' do 524 | session.clear 525 | redirect '/' 526 | end 527 | 528 | get '/faq' do 529 | @page_title = 'FAQ' 530 | haml :faq 531 | end 532 | 533 | helpers do 534 | def get_menu() 535 | if @session[:email] 536 | [ 537 | {:href => '/', :desc => 'Profile'}, 538 | {:href => '/user/preferences', :desc => 'Preferences'}, 539 | {:href => '/faq', :desc => 'FAQ'}, 540 | {:href => '/logout', :desc => 'Logout'} 541 | ] 542 | else 543 | [ 544 | {:href => (request.path_info == '/faq' ? '/' : '') + '#features', :desc => 'Features'}, 545 | {:href => (request.path_info == '/faq' ? '/' : '') + '#tour-head', :desc => 'Tour'}, 546 | {:href => '/faq', :desc => 'FAQ'}, 547 | {:href => (request.path_info == '/faq' ? '/' : '') + '#stack', :desc => 'Stack'}, 548 | {:href => "mailto:#{settings.CONFIG['dev_email_address']}", :desc => 'Contact Us'} 549 | ] 550 | end 551 | end 552 | end 553 | 554 | def csrf_token() 555 | Rack::Csrf.csrf_token(env) 556 | end 557 | 558 | def csrf_tag() 559 | Rack::Csrf.csrf_tag(env) 560 | end 561 | 562 | def get_flash(index=false) 563 | output = '' 564 | if !flash.keys.empty? 565 | output = '
' if index 566 | output += ' 567 | ' 571 | output += '
' if index 572 | end 573 | output 574 | end 575 | 576 | def get_additional_js() 577 | add_js = '' 578 | @additional_js.each do |js| 579 | add_js += "" 580 | end 581 | 582 | add_js 583 | end 584 | 585 | def get_head_js() 586 | js = '' 587 | if defined?(settings.production) && settings.production 588 | pageView = "ga('send', 'pageview');" 589 | pageView = "ga('send', 'pageview', '/#{@custom_url}');" if @custom_url 590 | logged_in = (defined?(session[:github_id]) && session[:github_id] ? 1 : 0) 591 | 592 | js += "" 598 | end 599 | js 600 | end 601 | 602 | end 603 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-notifier", 3 | "version": "0.2.0", 4 | "authors": [ 5 | "Andrea Usuelli " 6 | ], 7 | "license": "MIT", 8 | "private": true, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "bootstrap-social": "4.10.1", 18 | "bootstrap": "3.3.6", 19 | "jquery": "2.1.4", 20 | "font-awesome": "4.5.0", 21 | "mailcheck": "1.1.1", 22 | "html5shiv": "3.7.3", 23 | "respond": "1.4.2", 24 | "spin.js": "2.3.2", 25 | "octicons": "3.4.1" 26 | }, 27 | "resolutions": { 28 | "font-awesome": "4.4.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | ## Customize the test machine 2 | machine: 3 | services: 4 | - docker 5 | 6 | node: 7 | version: 0.12.0 8 | 9 | # Override /etc/hosts 10 | hosts: 11 | gitnotifier.local: 127.0.0.1 12 | 13 | dependencies: 14 | cache_directories: 15 | - "~/.cache/bower" 16 | - "~/.npm" 17 | 18 | override: 19 | - docker -v 20 | - sudo curl -L https://github.com/docker/compose/releases/download/1.5.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 21 | - sudo chmod +x /usr/local/bin/docker-compose 22 | - npm install -g casperjs@1.1.4 23 | - npm install -g bower 24 | - cp config/docker.env.example config/docker.env 25 | - sed -i 's/APP_CONFIG_GITHUB_CLIENT_ID=.*/APP_CONFIG_GITHUB_CLIENT_ID='"$APP_CONFIG_GITHUB_CLIENT_ID"'/g' config/docker.env 26 | - sed -i 's/APP_CONFIG_GITHUB_CLIENT_SECRET=.*/APP_CONFIG_GITHUB_CLIENT_SECRET='"$APP_CONFIG_GITHUB_CLIENT_SECRET"'/g' config/docker.env 27 | - bower install 28 | - docker-compose build 29 | - docker-compose up -d 30 | 31 | test: 32 | override: 33 | - casperjs test test/casper/unauthenticated.js 34 | post: 35 | - docker ps -a | grep gitnotifier_ | awk '{print $1}' | xargs -n 1 docker logs 36 | - docker-compose kill 37 | - sudo git reset --hard 38 | - sudo git clean -f -x -d 39 | 40 | deployment: 41 | hub: 42 | branch: /.*/ 43 | commands: 44 | - docker login -e "$DOCKER_EMAIL" -u "$DOCKER_USER" -p "$DOCKER_PASS" 45 | - bower install 46 | - docker build -f Dockerfile_puma -t gitnotifier/puma:latest . 47 | - docker build -f Dockerfile_sidekiq -t gitnotifier/sidekiq:latest . 48 | - docker build -f Dockerfile_enqueuer -t gitnotifier/enqueuer:latest . 49 | - docker tag gitnotifier/puma gitnotifier/puma:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 50 | - docker tag gitnotifier/sidekiq gitnotifier/sidekiq:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 51 | - docker tag gitnotifier/enqueuer gitnotifier/enqueuer:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 52 | - docker push gitnotifier/puma:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 53 | - docker push gitnotifier/sidekiq:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 54 | - docker push gitnotifier/enqueuer:`echo "$CIRCLE_BRANCH" | sed 's/[^a-zA-Z0-9_\-]/-/g'` 55 | - docker push gitnotifier/puma:latest 56 | - docker push gitnotifier/sidekiq:latest 57 | - docker push gitnotifier/enqueuer:latest 58 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | GC::Profiler.enable 2 | 3 | require 'sinatra/base' 4 | require "sinatra/reloader" 5 | require 'rack/csrf' 6 | require 'rack-flash' 7 | require 'yaml' 8 | require 'redis' 9 | require 'sidekiq' 10 | require_relative 'workers/notifications_checker' 11 | require_relative 'workers/send_email' 12 | require_relative 'app.rb' 13 | require 'newrelic-redis' 14 | require 'newrelic_rpm' # it should be the last entry in the require list 15 | 16 | run GitNotifier 17 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | github: 3 | client_id: APP_CONFIG_GITHUB_CLIENT_ID 4 | client_secret: APP_CONFIG_GITHUB_CLIENT_SECRET 5 | 6 | redis: 7 | host: APP_CONFIG_REDIS_HOST # localhost 8 | port: APP_CONFIG_REDIS_PORT # 6379 9 | db: APP_CONFIG_REDIS_DB # 0 10 | namespace: APP_CONFIG_REDIS_NAMESPACE # ghntfr 11 | 12 | statsd: 13 | host: APP_CONFIG_STATSD_HOST # localhost 14 | port: APP_CONFIG_STATSD_PORT # 8125 15 | 16 | mail: 17 | enabled: APP_CONFIG_MAIL_ENABLE # true 18 | method: APP_CONFIG_MAIL_METHOD # smtp 19 | from: APP_CONFIG_MAIL_FROM # Git Notifier 20 | host: APP_CONFIG_MAIL_HOST # smtp.example.org 21 | port: APP_CONFIG_MAIL_PORT # 587 22 | user: APP_CONFIG_MAIL_USER # someuser@somedomain.com 23 | password: APP_CONFIG_MAIL_PASSWORD # somepass 24 | ssl: APP_CONFIG_MAIL_SSL # true 25 | 26 | domain: APP_CONFIG_DOMAIN # localhost:4567 27 | 28 | secret: APP_CONFIG_SECRET # somesecret 29 | 30 | notifications_type: 31 | - star 32 | - fork 33 | - follow 34 | - unfollow 35 | - deleted 36 | - site-news 37 | 38 | notifications_frequency: 39 | - asap 40 | - daily 41 | - weekly 42 | 43 | deploy_id: APP_CONFIG_DEPLOY_ID # 1 44 | 45 | email_dev_on_signup: APP_CONFIG_EMAIL_DEV_ON_SIGNUP # false 46 | dev_email_address: APP_CONFIG_DEV_EMAIL_ADDRESS # some@email.com 47 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/config/.gitkeep -------------------------------------------------------------------------------- /config/docker.env.example: -------------------------------------------------------------------------------- 1 | # App 2 | 3 | APP_ENV=development 4 | APP_CONFIG_GITHUB_CLIENT_ID=someid 5 | APP_CONFIG_GITHUB_CLIENT_SECRET=somesecret 6 | APP_CONFIG_REDIS_HOST=redis 7 | APP_CONFIG_REDIS_PORT=6379 8 | APP_CONFIG_REDIS_DB=1 9 | APP_CONFIG_REDIS_NAMESPACE=ghntfr 10 | APP_CONFIG_STATSD_HOST=localhost 11 | APP_CONFIG_STATSD_PORT=8125 12 | APP_CONFIG_DOMAIN=gitnotifier.local 13 | APP_CONFIG_SECRET=somesecret 14 | APP_CONFIG_MAIL_ENABLE=true 15 | APP_CONFIG_MAIL_METHOD=smtp 16 | APP_CONFIG_MAIL_FROM="Git Notifier " 17 | APP_CONFIG_MAIL_HOST=smtp.example.org 18 | APP_CONFIG_MAIL_PORT=587 19 | APP_CONFIG_MAIL_USER=someuser@somedomain.com 20 | APP_CONFIG_MAIL_PASSWORD=somepass 21 | APP_CONFIG_MAIL_SSL=true 22 | APP_CONFIG_DEPLOY_ID=1 23 | APP_CONFIG_EMAIL_DEV_ON_SIGNUP=false 24 | APP_CONFIG_DEV_EMAIL_ADDRESS=some@email.com 25 | 26 | # Enqueuer 27 | 28 | APP_ENQUEUER_SLEEP_TIME=60 29 | 30 | # Nginx 31 | 32 | APP_UPSTREAM_PUMA=githubnotifier_puma_1 33 | -------------------------------------------------------------------------------- /config/newrelic.example.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python and Node applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated November 17, 2014 7 | # 8 | # This configuration file is custom generated for Usu. 9 | 10 | 11 | # Here are the settings that are common to all environments 12 | common: &default_settings 13 | # ============================== LICENSE KEY =============================== 14 | 15 | # You must specify the license key associated with your New Relic 16 | # account. This key binds your Agent's data to your account in the 17 | # New Relic service. 18 | license_key: 'APP_NEWRELIC_API_KEY' 19 | 20 | # Agent Enabled (Ruby/Rails Only) 21 | # Use this setting to force the agent to run or not run. 22 | # Default is 'auto' which means the agent will install and run only 23 | # if a valid dispatcher such as Mongrel is running. This prevents 24 | # it from running with Rake or the console. Set to false to 25 | # completely turn the agent off regardless of the other settings. 26 | # Valid values are true, false and auto. 27 | # 28 | # agent_enabled: auto 29 | 30 | # Application Name Set this to be the name of your application as 31 | # you'd like it show up in New Relic. The service will then auto-map 32 | # instances of your application into an "application" on your 33 | # dashboard page. If you want to map this instance into multiple 34 | # apps, like "AJAX Requests" and "All UI" then specify a semicolon 35 | # separated list of up to three distinct names, or a yaml list. 36 | # Defaults to the capitalized RAILS_ENV or RACK_ENV (i.e., 37 | # Production, Staging, etc) 38 | # 39 | # Example: 40 | # 41 | # app_name: 42 | # - Ajax Service 43 | # - All Services 44 | # 45 | # Caution: If you change this name, a new application will appear in the New 46 | # Relic user interface with the new name, and data will stop reporting to the 47 | # app with the old name. 48 | # 49 | # See https://newrelic.com/docs/site/renaming-applications for more details 50 | # on renaming your New Relic applications. 51 | # 52 | app_name: Git Notifier 53 | 54 | # When "true", the agent collects performance data about your 55 | # application and reports this data to the New Relic service at 56 | # newrelic.com. This global switch is normally overridden for each 57 | # environment below. (formerly called 'enabled') 58 | monitor_mode: true 59 | 60 | # Developer mode should be off in every environment but 61 | # development as it has very high overhead in memory. 62 | developer_mode: false 63 | 64 | # The newrelic agent generates its own log file to keep its logging 65 | # information separate from that of your application. Specify its 66 | # log level here. 67 | log_level: info 68 | 69 | # Optionally set the path to the log file This is expanded from the 70 | # root directory (may be relative or absolute, e.g. 'log/' or 71 | # '/var/log/') The agent will attempt to create this directory if it 72 | # does not exist. 73 | # log_file_path: 'log' 74 | 75 | # Optionally set the name of the log file, defaults to 'newrelic_agent.log' 76 | # log_file_name: 'newrelic_agent.log' 77 | 78 | # The newrelic agent communicates with the service via https by default. This 79 | # prevents eavesdropping on the performance metrics transmitted by the agent. 80 | # The encryption required by SSL introduces a nominal amount of CPU overhead, 81 | # which is performed asynchronously in a background thread. If you'd prefer 82 | # to send your metrics over http uncomment the following line. 83 | # ssl: false 84 | 85 | #============================== Browser Monitoring =============================== 86 | # New Relic Real User Monitoring gives you insight into the performance real users are 87 | # experiencing with your website. This is accomplished by measuring the time it takes for 88 | # your users' browsers to download and render your web pages by injecting a small amount 89 | # of JavaScript code into the header and footer of each page. 90 | browser_monitoring: 91 | # By default the agent automatically injects the monitoring JavaScript 92 | # into web pages. Set this attribute to false to turn off this behavior. 93 | auto_instrument: true 94 | 95 | # Proxy settings for connecting to the New Relic server. 96 | # 97 | # If a proxy is used, the host setting is required. Other settings 98 | # are optional. Default port is 8080. 99 | # 100 | # proxy_host: hostname 101 | # proxy_port: 8080 102 | # proxy_user: 103 | # proxy_pass: 104 | 105 | # The agent can optionally log all data it sends to New Relic servers to a 106 | # separate log file for human inspection and auditing purposes. To enable this 107 | # feature, change 'enabled' below to true. 108 | # See: https://newrelic.com/docs/ruby/audit-log 109 | audit_log: 110 | enabled: false 111 | 112 | # Tells transaction tracer and error collector (when enabled) 113 | # whether or not to capture HTTP params. When true, frameworks can 114 | # exclude HTTP parameters from being captured. 115 | # Rails: the RoR filter_parameter_logging excludes parameters 116 | # Java: create a config setting called "ignored_params" and set it to 117 | # a comma separated list of HTTP parameter names. 118 | # ex: ignored_params: credit_card, ssn, password 119 | capture_params: false 120 | 121 | # Transaction tracer captures deep information about slow 122 | # transactions and sends this to the New Relic service once a 123 | # minute. Included in the transaction is the exact call sequence of 124 | # the transactions including any SQL statements issued. 125 | transaction_tracer: 126 | 127 | # Transaction tracer is enabled by default. Set this to false to 128 | # turn it off. This feature is only available at the Professional 129 | # and above product levels. 130 | enabled: true 131 | 132 | # Threshold in seconds for when to collect a transaction 133 | # trace. When the response time of a controller action exceeds 134 | # this threshold, a transaction trace will be recorded and sent to 135 | # New Relic. Valid values are any float value, or (default) "apdex_f", 136 | # which will use the threshold for an dissatisfying Apdex 137 | # controller action - four times the Apdex T value. 138 | transaction_threshold: apdex_f 139 | 140 | # When transaction tracer is on, SQL statements can optionally be 141 | # recorded. The recorder has three modes, "off" which sends no 142 | # SQL, "raw" which sends the SQL statement in its original form, 143 | # and "obfuscated", which strips out numeric and string literals. 144 | record_sql: obfuscated 145 | 146 | # Threshold in seconds for when to collect stack trace for a SQL 147 | # call. In other words, when SQL statements exceed this threshold, 148 | # then capture and send to New Relic the current stack trace. This is 149 | # helpful for pinpointing where long SQL calls originate from. 150 | stack_trace_threshold: 0.500 151 | 152 | # Determines whether the agent will capture query plans for slow 153 | # SQL queries. Only supported in mysql and postgres. Should be 154 | # set to false when using other adapters. 155 | # explain_enabled: true 156 | 157 | # Threshold for query execution time below which query plans will 158 | # not be captured. Relevant only when `explain_enabled` is true. 159 | # explain_threshold: 0.5 160 | 161 | # Error collector captures information about uncaught exceptions and 162 | # sends them to New Relic for viewing 163 | error_collector: 164 | 165 | # Error collector is enabled by default. Set this to false to turn 166 | # it off. This feature is only available at the Professional and above 167 | # product levels. 168 | enabled: true 169 | 170 | # To stop specific errors from reporting to New Relic, set this property 171 | # to comma-separated values. Default is to ignore routing errors, 172 | # which are how 404's get triggered. 173 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 174 | 175 | # If you're interested in capturing memcache keys as though they 176 | # were SQL uncomment this flag. Note that this does increase 177 | # overhead slightly on every memcached call, and can have security 178 | # implications if your memcached keys are sensitive 179 | # capture_memcache_keys: true 180 | 181 | # Application Environments 182 | # ------------------------------------------ 183 | # Environment-specific settings are in this section. 184 | # For Rails applications, RAILS_ENV is used to determine the environment. 185 | # For Java applications, pass -Dnewrelic.environment to set 186 | # the environment. 187 | 188 | # NOTE if your application has other named environments, you should 189 | # provide newrelic configuration settings for these environments here. 190 | 191 | development: 192 | <<: *default_settings 193 | # Turn on communication to New Relic service in development mode 194 | monitor_mode: true 195 | app_name: Git Notifier (Development) 196 | 197 | # Rails Only - when running in Developer Mode, the New Relic Agent will 198 | # present performance information on the last 100 transactions you have 199 | # executed since starting the mongrel. 200 | # NOTE: There is substantial overhead when running in developer mode. 201 | # Do not use for production or load testing. 202 | developer_mode: true 203 | 204 | test: 205 | <<: *default_settings 206 | # It almost never makes sense to turn on the agent when running 207 | # unit, functional or integration tests or the like. 208 | monitor_mode: false 209 | 210 | # Turn on the agent in production for 24x7 monitoring. NewRelic 211 | # testing shows an average performance impact of < 5 ms per 212 | # transaction, you can leave this on all the time without 213 | # incurring any user-visible performance degradation. 214 | production: 215 | <<: *default_settings 216 | monitor_mode: true 217 | 218 | # Many applications have a staging environment which behaves 219 | # identically to production. Support for that environment is provided 220 | # here. By default, the staging environment has the agent turned on. 221 | staging: 222 | <<: *default_settings 223 | monitor_mode: true 224 | app_name: Git Notifier (Staging) 225 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env puma 2 | 3 | @dir = File.expand_path File.dirname(__FILE__) + '/../' 4 | 5 | # The directory to operate out of. 6 | # 7 | # The default is the current directory. 8 | # 9 | directory @dir 10 | 11 | # Set the environment in which the rack's app will run. The value must be a string. 12 | # 13 | # The default is “development”. 14 | # 15 | #environment 'development' 16 | 17 | # Daemonize the server into the background. Highly suggest that 18 | # this be combined with “pidfile” and “stdout_redirect”. 19 | # 20 | # The default is “false”. 21 | # 22 | # daemonize 23 | daemonize false 24 | 25 | # Store the pid of the server in the file at “path”. 26 | # 27 | pidfile "#{@dir}/tmp/pids/puma.pid" 28 | 29 | # Use “path” as the file to store the server info state. This is 30 | # used by “pumactl” to query and control the server. 31 | # 32 | state_path "#{@dir}/tmp/pids/puma.state" 33 | 34 | # Redirect STDOUT and STDERR to files specified. The 3rd parameter 35 | # (“append”) specifies whether the output is appended, the default is 36 | # “false”. 37 | # 38 | #stdout_redirect "#{@dir}/log/puma.stdout", "#{@dir}/log/puma.stderr", true 39 | 40 | # Disable request logging. 41 | # 42 | # The default is “false”. 43 | # 44 | # quiet 45 | 46 | # Configure “min” to be the minimum number of threads to use to answer 47 | # requests and “max” the maximum. 48 | # 49 | # The default is “0, 16”. 50 | # 51 | threads 0, 32 52 | 53 | # Bind the server to “url”. “tcp://”, “unix://” and “ssl://” are the only 54 | # accepted protocols. 55 | # 56 | # The default is “tcp://0.0.0.0:9292”. 57 | # 58 | # bind 'tcp://0.0.0.0:9292' 59 | # bind 'unix:///var/run/puma.sock' 60 | # bind 'unix:///var/run/puma.sock?umask=0111' 61 | # bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert' 62 | 63 | bind 'tcp://0.0.0.0:9292' 64 | #bind "unix://#{@dir}/tmp/sockets/puma.sock" 65 | 66 | # Instead of “bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'” you 67 | # can also use the “ssl_bind” option. 68 | # 69 | # ssl_bind '127.0.0.1', '9292', { key: path_to_key, cert: path_to_cert } 70 | 71 | # Code to run before doing a restart. This code should 72 | # close log files, database connections, etc. 73 | # 74 | # This can be called multiple times to add code each time. 75 | # 76 | # on_restart do 77 | # puts 'On restart...' 78 | # end 79 | 80 | # Command to use to restart puma. This should be just how to 81 | # load puma itself (ie. 'ruby -Ilib bin/puma'), not the arguments 82 | # to puma, as those are the same as the original process. 83 | # 84 | # restart_command '/u/app/lolcat/bin/restart_puma' 85 | 86 | # === Cluster mode === 87 | 88 | # How many worker processes to run. 89 | # 90 | # The default is “0”. 91 | # 92 | # workers 2 93 | 94 | # Code to run when a worker boots to setup the process before booting 95 | # the app. 96 | # 97 | # This can be called multiple times to add hooks. 98 | # 99 | # on_worker_boot do 100 | # puts 'On worker boot...' 101 | # end 102 | 103 | # Code to run when a worker boots to setup the process after booting 104 | # the app. 105 | # 106 | # This can be called multiple times to add hooks. 107 | # 108 | # after_worker_boot do 109 | # puts 'On worker boot...' 110 | # end 111 | 112 | # Code to run when a worker shutdown. 113 | # 114 | # 115 | # on_worker_shutdown do 116 | # puts 'On worker boot...' 117 | # end 118 | 119 | # Allow workers to reload bundler context when master process is issued 120 | # a USR1 signal. This allows proper reloading of gems while the master 121 | # is preserved across a phased-restart. (incompatible with preload_app) 122 | # (off by default) 123 | 124 | prune_bundler 125 | 126 | # Preload the application before starting the workers; this conflicts with 127 | # phased restart feature. (off by default) 128 | 129 | # preload_app! 130 | 131 | # Additional text to display in process listing 132 | # 133 | # tag 'app name' 134 | 135 | # Change the default timeout of workers 136 | # 137 | # worker_timeout 60 138 | 139 | # === Puma control rack application === 140 | 141 | # Start the puma control rack application on “url”. This application can 142 | # be communicated with to control the main server. Additionally, you can 143 | # provide an authentication token, so all requests to the control server 144 | # will need to include that token as a query parameter. This allows for 145 | # simple authentication. 146 | # 147 | # Check out https://github.com/puma/puma/blob/master/lib/puma/app/status.rb 148 | # to see what the app has available. 149 | # 150 | # activate_control_app 'unix:///var/run/pumactl.sock' 151 | # activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' } 152 | # activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true } 153 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | # set path to app that will be used to configure unicorn, 2 | # note the trailing slash in this example 3 | @dir = File.expand_path File.dirname(__FILE__) 4 | 5 | worker_processes 1 6 | working_directory @dir 7 | 8 | timeout 30 9 | 10 | # Specify path to socket unicorn listens to, 11 | # we will use this in our nginx.conf later 12 | listen "#{@dir}/tmp/sockets/puma.sock", :backlog => 64 13 | 14 | # Set process id path 15 | pid "#{@dir}/tmp/pids/unicorn.pid" 16 | 17 | # Set log file paths 18 | stderr_path "#{@dir}/log/unicorn.stderr.log" 19 | stdout_path "#{@dir}/log/unicorn.stdout.log" 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | sidekiq: 2 | build: . 3 | dockerfile: Dockerfile_sidekiq 4 | links: 5 | - redis 6 | volumes: 7 | - .:/usr/src/app 8 | env_file: config/docker.env 9 | 10 | nginx: 11 | build: . 12 | dockerfile: Dockerfile_nginx 13 | links: 14 | - puma 15 | ports: 16 | - "80:80" 17 | - "443:443" 18 | volumes: 19 | - .:/var/www/github-notifier/current 20 | env_file: config/docker.env 21 | 22 | puma: 23 | build: . 24 | dockerfile: Dockerfile_puma 25 | links: 26 | - redis 27 | volumes: 28 | - .:/usr/src/app 29 | env_file: config/docker.env 30 | ports: 31 | - "9292:9292" 32 | 33 | redis: 34 | image: redis:3 35 | command: redis-server --appendonly yes # --dbfilename dump.rdb --dir /data 36 | volumes: 37 | - /data 38 | -------------------------------------------------------------------------------- /docker/enqueuer/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /usr/src/app 5 | cp config.yml.example config.yml 6 | 7 | if [ "$APP_NEWRELIC_API_KEY" ]; then 8 | cp config/newrelic.example.yml config/newrelic.yml 9 | sed -i 's/APP_NEWRELIC_API_KEY/'"$APP_NEWRELIC_API_KEY"'/g' /usr/src/app/config/newrelic.yml 10 | fi 11 | 12 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_ID/'"$APP_CONFIG_GITHUB_CLIENT_ID"'/g' /usr/src/app/config.yml 13 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_SECRET/'"$APP_CONFIG_GITHUB_CLIENT_SECRET"'/g' /usr/src/app/config.yml 14 | 15 | sed -i 's/APP_CONFIG_REDIS_HOST/'"$APP_CONFIG_REDIS_HOST"'/g' /usr/src/app/config.yml 16 | sed -i 's/APP_CONFIG_REDIS_PORT/'"$APP_CONFIG_REDIS_PORT"'/g' /usr/src/app/config.yml 17 | sed -i 's/APP_CONFIG_REDIS_DB/'"$APP_CONFIG_REDIS_DB"'/g' /usr/src/app/config.yml 18 | sed -i 's/APP_CONFIG_REDIS_NAMESPACE/'"$APP_CONFIG_REDIS_NAMESPACE"'/g' /usr/src/app/config.yml 19 | 20 | sed -i 's/APP_CONFIG_STATSD_HOST/'"$APP_CONFIG_STATSD_HOST"'/g' /usr/src/app/config.yml 21 | sed -i 's/APP_CONFIG_STATSD_PORT/'"$APP_CONFIG_STATSD_PORT"'/g' /usr/src/app/config.yml 22 | 23 | sed -i 's/APP_CONFIG_DOMAIN/'"$APP_CONFIG_DOMAIN"'/g' /usr/src/app/config.yml 24 | 25 | sed -i 's/APP_CONFIG_SECRET/'"$APP_CONFIG_SECRET"'/g' /usr/src/app/config.yml 26 | 27 | sed -i 's/APP_CONFIG_DEPLOY_ID/'"$APP_CONFIG_DEPLOY_ID"'/g' /usr/src/app/config.yml 28 | 29 | sed -i 's/APP_CONFIG_EMAIL_DEV_ON_SIGNUP/'"$APP_CONFIG_EMAIL_DEV_ON_SIGNUP"'/g' /usr/src/app/config.yml 30 | sed -i 's/APP_CONFIG_DEV_EMAIL_ADDRESS/'"$APP_CONFIG_DEV_EMAIL_ADDRESS"'/g' /usr/src/app/config.yml 31 | 32 | export RUBY_ENV=$APP_ENV 33 | 34 | while true; do bundle exec /usr/local/bin/ruby scripts/job_enqueuer.rb; sleep "$APP_ENQUEUER_SLEEP_TIME"; done 35 | -------------------------------------------------------------------------------- /docker/nginx/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sed -i 's/APP_CONFIG_DOMAIN/'"$APP_CONFIG_DOMAIN"'/g' /etc/nginx/conf.d/gitnotifier 5 | 6 | sed -i 's/APP_UPSTREAM_PUMA/'"$APP_UPSTREAM_PUMA"'/g' /etc/nginx/conf.d/gitnotifier 7 | 8 | exec nginx -c /etc/nginx/nginx.conf 9 | -------------------------------------------------------------------------------- /docker/nginx/gitnotifier: -------------------------------------------------------------------------------- 1 | upstream puma { 2 | server puma:9292 fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80 default; 7 | server_name APP_CONFIG_DOMAIN; 8 | root /var/www/github-notifier/current/public; 9 | 10 | access_log /var/log/nginx/github.notifier.production.access.log main; 11 | error_log /var/log/nginx/github.notifier.production.error.log; 12 | 13 | location ~ ^/(img/|css/|js/|fonts/|humans.txt) { 14 | expires max; 15 | add_header Pragma public; 16 | add_header Cache-Control "public"; 17 | } 18 | 19 | location = /robots.txt { 20 | return 200 "User-agent: *\nAllow: /"; 21 | expires max; 22 | add_header Pragma public; 23 | add_header Cache-Control "public"; 24 | } 25 | 26 | location / { 27 | try_files $uri @app; 28 | } 29 | 30 | location @app { 31 | proxy_http_version 1.1; 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_set_header Host $http_host; 34 | proxy_redirect off; 35 | proxy_pass http://puma; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data www-data; 2 | worker_processes 1; 3 | pid /var/run/nginx.pid; 4 | worker_rlimit_nofile 20000; 5 | 6 | daemon off; 7 | 8 | events { 9 | worker_connections 20000; 10 | use epoll; 11 | } 12 | 13 | http { 14 | include mime.types; 15 | include /etc/nginx/fastcgi_params; 16 | 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | error_log /var/log/nginx/error.log; 25 | 26 | sendfile on; 27 | tcp_nopush on; 28 | server_names_hash_bucket_size 128; # this seems to be required for some vhosts 29 | client_max_body_size 12m; 30 | large_client_header_buffers 8 32k; 31 | 32 | keepalive_timeout 300; 33 | 34 | open_file_cache max=5000 inactive=180s; 35 | open_file_cache_valid 60s; 36 | open_file_cache_min_uses 1; 37 | open_file_cache_errors off; 38 | 39 | # output compression saves bandwidth 40 | gzip on; 41 | gzip_proxied any; 42 | gzip_http_version 1.1; 43 | gzip_min_length 300; 44 | gzip_comp_level 6; 45 | #gzip_buffers 4 8k; 46 | gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/atom+xml; 47 | #gzip_vary on; 48 | gzip_disable "MSIE [1-6]\."; 49 | 50 | ssl off; 51 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 52 | ssl_prefer_server_ciphers on; 53 | ssl_ciphers ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH; # https://community.qualys.com/blogs/securitylabs/2011/10/17/mitigating-the-beast-attack-on-tls 54 | 55 | # CloudFlare 56 | set_real_ip_from 199.27.128.0/21; 57 | set_real_ip_from 173.245.48.0/20; 58 | set_real_ip_from 103.21.244.0/22; 59 | set_real_ip_from 103.22.200.0/22; 60 | set_real_ip_from 103.31.4.0/22; 61 | set_real_ip_from 141.101.64.0/18; 62 | set_real_ip_from 108.162.192.0/18; 63 | set_real_ip_from 190.93.240.0/20; 64 | set_real_ip_from 188.114.96.0/20; 65 | set_real_ip_from 197.234.240.0/22; 66 | set_real_ip_from 198.41.128.0/17; 67 | set_real_ip_from 162.158.0.0/15; 68 | set_real_ip_from 104.16.0.0/12; 69 | 70 | real_ip_header X-Forwarded-For; 71 | 72 | include /etc/nginx/conf.d/*; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /docker/puma/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /usr/src/app 5 | cp config.yml.example config.yml 6 | 7 | if [ "$APP_NEWRELIC_API_KEY" ]; then 8 | cp config/newrelic.example.yml config/newrelic.yml 9 | sed -i 's/APP_NEWRELIC_API_KEY/'"$APP_NEWRELIC_API_KEY"'/g' /usr/src/app/config/newrelic.yml 10 | fi 11 | 12 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_ID/'"$APP_CONFIG_GITHUB_CLIENT_ID"'/g' /usr/src/app/config.yml 13 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_SECRET/'"$APP_CONFIG_GITHUB_CLIENT_SECRET"'/g' /usr/src/app/config.yml 14 | 15 | sed -i 's/APP_CONFIG_REDIS_HOST/'"$APP_CONFIG_REDIS_HOST"'/g' /usr/src/app/config.yml 16 | sed -i 's/APP_CONFIG_REDIS_PORT/'"$APP_CONFIG_REDIS_PORT"'/g' /usr/src/app/config.yml 17 | sed -i 's/APP_CONFIG_REDIS_DB/'"$APP_CONFIG_REDIS_DB"'/g' /usr/src/app/config.yml 18 | sed -i 's/APP_CONFIG_REDIS_NAMESPACE/'"$APP_CONFIG_REDIS_NAMESPACE"'/g' /usr/src/app/config.yml 19 | 20 | sed -i 's/APP_CONFIG_STATSD_HOST/'"$APP_CONFIG_STATSD_HOST"'/g' /usr/src/app/config.yml 21 | sed -i 's/APP_CONFIG_STATSD_PORT/'"$APP_CONFIG_STATSD_PORT"'/g' /usr/src/app/config.yml 22 | 23 | sed -i 's/APP_CONFIG_DOMAIN/'"$APP_CONFIG_DOMAIN"'/g' /usr/src/app/config.yml 24 | 25 | sed -i 's/APP_CONFIG_SECRET/'"$APP_CONFIG_SECRET"'/g' /usr/src/app/config.yml 26 | 27 | sed -i 's/APP_CONFIG_DEPLOY_ID/'"$APP_CONFIG_DEPLOY_ID"'/g' /usr/src/app/config.yml 28 | 29 | sed -i 's/APP_CONFIG_EMAIL_DEV_ON_SIGNUP/'"$APP_CONFIG_EMAIL_DEV_ON_SIGNUP"'/g' /usr/src/app/config.yml 30 | sed -i 's/APP_CONFIG_DEV_EMAIL_ADDRESS/'"$APP_CONFIG_DEV_EMAIL_ADDRESS"'/g' /usr/src/app/config.yml 31 | 32 | exec puma -C config/puma.rb -e "$APP_ENV" 33 | -------------------------------------------------------------------------------- /docker/sidekiq/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /usr/src/app 5 | cp config.yml.example config.yml 6 | 7 | if [ "$APP_NEWRELIC_API_KEY" ]; then 8 | cp config/newrelic.example.yml config/newrelic.yml 9 | sed -i 's/APP_NEWRELIC_API_KEY/'"$APP_NEWRELIC_API_KEY"'/g' /usr/src/app/config/newrelic.yml 10 | fi 11 | 12 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_ID/'"$APP_CONFIG_GITHUB_CLIENT_ID"'/g' /usr/src/app/config.yml 13 | sed -i 's/APP_CONFIG_GITHUB_CLIENT_SECRET/'"$APP_CONFIG_GITHUB_CLIENT_SECRET"'/g' /usr/src/app/config.yml 14 | 15 | sed -i 's/APP_CONFIG_REDIS_HOST/'"$APP_CONFIG_REDIS_HOST"'/g' /usr/src/app/config.yml 16 | sed -i 's/APP_CONFIG_REDIS_PORT/'"$APP_CONFIG_REDIS_PORT"'/g' /usr/src/app/config.yml 17 | sed -i 's/APP_CONFIG_REDIS_DB/'"$APP_CONFIG_REDIS_DB"'/g' /usr/src/app/config.yml 18 | sed -i 's/APP_CONFIG_REDIS_NAMESPACE/'"$APP_CONFIG_REDIS_NAMESPACE"'/g' /usr/src/app/config.yml 19 | 20 | sed -i 's/APP_CONFIG_STATSD_HOST/'"$APP_CONFIG_STATSD_HOST"'/g' /usr/src/app/config.yml 21 | sed -i 's/APP_CONFIG_STATSD_PORT/'"$APP_CONFIG_STATSD_PORT"'/g' /usr/src/app/config.yml 22 | 23 | sed -i 's/APP_CONFIG_DOMAIN/'"$APP_CONFIG_DOMAIN"'/g' /usr/src/app/config.yml 24 | 25 | sed -i 's/APP_CONFIG_SECRET/'"$APP_CONFIG_SECRET"'/g' /usr/src/app/config.yml 26 | 27 | sed -i 's/APP_CONFIG_MAIL_ENABLE/'"$APP_CONFIG_MAIL_ENABLE"'/g' /usr/src/app/config.yml 28 | sed -i 's/APP_CONFIG_MAIL_METHOD/'"$APP_CONFIG_MAIL_METHOD"'/g' /usr/src/app/config.yml 29 | sed -i 's/APP_CONFIG_MAIL_FROM/'"$APP_CONFIG_MAIL_FROM"'/g' /usr/src/app/config.yml 30 | sed -i 's/APP_CONFIG_MAIL_HOST/'"$APP_CONFIG_MAIL_HOST"'/g' /usr/src/app/config.yml 31 | sed -i 's/APP_CONFIG_MAIL_PORT/'"$APP_CONFIG_MAIL_PORT"'/g' /usr/src/app/config.yml 32 | sed -i 's/APP_CONFIG_MAIL_USER/'"$APP_CONFIG_MAIL_USER"'/g' /usr/src/app/config.yml 33 | sed -i 's/APP_CONFIG_MAIL_PASSWORD/'"$APP_CONFIG_MAIL_PASSWORD"'/g' /usr/src/app/config.yml 34 | sed -i 's/APP_CONFIG_MAIL_SSL/'"$APP_CONFIG_MAIL_SSL"'/g' /usr/src/app/config.yml 35 | 36 | sed -i 's/APP_CONFIG_DEPLOY_ID/'"$APP_CONFIG_DEPLOY_ID"'/g' /usr/src/app/config.yml 37 | 38 | sed -i 's/APP_CONFIG_EMAIL_DEV_ON_SIGNUP/'"$APP_CONFIG_EMAIL_DEV_ON_SIGNUP"'/g' /usr/src/app/config.yml 39 | sed -i 's/APP_CONFIG_DEV_EMAIL_ADDRESS/'"$APP_CONFIG_DEV_EMAIL_ADDRESS"'/g' /usr/src/app/config.yml 40 | 41 | exec bundle exec sidekiq -e "$APP_ENV" -r ./workers/init.rb -q notifications_checker -q send_email -q send_email_signup -q email_builder 42 | -------------------------------------------------------------------------------- /public/css/bootstrap-social.css: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap-social/bootstrap-social.css -------------------------------------------------------------------------------- /public/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/dist/css/bootstrap.min.css -------------------------------------------------------------------------------- /public/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/css/font-awesome.min.css -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | footer { 6 | position: absolute; 7 | bottom: 0; 8 | width: 100%; 9 | /* Set the fixed height of the footer here */ 10 | height: 60px; 11 | background: #303030; 12 | color: #E4F6FF; 13 | font-size: 1.0em; 14 | padding: 1.5em 0 0.8em; 15 | } 16 | body { 17 | font-size: 14px; 18 | margin-bottom: 60px; 19 | } 20 | h1, h2, h3, h4, h5, h6 { 21 | font-family: 'Montserrat', sans-serif; 22 | font-weight: 700; 23 | } 24 | .btn { 25 | border-radius: 3em; 26 | } 27 | .container { 28 | max-width: 900px; 29 | } 30 | .container-alternate { 31 | background: #7BBCD2; 32 | } 33 | .first-cnt { 34 | margin-top: 58px; 35 | } 36 | .navbar { 37 | background: #074f66; 38 | font-family: 'Montserrat', sans-serif; 39 | font-weight: 700; 40 | padding: 0.3em 0; 41 | } 42 | .navbar .navbar-brand { 43 | color: #ffffff; 44 | font-size: 1.6em; 45 | } 46 | .navbar ul.nav li a { 47 | color: #FFC200; 48 | padding-left: 0; 49 | padding-right: 0; 50 | margin: 0 1.5em; 51 | } 52 | .nav>li>a:focus, .nav>li>a:hover { 53 | background: inherit; 54 | background-color: inherit; 55 | } 56 | .navbar ul.nav li button { 57 | margin: 0.7em 0 0 1em; 58 | } 59 | .navbar-toggle { 60 | border-color: #ffffff; 61 | } 62 | .navbar-toggle > span { 63 | background-color: #ffffff; 64 | } 65 | .jumbotron { 66 | background: #7BBCD2; 67 | color: #333; 68 | padding: 4.5em 0 3.5em; 69 | text-align: center; 70 | margin-bottom: 0; 71 | } 72 | .jumbotron h1 { 73 | color: #074f66; 74 | font-size: 4em; 75 | } 76 | .jumbotron h2 { 77 | font-weight: normal; 78 | line-height: 1.4em; 79 | margin-bottom: 1.4em; 80 | font-size: 1.5em; 81 | } 82 | .orange { 83 | color: #eebf3f; 84 | } 85 | .btn-github { 86 | background: #efc56b; 87 | color: #074f66; 88 | } 89 | .subhead { 90 | font-size: 2em; 91 | text-align: center; 92 | margin: 2em 0 0.5em; 93 | } 94 | .benefits { 95 | margin-bottom: 3em; 96 | } 97 | .benefit { 98 | margin: 1em 0; 99 | text-align: center; 100 | } 101 | .benefit .benefit-ball { 102 | background: #f0f0f0; 103 | border-radius: 50%; 104 | color: #333; 105 | display: inline-block; 106 | line-height: 1em; 107 | padding: 3em; 108 | } 109 | .benefit .benefit-ball .glyphicon { 110 | font-size: 3em; 111 | position: relative; 112 | top: -3px; 113 | right: 1px; 114 | } 115 | .benefit h3 { 116 | font-size: 1.5em; 117 | } 118 | #tour { 119 | padding-bottom: 3em; 120 | } 121 | #tour img { 122 | width: 870px; 123 | } 124 | #stack { 125 | padding-bottom: 3em; 126 | } 127 | #stack img { 128 | height: 130px; 129 | display: inline; 130 | padding: 1em; 131 | } 132 | #tour-head { 133 | margin-bottom: 1em; 134 | } 135 | .faqs { 136 | margin-bottom: 3em; 137 | } 138 | .faqs p { 139 | line-height: 1.5em; 140 | margin: 1.2em 0; 141 | } 142 | footer a:link, footer a:visited, footer a:hover, footer a:active { 143 | color: inherit; 144 | } 145 | span.footer { 146 | padding-right: 5px; 147 | } 148 | .hidden { 149 | display: none; 150 | } 151 | @media screen and (max-width: 768px) { 152 | .navbar ul.nav li { 153 | text-align: center; 154 | } 155 | .navbar ul.nav li button { 156 | margin: 1em 0; 157 | } 158 | .jumbotron { 159 | font-size: 14px; 160 | padding: 6em 0 4em; 161 | } 162 | .benefit { 163 | margin-bottom: 2em; 164 | } 165 | .jumbotron h1 { 166 | font-size: 4em; 167 | } 168 | .jumbotron h2 { 169 | font-size: 1.2em; 170 | } 171 | } 172 | @media screen and (max-width: 480px) { 173 | body { 174 | font-size: 12px; 175 | } 176 | .jumbotron h1 { 177 | font-size: 2.7em; 178 | } 179 | .jumbotron h2 { 180 | font-size: 1.0em; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /public/css/octicons: -------------------------------------------------------------------------------- 1 | ../../bower_components/octicons/octicons -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.svg: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/fonts/fontawesome-webfont.svg -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- 1 | ../../bower_components/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/public/img/screenshot1.png -------------------------------------------------------------------------------- /public/img/stack/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/public/img/stack/redis.png -------------------------------------------------------------------------------- /public/img/stack/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/public/img/stack/ruby.png -------------------------------------------------------------------------------- /public/img/stack/sidekiq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/public/img/stack/sidekiq.png -------------------------------------------------------------------------------- /public/img/stack/sinatra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/public/img/stack/sinatra.png -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $(".nav-link").click(function(e) { 3 | var link = $(this); 4 | var href = link.attr("href"); 5 | if (href.indexOf('#') !== -1 && href.indexOf('/#') === -1) { 6 | e.preventDefault(); 7 | $("body").animate({scrollTop: $(href).offset().top - 80}, 500); 8 | link.closest(".navbar").find(".navbar-toggle:not(.collapsed)").click(); 9 | } 10 | }); 11 | 12 | $('[data-toggle="tooltip"]').tooltip(); 13 | 14 | if (document.getElementById('show_unsubscribe') instanceof Object) { 15 | $('a#show_unsubscribe').click(function() { 16 | $('div#unsubscribe_form').removeClass('hidden'); 17 | $('div#change_frequency_form,div#unsubscribe_question').addClass('hidden'); 18 | }); 19 | } 20 | 21 | if (document.getElementById('notifications_type') instanceof Object && document.getElementById('notifications_frequency') instanceof Object) { 22 | changePreferences(); 23 | } 24 | 25 | if (document.getElementById('events') instanceof Object) { 26 | getEvents(); 27 | } 28 | 29 | if (document.getElementById('signup') instanceof Object) { 30 | var $email = $('#other_email'); 31 | var $hint = $("#other_email_suggestion"); 32 | 33 | $email.on('focus', function() { 34 | $('#other_email_radio').prop("checked", true); 35 | }); 36 | 37 | $email.on('blur', function(e) { 38 | checkEmail(e); 39 | }); 40 | 41 | $('#signup').on('submit', function(e) { 42 | checkEmail(e); 43 | }); 44 | 45 | $hint.on('click', function() { 46 | $email.val($(".suggestion").text()); 47 | $hint.fadeOut(200, function() { 48 | $(this).empty(); 49 | }); 50 | return false; 51 | }); 52 | } 53 | 54 | function checkEmail(e) { 55 | $hint.css('display', 'none'); // Hide the hint 56 | $email.mailcheck({ 57 | suggested: function(element, suggestion) { 58 | if(!$hint.html()) { 59 | e.preventDefault(); 60 | var suggestion = "Did you mean " + 61 | "" + suggestion.address + "" 62 | + "@" + suggestion.domain + 63 | "?"; 64 | 65 | $hint.html(suggestion).fadeIn(150); 66 | } else { 67 | $(".address").html(suggestion.address); 68 | $(".domain").html(suggestion.domain); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | function getEvents() { 75 | $('div#spinner').spin('large'); 76 | getNextEvents(1); 77 | } 78 | 79 | function getNextEvents(page) { 80 | $.ajax({ 81 | url: '/api/events?page=' + page, 82 | method: 'GET', 83 | async: true, 84 | success: function(data) { 85 | data = JSON.parse(data); 86 | if (data.objects.length > 0) { 87 | $.each(data.objects, function(index, event) { 88 | switch(event.type) { 89 | case 'star': 90 | icon = 'star'; 91 | break; 92 | case 'fork': 93 | icon = 'repo-forked'; 94 | break; 95 | case 'follow': 96 | icon = 'person'; 97 | break; 98 | case 'unfollow': 99 | icon = 'person'; 100 | break; 101 | case 'deleted': 102 | icon = 'trashcan'; 103 | break; 104 | default: 105 | icon = ''; 106 | } 107 | 108 | var date = 'n/a'; 109 | if (event.timestamp) { 110 | var d = new Date(event.timestamp * 1000); 111 | date = d.toDateString(); 112 | } 113 | 114 | $("table#events").append($(' ' + event.body + "").hide().fadeIn(1000)); 115 | $('[data-toggle="tooltip"]').tooltip(); 116 | }); 117 | } else { 118 | if ($('table#events tbody').children().length === 0 && data.meta.eof) { 119 | $('div#noevents').removeClass('hidden'); 120 | } 121 | } 122 | 123 | if (!data.meta.eof) { 124 | getNextEvents(page + 1); 125 | } else { 126 | $('div#spinner').addClass('hidden').spin(false); 127 | } 128 | } 129 | }); 130 | } 131 | 132 | function changePreferences() { 133 | 134 | $('button#button_save_preferences').click(function() { 135 | 136 | var disabledNotifications = $("#notifications_type input:checkbox:not(:checked)").map(function() { 137 | return this.value; 138 | }).get(); 139 | 140 | var notificationFrequency = $("#notifications_frequency input[type='radio']:checked").val(); 141 | 142 | $.ajax({ 143 | beforeSend: function(xhr) { 144 | var token = $('div#preferences').data('csrf'); 145 | xhr.setRequestHeader('x-csrf-token', token); 146 | }, 147 | url : '/api/user/preferences', 148 | data : JSON.stringify( 149 | { 150 | "notifications_frequency":notificationFrequency, 151 | "disabled_notifications_type":disabledNotifications 152 | }), 153 | type : 'PATCH', 154 | contentType : 'application/json', 155 | processData: false, 156 | dataType: 'json', 157 | success: function() { 158 | if (window.location.pathname.indexOf('signup') > -1) { 159 | // Sign up 160 | window.location = '/'; 161 | } else { 162 | window.location.reload(); 163 | } 164 | }, 165 | error: function() { 166 | alert('An error occurred!'); 167 | } 168 | }); 169 | 170 | }); 171 | 172 | } 173 | 174 | // Google Analytics 175 | $('button#signup_button').click(function() { 176 | if (typeof(ga) == "function") { 177 | ga('send', 'event', 'button', 'click', 'signup', {useBeacon: true}); 178 | } 179 | }); 180 | 181 | }); 182 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/dist/js/bootstrap.min.js -------------------------------------------------------------------------------- /public/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/html5shiv/dist/html5shiv.min.js -------------------------------------------------------------------------------- /public/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/jquery/dist/jquery.min.js -------------------------------------------------------------------------------- /public/js/jquery.min.map: -------------------------------------------------------------------------------- 1 | ../../bower_components/jquery/dist/jquery.min.map -------------------------------------------------------------------------------- /public/js/jquery.spin.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/spin.js/jquery.spin.js -------------------------------------------------------------------------------- /public/js/mailcheck.min.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/mailcheck/src/mailcheck.min.js -------------------------------------------------------------------------------- /public/js/respond.min.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/respond/dest/respond.min.js -------------------------------------------------------------------------------- /public/js/spin.js: -------------------------------------------------------------------------------- 1 | ../../bower_components/spin.js/spin.js -------------------------------------------------------------------------------- /scripts/add_users.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'pp' 3 | require 'yaml' 4 | 5 | config_file = File.dirname(__FILE__) + '/../config.yml' 6 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 7 | CONFIG = YAML.load_file(config_file) 8 | 9 | redis = Redis.new(:driver => :hiredis, :host => CONFIG['redis']['host'], :port => CONFIG['redis']['port'], :db => CONFIG['redis']['db']) 10 | 11 | obj = {"github_id"=> "", 12 | "last_event_id"=>"2425257958", 13 | "followers"=> 14 | "[\"matteosister\",\"frapontillo\",\"robbixc\",\"alpacaaa\",\"davidefedrigo\",\"rvitaliy\",\"aleinside\",\"thomasvargiu\",\"runcom\",\"anatolinicolae\",\"giordan83\",\"maxcanna\",\"peelandsee\",\"nigrosimone\",\"squaini\",\"riccamastellone\",\"usutest\",\"gitnotifier\"]", 15 | "registered_on"=>"1416911683", 16 | "last_email_sent_on"=>"1424189703", 17 | "last_email_queued_on"=>"1424189703", 18 | "login"=>"andreausu", 19 | "token"=>"", 20 | "email"=>CONFIG['dev_email_address'], 21 | "notifications_frequency"=>"weekly", 22 | "disabled_notifications_type" => "[]", 23 | "email_confirmed" => "1", 24 | "first_check_completed" => "1" 25 | } 26 | 27 | prng = Random.new 28 | 29 | redis.pipelined do 30 | (0..999).each do 31 | rand_val = prng.rand(1..100000) 32 | redis.hmset "#{CONFIG['redis']['namespace']}:users:#{rand_val}", :github_id, rand_val, :last_event_id, obj['last_event_id'], :followers, obj['followers'], :registered_on, obj['registered_on'], :last_email_sent_on, obj['last_email_sent_on'], :login, obj['login'], :token, obj['token'], :email, obj['email'], :notifications_frequency, obj['notifications_frequency'], :disabled_notifications_type, obj['disabled_notifications_type'], :email_confirmed, obj['email_confirmed'], :first_check_completed, obj['first_check_completed'] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /scripts/delete_user_by_email.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'pp' 3 | require 'yaml' 4 | 5 | config_file = File.dirname(__FILE__) + '/../config.yml' 6 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 7 | CONFIG = YAML.load_file(config_file) 8 | 9 | redis = Redis.new(:driver => :hiredis, :host => CONFIG['redis']['host'], :port => CONFIG['redis']['port'], :db => CONFIG['redis']['db']) 10 | 11 | email_to_delete = ARGV[0] if ARGV[0] 12 | 13 | raise 'No email specified' unless email_to_delete 14 | 15 | keys_to_delete = [] 16 | users_keys = redis.keys("#{CONFIG['redis']['namespace']}:users:*") 17 | 18 | users_keys.each do |user_key| 19 | email = redis.hget(user_key, :email) 20 | if email == email_to_delete 21 | user_id = redis.hget(user_key, :github_id) 22 | token = redis.hget(user_key, :token) 23 | keys_to_delete = redis.keys "#{CONFIG['redis']['namespace']}:*:#{user_id}" 24 | keys_to_delete << "#{CONFIG['redis']['namespace']}:tokens:#{token}" 25 | end 26 | end 27 | 28 | redis.multi do 29 | keys_to_delete.each do |k| 30 | puts "Deleting #{k}..." 31 | redis.del k 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /scripts/delete_user_by_token.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'pp' 3 | require 'yaml' 4 | 5 | config_file = File.dirname(__FILE__) + '/../config.yml' 6 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 7 | CONFIG = YAML.load_file(config_file) 8 | 9 | redis = Redis.new(:driver => :hiredis, :host => CONFIG['redis']['host'], :port => CONFIG['redis']['port'], :db => CONFIG['redis']['db']) 10 | 11 | token = ARGV[0] if ARGV[0] 12 | 13 | raise 'No token specified' unless token && token.length == 40 14 | 15 | user_id = redis.get "#{CONFIG['redis']['namespace']}:tokens:#{token}" 16 | 17 | raise 'Token not found' unless user_id 18 | 19 | keys_to_delete = redis.keys "#{CONFIG['redis']['namespace']}:*:#{user_id}" 20 | keys_to_delete << "#{CONFIG['redis']['namespace']}:tokens:#{token}" 21 | 22 | redis.multi do 23 | keys_to_delete.each do |k| 24 | puts "Deleting #{k}..." 25 | redis.del k 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /scripts/job_enqueuer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'sidekiq' 4 | require 'redis' 5 | require_relative '../workers/notifications_checker' 6 | require_relative '../workers/email_builder' 7 | require 'datadog/statsd' 8 | require 'newrelic_rpm' # it should be the last entry in the require list 9 | 10 | config_file = File.dirname(__FILE__) + '/../config.yml' 11 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 12 | CONFIG = YAML.load_file(config_file) 13 | 14 | statsd = Datadog::Statsd.new(CONFIG['statsd']['host'], CONFIG['statsd']['port']) 15 | statsd.increment('ghntfr.scripts.job_enqueuer.start') 16 | 17 | users_keys = nil 18 | 19 | redis_conn = proc { 20 | Redis.new( 21 | :driver => :hiredis, 22 | :host => CONFIG['redis']['host'], 23 | :port => CONFIG['redis']['port'], 24 | :db => CONFIG['redis']['db'], 25 | network_timeout: 5 26 | ) 27 | } 28 | 29 | Sidekiq.configure_client do |config| 30 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 31 | end 32 | 33 | Sidekiq.redis do |conn| 34 | users_keys = conn.keys("#{CONFIG['redis']['namespace']}:users:*") 35 | end 36 | 37 | jobs_args = [] 38 | 39 | users_keys.each do |user_key| 40 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} Enqueueing #{user_key}..." 41 | jobs_args << [user_key] 42 | end 43 | 44 | Sidekiq::Client.push_bulk('queue' => 'notifications_checker', 'class' => NotificationsChecker, 'args' => jobs_args) 45 | 46 | users_keys = nil 47 | jobs_args = nil 48 | GC.start 49 | 50 | cmds = [] 51 | jobs_args = [] 52 | events_lists_keys = nil 53 | Sidekiq.redis do |conn| 54 | events_lists_keys = conn.keys("#{CONFIG['redis']['namespace']}:events:batch:*") 55 | conn.pipelined do 56 | events_lists_keys.each do |events_list_key| 57 | cmds << { future: conn.hgetall("#{CONFIG['redis']['namespace']}:users:" + events_list_key.split(':').last), object: events_list_key } 58 | end 59 | end 60 | end 61 | 62 | cmds.each do |c| 63 | while c[:future].value.is_a?(Redis::FutureNotReady) 64 | sleep(1.0 / 100.0) 65 | end 66 | 67 | user = c[:future].value 68 | events_list_key = c[:object] 69 | 70 | new_key = "#{CONFIG['redis']['namespace']}:processing:events:batch:" + events_list_key.split(':').last 71 | 72 | next if user['email_confirmed'] == "0" 73 | 74 | case user['notifications_frequency'] 75 | when 'asap' 76 | jobs_args << [new_key] 77 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} EmailBuilder job enqueued" 78 | when 'daily' 79 | if user['last_email_queued_on'].to_i <= (Time.now.to_i - (60 * 60 * 24)) # 1 day 80 | jobs_args << [new_key] 81 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} EmailBuilder job enqueued" 82 | else 83 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} Waiting for some more time before enqueuing the EmailBuilder job" 84 | end 85 | when 'weekly' 86 | if user['last_email_queued_on'].to_i <= (Time.now.to_i - (60 * 60 * 24 * 7)) # 7 days 87 | jobs_args << [new_key] 88 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} EmailBuilder job enqueued" 89 | else 90 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} Waiting for some more time before enqueuing the EmailBuilder job" 91 | end 92 | end 93 | 94 | end 95 | 96 | Sidekiq.redis do |conn| 97 | conn.multi do 98 | jobs_args.each do |events_list_key| 99 | conn.rename("#{CONFIG['redis']['namespace']}:events:batch:" + events_list_key[0].split(':').last, events_list_key[0]) 100 | end 101 | Sidekiq::Client.push_bulk('queue' => 'email_builder', 'class' => EmailBuilder, 'args' => jobs_args) 102 | end 103 | end 104 | 105 | statsd.increment('ghntfr.scripts.job_enqueuer.finish') 106 | -------------------------------------------------------------------------------- /scripts/migrations/0_add_last_queued_on.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'sidekiq' 4 | require 'redis' 5 | require 'newrelic_rpm' # it should be the last entry in the require list 6 | 7 | config_file = File.dirname(__FILE__) + '/../../config.yml' 8 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 9 | CONFIG = YAML.load_file(config_file) 10 | 11 | conn = Redis.new( 12 | :driver => :hiredis, 13 | :host => CONFIG['redis']['host'], 14 | :port => CONFIG['redis']['port'], 15 | :db => CONFIG['redis']['db'], 16 | network_timeout: 5 17 | ) 18 | 19 | users_keys = conn.keys("#{CONFIG['redis']['namespace']}:users:*") 20 | 21 | users_keys.each do |user_key| 22 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} Execution migration on #{user_key}..." 23 | 24 | last_email_sent_on = conn.hget(user_key, :last_email_sent_on) 25 | conn.hset(user_key, :last_email_queued_on, last_email_sent_on) 26 | end 27 | -------------------------------------------------------------------------------- /scripts/send_email.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'date' 5 | require 'sidekiq' 6 | require 'redis' 7 | require 'pp' 8 | require_relative '../workers/send_email' 9 | require 'newrelic_rpm' # it should be the last entry in the require list 10 | 11 | def time_rand from = 0.0, to = Time.now 12 | Time.at(from + rand * (to.to_f - from.to_f)) 13 | end 14 | 15 | def inject_day(events) 16 | previousEvent = nil 17 | events.map! do |event| 18 | if previousEvent.nil? || (Time.at(previousEvent[:timestamp]).strftime('%d') != Time.at(event[:timestamp]).strftime('%d')) 19 | event[:day] = Time.at(event[:timestamp]).strftime('%A, %b %e') 20 | end 21 | previousEvent = event 22 | end 23 | 24 | events 25 | end 26 | 27 | config_file = File.dirname(__FILE__) + '/../config.yml' 28 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 29 | CONFIG = YAML.load_file(config_file) 30 | 31 | options = {} 32 | OptionParser.new do |opts| 33 | opts.banner = "Usage: #{$0} [options]" 34 | 35 | opts.on("-n", "--number NUMBER", "Number of events") { |v| options[:events_number] = v } 36 | opts.on("-m", "--mode MODE", "Notification mode (asap, daily, weekly, confirm, news)") { |v| options[:email_mode] = v } 37 | opts.on("-t", "--to EMAIL_TO", "Email to") { |v| options[:email_to] = v } 38 | end.parse! 39 | 40 | raise 'Missing Email to' if options[:email_to].nil? 41 | raise 'Missing Email mode' if options[:email_mode].nil? 42 | 43 | redis_conn = proc { 44 | Redis.new( 45 | :driver => :hiredis, 46 | :host => CONFIG['redis']['host'], 47 | :port => CONFIG['redis']['port'], 48 | :db => CONFIG['redis']['db'], 49 | network_timeout: 5 50 | ) 51 | } 52 | 53 | Sidekiq.configure_client do |config| 54 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 55 | end 56 | 57 | case options[:email_mode] 58 | when 'confirm' 59 | user = 'andreausu' 60 | link = "https://#{CONFIG['domain']}/signup/confirm?id=999&email=#{CGI.escape(options[:email_to])}&expiry=123456789&v=jwvyewgfuyewgyiwegf" 61 | Sidekiq::Client.push( 62 | 'queue' => 'send_email_signup', 63 | 'class' => SendEmail, 64 | 'args' => [options[:email_to], 'Confirm your Git Notifier email address!', 'html', 'confirm', {:confirm_link => link, :username => user}] 65 | ) 66 | when 'news', 'newsletter' 67 | user = 'andreausu' 68 | unsubscribe_url = URI.escape("https://#{CONFIG['domain']}/unsubscribe?id=9999&expiry=123456789&v=dofhweuhf37365erdvhkbj") 69 | 70 | Sidekiq::Client.push( 71 | 'queue' => 'send_email', 72 | 'class' => SendEmail, 73 | 'args' => [ 74 | options[:email_to], 75 | "GitHub Notifier changes its name", 76 | 'html', 77 | 'newsletter', 78 | { 79 | :content => "Hi #{user},

sorry about the extra email, we just wanted to let you know that due to trademark concerns raised by the GitHub legal team we are changing the website name and all its associated entities from GitHub Notifier to Git Notifier.

Thank you for using our service!", 80 | :site_url => "https://#{CONFIG['domain']}", 81 | :unsubscribe_url => unsubscribe_url 82 | } 83 | ] 84 | ) 85 | 86 | when 'asap', 'daily', 'weekly' 87 | raise 'Missing Number of events' if options[:events_number].nil? 88 | 89 | USERNAMES = [ 90 | 'antirez', 91 | 'foobar', 92 | 'sceriffowoody', 93 | 'buzz-lightyear', 94 | 'madhatter', 95 | 'alice', 96 | 'dinah', 97 | 'kaiserjacob', 98 | 'mr.broccolo', 99 | 'leoncino', 100 | 'cheshirecat', 101 | 'whiterabbit', 102 | 'labestia', 103 | 'carlo', 104 | 'cesare', 105 | 'unicorno', 106 | 'cose_belle', 107 | 'uolli', 108 | 'ivaaaa', 109 | 'orsetto', 110 | 'gattini', 111 | 'andreausu', 112 | 'bobinsky', 113 | 'ventilatore', 114 | 'slinkydog', 115 | 'starmale1', 116 | 'oreste', 117 | 'biagio', 118 | 'il_tricheco', 119 | 'nonchere', 120 | 'regina-di-cuori', 121 | 'carte', 122 | 'leopoldo', 123 | 'pollo', 124 | 'mr.smith', 125 | 'swagswag', 126 | 'bacca', 127 | 'juvemerda', 128 | 'milano', 129 | 'acmilan', 130 | 'andem', 131 | 'tireminans', 132 | 'sepuminga', 133 | 'bohboh', 134 | 'vitamine', 135 | 'ginocchio', 136 | 'patella', 137 | 'gelenko', 138 | 'sbilenko', 139 | 'sugruuu', 140 | 'amica_copertina' 141 | ] 142 | 143 | REPOSITORIES = [ 144 | 'git-notifier', 145 | 'redis', 146 | 'coreos', 147 | 'etcd', 148 | 'docker', 149 | 'reactjs', 150 | 'coolproject', 151 | 'letsencrypt', 152 | 'ansible', 153 | 'linux', 154 | 'xhyve', 155 | 'flynn', 156 | 'falcor', 157 | 'graphql', 158 | 'influxdb', 159 | 'elasticsearch', 160 | 'gitnotifier-provisioning', 161 | 'CodiceFiscale', 162 | 'vulcand', 163 | 'blog', 164 | 'homebrew', 165 | 'christmas-countdown' 166 | ] 167 | 168 | ACTIONS = [ 169 | 'star', 170 | 'fork', 171 | 'follow', 172 | 'unfollow', 173 | 'deleted' 174 | ] 175 | 176 | timestamp = Time.now.to_i 177 | user = 'andreausu' # USERNAMES.sample 178 | emailEvents = [] 179 | (1..options[:events_number].to_i).each do |n| 180 | user_action = USERNAMES.sample 181 | repository = REPOSITORIES.sample 182 | case ACTIONS.sample 183 | when 'star' 184 | html = "#{user_action} starred #{repository}" 185 | when 'fork' 186 | html = "#{user_action} forked #{repository} to #{repository}" 187 | when 'follow' 188 | html = "#{user_action} started following you" 189 | when 'unfollow' 190 | html = "#{user_action} is not following you anymore" 191 | when 'deleted' 192 | html = "#{user_action} that was following you has been deleted" 193 | end 194 | timestamp = time_rand(Time.now - 604800) if options[:email_mode] == 'weekly' 195 | emailEvents << { 196 | :html => html, 197 | :text => html.gsub(//, "\r\n").gsub(/<\/?[^>]*>/, ''), 198 | :timestamp => timestamp 199 | } 200 | end 201 | 202 | inject_day(emailEvents) if options[:email_mode] == 'weekly' 203 | 204 | subject = "You have #{emailEvents.length == 1 ? 'a new notification' : emailEvents.length.to_s + ' new notifications'}" 205 | notificationsText = subject + (emailEvents.length == 1 ? "!
You notification was received on #{Time.at(emailEvents[0][:timestamp]).strftime('%A %b %e')} at #{Time.at(emailEvents[0][:timestamp]).strftime('%k:%M')}." : "!
Your last notification was received on #{Time.at(emailEvents[0][:timestamp]).strftime('%A %b %e')} at #{Time.at(emailEvents[0][:timestamp]).strftime('%k:%M')}.") 206 | 207 | case options[:email_mode] 208 | when 'asap' 209 | subject = subject 210 | when 'daily' 211 | subject = "#{Time.now.strftime('%b %e')} daily report: #{subject}" 212 | when 'weekly' 213 | subject = "#{Time.now.strftime('%b %e')} weekly report: #{subject}" 214 | end 215 | 216 | Sidekiq::Client.push( 217 | 'queue' => 'send_email', 218 | 'class' => SendEmail, 219 | 'args' => [ 220 | options[:email_to], 221 | subject, 222 | 'html', 223 | 'notification', 224 | { 225 | :events => emailEvents, 226 | :notifications_text => notificationsText, 227 | :subject => subject, 228 | :username => user, 229 | :site_url => "https://gitnotifier.io", 230 | :unsubscribe_url => "https://gitnotifier.io" 231 | } 232 | ] 233 | ) 234 | end 235 | -------------------------------------------------------------------------------- /scripts/send_newsletter.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'sidekiq' 4 | require 'redis' 5 | require 'pp' 6 | require_relative '../workers/send_email' 7 | require 'newrelic_rpm' # it should be the last entry in the require list 8 | 9 | config_file = File.dirname(__FILE__) + '/../config.yml' 10 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 11 | CONFIG = YAML.load_file(config_file) 12 | 13 | redis_conn = proc { 14 | Redis.new( 15 | :driver => :hiredis, 16 | :host => CONFIG['redis']['host'], 17 | :port => CONFIG['redis']['port'], 18 | :db => CONFIG['redis']['db'], 19 | network_timeout: 5 20 | ) 21 | } 22 | 23 | Sidekiq.configure_client do |config| 24 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 25 | end 26 | 27 | users_keys = Sidekiq.redis do |conn| 28 | conn.keys("#{CONFIG['redis']['namespace']}:users:*") 29 | end 30 | 31 | users_keys.each do |user_key| 32 | puts "#{Time.now.strftime("%Y-%m-%dT%l:%M:%S%z")} Enqueueing #{user_key}..." 33 | user = Sidekiq.redis { |conn| conn.hgetall(user_key) } 34 | email = user['email'] 35 | next if user['email_confirmed'] == 0 || user['disabled_notifications_type'].include?('site-news') # The user doesn't want to receive newsletters 36 | 37 | expiry = (Time.now + 31536000).to_i.to_s 38 | 39 | digest = OpenSSL::Digest.new('sha512') 40 | hmac = OpenSSL::HMAC.hexdigest(digest, CONFIG['secret'], user['github_id'] + expiry) 41 | 42 | unsubscribe_url = URI.escape("https://#{CONFIG['domain']}/unsubscribe?id=#{user['github_id']}&expiry=#{expiry}&v=#{hmac}") 43 | 44 | Sidekiq::Client.push( 45 | 'queue' => 'send_email', 46 | 'class' => SendEmail, 47 | 'args' => [ 48 | email, 49 | "GitHub Notifier changes its name", 50 | 'html', 51 | 'newsletter', 52 | { 53 | :content => "Hi #{user['login']},

sorry about the extra email, we just wanted to let you know that due to trademark concerns raised by the GitHub legal team we are changing the website name and all its associated entities from GitHub Notifier to Git Notifier.

Thank you for using our service!", 54 | :site_url => "https://#{CONFIG['domain']}", 55 | :unsubscribe_url => unsubscribe_url 56 | } 57 | ] 58 | ) 59 | 60 | end 61 | -------------------------------------------------------------------------------- /sidekiq-web/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ssh -N -L 0.0.0.0:6379:localhost:6379 root@server.ip -p server_port 3 | 4 | docker-compose run --service-ports --entrypoint bash puma 5 | 6 | vim config.yml -> host: 192.168.99.1 # or localhost for linux 7 | 8 | cd sidekiq-web 9 | 10 | rackup -o 0.0.0.0 11 | 12 | OS X: http://192.168.99.1:9292 13 | Linux: http://localhost:9292 14 | ``` 15 | -------------------------------------------------------------------------------- /sidekiq-web/config.ru: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'sidekiq' 3 | 4 | config_file = File.dirname(__FILE__) + '/../config.yml' 5 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 6 | CONFIG = YAML.load_file(config_file) 7 | 8 | redis_conn = proc { 9 | Redis.new( 10 | :driver => :hiredis, 11 | :host => CONFIG['redis']['host'], 12 | :port => CONFIG['redis']['port'], 13 | :db => CONFIG['redis']['db'], 14 | network_timeout: 5 15 | ) 16 | } 17 | 18 | Sidekiq.configure_client do |config| 19 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 20 | end 21 | 22 | require 'sidekiq/web' 23 | 24 | Sidekiq::Web.use Rack::Session::Cookie, :secret => CONFIG['secret'] 25 | Sidekiq::Web.instance_eval { @middleware.rotate!(-1) } 26 | 27 | run Sidekiq::Web 28 | -------------------------------------------------------------------------------- /test/casper/authenticated.js: -------------------------------------------------------------------------------- 1 | casper.test.setUp(function () { 2 | phantom.addCookie({ 3 | domain: 'gitnotifier.local', 4 | name: 'rack.session', 5 | value: casper.cli.get("cookie") 6 | }); 7 | }); 8 | 9 | casper.test.begin("Home page", 3, function suite(test) { 10 | casper.start("http://gitnotifier.local/", function() { 11 | test.assertTitle("GitNotifier - Profile", "Page title is correct"); 12 | test.assertExists('table#events', "Events table found found"); 13 | test.assertEval(function() { 14 | return __utils__.findAll("table#events tr").length >= 5; 15 | }, "there are some events"); 16 | }); 17 | 18 | casper.run(function() { 19 | test.done(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/casper/signup.js: -------------------------------------------------------------------------------- 1 | var onWaitTimeout = function() { 2 | this.capture('failure_timeout.png'); 3 | casper.test.fail('Wait timeout occurred!'); 4 | }; 5 | 6 | casper.on("page.error", function(msg, trace) { 7 | /*this.echo("Error: " + msg, "ERROR"); 8 | this.echo("file: " + trace[0].file, "WARNING"); 9 | this.echo("line: " + trace[0].line, "WARNING"); 10 | this.echo("function: " + trace[0]["function"], "WARNING"); 11 | errors.push(msg);*/ 12 | console.log(msg); 13 | console.log(trace); 14 | }); 15 | 16 | casper.options.onWaitTimeout = onWaitTimeout; 17 | casper.options.viewportSize = {width: 1280, height: 800}; 18 | 19 | casper.test.on('fail', function () { 20 | casper.capture('failure.png'); 21 | }); 22 | 23 | casper.test.begin("Signup process", 24, function suite(test) { 24 | casper.start("http://gitnotifier.local/", function() { 25 | test.assertTitle("GitNotifier - Notifications for stars, forks, follow and unfollow", "Page title is correct"); 26 | test.assertExists('a.btn-github', "Signup button found"); 27 | this.click('a.btn-github'); 28 | }); 29 | 30 | casper.then(function() { 31 | test.assertUrlMatch(/github\.com/, "We are on GitHub"); 32 | casper.waitForSelector('form input[name="password"]', function() { 33 | this.fillSelectors('div#login form', { 34 | 'input[name="login"]' : this.cli.get("username"), 35 | 'input[name="password"]': this.cli.get("password") 36 | }, true); 37 | }); 38 | }); 39 | 40 | casper.then(function() { 41 | if (this.exists('button[name="authorize"]')) { 42 | this.click('button[name="authorize"]'); 43 | } 44 | }); 45 | 46 | casper.then(function() { 47 | test.assertUrlMatch(/gitnotifier/, "We are back on Git Notifier"); 48 | this.waitForSelector('button#signup_button', function() { 49 | test.assertVisible('input[name="email"]', 'Main email is visible'); 50 | test.assertVisible('input[name="other_email"]', 'Other email is visible'); 51 | test.assertVisible('button#signup_button', 'Signup button is visible'); 52 | this.click('input#other_email_radio'); 53 | this.fillSelectors('form#signup', { 54 | 'input[name="other_email"]' : 'gitnotifier@gnail.com', 55 | }, false); 56 | this.waitUntilVisible('div#other_email_suggestion a.domain', function() { 57 | test.assertVisible('div#other_email_suggestion a.domain', 'Wrong domain suggestion is visible'); 58 | this.click('div#other_email_suggestion a.domain'); 59 | test.assertField({type: 'css', path: 'input#other_email'}, 'gitnotifier@gmail.com', 'Domain replaced!'); 60 | this.wait('500', function() { 61 | test.assertNotVisible('div#other_email_suggestion a.domain', 'Suggestion is not visible anymore'); 62 | this.click('button#signup_button'); 63 | }); 64 | }); 65 | }); 66 | }); 67 | 68 | casper.then(function() { 69 | this.waitUntilVisible('button#button_save_preferences', function() { 70 | test.assertTextExists('We have sent an email to gitnotifier@gmail.com, please open it and click on the link inside to activate your account', 'Flash alert "confirm e-mail address" is present'); 71 | test.assertVisible('div.alert.alert-success', 'Flash alert "confirm e-mail address" is visible'); 72 | test.assertTextExists('Choose the type of notifications you wish to receive', 'Text type of notifications is present'); 73 | test.assertTextExists('Choose at which frequency we should send you the notifications', 'Text frequency of notifications is present'); 74 | test.assertTextExists('Star', 'Text Star is present'); 75 | test.assertTextExists('Fork', 'Text Fork is present'); 76 | test.assertTextExists('Follow', 'Text Follow is present'); 77 | test.assertTextExists('Unfollow', 'Text Unfollow is present'); 78 | test.assertTextExists('Deleted', 'Text Deleted is present'); 79 | test.assertTextExists('Site-news', 'Text Site-news is present'); 80 | 81 | test.assertTextExists('Asap', 'Text Asap is present'); 82 | test.assertTextExists('Daily', 'Text Daily is present'); 83 | test.assertTextExists('Weekly', 'Text Weekly is present'); 84 | 85 | //this.click('input#asap'); 86 | //this.click('input#deleted'); 87 | //this.clickLabel('Save', 'button'); 88 | this.clickLabel('Profile', 'a'); 89 | }); 90 | }); 91 | 92 | casper.then(function() { 93 | this.waitForSelector('table#events > tbody > tr > td > span', function() { 94 | test.assertTextExists('andreausu starred your project', 'Text for the notification is present'); 95 | this.clickLabel('Preferences', 'a'); 96 | }); 97 | }); 98 | 99 | casper.then(function() { 100 | this.capture('test2.jpg', undefined, { 101 | format: 'jpg', 102 | quality: 75 103 | }); 104 | }); 105 | 106 | casper.run(function() { 107 | test.done(); 108 | }); 109 | }); 110 | 111 | 112 | // A8o_IF!BgHoltE0ZbOFX3LGUlziL 113 | -------------------------------------------------------------------------------- /test/casper/unauthenticated.js: -------------------------------------------------------------------------------- 1 | casper.test.begin("Home page", 2, function suite(test) { 2 | casper.start("http://gitnotifier.local/", function() { 3 | test.assertTitle("GitNotifier - Notifications for stars, forks, follow and unfollow", "Page title is correct"); 4 | test.assertExists('a.btn-github', "Signup button found"); 5 | }); 6 | 7 | casper.run(function() { 8 | test.done(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreausu/git-notifier/25596cc254e6fed411f0d4034c009a34221b1141/tmp/.gitkeep -------------------------------------------------------------------------------- /tmp/pids/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tmp/sockets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /views/email/confirm.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | 73 | 74 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 59 | 60 |
19 | Git Notifier 20 |
24 | 25 | 26 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 47 | 50 | 51 | 52 | 56 | 57 |
27 | Hello <%= username %>!
28 | Thank you for signing up, you're wonderful :) 29 |
33 | Before you can start receiving GitHub notifications you need to confirm your email address. 34 |
38 | Please confirm your email address by clicking the link below. 39 |
43 | If you have any questions feel free to reach out by replying to this email or by tweeting us, you can find the link at the bottom of this email. 44 |
48 | Confirm email address 49 |
53 | Thanks!
54 | - The Git Notifier Team 55 |
58 |
61 | 70 |
71 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /views/email/confirm.txt: -------------------------------------------------------------------------------- 1 | Hello <%= username %>! 2 | Thank you for signing up, you're wonderful :) 3 | 4 | Before you can start receiving GitHub notifications you need to confirm your email address. 5 | Please confirm your email address by clicking the link below. 6 | <%= confirm_link %> 7 | 8 | If you have any questions feel free to reach out by replying to the email or by writing to us on Twitter, you can find the link at the bottom of this email. 9 | 10 | - The Git Notifier Team 11 | 12 | Follow @GitNotifier on Twitter: https://twitter.com/GitNotifier 13 | -------------------------------------------------------------------------------- /views/email/empty.txt: -------------------------------------------------------------------------------- 1 | <%= content %> 2 | -------------------------------------------------------------------------------- /views/email/newsletter.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 52 | 53 | 54 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 38 | 39 |
19 | Git Notifier 20 |
24 | 25 | 26 | 29 | 30 | 31 | 35 | 36 |
27 | <%= content %> 28 |
32 | Best,
33 | - The Git Notifier Team 34 |
37 |
40 | 50 |
51 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /views/email/newsletter.txt: -------------------------------------------------------------------------------- 1 | <%= strip_html(content) %> 2 | -------------------------------------------------------------------------------- /views/email/notification.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | 73 | 74 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 58 | 59 |
19 | Git Notifier 20 |
24 | 25 | 26 | 30 | 31 | <% for event in events %> 32 | <% if defined?(event['day']) and !event['day'].nil? %> 33 | 34 | 37 | 38 | <% end %> 39 | 40 | 43 | 44 | <% end %> 45 | 46 | 49 | 50 | 51 | 55 | 56 |
27 | Hello <%= defined?(username) ? username : 'dear user' %> :)
28 | <%= notifications_text %> 29 |
35 | <%= event['day'] %> 36 |
41 | <%= event['html'] %> 42 |
47 | View your full notifications history 48 |
52 | Best,
53 | - The Git Notifier Team 54 |
57 |
60 | 70 |
71 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /views/email/notification.txt: -------------------------------------------------------------------------------- 1 | Hello <%= defined?(username) ? username : 'dear user' %> :) 2 | <%= strip_html(notifications_text) %> 3 | <% for event in events %> 4 | + <% if defined?(event['day']) and !event['day'].nil? %>[<%= event['day'] %>] <% end %><%= strip_html(event['html']) %><% end %> 5 | 6 | View your full notifications history: <%= site_url %> 7 | 8 | Unsubscribe: <%= unsubscribe_url %> 9 | -------------------------------------------------------------------------------- /views/email/src/confirm.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | 73 | 74 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 59 | 60 |
19 | Git Notifier 20 |
24 | 25 | 26 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 47 | 50 | 51 | 52 | 56 | 57 |
27 | Hello <%= username %>!
28 | Thank you for signing up, you're wonderful :) 29 |
33 | Before you can start receiving GitHub notifications you need to confirm your email address. 34 |
38 | Please confirm your email address by clicking the link below. 39 |
43 | If you have any questions feel free to reach out by replying to this email or by tweeting us, you can find the link at the bottom of this email. 44 |
48 | Confirm email address 49 |
53 | Thanks!
54 | - The Git Notifier Team 55 |
58 |
61 | 70 |
71 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /views/email/src/css/styles.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | GLOBAL 3 | A very basic CSS reset 4 | ------------------------------------- */ 5 | * { 6 | margin: 0; 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | box-sizing: border-box; 9 | font-size: 14px; 10 | } 11 | 12 | img { 13 | max-width: 100%; 14 | } 15 | 16 | body { 17 | -webkit-font-smoothing: antialiased; 18 | -webkit-text-size-adjust: none; 19 | width: 100% !important; 20 | height: 100%; 21 | line-height: 1.6em; 22 | /* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */ 23 | /*line-height: 22px;*/ 24 | } 25 | 26 | /* Let's make sure all tables have defaults */ 27 | table td { 28 | vertical-align: top; 29 | } 30 | 31 | /* ------------------------------------- 32 | BODY & CONTAINER 33 | ------------------------------------- */ 34 | body { 35 | background-color: #f6f6f6; 36 | } 37 | 38 | .body-wrap { 39 | background-color: #f6f6f6; 40 | width: 100%; 41 | } 42 | 43 | .container { 44 | display: block !important; 45 | max-width: 600px !important; 46 | margin: 0 auto !important; 47 | /* makes it centered */ 48 | clear: both !important; 49 | } 50 | 51 | .content { 52 | max-width: 600px; 53 | margin: 0 auto; 54 | display: block; 55 | padding: 20px; 56 | } 57 | 58 | .centered { 59 | text-align: center; 60 | } 61 | 62 | .summary { 63 | font-size: 18px; 64 | font-weight: 600; 65 | } 66 | 67 | /* ------------------------------------- 68 | HEADER, FOOTER, MAIN 69 | ------------------------------------- */ 70 | .main { 71 | background-color: #fff; 72 | border: 1px solid #e9e9e9; 73 | border-radius: 3px; 74 | } 75 | 76 | .content-wrap { 77 | padding: 20px; 78 | } 79 | 80 | .content-block { 81 | padding: 0 0 5px; 82 | } 83 | 84 | .header { 85 | width: 100%; 86 | margin-bottom: 20px; 87 | } 88 | 89 | .footer { 90 | width: 100%; 91 | clear: both; 92 | color: #999; 93 | padding: 20px; 94 | } 95 | .footer p, .footer a, .footer td { 96 | color: #999; 97 | font-size: 12px; 98 | } 99 | 100 | /* ------------------------------------- 101 | TYPOGRAPHY 102 | ------------------------------------- */ 103 | h1, h2, h3 { 104 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 105 | color: #000; 106 | margin: 40px 0 0; 107 | line-height: 1.2em; 108 | font-weight: 400; 109 | } 110 | 111 | h1 { 112 | font-size: 32px; 113 | font-weight: 500; 114 | /* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */ 115 | /*line-height: 38px;*/ 116 | } 117 | 118 | h2 { 119 | font-size: 24px; 120 | /* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */ 121 | /*line-height: 29px;*/ 122 | } 123 | 124 | h3 { 125 | font-size: 18px; 126 | /* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */ 127 | /*line-height: 22px;*/ 128 | } 129 | 130 | h4 { 131 | font-size: 14px; 132 | font-weight: 600; 133 | } 134 | 135 | p, ul, ol { 136 | margin-bottom: 10px; 137 | font-weight: normal; 138 | } 139 | p li, ul li, ol li { 140 | margin-left: 5px; 141 | list-style-position: inside; 142 | } 143 | 144 | /* ------------------------------------- 145 | LINKS & BUTTONS 146 | ------------------------------------- */ 147 | a { 148 | color: #348eda; 149 | text-decoration: underline; 150 | } 151 | 152 | .btn-primary { 153 | text-decoration: none; 154 | color: #FFF; 155 | background-color: #348eda; 156 | border: solid #348eda; 157 | border-width: 10px 20px; 158 | line-height: 2em; 159 | /* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */ 160 | /*line-height: 28px;*/ 161 | font-weight: bold; 162 | text-align: center; 163 | cursor: pointer; 164 | display: inline-block; 165 | border-radius: 5px; 166 | text-transform: capitalize; 167 | } 168 | 169 | /* ------------------------------------- 170 | OTHER STYLES THAT MIGHT BE USEFUL 171 | ------------------------------------- */ 172 | .last { 173 | margin-bottom: 0; 174 | } 175 | 176 | .first { 177 | margin-top: 0; 178 | } 179 | 180 | .aligncenter { 181 | text-align: center; 182 | } 183 | 184 | .alignright { 185 | text-align: right; 186 | } 187 | 188 | .alignleft { 189 | text-align: left; 190 | } 191 | 192 | .clear { 193 | clear: both; 194 | } 195 | 196 | /* ------------------------------------- 197 | ALERTS 198 | Change the class depending on warning email, good email or bad email 199 | ------------------------------------- */ 200 | .alert { 201 | color: #074f66; 202 | font-weight: 800; 203 | font-size: 26px; 204 | padding: 20px 0 0 0; 205 | text-align: center; 206 | border-radius: 3px 3px 0 0; 207 | } 208 | .alert a { 209 | color: #fff; 210 | text-decoration: none; 211 | font-weight: 500; 212 | font-size: 16px; 213 | } 214 | .alert.alert-warning { 215 | background-color: #FF9F00; 216 | } 217 | .alert.alert-bad { 218 | background-color: #D0021B; 219 | } 220 | .alert.alert-good { 221 | background-color: #68B90F; 222 | } 223 | 224 | /* ------------------------------------- 225 | INVOICE 226 | Styles for the billing table 227 | ------------------------------------- */ 228 | .invoice { 229 | margin: 40px auto; 230 | text-align: left; 231 | width: 80%; 232 | } 233 | .invoice td { 234 | padding: 5px 0; 235 | } 236 | .invoice .invoice-items { 237 | width: 100%; 238 | } 239 | .invoice .invoice-items td { 240 | border-top: #eee 1px solid; 241 | } 242 | .invoice .invoice-items .total td { 243 | border-top: 2px solid #333; 244 | border-bottom: 2px solid #333; 245 | font-weight: 700; 246 | } 247 | 248 | /* ------------------------------------- 249 | RESPONSIVE AND MOBILE FRIENDLY STYLES 250 | ------------------------------------- */ 251 | @media only screen and (max-width: 640px) { 252 | body { 253 | padding: 0 !important; 254 | } 255 | 256 | h1, h2, h3, h4 { 257 | font-weight: 800 !important; 258 | margin: 20px 0 5px !important; 259 | } 260 | 261 | h1 { 262 | font-size: 22px !important; 263 | } 264 | 265 | h2 { 266 | font-size: 18px !important; 267 | } 268 | 269 | h3 { 270 | font-size: 16px !important; 271 | } 272 | 273 | .container { 274 | padding: 0 !important; 275 | width: 100% !important; 276 | } 277 | 278 | .content { 279 | padding: 0 !important; 280 | } 281 | 282 | .content-wrap { 283 | padding: 10px !important; 284 | } 285 | 286 | .invoice { 287 | width: 100% !important; 288 | } 289 | } 290 | 291 | /* ------------------------------------- 292 | CUSTOM RULES 293 | ------------------------------------- */ 294 | 295 | .notifications-day { 296 | font-size: 16px; 297 | padding: 8px 0 0 7px; 298 | } 299 | 300 | .action-button { 301 | padding: 15px 0 0 5px; 302 | } 303 | 304 | .greetings { 305 | padding: 0 0 10px 0; 306 | } 307 | 308 | .signature { 309 | padding: 15px 0 0 0; 310 | } 311 | 312 | /*# sourceMappingURL=styles.css.map */ 313 | -------------------------------------------------------------------------------- /views/email/src/newsletter.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 52 | 53 | 54 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 38 | 39 |
19 | Git Notifier 20 |
24 | 25 | 26 | 29 | 30 | 31 | 35 | 36 |
27 | <%= content %> 28 |
32 | Best,
33 | - The Git Notifier Team 34 |
37 |
40 | 50 |
51 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /views/email/src/notification.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 72 | 73 | 74 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 58 | 59 |
19 | Git Notifier 20 |
24 | 25 | 26 | 30 | 31 | <% for event in events %> 32 | <% if defined?(event['day']) and !event['day'].nil? %> 33 | 34 | 37 | 38 | <% end %> 39 | 40 | 43 | 44 | <% end %> 45 | 46 | 49 | 50 | 51 | 55 | 56 |
27 | Hello <%= defined?(username) ? username : 'dear user' %> :)
28 | <%= notifications_text %> 29 |
35 | <%= event['day'] %> 36 |
41 | <%= event['html'] %> 42 |
47 | View your full notifications history 48 |
52 | Best,
53 | - The Git Notifier Team 54 |
57 |
60 | 70 |
71 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /views/events.haml: -------------------------------------------------------------------------------- 1 | %table.table.table-responsive{ :id => 'events', :'data-id' => github_id } 2 | %tbody 3 | %div#spinner 4 | %div#noevents.hidden 5 | %h4 No events found. Your projects probably haven't been starred or forked yet. 6 | -------------------------------------------------------------------------------- /views/faq.haml: -------------------------------------------------------------------------------- 1 | %section#faq-list 2 | 3 | %h3 What is Git Notifier? 4 | .answer 5 | %p Git Notifier is a service that helps you keep track of all the events happening to your GitHub user and repositories.
If you're watching a lot of repositories and following a lot of people your timeline will most certainly be cluttered in events and it's easy to miss something that could be nice to know, like somebody that starred or started watching one of your repositories or even following you and Git Notifier will even let you know when somebody is no longer following you.
You can decide how often you wish to receive notifications (as soon as possible or in a nice daily or weekly report) and for which events. 6 | 7 | %h3 Is it free? 8 | .answer 9 | %p Short answer: yes. In the future we may have to offer premium services (eg: better email deliverability, notifications checks happening more often) for a small fee should the server costs increase too much.
Anyway, since this project is completely open source you will always be able to just host you own version of Git Notifier for free. 10 | 11 | %h3 Will you send me spam? 12 | .answer 13 | %p No, never, we all hate spammers so I will never be one. 14 | 15 | %h3 Can I choose to receive only some type of notifications? 16 | .answer 17 | %p Yes, you can select which type of notifications you wish to receive right from your profile preferences, just login and you'll see the link in the top bar. 18 | 19 | %h3 I’m receiving too many emails, what can I do? 20 | .answer 21 | %p Glad you asked, just login and go to your preferences, there you will be able to change the freqency and type of the notifications you wish to receive. 22 | 23 | %h3 How do I unsubscribe? 24 | .answer 25 | %p You can find an unsubscribe link at the bottom of every email that you receive from us. 26 | 27 | %h3 I find this service useful, how can I help to keep it running? 28 | .answer 29 | %p Thanks for asking! You could help improve the service by contributing to our public GitHub repository, or you could make a donation using PayPal or Bitcoin: 12NzvEg19KZoNuQzT39mcmvDD7K14wJenR 30 | 31 | %h3 Is Git Notifier open source? Can I get the source code? 32 | .answer 33 | %p Yes! We love OSS, you can find all the code that powers this service on GitHub. This project will always be open source. 34 | 35 | %h3 Who built Git Notifier? 36 | .answer 37 | %p Andrea Usuelli 38 | 39 | %h3 Is Git Notifier affiliated in any way with GitHub? 40 | .answer 41 | %p No, Git Notifier is not an official GitHub project. 42 | -------------------------------------------------------------------------------- /views/index.haml: -------------------------------------------------------------------------------- 1 | #top.jumbotron 2 | .container 3 | %h1< 4 | Git 5 | %span.orange> Notifier 6 | %h2 Get email notifications when someone stars or forks
one of your GitHub repos and follows/unfollows you. 7 | %div.col-md-4.col-md-offset-4 8 | %a.btn.btn-lg.btn-block.btn-social.btn-github(href="/authorize") 9 | %i.fa.fa-github 10 | Sign in with GitHub 11 | 12 | .container 13 | %h3#features.subhead Features 14 | .row.benefits 15 | .col-md-4.col-sm-6.benefit 16 | .benefit-ball 17 | %span.glyphicon.glyphicon-envelope 18 | %h3 Never miss an event 19 | %p We will send you notifications when someone stars or forks one of your repositories and when someone follows or unfollows you. 20 | .col-md-4.col-sm-6.benefit 21 | .benefit-ball 22 | %span.glyphicon.glyphicon-time 23 | %h3 asap, daily or weekly 24 | %p Choose the frequency of the notifications that best suits you. We can send you every notification asap or a nice summary daily or weekly. 25 | .col-md-4.col-sm-6.benefit 26 | .benefit-ball 27 | %span.glyphicon.glyphicon-wrench 28 | %h3 Set up and forget 29 | %p The sign up process takes less than 30 seconds and you will start receiving meaningful notifications without the need of ever doing anything else. 30 | 31 | .container-alternate 32 | .container#tour 33 | %h3#tour-head.subhead Weekly report example 34 | .row 35 | .col-md-12 36 | %img.img-responsive{:src => "img/screenshot1.png", :alt => 'Weekly report screenshot'} 37 | 38 | .container 39 | %h3#faqs.subhead Frequently Asked Questions 40 | .row.faqs 41 | %p.col-md-4.col-sm-6 42 | %strong What is Git Notifier? 43 | %br 44 | Git Notifier is a service that helps you keep track of all the events happening to your GitHub user and repositories.
45 | If you're watching a lot of repositories and following a lot of people your timeline will most certainly be cluttered in events 46 | and it's easy to miss something that could be nice to know, like somebody that starred or started watching 47 | one of your repositories or even following you and Git Notifier will even let you know when somebody is no longer following you.
48 | You can decide how often you wish to receive notifications (as soon as possible or in a nice daily or weekly report) and for which events. 49 | %p.col-md-4.col-sm-6 50 | %strong Is it free? 51 | %br 52 | Yes! In the future we may have to offer premium services (eg: better email deliverability, ...) 53 | for a small fee should the server costs increase too much.
54 | Anyway, since this project is open source you will always be able to just host your own version of Git Notifier for free. 55 | %p.col-md-4.col-sm-6 56 | %strong Is Git Notifier open source? 57 | %br 58 | Yes! We love OSS, so rest assured that this project will always be completely open source.
59 | You can find all the code that powers this service on GitHub.
60 | You are more than welcome to contibute if you'd like to! 61 | %p.col-md-4.col-sm-6 62 | %strong Will you send me spam? 63 | %br 64 | No, never, we all hate spammers so I will never be one. 65 | %p.col-md-4.col-sm-6 66 | %strong Can I choose to receive only some type of notifications? 67 | %br 68 | Yes, you can select which type of notifications you wish to receive right from your profile preferences, 69 | just login and you'll see the link in the top bar.
70 | From there you will also be able to change the notifications frequency. 71 | 72 | .container-alternate 73 | .container#stack 74 | %h3.subhead Stack 75 | .row.stack 76 | .col-md-10.col-md-offset-1.text-center 77 | %a{:href => 'http://www.ruby-lang.org'} 78 | %img.img-responsive{:src => "img/stack/ruby.png", :'aria-hidden' => "true", :'data-toggle' => "tooltip", :'data-placement' => "top", :title => "Ruby", :alt => 'Ruby logo'} 79 | %a{:href => 'http://www.sinatrarb.com'} 80 | %img.img-responsive{:src => "img/stack/sinatra.png", :'aria-hidden' => "true", :'data-toggle' => "tooltip", :'data-placement' => "top", :title => "Sinatra", :alt => 'Sinatra logo'} 81 | %a{:href => 'http://redis.io'} 82 | %img.img-responsive{:src => "img/stack/redis.png", :'aria-hidden' => "true", :'data-toggle' => "tooltip", :'data-placement' => "top", :title => "Redis", :alt => 'Redis logo'} 83 | %a{:href => 'http://sidekiq.org'} 84 | %img.img-responsive{:src => "img/stack/sidekiq.png", :'aria-hidden' => "true", :'data-toggle' => "tooltip", :'data-placement' => "top", :title => "Sidekiq", :alt => 'Sidekiq logo'} 85 | -------------------------------------------------------------------------------- /views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html(lang="en") 3 | %head 4 | %meta(charset="utf-8") 5 | %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") 6 | %meta(name="viewport" content="width=device-width, initial-scale=1") 7 | %title GitNotifier#{(defined?(@page_title) ? ' - ' + @page_title : '')} 8 | 9 | %link(href="/css/bootstrap.min.css?v=#{@deploy_id}" rel="stylesheet") 10 | %link(href="/css/font-awesome.min.css?v=#{@deploy_id}" rel="stylesheet") 11 | %link(href="/css/bootstrap-social.css?v=#{@deploy_id}" rel="stylesheet") 12 | %link(href="/css/main.css?v=#{@deploy_id}" rel="stylesheet") 13 | %link(href="/css/octicons/octicons.css?v=#{@deploy_id}" rel="stylesheet") 14 | %link{:href => "https://fonts.googleapis.com/css?family=Lato:300,400,300italic,400italic", :rel => "stylesheet", :type => "text/css"} 15 | %link{:href => "https://fonts.googleapis.com/css?family=Montserrat:400,700", :rel => "stylesheet", :type => "text/css"}/ 16 | 17 | / HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries 18 | / WARNING: Respond.js doesn't work if you view the page via file:// 19 | / [if lt IE 9] 20 | %script(src="/js/html5shiv.min.js?v=#{@deploy_id}") 21 | %script(src="/js/respond.min.js?v=#{@deploy_id}") 22 | =get_head_js() 23 | %body 24 | %nav.navbar.navbar-fixed-top{:role => "navigation"} 25 | .container 26 | .navbar-header 27 | %button.navbar-toggle{"data-target" => ".navbar-ex1-collapse", "data-toggle" => "collapse", :type => "button"} 28 | %span.icon-bar 29 | %span.icon-bar 30 | %span.icon-bar 31 | %a.navbar-brand.nav-link{:href => "/"} GitNotifier 32 | .collapse.navbar-collapse.navbar-ex1-collapse 33 | %ul.nav.navbar-nav.navbar-right 34 | - get_menu().each do |item| 35 | %li 36 | %a.nav-link{:href => item[:href]} #{item[:desc]} 37 | 38 | - unless defined? index and index 39 | .container.first-cnt 40 | =get_flash() 41 | =yield 42 | - else 43 | =get_flash(index) 44 | =yield 45 | 46 | %footer 47 | .container 48 | %p.text-center 49 | %span.footer 50 | %a{:href => "/"} Home 51 | %span.footer 52 | %a{:href => "/faq"} FAQ 53 | %span.footer 54 | %a{:href => "http://status.gitnotifier.io"} Status 55 | %span.footer 56 | %a{:href => "mailto:#{settings.CONFIG['dev_email_address']}"} Support 57 | %span.footer 58 | %a{:href => "https://github.com/andreausu/git-notifier"} 59 | %i.fa.fa-github.fa-lg 60 | %span.footer 61 | %a{:href => "https://twitter.com/gitnotifier"} 62 | %i.fa.fa-twitter.fa-lg 63 | %span.footer 64 | %a{:href => "https://www.facebook.com/gitnotifier"} 65 | %i.fa.fa-facebook 66 | 67 | %script(src="/js/jquery.min.js?v=#{@deploy_id}") 68 | %script(src="/js/bootstrap.min.js?v=#{@deploy_id}") 69 | =get_additional_js() 70 | %script(src="/js/app.js?v=#{@deploy_id}") 71 | -------------------------------------------------------------------------------- /views/preferences.haml: -------------------------------------------------------------------------------- 1 | %div#preferences{:'data-csrf' => csrf_token()} 2 | %div#notifications_type 3 | %h5 Choose the type of notifications you wish to receive 4 | - notifications_type.each do |notif| 5 | .checkbox 6 | %label 7 | %input{ :type => "checkbox", :name => "notifications[]", :id => notif, :value => notif, :checked => (!disabled_notifications_type.include? notif)} 8 | = notif.capitalize 9 | 10 | %br 11 | 12 | %div#notifications_frequency 13 | %h5 Choose at which frequency we should send you the notifications 14 | - notifications_frequency.each do |notif| 15 | .radio 16 | %label 17 | %input{ :type => "radio", :name => 'notifications_frequency', :id => notif, :value => notif, :checked => (notif == current_frequency)} 18 | = notif.capitalize 19 | 20 | %button(type="submit" class="btn btn-default" id="button_save_preferences") Save 21 | -------------------------------------------------------------------------------- /views/signup.haml: -------------------------------------------------------------------------------- 1 | %form(role="form" id="signup" action="/signup" method="post") 2 | = csrf_tag() 3 | .form-group 4 | - email_addresses.each do |email| 5 | .radio 6 | %label 7 | %input(type="radio" name="email" value="#{email}" checked) 8 | = email 9 | .radio 10 | %label 11 | %input(type="radio" name="email" id="other_email_radio" value="other_email") 12 | %input(id="other_email" type="email" name="other_email" value="" placeholder="Choose a different email") 13 | %div#other_email_suggestion.form_suggestion 14 | %button(type="submit" class="btn btn-default" id="signup_button") 15 | Signup 16 | -------------------------------------------------------------------------------- /views/unsubscribe.haml: -------------------------------------------------------------------------------- 1 | %div#unsubscribe_question 2 | %h3 Getting too many emails?
You can choose to receive a nice daily or weekly report instead! 3 | %h4 Otherwise, click here to unsubscribe. 4 | 5 | %div#unsubscribe_form.hidden(style="padding-bottom: 20px;") 6 | %form(role="form" action="/unsubscribe" method="post") 7 | = csrf_tag() 8 | .form-group 9 | - notifications_type.each do |notif| 10 | .checkbox 11 | %label 12 | %input{ :type => "checkbox", :name => "notifications[]", :id => notif, :value => notif, :checked => (!disabled_notifications_type.include? notif)} 13 | = notif 14 | 15 | %input{ :type => "hidden", :name => "id", :value => github_id} 16 | %input{ :type => "hidden", :name => "timestamp", :value => timestamp} 17 | %input{ :type => "hidden", :name => "v", :value => hmac} 18 | %button(type="submit" class="btn btn-default" name="unsubscribe") Change preferences 19 | %button(type="submit" class="btn btn-default" name="unsubscribe_all") Unsubscribe all 20 | 21 | %div#change_frequency_form(style="padding-bottom: 20px;") 22 | %form(role="form" action="/unsubscribe" method="post") 23 | = csrf_tag() 24 | .form-group 25 | - notifications_frequency.each do |notif| 26 | .radio 27 | %label 28 | %input{ :type => "radio", :name => 'notifications_frequency', :id => notif, :value => notif, :checked => (notif == current_frequency)} 29 | = notif.capitalize 30 | 31 | %input{ :type => "hidden", :name => "id", :value => github_id} 32 | %input{ :type => "hidden", :name => "timestamp", :value => timestamp} 33 | %input{ :type => "hidden", :name => "v", :value => hmac} 34 | %button(type="submit" class="btn btn-default" name="change_frequency") Save 35 | -------------------------------------------------------------------------------- /workers/email_builder.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'pp' 3 | require 'uri' 4 | require_relative 'send_email' 5 | 6 | class EmailBuilder 7 | include Sidekiq::Worker 8 | sidekiq_options :queue => :email_builder 9 | STATSD = Datadog::Statsd.new() unless defined? STATSD 10 | def perform(events_list_key) 11 | STATSD.increment('ghntfr.workers.email_builder.start') 12 | 13 | events = nil 14 | user = nil 15 | emailEvents = [] 16 | event_ids = [] 17 | 18 | Sidekiq.redis do |conn| 19 | events = conn.lrange(events_list_key, 0, '-1') 20 | user = conn.hgetall("#{CONFIG['redis']['namespace']}:users:" + events_list_key.split(':').last) 21 | end 22 | 23 | events.each do |event| 24 | event = JSON.parse(event) 25 | type = event['type'] 26 | entity = event['entity'] 27 | event_name = '' 28 | 29 | next if user['disabled_notifications_type'] && user['disabled_notifications_type'].include?(type) 30 | 31 | timestamp = Time.now.to_i 32 | timestamp_at_midnight = (timestamp - timestamp % (3600*24)).to_s 33 | 34 | if entity['id'] 35 | if type == 'follow' || type == 'unfollow' 36 | timestamp = Time.now.to_i 37 | event_name = "#{entity['id']}_#{type}_#{timestamp_at_midnight}" 38 | else 39 | event_name = "#{entity['id']}_#{timestamp_at_midnight}" 40 | end 41 | else 42 | event_ids << "#{entity}_#{timestamp_at_midnight}" 43 | end 44 | 45 | event_ids << event_name 46 | 47 | case type 48 | when 'star' 49 | html = "#{entity['actor']['login']} starred #{entity['repo']['name'][/\/(.+)/, 1]}" 50 | when 'fork' 51 | html = "#{entity['actor']['login']} forked #{entity['repo']['name'][/\/(.+)/, 1]} to #{entity['payload']['forkee']['full_name']}" 52 | when 'follow' 53 | html = "#{entity['login']} started following you" 54 | when 'unfollow' 55 | html = "#{entity['login']} is not following you anymore" 56 | when 'deleted' 57 | html = "#{entity} that was following you has been deleted" 58 | end 59 | 60 | emailEvents << {:html => html, :timestamp => event['timestamp']} 61 | end 62 | 63 | unless emailEvents.empty? 64 | emailEvents = inject_day(emailEvents) if user['notifications_frequency'] == 'weekly' 65 | 66 | expiry = (Time.now + 31536000).to_i.to_s 67 | 68 | digest = OpenSSL::Digest.new('sha512') 69 | hmac = OpenSSL::HMAC.hexdigest(digest, CONFIG['secret'], user['github_id'] + expiry) 70 | 71 | unsubscribe_url = URI.escape("https://#{CONFIG['domain']}/unsubscribe?id=#{user['github_id']}&expiry=#{expiry}&v=#{hmac}") 72 | 73 | to = user['email'] 74 | subject = "You have #{emailEvents.length == 1 ? 'a new notification' : emailEvents.length.to_s + ' new notifications'}" 75 | notificationsText = subject + (emailEvents.length == 1 ? "!
You notification was received on #{Time.at(emailEvents[0][:timestamp]).strftime('%A %b %e')} at #{Time.at(emailEvents[0][:timestamp]).strftime('%k:%M')}." : "!
Your last notification was received on #{Time.at(emailEvents[0][:timestamp]).strftime('%A %b %e')} at #{Time.at(emailEvents[0][:timestamp]).strftime('%k:%M')}.") 76 | 77 | case user['notifications_frequency'] 78 | when 'daily' 79 | subject = "#{Time.now.strftime('%b %e')} daily report: #{subject}" 80 | when 'weekly' 81 | subject = "#{Time.now.strftime('%b %e')} weekly report: #{subject}" 82 | end 83 | 84 | SendEmail.perform_async( 85 | to, 86 | subject, 87 | 'html', 88 | 'notification', 89 | {:events => emailEvents, :username => user['login'], :unsubscribe_url => unsubscribe_url, :notifications_text => notificationsText, :site_url => "https://#{CONFIG['domain']}/?utm_source=notifications&utm_medium=email&utm_campaign=timeline&utm_content=#{user['notifications_frequency']}"}, 90 | events_list_key, 91 | "#{CONFIG['redis']['namespace']}:locks:email:#{user['github_id']}", 92 | event_ids, 93 | "#{CONFIG['redis']['namespace']}:users:#{user['github_id']}" 94 | ) unless user['email_confirmed'] == "0" 95 | end 96 | 97 | Sidekiq.redis do |conn| 98 | conn.hset("#{CONFIG['redis']['namespace']}:users:" + events_list_key.split(':').last, :last_email_queued_on, Time.now.to_i) 99 | end 100 | 101 | STATSD.increment('ghntfr.workers.email_builder.finish') 102 | end 103 | 104 | def inject_day(events) 105 | previousEvent = nil 106 | events.map! do |event| 107 | if previousEvent.nil? || (Time.at(previousEvent[:timestamp]).strftime('%d') != Time.at(event[:timestamp]).strftime('%d')) 108 | event[:day] = Time.at(event[:timestamp]).strftime('%A, %b %e') 109 | end 110 | previousEvent = event 111 | end 112 | 113 | events 114 | end 115 | 116 | end 117 | -------------------------------------------------------------------------------- /workers/init.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'datadog/statsd' 4 | 5 | GC::Profiler.enable 6 | 7 | config_file = File.dirname(__FILE__) + '/../config.yml' 8 | fail "Configuration file " + config_file + " missing!" unless File.exist?(config_file) 9 | CONFIG = YAML.load_file(config_file) 10 | 11 | STATSD = Datadog::Statsd.new(CONFIG['statsd']['host'], CONFIG['statsd']['port']) 12 | 13 | redis_conn = proc { 14 | Redis.new( 15 | :driver => :hiredis, 16 | :host => CONFIG['redis']['host'], 17 | :port => CONFIG['redis']['port'], 18 | :db => CONFIG['redis']['db'], 19 | network_timeout: 5 20 | ) 21 | } 22 | 23 | Sidekiq.configure_client do |config| 24 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 25 | end 26 | 27 | Sidekiq.configure_server do |config| 28 | config.redis = ConnectionPool.new(size: 27, &redis_conn) 29 | end 30 | 31 | require_relative 'notifications_checker' 32 | require_relative 'email_builder' 33 | require_relative 'send_email' 34 | require 'newrelic-redis' 35 | require 'newrelic_rpm' # it should be the last entry in the require list 36 | -------------------------------------------------------------------------------- /workers/notifications_checker.rb: -------------------------------------------------------------------------------- 1 | require 'github_api' 2 | require 'pp' 3 | require 'json' 4 | require 'net/http' 5 | require 'datadog/statsd' 6 | 7 | class NotificationsChecker 8 | include Sidekiq::Worker 9 | sidekiq_options :queue => :notifications_checker, :retry => false, :dead => false 10 | @first_time = nil 11 | @new_events = nil 12 | STATSD = Datadog::Statsd.new() unless defined? STATSD 13 | def perform(user_key, first_time = false) 14 | STATSD.increment('ghntfr.workers.notifications_checker.start') 15 | @new_events = [] 16 | puts 'Started processing ' + user_key 17 | @first_time = first_time 18 | 19 | lock_key = "#{CONFIG['redis']['namespace']}:locks:notifications_checker:" + user_key.split(':').last 20 | if Sidekiq.redis { |conn| conn.get(lock_key) } 21 | puts "Notifications check already in progress! Lock found in #{lock_key}" 22 | return 23 | else 24 | Sidekiq.redis { |conn| conn.set(lock_key, 0, {:ex => 210}) } # lock for 3:30 minutes max 25 | end 26 | 27 | user = Sidekiq.redis { |conn| conn.hgetall(user_key) } 28 | pp user 29 | 30 | github = Github.new( 31 | client_id: CONFIG['github']['client_id'], 32 | client_secret: CONFIG['github']['client_secret'], 33 | oauth_token: user['token'] 34 | ) 35 | 36 | last_event_id = 0 37 | 38 | puts "Checking new events..." 39 | 40 | begin 41 | response = github.activity.events.received user['login'] 42 | rescue Exception => e 43 | NewRelic::Agent.notice_error(e) 44 | puts e.message 45 | puts e.backtrace.inspect 46 | Sidekiq.redis { |conn| conn.del(lock_key) } 47 | return 48 | end 49 | 50 | catch (:break) do 51 | response.each_page do |page| 52 | page = page.to_a 53 | page.each do |event| 54 | last_event_id = event[:id].to_i unless last_event_id > 0 55 | throw :break if event[:id].to_i <= user['last_event_id'].to_i 56 | case event[:type] 57 | when 'WatchEvent' 58 | if event[:repo][:name].include? user['login'] 59 | puts "#{event[:actor][:login]} starred your project #{event[:repo][:name]}" 60 | on_new_event('star', event) 61 | end 62 | when 'ForkEvent' 63 | if event[:repo][:name].include? user['login'] 64 | puts "#{event[:actor][:login]} forked your project #{event[:repo][:name]}" 65 | on_new_event('fork', event) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | Sidekiq.redis { |conn| conn.hset(user_key, :last_event_id, last_event_id) } 72 | 73 | puts "Checking new followers..." 74 | 75 | followers = {} 76 | begin 77 | response = github.users.followers.list 78 | rescue Exception => e 79 | NewRelic::Agent.notice_error(e) 80 | puts e.message 81 | puts e.backtrace.inspect 82 | Sidekiq.redis { |conn| conn.del(lock_key) } 83 | return 84 | end 85 | 86 | response.each_page do |page| 87 | page = page.to_a 88 | page.each do |follower| 89 | followers[follower[:login]] = follower 90 | end 91 | end 92 | 93 | if user['followers'] 94 | user['followers'] = JSON.parse(user['followers']) 95 | new_followers = followers.keys - user['followers'] 96 | unfollowed = user['followers'] - followers.keys 97 | new_followers.each do |login| 98 | on_new_event('follow', followers[login]) 99 | end 100 | unfollowed.each do |login| 101 | begin 102 | response = github.users.get(user: login) 103 | on_new_event('unfollow', response.body) 104 | rescue Github::Error::NotFound 105 | on_new_event('deleted', login) 106 | rescue Exception => e 107 | NewRelic::Agent.notice_error(e) 108 | puts e.message 109 | puts e.backtrace.inspect 110 | Sidekiq.redis { |conn| conn.del(lock_key) } 111 | return 112 | end 113 | end 114 | end 115 | 116 | Sidekiq.redis do |conn| 117 | conn.pipelined do 118 | conn.hset(user_key, :followers, JSON.generate(followers.keys)) 119 | conn.del(lock_key) 120 | conn.hset(user_key, :first_check_completed, 1) if @first_time 121 | end 122 | end 123 | 124 | if !@new_events.empty? 125 | Sidekiq.redis do |conn| 126 | conn.pipelined do 127 | @new_events.each do |event| 128 | conn.lpush( 129 | "#{CONFIG['redis']['namespace']}:events:batch:#{user['github_id']}", 130 | JSON.generate(event) 131 | ) unless @first_time || !user['email'] 132 | 133 | conn.lpush( 134 | "#{CONFIG['redis']['namespace']}:events:#{user['github_id']}", 135 | JSON.generate(event) 136 | ) 137 | #conn.ltrim "#{CONFIG['redis']['namespace']}:events:#{user['github_id']}", 0, 99 138 | end 139 | end 140 | end 141 | enqueue_email_builder(user) unless @first_time 142 | end 143 | STATSD.increment('ghntfr.workers.notifications_checker.finish') 144 | end 145 | 146 | def on_new_event(type, entity) 147 | timestamp = nil 148 | unless @first_time 149 | if type == 'fork' && defined?(entity['payload']['forkee']['created_at']) 150 | timestamp = DateTime.parse(entity['payload']['forkee']['created_at']).to_time.to_i 151 | else 152 | timestamp = Time.now.to_i 153 | end 154 | end 155 | @new_events.unshift({:type => type, :entity => entity, :timestamp => timestamp}) 156 | end 157 | 158 | def enqueue_email_builder(user) 159 | new_key = "#{CONFIG['redis']['namespace']}:processing:events:batch:#{user['github_id']}" 160 | 161 | enqueue = false 162 | 163 | return if user['email_confirmed'] == "0" 164 | 165 | case user['notifications_frequency'] 166 | when 'asap' 167 | enqueue = true 168 | puts "EmailBuilder job enqueued" 169 | when 'daily' 170 | if user['last_email_queued_on'].to_i <= (Time.now.to_i - (60 * 60 * 24)) # 1 day 171 | enqueue = true 172 | puts "EmailBuilder job enqueued" 173 | else 174 | puts "Waiting for some more time before enqueuing the EmailBuilder job" 175 | end 176 | when 'weekly' 177 | if user['last_email_queued_on'].to_i <= (Time.now.to_i - (60 * 60 * 24 * 7)) # 7 days 178 | enqueue = true 179 | puts "EmailBuilder job enqueued" 180 | else 181 | puts "Waiting for some more time before enqueuing the EmailBuilder job" 182 | end 183 | end 184 | 185 | if enqueue 186 | begin 187 | Sidekiq.redis { |conn| conn.rename("#{CONFIG['redis']['namespace']}:events:batch:#{user['github_id']}", new_key) } 188 | EmailBuilder.perform_async(new_key) 189 | rescue Redis::CommandError => e 190 | NewRelic::Agent.notice_error(e) 191 | rescue Exception => e 192 | NewRelic::Agent.notice_error(e) 193 | Sidekiq.redis { |conn| conn.del(lock_key) } 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /workers/send_email.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'erb' 3 | require 'mail' 4 | # This is trying to workaround an issue in `mail` gem 5 | # Issue: https://github.com/mikel/mail/issues/912 6 | # 7 | # Since with current version (2.6.3) it is using `autoload` 8 | # And as mentioned by a comment in the issue above 9 | # It might not be thread-safe and 10 | # might have problem in threaded environment like Sidekiq workers 11 | # 12 | # So we try to require the file manually here to avoid 13 | # "uninitialized constant" error 14 | # 15 | # This is merely a workaround since 16 | # it should fixed by not using the `autoload` 17 | require "mail/parsers/content_type_parser" 18 | require 'datadog/statsd' 19 | 20 | class SendEmail 21 | include Sidekiq::Worker 22 | sidekiq_options :queue => :send_email 23 | STATSD = Datadog::Statsd.new() unless defined? STATSD 24 | def perform(to, subject, content_type = 'text', template = nil, locals = {}, delete_key = nil, lock_key = nil, lock_id = nil, user_id = nil) 25 | STATSD.increment('ghntfr.workers.send_email.start') 26 | 27 | raise "Missing template!" unless template 28 | 29 | if lock_id && Sidekiq.redis { |conn| conn.zscore(lock_key, JSON.generate(lock_id)) } 30 | puts "Email already sent! #{lock_id} found in #{lock_key}" 31 | return 32 | end 33 | 34 | mail = Mail.new do 35 | from CONFIG['mail']['from'] 36 | to to 37 | subject subject 38 | end 39 | 40 | textTemplate = File.dirname(__FILE__) + "/../views/email/#{template}.txt" 41 | textBody = ERB.new(File.read(textTemplate)).result(OpenStruct.new(locals).instance_eval { binding }) 42 | 43 | if content_type == 'html' 44 | 45 | htmlTemplate = File.dirname(__FILE__) + "/../views/email/#{template}.erb" 46 | htmlBody = ERB.new(File.read(htmlTemplate)).result(OpenStruct.new(locals).instance_eval { binding }) 47 | 48 | html_part = Mail::Part.new do 49 | content_type 'text/html; charset=UTF-8' 50 | body htmlBody 51 | end 52 | text_part = Mail::Part.new do 53 | body textBody 54 | end 55 | 56 | mail.html_part = html_part 57 | mail.text_part = text_part 58 | else 59 | mail.body textBody 60 | end 61 | 62 | if CONFIG['mail']['method'] == 'sendmail' 63 | mail.delivery_method(:sendmail) 64 | else 65 | opts = {address: CONFIG['mail']['host'], port: CONFIG['mail']['port'], enable_starttls_auto: CONFIG['mail']['ssl']} 66 | opts[:user_name] = CONFIG['mail']['user'] unless CONFIG['mail']['user'].nil? || CONFIG['mail']['user'].empty? 67 | opts[:password] = CONFIG['mail']['password'] unless CONFIG['mail']['user'].nil? || CONFIG['mail']['password'].empty? 68 | 69 | mail.delivery_method(:smtp, opts) 70 | end 71 | 72 | mail.deliver if CONFIG['mail']['enabled'] 73 | 74 | Sidekiq.redis do |conn| 75 | conn.hset(user_id, :last_email_sent_on, Time.now.to_i) if user_id 76 | conn.del(delete_key) if delete_key 77 | conn.zadd(lock_key, Time.now.to_i, JSON.generate(lock_id)) if lock_id 78 | end 79 | end 80 | STATSD.increment('ghntfr.workers.send_email.finish') 81 | end 82 | 83 | def strip_html(string) 84 | string.gsub(//, "\r\n").gsub(/<\/?[^>]*>/, '') 85 | end 86 | --------------------------------------------------------------------------------