├── .env.sample ├── .env.test ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── PRIVACY.md ├── Procfile ├── README.md ├── Rakefile ├── app.json ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── arrow.svg │ │ ├── github.png │ │ ├── heroku.png │ │ ├── logo-github.svg │ │ ├── logo-heroku.svg │ │ ├── logo-slack.svg │ │ ├── screencap.png │ │ ├── slack.png │ │ ├── sound-wave.png │ │ └── sound-wave.svg │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── channels │ │ │ └── .keep │ └── stylesheets │ │ ├── application.css │ │ └── screen.sass ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── application_controller.rb │ ├── commands_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── webhook_validations.rb │ ├── pages_controller.rb │ ├── sessions_controller.rb │ └── webhooks_controller.rb ├── helpers │ ├── application_helper.rb │ └── sessions_helper.rb ├── jobs │ ├── application_job.rb │ ├── command_runner_job.rb │ └── webhook_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── command.rb │ ├── commands │ │ ├── default.rb │ │ ├── help.rb │ │ ├── organization.rb │ │ └── route.rb │ ├── concerns │ │ ├── .keep │ │ ├── attribute_encryption.rb │ │ ├── command_onboarding.rb │ │ ├── octokit_methods.rb │ │ ├── organization_helpers.rb │ │ └── repository_helpers.rb │ ├── git_hub.rb │ ├── git_hub │ │ ├── event_messages.rb │ │ ├── event_messages │ │ │ ├── create.rb │ │ │ ├── delete.rb │ │ │ ├── deployment_status.rb │ │ │ ├── ping.rb │ │ │ ├── pull_request.rb │ │ │ ├── push.rb │ │ │ ├── status.rb │ │ │ └── unknown.rb │ │ ├── organization.rb │ │ └── repository.rb │ ├── slack_h_q.rb │ └── slack_h_q │ │ ├── team.rb │ │ └── user.rb └── views │ ├── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ └── pages │ ├── _github-logo.html.erb │ ├── _heroku-logo.html.erb │ ├── _slack-logo.html.erb │ ├── index.html.erb │ └── support.html.erb ├── bin ├── bundle ├── cibuild ├── rails ├── rake ├── setup └── update ├── circle.yml ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── initializers │ ├── active_record_belongs_to_required_by_default.rb │ ├── admin_constraint.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── callback_terminator.rb │ ├── coal_car.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── omniauth.rb │ ├── per_form_csrf_tokens.rb │ ├── request_forgery_protection.rb │ ├── session_store.rb │ ├── sidekiq.rb │ ├── slack_application.rb │ ├── slack_h_q.rb │ ├── ssl_options.rb │ ├── to_time_preserves_timezone.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20160611165409_enable_uuid_extension.rb │ ├── 20160612002222_create_slack_teams.rb │ ├── 20160612201904_fix_bot_tokens.rb │ ├── 20160612204943_fix_slack_team_id.rb │ ├── 20160612224739_create_commands.rb │ └── 20160613174641_create_git_hub_organizations.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── db.rake ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── spec ├── fixtures │ ├── api.github.com │ │ └── orgs │ │ │ └── atmos-org │ │ │ ├── hooks │ │ │ ├── configured.json │ │ │ ├── create.json │ │ │ └── empty.json │ │ │ └── index.json │ ├── slack.com │ │ └── identity.json │ └── webhooks │ │ ├── create-branch.json │ │ ├── create-tag.json │ │ ├── delete.json │ │ ├── deployment_status-failure.json │ │ ├── deployment_status-full-ref.json │ │ ├── deployment_status-missing-target-url.json │ │ ├── deployment_status-pending-chat.json │ │ ├── deployment_status-pending-releasing.json │ │ ├── deployment_status-pending.json │ │ ├── deployment_status-success.json │ │ ├── ping.json │ │ ├── pull_request-merged.json │ │ ├── pull_request-opened.json │ │ ├── push-1-commit.json │ │ ├── push-2-commits.json │ │ ├── status-changeling-success-fork.json │ │ ├── status-changeling-success.json │ │ ├── status-circle-failure.json │ │ ├── status-circle-pending.json │ │ ├── status-circle-success.json │ │ ├── status-heroku-success.json │ │ ├── status-travis-failure.json │ │ ├── status-travis-pending.json │ │ └── status-travis-success.json ├── models │ ├── command_spec.rb │ ├── commands │ │ ├── organization_spec.rb │ │ └── route_spec.rb │ ├── git_hub │ │ ├── event_messages │ │ │ ├── create_spec.rb │ │ │ ├── delete_spec.rb │ │ │ ├── deployment_status_spec.rb │ │ │ ├── ping_spec.rb │ │ │ ├── pull_request_spec.rb │ │ │ ├── push_spec.rb │ │ │ ├── status_spec.rb │ │ │ └── unknown_spec.rb │ │ ├── organization_spec.rb │ │ └── repository_spec.rb │ └── slack_h_q │ │ ├── team_spec.rb │ │ └── user_spec.rb ├── rails_helper.rb ├── request │ ├── boomtown_spec.rb │ ├── commands_spec.rb │ ├── github_auth_spec.rb │ ├── health_spec.rb │ ├── installation_spec.rb │ ├── pages_spec.rb │ ├── root_spec.rb │ ├── sidekiq_web_spec.rb │ └── slack_auth_spec.rb ├── spec_helper.rb └── support │ ├── fixtures_helpers.rb │ ├── omni_auth_helpers.rb │ ├── slack_helpers.rb │ └── webmock_helpers.rb ├── tmp └── .keep └── vendor ├── assets ├── javascripts │ └── .keep └── stylesheets │ └── .keep └── cache ├── actioncable-5.0.0.1.gem ├── actionmailer-5.0.0.1.gem ├── actionpack-5.0.0.1.gem ├── actionview-5.0.0.1.gem ├── activejob-5.0.0.1.gem ├── activemodel-5.0.0.1.gem ├── activerecord-5.0.0.1.gem ├── activesupport-5.0.0.1.gem ├── addressable-2.4.0.gem ├── aggregate-0.2.2.gem ├── arel-7.1.4.gem ├── ast-2.3.0.gem ├── atomic-1.1.99.gem ├── builder-3.2.2.gem ├── byebug-9.0.6.gem ├── chunky_png-1.3.7.gem ├── coal_car-0.2.6.gem ├── coffee-rails-4.1.1.gem ├── coffee-script-2.4.1.gem ├── coffee-script-source-1.10.0.gem ├── compass-1.0.3.gem ├── compass-core-1.0.3.gem ├── compass-import-once-1.0.5.gem ├── compass-rails-3.0.2.gem ├── concurrent-ruby-1.0.2.gem ├── concurrent-ruby-ext-1.0.2.gem ├── connection_pool-2.2.0.gem ├── crack-0.4.3.gem ├── debugger-ruby_core_source-1.3.8.gem ├── diff-lcs-1.2.5.gem ├── dotenv-2.1.1.gem ├── dotenv-rails-2.1.1.gem ├── erubis-2.7.0.gem ├── execjs-2.7.0.gem ├── faraday-0.9.2.gem ├── faraday_middleware-0.10.0.gem ├── ffi-1.9.14.gem ├── finagle-thrift-1.4.2.gem ├── gli-2.14.0.gem ├── globalid-0.3.7.gem ├── hashdiff-0.3.0.gem ├── hashie-3.4.6.gem ├── hetchy-1.0.0.gem ├── i18n-0.7.0.gem ├── jquery-rails-4.2.1.gem ├── json-2.0.2.gem ├── jwt-1.5.6.gem ├── librato-metrics-1.6.1.gem ├── librato-rack-1.1.0.gem ├── librato-rails-1.4.2.gem ├── listen-3.0.8.gem ├── lograge-0.4.1.gem ├── loofah-2.0.3.gem ├── mail-2.6.4.gem ├── method_source-0.8.2.gem ├── mime-types-3.1.gem ├── mime-types-data-3.2016.0521.gem ├── mini_portile2-2.1.0.gem ├── minitest-5.9.1.gem ├── multi_json-1.12.1.gem ├── multi_xml-0.5.5.gem ├── multipart-post-2.0.0.gem ├── nio4r-1.2.1.gem ├── nokogiri-1.6.8.1.gem ├── oauth2-1.2.0.gem ├── octokit-4.3.0.gem ├── omniauth-1.3.1.gem ├── omniauth-github-1.1.2.gem ├── omniauth-oauth2-1.3.1.gem ├── omniauth-slack-2.3.0.gem ├── parser-2.3.1.4.gem ├── peek-0.2.0.gem ├── peek-faraday-2.0.0.gem ├── peek-performance_bar-1.2.1.gem ├── peek-pg-1.2.0.gem ├── peek-rblineprof-0.1.2.gem ├── peek-redis-1.2.0.gem ├── pg-0.19.0.gem ├── powerpack-0.1.1.gem ├── puma-3.6.0.gem ├── rack-2.0.1.gem ├── rack-protection-1.5.3.gem ├── rack-test-0.6.3.gem ├── rails-5.0.0.1.gem ├── rails-dom-testing-2.0.1.gem ├── rails-html-sanitizer-1.0.3.gem ├── railties-5.0.0.1.gem ├── rainbow-2.1.0.gem ├── rake-11.2.2.gem ├── rb-fsevent-0.9.7.gem ├── rb-inotify-0.9.7.gem ├── rblineprof-0.3.7.gem ├── rbnacl-4.0.1.gem ├── rbnacl-libsodium-1.0.11.gem ├── redis-3.3.1.gem ├── rspec-core-3.5.4.gem ├── rspec-expectations-3.5.0.gem ├── rspec-mocks-3.5.0.gem ├── rspec-rails-3.5.0.gem ├── rspec-support-3.5.0.gem ├── rubocop-0.44.1.gem ├── ruby-progressbar-1.8.1.gem ├── safe_yaml-1.0.4.gem ├── sass-3.4.22.gem ├── sass-rails-5.0.6.gem ├── sawyer-0.7.0.gem ├── sentry-raven-2.1.4.gem ├── sidekiq-4.2.2.gem ├── slack-ruby-client-0.7.7.gem ├── sprockets-3.7.0.gem ├── sprockets-rails-3.2.0.gem ├── sucker_punch-2.0.2.gem ├── thor-0.19.1.gem ├── thread_safe-0.3.5.gem ├── thrift-0.9.3.0.gem ├── tilt-2.0.5.gem ├── tzinfo-1.2.2.gem ├── uglifier-3.0.2.gem ├── unicode-display_width-1.1.1.gem ├── webmock-2.1.0.gem ├── websocket-driver-0.6.4.gem ├── websocket-extensions-0.1.2.gem └── zipkin-tracer-0.18.6.gem /.env.sample: -------------------------------------------------------------------------------- 1 | APP_NAME=speakerboxxx 2 | GITHUB_ADMIN_LOGINS="atmos" 3 | GITHUB_OAUTH_ID="" 4 | GITHUB_OAUTH_SECRET="" 5 | HOSTNAME="" 6 | LIBRATO_PASSWORD="" 7 | LIBRATO_SOURCE="" 8 | LIBRATO_TOKEN="" 9 | LIBRATO_USER="" 10 | PRIVACY_POLICY_URL="http://www.example.com/privacy" 11 | RAILS_ENV="development" 12 | RAILS_SECRET_KEY_BASE="" 13 | RBNACL_SECRET="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64)" 14 | REDIS_URL="redis://localhost:6379" 15 | ROLLBAR_ACCESS_TOKEN="" 16 | SLACK_APP_URL="http://localhost:5000" 17 | SLACK_SLASH_COMMAND_PREFIX="/gh-development" 18 | SLACK_SLASH_COMMAND_TOKEN="" 19 | SLACK_OAUTH_ID="" 20 | SLACK_OAUTH_SECRET="" 21 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | APP_NAME=speakerboxxx 2 | RACK_ENV=test 3 | RAILS_ENV=test 4 | RBNACL_SECRET=2wb1hlWMZwlLGxstjwG6EIwAIy9VFZfxV0jVQtqhp94= 5 | GITHUB_ADMIN_LOGINS="atmos" 6 | GITHUB_OAUTH_ID="" 7 | GITHUB_OAUTH_SECRET="" 8 | HEROKU_OAUTH_ID="" 9 | HEROKU_OAUTH_SECRET="" 10 | HOSTNAME="" 11 | LIBRATO_PASSWORD="" 12 | LIBRATO_SOURCE="" 13 | LIBRATO_TOKEN="" 14 | LIBRATO_USER="" 15 | PRIVACY_POLICY_URL="http://www.example.com/privacy" 16 | RAILS_ENV="" 17 | RAILS_SECRET_KEY_BASE="" 18 | REDIS_URL="redis://localhost:6379" 19 | ROLLBAR_ACCESS_TOKEN="" 20 | SLACK_APP_URL="http://localhost:5000" 21 | SLACK_SLASH_COMMAND_TOKEN="" 22 | SLACK_OAUTH_ID="" 23 | SLACK_OAUTH_SECRET="" 24 | YUBIEXPIRE_HOSTNAME="" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore Byebug command history file. 21 | .byebug_history 22 | 23 | .env 24 | .ruby-version 25 | .node-version 26 | public/assets 27 | vendor/gems 28 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Ruby linting configuration. 2 | # We only worry about two kinds of issues: 'error' and anything less than that. 3 | # Error is not about severity, but about taste. Simple style choices that 4 | # never have a great excuse to be broken, such as 1.9 JSON-like hash syntax, 5 | # are errors. Choices that tend to have good exceptions in practice, such as 6 | # line length, are warnings. 7 | 8 | # If you'd like to make changes, a full list of available issues is at 9 | # https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml 10 | # A list of configurable issues is at: 11 | # https://github.com/bbatsov/rubocop/blob/master/config/default.yml 12 | # 13 | # If you disable a check, document why. 14 | 15 | 16 | StringLiterals: 17 | EnforcedStyle: double_quotes 18 | Severity: error 19 | 20 | HashSyntax: 21 | EnforcedStyle: ruby19 22 | UseHashRocketsWithSymbolValues: false 23 | Severity: error 24 | Exclude: 25 | - !ruby/regexp /db\/schema.rb/ 26 | 27 | AlignHash: 28 | SupportedLastArgumentHashStyles: always_ignore 29 | 30 | AlignParameters: 31 | Enabled: false # This is usually true, but we often want to roll back to 32 | # the start of a line. 33 | 34 | Attr: 35 | Enabled: false # We have no styleguide guidance here, and it seems to be 36 | # in frequent use. 37 | 38 | ClassAndModuleChildren: 39 | Enabled: false # module X<\n>module Y is just as good as module X::Y. 40 | 41 | Documentation: 42 | Exclude: 43 | - !ruby/regexp /spec\/*\/*/ 44 | 45 | ModuleLength: 46 | Max: 110 47 | Exclude: 48 | - !ruby/regexp /spec\/*\/*/ 49 | 50 | ClassLength: 51 | Max: 150 52 | Exclude: 53 | - !ruby/regexp /spec\/*\/*/ 54 | 55 | PercentLiteralDelimiters: 56 | PreferredDelimiters: 57 | '%w': '{}' 58 | 59 | LineLength: 60 | Max: 80 61 | Severity: warning 62 | 63 | MultilineTernaryOperator: 64 | Severity: error 65 | 66 | UnreachableCode: 67 | Severity: error 68 | 69 | AndOr: 70 | Severity: error 71 | 72 | EndAlignment: 73 | Severity: error 74 | 75 | IndentationWidth: 76 | Severity: error 77 | 78 | MethodLength: 79 | CountComments: false # count full line comments? 80 | Max: 25 81 | Severity: error 82 | 83 | FrozenStringLiteralComment: 84 | Enabled: false # Adding comment to all files... 85 | 86 | Alias: 87 | Enabled: false # We have no guidance on alias vs alias_method 88 | 89 | RedundantSelf: 90 | Enabled: false # Sometimes a self.field is a bit more clear 91 | 92 | IfUnlessModifier: 93 | Enabled: false 94 | 95 | Metrics/AbcSize: 96 | Max: 16 97 | 98 | AllCops: 99 | TargetRubyVersion: 2.3 100 | # Include gemspec and Rakefile 101 | Include: 102 | - '**/*.gemspec' 103 | - '**/*.rake' 104 | - '**/Gemfile' 105 | - '**/Rakefile' 106 | - '**/Capfile' 107 | - '**/Guardfile' 108 | - '**/Podfile' 109 | - '**/Vagrantfile' 110 | Exclude: 111 | - 'db/**/*' 112 | - 'bin/*' 113 | - 'script/**/*' 114 | - 'config/**/*' 115 | - 'vendor/**/*' 116 | # By default, the rails cops are not run. Override in project or home 117 | # directory .rubocop.yml files, or by giving the -R/--rails option. 118 | Rails: 119 | Enabled: true 120 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - postgresql 3 | - redis-server 4 | language: ruby 5 | cache: bundler 6 | rvm: 7 | - 2.3.1 8 | bundler_args: "--without=development" 9 | env: 10 | global: 11 | - RBNACL_SECRET=xBPxUfoJRRXOfA9Zol1v6qNmXMjj/vJHG5OUpceXnwQ= 12 | before_script: 13 | - bin/setup 14 | script: bundle exec bin/cibuild 15 | notifications: 16 | email: false 17 | addons: 18 | postgresql: 9.4 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "2.3.1" 4 | 5 | gem "addressable", require: "addressable/uri" 6 | gem "coal_car", "~> 0.2" 7 | gem "coffee-rails", "~> 4.1.0" 8 | gem "compass-rails" 9 | gem "jquery-rails" 10 | gem "octokit" 11 | gem "omniauth-github" 12 | gem "omniauth-slack", "2.3.0" 13 | gem "peek" 14 | gem "peek-faraday" 15 | gem "peek-pg" 16 | gem "peek-redis" 17 | gem "peek-rblineprof" 18 | gem "peek-performance_bar" 19 | gem "pg", "~> 0.18" 20 | gem "puma", "~> 3.0" 21 | gem "rake", "=11.2.2" 22 | gem "rails", ">= 5.0.0.1", "< 5.1" 23 | gem "sass-rails", "~> 5.0" 24 | gem "sidekiq" 25 | gem "slack-ruby-client" 26 | gem "uglifier", ">= 1.3.0" 27 | 28 | group :development, :test do 29 | gem "byebug" 30 | gem "dotenv-rails" 31 | end 32 | 33 | group :development do 34 | gem "listen", "~> 3.0.5" 35 | end 36 | 37 | group :test do 38 | gem "rspec-rails", "3.5.0" 39 | gem "rubocop" 40 | gem "webmock" 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Corey Donohoe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development} 2 | worker: /usr/bin/env LIBRATO_AUTORUN=1 bundle exec sidekiq 3 | release: rake db:migrate 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speakerboxxx... 2 | 3 | This let's you route your company's GitHub notifications in a smart way. 4 | 5 | ## Usage 6 | 7 | ### Installing 8 | 9 | Add to Slack 10 | 11 | ### Chat Enabling and Routing 12 | 13 | ![](https://cloud.githubusercontent.com/assets/38/16897654/85ab2bc2-4b6c-11e6-8834-20f3fb8cef56.png) 14 | 15 | ## Testing 16 | 17 | 18 | ```console 19 | $ bin/setup 20 | $ bin/cibuild 21 | ``` 22 | 23 | ### Deploying your own to Heroku 24 | 25 | * [Create a GitHub OAuth app](https://github.com/settings/applications/new) 26 | * [Create a Slack app](https://api.slack.com/apps/new) 27 | * [Create this app on Heroku](https://heroku.com/deploy?template=https://github.com/atmos/speakerboxxx) 28 | * heroku plugins:install heroku-redis 29 | * heroku redis:promote HEROKU_REDIS_SOMECOLOR -a heroku-app-you-created 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, for example 2 | # lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speakerboxxx", 3 | "description": "Something about this app", 4 | "keywords": [ 5 | "github", 6 | "notifications", 7 | "slack", 8 | "slash command", 9 | "chatops" 10 | ], 11 | "addons": [ 12 | "heroku-redis:premium-0", 13 | "heroku-postgresql:standard-0" 14 | ], 15 | "env": { 16 | "APP_NAME": { 17 | "description": "The application name reported from the /health endpoint", 18 | "value": "speakerboxxx" 19 | }, 20 | "GITHUB_ADMIN_LOGINS": { 21 | "description": "A comma delimited list of GitHub logins that can access sidekiq-web" 22 | }, 23 | "GITHUB_OAUTH_ID": { 24 | "description": "The GitHub OAuth client id for being an OAuth consumer" 25 | }, 26 | "GITHUB_OAUTH_SECRET": { 27 | "description": "The GitHub OAuth client secret for being an OAuth consumer" 28 | }, 29 | "HOSTNAME": { 30 | "description": "The FQDN that the server is running at" 31 | }, 32 | "RACK_ENV": { 33 | "description": "The rack environment used to differentiate production from staging", 34 | "default": "production" 35 | }, 36 | "RAILS_ENV": { 37 | "description": "The rails environment used to differentiate production from staging", 38 | "default": "production" 39 | }, 40 | "RAILS_SERVE_STATIC_FILES": { 41 | "description": "Whether or not rails should serve static assets from the ruby stack", 42 | "default": "enabled" 43 | }, 44 | "RBNACL_SECRET": { 45 | "description": "The secret used for writing encrypted values to the database" 46 | }, 47 | "SECRET_KEY_BASE": { 48 | "description": "The session secret used to encrypt and sign session cookies", 49 | "generator": "secret" 50 | }, 51 | "SLACK_APP_URL": { 52 | "description": "The direct link to your app in the Slack App Store" 53 | }, 54 | "SLACK_OAUTH_ID": { 55 | "description": "The Slack OAuth client id for being an OAuth consumer" 56 | }, 57 | "SLACK_OAUTH_SECRET": { 58 | "description": "The Slack OAuth client secret for being an OAuth consumer" 59 | }, 60 | "SLACK_SLASH_COMMAND_TOKEN": { 61 | "description": "The token from slack to ensure it's a valid incoming request" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 26 | 27 | 28 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/github.png -------------------------------------------------------------------------------- /app/assets/images/heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/heroku.png -------------------------------------------------------------------------------- /app/assets/images/logo-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 24 | 25 | 26 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/assets/images/logo-heroku.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 25 | 26 | 27 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/assets/images/logo-slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 24 | 25 | 26 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/assets/images/screencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/screencap.png -------------------------------------------------------------------------------- /app/assets/images/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/slack.png -------------------------------------------------------------------------------- /app/assets/images/sound-wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/images/sound-wave.png -------------------------------------------------------------------------------- /app/assets/images/sound-wave.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. Action Cable runs 2 | # in a loop that does not support auto reloading. 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. Action Cable runs 2 | # in a loop that does not support auto reloading. 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # Top-level controller all web endpoints inherit from 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception 4 | 5 | def install 6 | redirect_to "/auth/slack_install" 7 | end 8 | 9 | private 10 | 11 | def peek_enabled? 12 | true 13 | end 14 | 15 | def authenticate! 16 | true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/commands_controller.rb: -------------------------------------------------------------------------------- 1 | # HTTP Endpoint for handling incoming slash commands 2 | class CommandsController < ApplicationController 3 | protect_from_forgery with: :null_session 4 | 5 | def create 6 | if slack_token_valid? 7 | if current_user && current_user.github_token 8 | command = current_user.create_command_for(params) 9 | render json: command.default_response.to_json, status: 201 10 | else 11 | command = Command.from_params(params) 12 | render json: command.authenticate_github_response, status: 201 13 | end 14 | else 15 | render json: {}, status: 403 16 | end 17 | end 18 | 19 | private 20 | 21 | def current_user 22 | @current_user ||= SlackHQ::User.find_by(slack_user_id: params[:user_id]) 23 | end 24 | 25 | def slack_token 26 | ENV["SLACK_SLASH_COMMAND_TOKEN"] 27 | end 28 | 29 | def slack_token_valid? 30 | ActiveSupport::SecurityUtils.secure_compare(params[:token], slack_token) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/webhook_validations.rb: -------------------------------------------------------------------------------- 1 | # Concern for handling GitHub organization webhooks 2 | module WebhookValidations 3 | extend ActiveSupport::Concern 4 | 5 | def verify_incoming_github_webhook_address! 6 | sources = ["192.30.252.0/22"] 7 | if Rails.env.development? 8 | sources << "127.0.0.1/32" 9 | end 10 | 11 | if sources.any? { |block| IPAddr.new(block).include?(request.ip) } 12 | true 13 | else 14 | render json: {}, status: :forbidden 15 | end 16 | end 17 | 18 | def verify_github_signature! 19 | return false unless github_signature_verification_enabled? 20 | 21 | request.body.rewind 22 | signature = signature_for_github_payload(request.body.read) 23 | 24 | verified = ActiveSupport::SecurityUtils.secure_compare( 25 | signature, 26 | github_webhook_signature 27 | ) 28 | return true if verified 29 | 30 | render json: {}, status: :forbidden 31 | end 32 | 33 | def signature_for_github_payload(payload) 34 | hex = OpenSSL::HMAC.hexdigest(github_digest, github_webhook_secret, payload) 35 | "sha1=#{hex}" 36 | end 37 | 38 | def github_digest 39 | OpenSSL::Digest.new("sha1") 40 | end 41 | 42 | def webhook_slack_team 43 | @webhook_slack_team ||= SlackHQ::Team.find_by(team_id: params[:team_id]) 44 | end 45 | 46 | # rubocop:disable Metrics/LineLength 47 | def github_webhook_secret 48 | @github_webhook_secret ||= 49 | webhook_slack_team.organizations.find_by(name: params[:org_name]).webhook_secret 50 | end 51 | # rubocop:enable Metrics/LineLength 52 | 53 | def github_webhook_signature 54 | request.headers["HTTP_X_HUB_SIGNATURE"] 55 | end 56 | 57 | def github_signature_verification_enabled? 58 | github_webhook_secret && github_webhook_signature 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # The place to put static web pages 2 | class PagesController < ApplicationController 3 | def index 4 | end 5 | 6 | def support 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # Session controller for authenticating users with GitHub/Slack 2 | class SessionsController < ApplicationController 3 | include SessionsHelper 4 | 5 | def create_github 6 | user = SlackHQ::User.find(session[:user_id]) 7 | user.github_login = omniauth_info["info"]["nickname"] 8 | user.github_token = omniauth_info["credentials"]["token"] 9 | user.save 10 | 11 | redirect_to "/" 12 | rescue ActiveRecord::RecordNotFound 13 | redirect_to "/auth/slack?origin=#{omniauth_origin}" 14 | end 15 | 16 | def create_slack 17 | user = SlackHQ::User.from_omniauth(omniauth_info) 18 | 19 | session[:user_id] = user.id 20 | redirect_to "/auth/github" 21 | end 22 | 23 | def install_slack 24 | team = SlackHQ::Team.from_omniauth(omniauth_info) 25 | 26 | user = team.users.find_or_initialize_by( 27 | slack_user_id: omniauth_info_user_id 28 | ) 29 | 30 | user.slack_user_name = omniauth_info["info"]["user"] 31 | user.save 32 | 33 | session[:user_id] = user.id 34 | redirect_to "/auth/github" 35 | end 36 | 37 | def destroy 38 | session.clear 39 | redirect_to root_url, notice: "Signed out!" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/webhooks_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for handling incoming GitHub Organization webhooks 2 | class WebhooksController < ApplicationController 3 | include WebhookValidations 4 | 5 | before_action :verify_incoming_github_webhook_address! 6 | before_action :verify_github_signature! 7 | skip_before_action :verify_authenticity_token, only: [:create] 8 | 9 | def create 10 | WebhookJob.perform_later( 11 | team_id: params[:team_id], 12 | org_name: params[:org_name], 13 | event_type: request.headers["HTTP_X_GITHUB_EVENT"], 14 | delivery_id: request.headers["HTTP_X_GITHUB_DELIVERY"], 15 | body: full_request_body 16 | ) 17 | render json: {}, status: :created 18 | end 19 | 20 | private 21 | 22 | def full_request_body 23 | request.body.rewind 24 | request.body.read.force_encoding("utf-8") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Shared helpers available to views 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | # Helpers related to session authentication 2 | module SessionsHelper 3 | def decoded_omniauth_origin_provider 4 | decoded_omniauth_origin && 5 | %(heroku github).include?(decoded_omniauth_origin[:provider].to_s) && 6 | decoded_omniauth_origin[:provider] 7 | end 8 | 9 | def decoded_params_origin 10 | JSON.parse(Base64.decode64(params[:origin])).with_indifferent_access 11 | rescue 12 | nil 13 | end 14 | 15 | def decoded_omniauth_origin 16 | JSON.parse(Base64.decode64(omniauth_origin)).with_indifferent_access 17 | rescue 18 | nil 19 | end 20 | 21 | def omniauth_origin 22 | request.env["omniauth.origin"] 23 | end 24 | 25 | def omniauth_info_user_id 26 | omniauth_info["info"]["user_id"] 27 | end 28 | 29 | def omniauth_bot_info 30 | omniauth_info["extra"] && omniauth_info["extra"]["bot_info"] 31 | end 32 | 33 | def omniauth_refresh_token 34 | omniauth_info["credentials"]["refresh_token"] 35 | end 36 | 37 | def omniauth_expiration 38 | Time.at(omniauth_info["credentials"]["expires_at"]).utc 39 | end 40 | 41 | def omniauth_info 42 | request.env["omniauth.auth"] 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/command_runner_job.rb: -------------------------------------------------------------------------------- 1 | # Job to handle slash command running 2 | class CommandRunnerJob < ApplicationJob 3 | queue_as :default 4 | 5 | def perform(*args) 6 | command_id = args.first.fetch(:command_id) 7 | command = Command.find(command_id) 8 | command.run 9 | command.processed_at = Time.now.utc 10 | command.save 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/webhook_job.rb: -------------------------------------------------------------------------------- 1 | # Job to handle incoming GitHub webhooks 2 | class WebhookJob < ApplicationJob 3 | queue_as :default 4 | 5 | def self.redis 6 | @redis ||= Redis.new 7 | end 8 | 9 | def redis 10 | self.class.redis 11 | end 12 | 13 | def cache_ts!(key, ts) 14 | redis.set(key, ts, ex: 1.hour) 15 | end 16 | 17 | def cache_ts(key) 18 | redis.get(key) 19 | end 20 | 21 | # rubocop:disable Metrics/AbcSize 22 | def post_back(team, response, event_type, body) 23 | case event_type 24 | when "deployment_status" 25 | cache_key = "#{team.id}-#{response[:channel]}-" \ 26 | "#{event_type}-#{body['deployment']['id']}" 27 | 28 | Rails.logger.info at: "webhook-deployment-status", key: cache_key 29 | if cache_ts(cache_key) 30 | team.bot.chat_update(response.merge(ts: cache_ts(cache_key))) 31 | team.bot.chat_postMessage( 32 | response.merge(thread_ts: cache_ts(cache_key)) 33 | ) 34 | else 35 | message = team.bot.chat_postMessage(response) 36 | team.bot.chat_postMessage( 37 | response.merge(thread_ts: message.ts) 38 | ) 39 | cache_ts!(cache_key, message.ts) 40 | end 41 | else 42 | team.bot.chat_postMessage(response) 43 | end 44 | rescue Slack::Web::Api::Error 45 | Rails.logger.info "Unable to route #{team.team_id}: #{response.inspect}" 46 | nil 47 | end 48 | 49 | # rubocop:disable Metrics/CyclomaticComplexity 50 | # rubocop:disable Metrics/PerceivedComplexity 51 | def perform(*args) 52 | team_id = args.first.fetch(:team_id) 53 | team = SlackHQ::Team.find_by(team_id: team_id) 54 | 55 | return if team.nil? 56 | 57 | org_name = args.first.fetch(:org_name) 58 | org = team.organizations.find_by(name: org_name) 59 | 60 | body = args.first.fetch(:body) 61 | event_type = args.first.fetch(:event_type) 62 | _delivery_id = args.first.fetch(:delivery_id) 63 | 64 | if Rails.env.development? 65 | File.open("tmp/#{event_type}-#{Time.now.to_i}.json", "w") do |fp| 66 | fp.puts body 67 | end 68 | end 69 | 70 | handler = GitHub::EventMessages.handler_for(event_type, JSON.parse(body)) 71 | return unless handler && handler.response 72 | response = handler.response 73 | channel = org.default_room_for(handler.repo_name) 74 | 75 | response[:channel] = channel 76 | 77 | post_back(team, response, event_type, JSON.parse(body)) 78 | 79 | return unless event_type == "deployment_status" && handler.chat_deployment? 80 | chat_channel = "##{handler.chat_deployment_room}" 81 | return unless chat_channel != channel && chat_channel != "#privategroup" 82 | response[:channel] = chat_channel 83 | post_back(team, response, event_type, JSON.parse(body)) 84 | end 85 | # rubocop:enable Metrics/PerceivedComplexity 86 | # rubocop:enable Metrics/CyclomaticComplexity 87 | # rubocop:enable Metrics/AbcSize 88 | end 89 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # Shared helpers available to mailers 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: "from@example.com" 4 | layout "mailer" 5 | end 6 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # Top-level DB Record used across the app 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | -------------------------------------------------------------------------------- /app/models/command.rb: -------------------------------------------------------------------------------- 1 | # A command a Slack User issued 2 | class Command < ApplicationRecord 3 | include CommandOnboarding 4 | 5 | belongs_to :user, class_name: "SlackHQ::User", required: false 6 | 7 | before_validation :extract_cli_args, on: :create 8 | 9 | def self.from_params(params) 10 | create( 11 | channel_id: params[:channel_id], 12 | channel_name: params[:channel_name], 13 | command: params[:command], 14 | command_text: params[:text], 15 | response_url: params[:response_url], 16 | slack_user_id: params[:user_id], 17 | team_id: params[:team_id], 18 | team_domain: params[:team_domain] 19 | ) 20 | end 21 | 22 | def run 23 | handler.run 24 | postback_message(handler.response) 25 | end 26 | 27 | def handler 28 | @handler ||= case task 29 | when "org" 30 | Commands::Organization.new(self) 31 | when "route" 32 | Commands::Route.new(self) 33 | else 34 | Commands::Help.new(self) 35 | end 36 | end 37 | 38 | def description 39 | if application 40 | "Running(#{id}): '#{task}:#{subtask}' for #{application}..." 41 | else 42 | "Running(#{id}): '#{task}:#{subtask}'..." 43 | end 44 | end 45 | 46 | def notify_user_of_success! 47 | user = User.find_by(slack_user_id: slack_user_id) 48 | return unless user 49 | name = "<@#{user.slack_user_id}|#{user.slack_user_name}>" 50 | postback_message(text_response("#{name} you're all set. :tada:")) 51 | end 52 | 53 | def default_response 54 | { response_type: "in_channel" } 55 | end 56 | 57 | def text_response(text) 58 | { text: text, response_type: "in_channel" } 59 | end 60 | 61 | def postback_message(message) 62 | response = client.post do |request| 63 | request.url callback_uri.path 64 | request.body = message.to_json 65 | request.headers["Content-Type"] = "application/json" 66 | end 67 | 68 | Rails.logger.info response.body 69 | rescue StandardError => e 70 | Rails.logger.info "Unable to post back to slack: '#{e.inspect}'" 71 | end 72 | 73 | def slack_team 74 | @slack_team ||= SlackHQ::Team.find_by(team_id: team_id) 75 | end 76 | 77 | private 78 | 79 | def callback_uri 80 | @callback_uri ||= ::Addressable::URI.parse(response_url) 81 | end 82 | 83 | def client 84 | @client ||= Faraday.new(url: "https://hooks.slack.com") 85 | end 86 | 87 | def extract_cli_args 88 | self.subtask = "default" 89 | 90 | match = command_text.match(/-a ([-_\.0-9a-z]+)/) 91 | self.application = match[1] if match 92 | 93 | match = command_text.match(/^([-_\.0-9a-z]+)(?:\:([^\s]+))/) 94 | if match 95 | self.task = match[1] 96 | self.subtask = match[2] 97 | end 98 | 99 | match = command_text.match(/^([-_\.0-9a-z]+)\s*/) 100 | self.task = match[1] if match 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /app/models/commands/default.rb: -------------------------------------------------------------------------------- 1 | # Namespace for containing slash Commands 2 | module Commands 3 | # Top-level class for implementing chat commands 4 | class Default 5 | include ActionView::Helpers::DateHelper 6 | 7 | COLOR = "#6567a5".freeze 8 | UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ 9 | 10 | REPO_REGEX = %r{([-_\.0-9a-z]+)/([-_\.0-9a-z]+)} 11 | 12 | attr_reader :client, :command, :description, :response 13 | delegate :application, :subtask, :task, :user, :slack_team, to: :@command 14 | 15 | def initialize(command) 16 | @command = command 17 | 18 | @description = command.description.gsub("Running", "Ran") 19 | @response = { text: description, response_type: "in_channel" } 20 | end 21 | 22 | def run 23 | # Overridden in each subclass 24 | end 25 | 26 | def self.help_documentation 27 | [] 28 | end 29 | 30 | def slash_command_prefix 31 | Speakerboxxx.slash_command_prefix 32 | end 33 | 34 | def help_for_task 35 | { 36 | response_type: "in_channel", 37 | attachments: [ 38 | { 39 | text: self.class.help_documentation.join("\n"), 40 | pretext: "Run #{slash_command_prefix} help", 41 | fallback: "Help commands from the GitHub integration", 42 | title: "Available speakerboxxx #{task} commands:", 43 | title_link: "https://speakerboxxx.atmos.org" 44 | } 45 | ] 46 | } 47 | end 48 | 49 | def error_response_for(text) 50 | { response_type: "in_channel", 51 | attachments: [{ text: text, color: "#f00" }] } 52 | end 53 | 54 | def response_for(text) 55 | { text: text, response_type: "in_channel" } 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/models/commands/help.rb: -------------------------------------------------------------------------------- 1 | # Namespace for containing slash Commands 2 | module Commands 3 | # Top-level class for implementing Heroku commands 4 | class Help < Default 5 | def initialize(command) 6 | super(command) 7 | end 8 | 9 | def self.help_documentation 10 | [ 11 | Commands::Organization.help_documentation, 12 | Commands::Route.help_documentation 13 | ].flatten 14 | end 15 | 16 | def run 17 | @response = help_for_task 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/commands/organization.rb: -------------------------------------------------------------------------------- 1 | # Namespace for containing slash Commands 2 | module Commands 3 | # Top-level class for implementing GitHub org commands 4 | class Organization < Default 5 | include OrganizationHelpers 6 | def initialize(command) 7 | super(command) 8 | end 9 | 10 | def self.help_documentation 11 | [ 12 | "org:info heroku - Get information about a GitHub organization", 13 | "org:enable heroku #my-default-channel - " \ 14 | "Enable the heroku organization's webhooks", 15 | "org:disable heroku - Disable the heroku organization's webhooks" 16 | ] 17 | end 18 | 19 | def run 20 | if organization? 21 | case subtask 22 | when "info" 23 | info_subtask 24 | when "enable" 25 | enable_subtask 26 | when "disable" 27 | disable_subtask 28 | when "default" 29 | default_subtask 30 | else 31 | @response = help_for_task 32 | end 33 | else 34 | @response = response_for("#{organization_name} isn't an organization.") 35 | end 36 | end 37 | 38 | def github_organization 39 | @github_organization ||= 40 | slack_team.organizations.find_or_create_by(name: organization_name) 41 | end 42 | 43 | def enable_subtask 44 | unless channel_name.blank? 45 | github_organization.default_room = channel_name 46 | github_organization.save 47 | end 48 | 49 | unless org_hooks? 50 | create_organization_hook(github_organization.webhook_secret) 51 | @organization_hooks = nil 52 | end 53 | 54 | @response = response_for( 55 | "#{organization_name} webhooks are enabled. Defaulting to " \ 56 | "#{github_organization.default_room} " \ 57 | "<#{organization_webhook_view_url}|View>" 58 | ) 59 | end 60 | 61 | def disable_subtask 62 | if org_hooks? 63 | if remove_organization_hook 64 | @response = response_for( 65 | "#{organization_name} webhooks are disabled." 66 | ) 67 | else 68 | @response = response_for( 69 | "Problem removing #{organization_name}'s webhooks." 70 | ) 71 | end 72 | else 73 | @response = response_for( 74 | "#{organization_name} is already disabled." 75 | ) 76 | end 77 | end 78 | 79 | def info_subtask 80 | if org_hooks? 81 | @response = response_for( 82 | "#{organization_name} is good to go. " \ 83 | "Defaulting to #{github_organization.default_room}" 84 | ) 85 | else 86 | @response = response_for("#{organization_name} isn't configured.") 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /app/models/commands/route.rb: -------------------------------------------------------------------------------- 1 | # Namespace for containing slash Commands 2 | module Commands 3 | # Top-level class for implementing GitHub repo routing 4 | class Route < Default 5 | include RepositoryHelpers 6 | def initialize(command) 7 | super(command) 8 | end 9 | 10 | def self.help_documentation 11 | [ 12 | "route atmos-org/speakerboxxx #tools - Route repo messages to a room.", 13 | "route:info atmos-org/speakerboxxx - Information about repo routing.", 14 | "route:help - Help information about repository routing." 15 | ] 16 | end 17 | 18 | def run 19 | if slack_team_github_organization 20 | case subtask 21 | when "info" 22 | info_subtask 23 | when "default", "", nil 24 | route_subtask 25 | else 26 | @response = help_for_task 27 | end 28 | else 29 | @response = response_for( 30 | "#{organization_name} isn't configured. Use org:enable" 31 | ) 32 | end 33 | end 34 | 35 | def info_subtask 36 | default_room = slack_team_github_organization.default_room 37 | @response = response_for( 38 | "#{name_with_owner} is configured and routing to #{default_room}." 39 | ) 40 | end 41 | 42 | def create_default_repostitory_name 43 | repo = slack_team_github_organization.repositories.find_or_create_by( 44 | name: repository_name 45 | ).tap { |r| r.default_room = room_name } 46 | repo.save 47 | repo 48 | end 49 | 50 | def route_subtask 51 | repo = create_default_repostitory_name 52 | @response = response_for( 53 | "#{name_with_owner} is configured and routing to #{repo.default_room}." 54 | ) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/attribute_encryption.rb: -------------------------------------------------------------------------------- 1 | # Module for handling encrypting/descrypting strings to columns. 2 | module AttributeEncryption 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | end 7 | 8 | # Things exposed to the included class as class methods 9 | module ClassMethods 10 | def rbnacl_secret 11 | ENV["RBNACL_SECRET"] || 12 | raise("No RBNACL_SECRET environmental variable set") 13 | end 14 | 15 | def rbnacl_secret_bytes 16 | rbnacl_secret.unpack("m0").first 17 | end 18 | end 19 | 20 | def rbnacl_simple_box 21 | @rbnacl_simple_box ||= 22 | RbNaCl::SimpleBox.from_secret_key(self.class.rbnacl_secret_bytes) 23 | end 24 | 25 | def decrypt_value(value) 26 | rbnacl_simple_box.decrypt(Base64.decode64(value)) 27 | rescue RbNaCl::CryptoError, NoMethodError 28 | nil 29 | end 30 | 31 | def encrypt_value(value) 32 | return if value.blank? 33 | Base64.encode64(rbnacl_simple_box.encrypt(value)).chomp 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/concerns/command_onboarding.rb: -------------------------------------------------------------------------------- 1 | # Module for handling redirection for onboarding users 2 | module CommandOnboarding 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | end 7 | 8 | # Things exposed to the included class as class methods 9 | module ClassMethods 10 | end 11 | 12 | def auth_url_prefix 13 | "https://#{ENV['HOSTNAME']}/auth" 14 | end 15 | 16 | def slack_auth_url 17 | "#{auth_url_prefix}/slack?origin=#{encoded_origin_hash(:github)}" \ 18 | "&team=#{team_id}" 19 | end 20 | 21 | def authenticate_github_response 22 | { 23 | response_type: "in_channel", 24 | text: "Please <#{slack_auth_url}|Sign in to GitHub>." 25 | } 26 | end 27 | 28 | def origin_hash(provider_name) 29 | { 30 | uri: "slack://channel?team=#{team_id}&id=#{channel_id}", 31 | team: team_id, 32 | token: id, 33 | provider: provider_name 34 | } 35 | end 36 | 37 | def encoded_origin_hash(provider_name = :heroku) 38 | data = JSON.dump(origin_hash(provider_name)) 39 | Base64.encode64(data).split("\n").join("") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/models/concerns/octokit_methods.rb: -------------------------------------------------------------------------------- 1 | # Module for handling encrypting/descrypting strings to columns. 2 | module OctokitMethods 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | end 7 | 8 | # Things exposed to the included class as class methods 9 | module ClassMethods 10 | end 11 | 12 | def github_api_client 13 | @github_api_client ||= Octokit::Client.new(access_token: github_token) 14 | end 15 | 16 | def organization_info 17 | github_api_client.org_hooks 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/concerns/organization_helpers.rb: -------------------------------------------------------------------------------- 1 | # Module for handling interacting with GitHub organizations 2 | module OrganizationHelpers 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | end 7 | 8 | # Things exposed to the included class as class methods 9 | module ClassMethods 10 | end 11 | 12 | def organization_name_pattern 13 | /org(?:\:[^\s]+)? ([^\s]+)\s*([^\s]+)?\s*/ 14 | end 15 | 16 | def github_api 17 | user.github_api_client 18 | end 19 | 20 | def organization_name 21 | @organization_name ||= 22 | command.command_text.match(organization_name_pattern)[1] 23 | end 24 | 25 | def channel_name 26 | @channel_name ||= command.command_text.match(organization_name_pattern)[2] 27 | end 28 | 29 | def organization 30 | @organization ||= begin 31 | github_api.organization(organization_name) 32 | rescue Octokit::NotFound 33 | nil 34 | end 35 | end 36 | 37 | def organization_hooks 38 | @organization_hooks ||= begin 39 | github_api.org_hooks(organization_name) 40 | rescue Octokit::NotFound 41 | nil 42 | end 43 | end 44 | 45 | def create_organization_hook(secret) 46 | config = { 47 | url: organization_webhook_url, 48 | secret: secret, 49 | content_type: "json" 50 | } 51 | options = { 52 | name: "web", 53 | events: ["*"], 54 | active: true 55 | } 56 | create_github_organization_hook(config, options) 57 | end 58 | 59 | def create_github_organization_hook(config, options) 60 | github_api.create_org_hook(organization_name, config, options) 61 | rescue Octokit::NotFound 62 | nil 63 | end 64 | 65 | def remove_organization_hook 66 | return unless org_hook 67 | github_api.remove_org_hook(organization_name, org_hook.id) 68 | rescue Octokit::NotFound 69 | nil 70 | end 71 | 72 | def organization? 73 | !organization.nil? 74 | end 75 | 76 | def organization_webhook_view_url 77 | return unless org_hook 78 | "https://github.com/organizations/#{organization_name}/" \ 79 | "settings/hooks/#{org_hook.id}" 80 | end 81 | 82 | def organization_webhook_url 83 | "https://#{ENV['HOSTNAME']}" \ 84 | "/webhooks/#{slack_team.team_id}/github/#{organization_name}" 85 | end 86 | 87 | def org_hook 88 | return if organization_hooks.nil? 89 | organization_hooks.find do |hook| 90 | hook.config.url == organization_webhook_url 91 | end 92 | end 93 | 94 | def org_hooks? 95 | !org_hook.nil? 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /app/models/concerns/repository_helpers.rb: -------------------------------------------------------------------------------- 1 | # Module for handling interacting with GitHub organizations 2 | module RepositoryHelpers 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | end 7 | 8 | # Things exposed to the included class as class methods 9 | module ClassMethods 10 | end 11 | 12 | def organization_name_pattern 13 | /route(?:\:[^\s]+)? (.*)\s*/ 14 | end 15 | 16 | def github_api 17 | user.github_api_client 18 | end 19 | 20 | def slack_team 21 | @slack_team ||= command.slack_team 22 | end 23 | 24 | def valid_slug 25 | Regexp.new("([-_\.0-9a-z]+/[-_\.0-9a-z]+)\s*(#[-_\.0-9a-z]+)?") 26 | end 27 | 28 | def room_name 29 | @room_name ||= command.command_text.match(valid_slug)[2] 30 | end 31 | 32 | def name_with_owner 33 | @name_with_owner ||= command.command_text.match(valid_slug)[1] 34 | end 35 | 36 | def organization_name 37 | return unless name_with_owner 38 | name_with_owner.split("/").first 39 | end 40 | 41 | def repository_name 42 | return unless name_with_owner 43 | name_with_owner.split("/").last 44 | end 45 | 46 | def organization 47 | @organization ||= begin 48 | github_api.organization(organization_name) 49 | rescue Octokit::NotFound 50 | nil 51 | end 52 | end 53 | 54 | def organization_webhooks 55 | @organization_webhooks ||= begin 56 | github_api.org_hooks(organization_name) 57 | rescue Octokit::NotFound 58 | nil 59 | end 60 | end 61 | 62 | def organization? 63 | !organization.nil? 64 | end 65 | 66 | def org_hook 67 | return if organization_webhooks.nil? 68 | organization_webhooks.find do |hook| 69 | hook.config.url == organization_webhook_url 70 | end 71 | end 72 | 73 | def org_hooks? 74 | !org_hook.nil? 75 | end 76 | 77 | def organization_webhook_url 78 | "https://#{ENV['HOSTNAME']}" \ 79 | "/webhooks/#{slack_team.team_id}/github/#{organization_name}" 80 | end 81 | 82 | def slack_team_github_organization 83 | slack_team.organizations.find do |org| 84 | org.name == organization_name 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /app/models/git_hub.rb: -------------------------------------------------------------------------------- 1 | # Wrapper module for GitHub related models 2 | module GitHub 3 | def self.table_name_prefix 4 | "git_hub_" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages.rb: -------------------------------------------------------------------------------- 1 | # Container module for GitHub webhook events 2 | module GitHub::EventMessages 3 | def self.suppressed_actions 4 | %w{commit_comment deployment fork issues issue_comment \ 5 | member membership pull_request_review pull_request_review_comment \ 6 | release repository team_add watch} 7 | end 8 | 9 | # rubocop:disable Metrics/CyclomaticComplexity 10 | def self.handler_for(event_type, body) 11 | case event_type 12 | when "create" 13 | GitHub::EventMessages::Create.new(body) 14 | when "delete" 15 | GitHub::EventMessages::Delete.new(body) 16 | when "ping" 17 | GitHub::EventMessages::Ping.new(body) 18 | when "push" 19 | GitHub::EventMessages::Push.new(body) 20 | when "pull_request" 21 | GitHub::EventMessages::PullRequest.new(body) 22 | when "status" 23 | GitHub::EventMessages::Status.new(body) 24 | when "deployment_status" 25 | GitHub::EventMessages::DeploymentStatus.new(body) 26 | when *suppressed_actions 27 | nil 28 | else 29 | GitHub::EventMessages::Unknown.new(body, event_type) 30 | end 31 | end 32 | # rubocop:enable Metrics/CyclomaticComplexity 33 | end 34 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/create.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub Create Webhook 3 | class Create 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | body["repository"]["name"] 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def branch 18 | @branch ||= body["ref"].sub("refs/heads/", "") 19 | end 20 | 21 | def repo_url 22 | "https://github.com/#{full_name}" 23 | end 24 | 25 | def branch_url 26 | "https://github.com/#{full_name}/tree/#{branch}" 27 | end 28 | 29 | def title 30 | "<#{repo_url}|[#{repo_name}]> #{commit_author} tagged " \ 31 | "<#{branch_url}|#{branch}>." 32 | end 33 | 34 | def fallback_title 35 | "[#{full_name}] #{commit_author} tagged \"#{branch}\"." 36 | end 37 | 38 | def commit_author 39 | body["sender"]["login"] 40 | end 41 | 42 | def response 43 | return unless body["ref_type"] == "tag" 44 | { 45 | channel: "#notifications", 46 | attachments: [ 47 | { 48 | fallback: fallback_title, 49 | color: "#4785c0", 50 | text: title, 51 | mrkdwn_in: [:text, :pretext] 52 | } 53 | ] 54 | } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/delete.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub Delete Webhook 3 | class Delete 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | body["repository"]["name"] 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def branch 18 | @branch ||= body["ref"].sub("refs/heads/", "") 19 | end 20 | 21 | def repo_url 22 | "https://github.com/#{full_name}" 23 | end 24 | 25 | def branch_url 26 | "https://github.com/#{full_name}/tree/#{branch}" 27 | end 28 | 29 | def title 30 | "<#{repo_url}|[#{repo_name}]> The branch \"#{branch}\" " \ 31 | " was deleted by #{commit_author}." 32 | end 33 | 34 | def fallback_title 35 | "[#{full_name}] The branch \"#{branch}\" was deleted by #{commit_author}." 36 | end 37 | 38 | def commit_author 39 | body["sender"]["login"] 40 | end 41 | 42 | def response 43 | { 44 | channel: "#notifications", 45 | attachments: [ 46 | { 47 | fallback: fallback_title, 48 | color: "#4785c0", 49 | text: title, 50 | mrkdwn_in: [:text, :pretext] 51 | } 52 | ] 53 | } 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/ping.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub Ping Webhook 3 | class Ping 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | nil 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def organization 18 | body["organization"] 19 | end 20 | 21 | def hook_id 22 | body["hook_id"] 23 | end 24 | 25 | def org_name 26 | organization["login"] 27 | end 28 | 29 | def zen 30 | body["zen"] 31 | end 32 | 33 | def title 34 | "Webhooks enabled: :bell:" 35 | end 36 | 37 | def hook_link 38 | "#{org_link}/hooks/#{hook_id}" 39 | end 40 | 41 | def org_link 42 | "https://github.com/organizations/#{org_name}/settings" 43 | end 44 | 45 | # rubocop:disable Metrics/MethodLength 46 | def response 47 | { 48 | channel: "#notifications", 49 | attachments: [ 50 | { 51 | color: "#4785c0", 52 | title: title, 53 | fields: [ 54 | { 55 | title: "Organization", 56 | value: "<#{org_link}|#{org_name}>", 57 | short: true 58 | }, 59 | { 60 | title: "Admin", 61 | value: "<#{hook_link}|On GitHub>", 62 | short: true 63 | }, 64 | { 65 | title: "Status", 66 | value: "Enabled", 67 | short: true 68 | }, 69 | { 70 | title: "Zen", 71 | value: zen, 72 | short: true 73 | } 74 | ], 75 | mrkdwn_in: [:text, :pretext] 76 | } 77 | ] 78 | } 79 | end 80 | # rubocop:enable Metrics/MethodLength 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/pull_request.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub PullRequest Webhook 3 | class PullRequest 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | body["repository"]["name"] 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def branch 18 | @branch ||= body["branches"].first 19 | end 20 | 21 | def branch_name 22 | body["pull_request"]["head"]["ref"] 23 | end 24 | 25 | def number 26 | body["number"] 27 | end 28 | 29 | def branch_url 30 | "https://github.com/#{full_name}/tree/#{branch_name}" 31 | end 32 | 33 | def sha_url 34 | "https://github.com/#{full_name}/tree/#{short_sha}" 35 | end 36 | 37 | def title 38 | "[#{full_name}] Pull request #{action} by #{author_link}" 39 | end 40 | 41 | def fallback_title 42 | "[#{full_name}] Pull request ##{number} #{action} by #{author}" 43 | end 44 | 45 | def action 46 | case body["pull_request"]["merged"] 47 | when true 48 | "merged" 49 | else 50 | body["action"] 51 | end 52 | end 53 | 54 | def short_sha 55 | body["sha"][0..7] 56 | end 57 | 58 | def author_link 59 | "" 60 | end 61 | 62 | def author 63 | body["sender"]["login"] 64 | end 65 | 66 | def pull_request_title 67 | body["pull_request"]["title"] 68 | end 69 | 70 | def pull_request_url 71 | body["pull_request"]["html_url"] 72 | end 73 | 74 | def pull_request_body 75 | body["pull_request"]["body"] 76 | end 77 | 78 | def suppressed_actions 79 | %w{assigned edited labeled synchronize unassigned unlabeled} 80 | end 81 | 82 | def response_color 83 | case action 84 | when "closed" 85 | "#f00" 86 | when "merged" 87 | "#6e5692" 88 | else 89 | "#36a64f" 90 | end 91 | end 92 | 93 | def response 94 | return if suppressed_actions.include?(action) 95 | { 96 | channel: "#notifications", 97 | text: title, 98 | attachments: [ 99 | { 100 | color: response_color, 101 | fallback: fallback_title, 102 | title: "##{number} #{pull_request_title}", 103 | title_link: pull_request_url, 104 | text: pull_request_body, 105 | mrkdwn_in: [:text, :pretext] 106 | } 107 | ] 108 | } 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/push.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub Push Webhook 3 | class Push 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | body["repository"]["name"] 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def branch 18 | @branch ||= body["ref"].sub("refs/heads/", "") 19 | end 20 | 21 | def branch_url 22 | "https://github.com/#{full_name}/tree/#{branch}" 23 | end 24 | 25 | def commits_text 26 | ActionController::Base.helpers.pluralize(commits.size, "new commit") 27 | end 28 | 29 | def title 30 | "<#{branch_url}|[#{repo_name}:#{branch}]> " \ 31 | "#{commits_text} by #{commit_author}:" 32 | end 33 | 34 | def fallback_title 35 | "[#{repo_name}:#{branch}] #{commits_text} by #{commit_author}" 36 | end 37 | 38 | def short_sha 39 | body["after"][0..7] 40 | end 41 | 42 | def commit_author 43 | commits.any? && commits.first["committer"]["name"] 44 | end 45 | 46 | def commit_comment_description 47 | commits.any? && commits.first["message"] 48 | end 49 | 50 | def commit_comment 51 | "#{commit_comment_description} - #{commit_author}" 52 | end 53 | 54 | def commits 55 | body["commits"] 56 | end 57 | 58 | def response 59 | return if commits.empty? 60 | return if short_sha == "00000000" 61 | { 62 | channel: "#notifications", 63 | text: title, 64 | attachments: [ 65 | { 66 | fallback: fallback_title, 67 | color: "#4785c0", 68 | text: "`#{short_sha}` #{commit_comment}", 69 | mrkdwn_in: [:text, :pretext] 70 | } 71 | ] 72 | } 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/status.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on a GitHub Status Webhook 3 | class Status 4 | attr_accessor :body 5 | def initialize(body) 6 | @body = body 7 | end 8 | 9 | def repo_name 10 | body["repository"]["name"] 11 | end 12 | 13 | def full_name 14 | body["repository"]["full_name"] 15 | end 16 | 17 | def branch 18 | @branch ||= branch! 19 | end 20 | 21 | def branch! 22 | if body["branches"].any? 23 | body["branches"].first["name"] 24 | else 25 | short_sha 26 | end 27 | end 28 | 29 | def branch_ref 30 | if short_sha == branch 31 | short_sha 32 | else 33 | branch 34 | end 35 | end 36 | 37 | def target_url 38 | body["target_url"] 39 | end 40 | 41 | def repo_url 42 | "https://github.com/#{full_name}" 43 | end 44 | 45 | def branch_url 46 | "#{repo_url}/tree/#{branch}" 47 | end 48 | 49 | def sha_url 50 | "#{repo_url}/tree/#{short_sha}" 51 | end 52 | 53 | def title 54 | "<#{branch_url}|[#{repo_name}:#{branch}]>" 55 | end 56 | 57 | def short_sha 58 | body["sha"][0..7] 59 | end 60 | 61 | def actor 62 | commit["committer"]["login"] 63 | rescue StandardError 64 | "Unknown" 65 | end 66 | 67 | def commit 68 | body["commit"] 69 | end 70 | 71 | def state_description 72 | case body["state"] 73 | when "success" 74 | "was successful" 75 | else 76 | "failed" 77 | end 78 | end 79 | 80 | def message_color 81 | body["state"] == "success" ? "#36a64f" : "#f00" 82 | end 83 | 84 | def actor_description 85 | case actor 86 | when "web-flow" 87 | "" 88 | else 89 | " for #{actor}" 90 | end 91 | end 92 | 93 | def changeling_description 94 | body["description"].gsub("All requirements completed. ", "") 95 | end 96 | 97 | def footer_text 98 | case body["context"] 99 | when "ci/circleci" 100 | "circle-ci built #{short_sha}#{actor_description}" 101 | when "continuous-integration/travis-ci/push" 102 | "Travis-CI built #{short_sha}#{actor_description}" 103 | when "heroku/compliance" 104 | "Changeling: #{changeling_description}" 105 | when "continuous-integration/heroku" 106 | "Heroku-CI built #{short_sha}#{actor_description}" 107 | else 108 | "Unknown" 109 | end 110 | end 111 | 112 | # rubocop:disable Metrics/LineLength 113 | def footer_icon 114 | case body["context"] 115 | when "ci/circleci" 116 | "https://cloud.githubusercontent.com/assets/38/16295346/2b121e26-38db-11e6-9c4f-ee905519fdf3.png" 117 | when "continuous-integration/travis-ci/push" 118 | "https://cloud.githubusercontent.com/assets/483012/22075201/56ed65da-ddab-11e6-954c-2636206bf6a4.png" 119 | when "heroku/compliance", "continuous-integration/heroku" 120 | "https://cloud.githubusercontent.com/assets/38/16531791/ebc00ff4-3f82-11e6-919b-693a5cf9183a.png" 121 | when "vulnerabilities/gems" 122 | "https://cloud.githubusercontent.com/assets/38/16547100/e23b670e-4116-11e6-8b38-bac1b4c853f0.jpg" 123 | end 124 | end 125 | # rubocop:enable Metrics/LineLength 126 | 127 | def suppressed_contexts 128 | %w{codeclimate continuous-integration/travis-ci/pr vulnerabilities/gems} 129 | end 130 | 131 | def attachment_text 132 | "<#{repo_url}|#{repo_name}> build of <#{branch_url}|#{branch_ref}> " \ 133 | "#{state_description}. <#{target_url}|Details>" 134 | end 135 | 136 | def fallback_text 137 | "#{repo_name} build of #{branch} was #{state_description}" 138 | end 139 | 140 | def response 141 | return if body["state"] == "pending" 142 | return if suppressed_contexts.include?(body["context"]) 143 | { 144 | channel: "#notifications", 145 | attachments: [ 146 | { 147 | fallback: fallback_text, 148 | color: message_color, 149 | text: attachment_text, 150 | footer: footer_text, 151 | footer_icon: footer_icon, 152 | mrkdwn_in: [:text, :pretext] 153 | } 154 | ] 155 | } 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /app/models/git_hub/event_messages/unknown.rb: -------------------------------------------------------------------------------- 1 | module GitHub::EventMessages 2 | # Class to generate Slack Messages based on an known GitHub Webhook 3 | class Unknown 4 | attr_accessor :body, :event_type, :repo_name 5 | def initialize(body, event_type) 6 | @body = body 7 | @repo_name = nil 8 | @event_type = event_type 9 | end 10 | 11 | def title 12 | "#{event_type} :smiley:" 13 | end 14 | 15 | def fallback_title 16 | title 17 | end 18 | 19 | def commit_author 20 | body["sender"]["login"] 21 | end 22 | 23 | def response 24 | { 25 | channel: "#notifications", 26 | attachments: [ 27 | { 28 | text: title, 29 | color: "#4785c0", 30 | fallback: fallback_title, 31 | mrkdwn_in: [:text, :pretext] 32 | } 33 | ] 34 | } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/git_hub/organization.rb: -------------------------------------------------------------------------------- 1 | # DB record representing a configured GitHub organization 2 | class GitHub::Organization < ApplicationRecord 3 | include AttributeEncryption 4 | 5 | belongs_to :team, class_name: "SlackHQ::Team", required: true 6 | has_many :repositories 7 | 8 | validates :name, presence: true 9 | validates :default_room, presence: true 10 | validates :enc_webhook_secret, presence: true 11 | 12 | before_validation :set_webhook_secret, on: :create 13 | 14 | def webhook_secret=(value) 15 | self[:enc_webhook_secret] = encrypt_value(value) 16 | end 17 | 18 | def webhook_secret 19 | decrypt_value(enc_webhook_secret) 20 | end 21 | 22 | def set_webhook_secret 23 | self.webhook_secret = SecureRandom.hex(24) 24 | end 25 | 26 | def default_room_for(name) 27 | return default_room if name.blank? 28 | repo = repositories.find { |r| r.name == name } 29 | if repo 30 | repo.default_room 31 | else 32 | default_room 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/git_hub/repository.rb: -------------------------------------------------------------------------------- 1 | # DB record representing a configured GitHub repository 2 | class GitHub::Repository < ApplicationRecord 3 | belongs_to :organization, required: true 4 | 5 | validates :name, presence: true 6 | end 7 | -------------------------------------------------------------------------------- /app/models/slack_h_q.rb: -------------------------------------------------------------------------------- 1 | # Top-level module to classify slack models 2 | module SlackHQ 3 | def self.table_name_prefix 4 | "slack_" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/slack_h_q/team.rb: -------------------------------------------------------------------------------- 1 | # DB record representing a slack team installation 2 | class SlackHQ::Team < ApplicationRecord 3 | include AttributeEncryption 4 | 5 | has_many :users 6 | has_many :organizations, class_name: "GitHub::Organization" 7 | 8 | validates :bot_user_id, presence: true 9 | validates :enc_bot_token, presence: true 10 | 11 | def self.from_omniauth(omniauth_info) 12 | team = find_or_initialize_by( 13 | team_id: omniauth_info["info"]["team_id"] 14 | ) 15 | 16 | if team.bot_token.blank? 17 | bot_info = omniauth_info["extra"] && omniauth_info["extra"]["bot_info"] 18 | bot_token = bot_info["bot_access_token"] 19 | team.bot_token = bot_token unless bot_token.blank? 20 | team.bot_user_id = bot_info["bot_user_id"] 21 | end 22 | 23 | team.save if team.new_record? 24 | team 25 | end 26 | 27 | def bot_token=(value) 28 | self[:enc_bot_token] = encrypt_value(value) 29 | end 30 | 31 | def bot_token 32 | decrypt_value(enc_bot_token) 33 | end 34 | 35 | def bot 36 | @bot ||= Slack::Web::Client.new(token: bot_token, logger: Rails.logger) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/slack_h_q/user.rb: -------------------------------------------------------------------------------- 1 | # DB record representing a user in slack team installation 2 | class SlackHQ::User < ApplicationRecord 3 | include AttributeEncryption 4 | include OctokitMethods 5 | 6 | belongs_to :team 7 | has_many :commands, class_name: "Command" 8 | 9 | def self.omniauth_user_data(omniauth_info) 10 | token = omniauth_info[:credentials][:token] 11 | response = slack_client.get("/api/users.identity?token=#{token}") 12 | 13 | JSON.parse(response.body) 14 | end 15 | 16 | def self.from_omniauth(omniauth_info) 17 | body = omniauth_user_data(omniauth_info) 18 | team = SlackHQ::Team.find_or_initialize_by( 19 | team_id: body["team"]["id"] 20 | ) 21 | team.save if team.new_record? 22 | 23 | user = team.users.find_or_initialize_by( 24 | slack_user_id: body["user"]["id"] 25 | ) 26 | user.slack_user_name = body["user"]["name"] 27 | user.save 28 | user 29 | end 30 | 31 | def self.slack_client 32 | Faraday.new(url: "https://slack.com") do |connection| 33 | connection.headers["Content-Type"] = "application/json" 34 | connection.adapter Faraday.default_adapter 35 | end 36 | end 37 | 38 | def github_token=(value) 39 | self[:enc_github_token] = encrypt_value(value) 40 | end 41 | 42 | def github_token 43 | decrypt_value(enc_github_token) 44 | end 45 | 46 | def create_command_for(params) 47 | command = commands.create( 48 | channel_id: params[:channel_id], 49 | channel_name: params[:channel_name], 50 | command: params[:command], 51 | command_text: params[:text], 52 | response_url: params[:response_url], 53 | team_id: params[:team_id], 54 | team_domain: params[:team_domain] 55 | ) 56 | CommandRunnerJob.perform_later(command_id: command.id) 57 | command 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Team focused GitHub Notifications in Slack 5 | <%= csrf_meta_tags %> 6 | 7 | 8 | 9 | <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> 10 | 11 | 12 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %> 13 | 14 | 15 | 16 |
17 | <%= yield %> 18 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/pages/_github-logo.html.erb: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /app/views/pages/_heroku-logo.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/views/pages/_slack-logo.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/views/pages/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

speakerboxxx/the-love-below

3 |

ChatOps management of GitHub notifications

4 |
5 | 6 |
7 | 24 |
25 |
    26 |
  • Teams with different rooms all under one GitHub organization.
  • 27 |
  • Use chat to route to specific rooms.
  • 28 |
29 |

30 | Install Speakerboxxx for Slack 31 |

32 | 33 | ChatOps with Slack and GitHub 34 |
35 |
36 | -------------------------------------------------------------------------------- /app/views/pages/support.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

speakerboxxx/the-love-below

3 |

ChatOps management of GitHub notifications

4 |
5 | 6 |
7 |
8 |

9 | Email Support 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f .node-version ]; then 4 | echo "4.6.0" > .node-version 5 | fi 6 | 7 | if [ ! -f .env.test ]; then 8 | cp .env.sample .env.test 9 | fi 10 | 11 | set -x 12 | git log -n 1 HEAD | cat 13 | node -v 14 | ruby -v 15 | bundle -v 16 | set +x 17 | set -e 18 | 19 | export RACK_ENV=test 20 | export RAILS_ENV=test 21 | 22 | 23 | bundle exec rake db:create 24 | bundle exec rake db:migrate 25 | bundle exec rake assets:precompile >/dev/null 2>&1 26 | bundle exec rspec spec 27 | bundle exec rubocop 28 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "pathname" 3 | require "fileutils" 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path("../../", __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | puts "== Installing dependencies ==" 15 | system! "gem install bundler --conservative" 16 | system("bundle check") || system!("bundle install") 17 | 18 | puts "\n== Preparing database ==" 19 | system! "bin/rails db:setup" 20 | 21 | puts "\n== Removing old logs and tempfiles ==" 22 | system! "bin/rails log:clear tmp:clear" 23 | 24 | puts "\n== Restarting application server ==" 25 | system! "bin/rails restart" 26 | end 27 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | post: 3 | - bundle exec rubocop 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "active_model/railtie" 4 | require "active_job/railtie" 5 | require "active_record/railtie" 6 | require "action_controller/railtie" 7 | require "action_mailer/railtie" 8 | require "action_view/railtie" 9 | require "action_cable/engine" 10 | require "sprockets/railtie" 11 | 12 | # Require the gems listed in Gemfile, including any gems 13 | # you've limited to :test, :development, or :production. 14 | Bundler.require(*Rails.groups) 15 | 16 | module Speakerboxxx 17 | class Application < Rails::Application 18 | config.active_job.queue_adapter = :sidekiq 19 | 20 | config.generators do |g| 21 | g.assets = false 22 | g.helper = false 23 | g.view_specs = false 24 | end 25 | 26 | config.lograge.enabled = true 27 | config.lograge.custom_options = lambda do |event| 28 | { request_id: event.payload[:request_id] } 29 | end 30 | 31 | config.generators do |g| 32 | g.orm :active_record, primary_key_type: :uuid 33 | end 34 | 35 | # Settings in config/environments/* take precedence over those specified here. 36 | # Application configuration should go into files in config/initializers 37 | # -- all .rb files in that directory are automatically loaded. 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket. 2 | production: 3 | adapter: redis 4 | url: redis://localhost:6379/1 5 | 6 | development: 7 | adapter: async 8 | 9 | test: 10 | adapter: async 11 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | timeout: 5000 4 | 5 | development: 6 | <<: *default 7 | database: speakerboxxx_development 8 | test: 9 | <<: *default 10 | database: speakerboxxx_test 11 | production: 12 | <<: *default 13 | database: speakerboxxx_production 14 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | config.active_job.queue_adapter = :inline 46 | 47 | # Raises error for missing translations 48 | # config.action_view.raise_on_missing_translations = true 49 | 50 | # Use an evented file watcher to asynchronously detect changes in source code, 51 | # routes, locales, etc. This feature depends on the listen gem. 52 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 53 | end 54 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Action Cable endpoint configuration 38 | # config.action_cable.url = 'wss://example.com/cable' 39 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 40 | 41 | # Don't mount Action Cable in the main server process. 42 | # config.action_cable.mount_path = nil 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | config.log_tags = [ :request_id ] 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Use a real queuing backend for Active Job (and separate queues per environment) 58 | # config.active_job.queue_adapter = :resque 59 | # config.active_job.queue_name_prefix = "speakerboxxx_#{Rails.env}" 60 | config.action_mailer.perform_caching = false 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require 'syslog/logger' 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 79 | 80 | if ENV["RAILS_LOG_TO_STDOUT"].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | end 89 | -------------------------------------------------------------------------------- /config/environments/staging.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Action Cable endpoint configuration 38 | # config.action_cable.url = 'wss://example.com/cable' 39 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 40 | 41 | # Don't mount Action Cable in the main server process. 42 | # config.action_cable.mount_path = nil 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | config.log_tags = [ :request_id ] 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Use a real queuing backend for Active Job (and separate queues per environment) 58 | # config.active_job.queue_adapter = :resque 59 | # config.active_job.queue_name_prefix = "speakerboxxx_#{Rails.env}" 60 | config.action_mailer.perform_caching = false 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require 'syslog/logger' 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 79 | 80 | if ENV["RAILS_LOG_TO_STDOUT"].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | end 89 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/active_record_belongs_to_required_by_default.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Require `belongs_to` associations by default. This is a new Rails 5.0 4 | # default, so it is introduced as a configuration option to ensure that apps 5 | # made on earlier versions of Rails are not affected when upgrading. 6 | Rails.application.config.active_record.belongs_to_required_by_default = true 7 | -------------------------------------------------------------------------------- /config/initializers/admin_constraint.rb: -------------------------------------------------------------------------------- 1 | class AdminConstraint 2 | GITHUB_ADMIN_LOGINS = ENV.fetch("GITHUB_ADMIN_LOGINS", "").split(",").freeze 3 | 4 | def matches?(request) 5 | return false unless request.session[:user_id] 6 | u = SlackHQ::User.find(request.session[:user_id]) 7 | return false unless u.github_login.present? 8 | GITHUB_ADMIN_LOGINS.include?(u.github_login) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/callback_terminator.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Do not halt callback chains when a callback returns false. This is a new 4 | # Rails 5.0 default, so it is introduced as a configuration option to ensure 5 | # that apps made with earlier versions of Rails are not affected when upgrading. 6 | ActiveSupport.halt_callback_chains_on_return_false = false 7 | -------------------------------------------------------------------------------- /config/initializers/coal_car.rb: -------------------------------------------------------------------------------- 1 | ENV["APP_NAME"] ||= "speakerboxxx" 2 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | 6 | Raven.configure do |config| 7 | config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | provider :github, ENV["GITHUB_OAUTH_ID"], ENV["GITHUB_OAUTH_SECRET"], scope: "user,admin:org_hook" 3 | end 4 | 5 | Rails.application.config.middleware.use OmniAuth::Builder do 6 | provider :slack, ENV["SLACK_OAUTH_ID"], ENV["SLACK_OAUTH_SECRET"], scope: "identity.basic" 7 | provider :slack, ENV["SLACK_OAUTH_ID"], ENV["SLACK_OAUTH_SECRET"], scope: "identify,commands,bot", name: :slack_install 8 | end 9 | 10 | OmniAuth.config.logger = Rails.logger 11 | -------------------------------------------------------------------------------- /config/initializers/per_form_csrf_tokens.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable per-form CSRF tokens. 4 | Rails.application.config.action_controller.per_form_csrf_tokens = true 5 | -------------------------------------------------------------------------------- /config/initializers/request_forgery_protection.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable origin-checking CSRF mitigation. 4 | Rails.application.config.action_controller.forgery_protection_origin_check = true 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_speakerboxxx_session' 4 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq" 2 | -------------------------------------------------------------------------------- /config/initializers/slack_application.rb: -------------------------------------------------------------------------------- 1 | module Speakerboxxx 2 | def self.slash_command_prefix 3 | ENV["SLACK_SLASH_COMMAND_PREFIX"] || "/gh" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/slack_h_q.rb: -------------------------------------------------------------------------------- 1 | require "slack_h_q" 2 | require "slack_h_q/team" 3 | require "slack_h_q/user" 4 | -------------------------------------------------------------------------------- /config/initializers/ssl_options.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure SSL options to enable HSTS with subdomains. 4 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 5 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq/web" 2 | 3 | Rails.application.routes.draw do 4 | mount Peek::Railtie => "/peek" 5 | 6 | get "/auth/failure", to: "sessions#destroy" 7 | get "/auth/github/callback", to: "sessions#create_github" 8 | get "/auth/slack/callback", to: "sessions#create_slack" 9 | get "/auth/slack_install/callback", to: "sessions#install_slack" 10 | 11 | post "/signout", to: "sessions#destroy" 12 | 13 | get "/install", to: "application#install" 14 | 15 | get "/support", to: "pages#support" 16 | 17 | post "/webhooks/:team_id/github/:org_name", to: "webhooks#create" 18 | 19 | post "/commands", to: "commands#create" 20 | 21 | mount Sidekiq::Web => "/sidekiq", constraints: AdminConstraint.new 22 | 23 | root to: "pages#index" 24 | end 25 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: b983a18f114470d899bdd12041b83faa13e367b445e633a7111232d0ab4febe6479f0da6853db3f759630d3b067e38709dffcac85c2100379268ca07d413906f 15 | 16 | test: 17 | secret_key_base: d82bac3869c871bdd72ea301f74bdbe3094fe1a7f6482acd1a3ce66f68190155d55d63b06b74ca963de4734c7825d124f2eb21af7c2fb10bea5b35113b93c2e9 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | staging: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | production: 24 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 25 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20160611165409_enable_uuid_extension.rb: -------------------------------------------------------------------------------- 1 | class EnableUuidExtension < ActiveRecord::Migration 2 | def change 3 | enable_extension 'uuid-ossp' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160612002222_create_slack_teams.rb: -------------------------------------------------------------------------------- 1 | class CreateSlackTeams < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :slack_teams, id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 4 | t.string "enc_bot_token", null: false 5 | t.string "team_id", null: false 6 | t.string "team_domain" 7 | t.timestamps 8 | end 9 | 10 | create_table :slack_users, id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 11 | t.string "slack_user_id", null: false 12 | t.string "slack_user_name", null: false 13 | t.string "slack_team_id", null: false 14 | t.string "github_id" 15 | t.string "github_login" 16 | t.string "enc_github_token" 17 | t.timestamps 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20160612201904_fix_bot_tokens.rb: -------------------------------------------------------------------------------- 1 | class FixBotTokens < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :slack_teams, :enc_bot_token 4 | 5 | add_column :slack_teams, :bot_user_id, :string, null: true 6 | add_column :slack_teams, :enc_bot_token, :string, null: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20160612204943_fix_slack_team_id.rb: -------------------------------------------------------------------------------- 1 | class FixSlackTeamId < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :slack_users, :slack_team_id 4 | 5 | add_column :slack_users, :team_id, :uuid, null: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20160612224739_create_commands.rb: -------------------------------------------------------------------------------- 1 | class CreateCommands < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :commands, id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 4 | t.string "args" 5 | t.string "application" 6 | t.string "channel_id", null: false 7 | t.string "channel_name", null: false 8 | t.string "command", null: false 9 | t.string "command_text" 10 | t.string "response_url", null: false 11 | t.string "subtask" 12 | t.string "slack_user_id" 13 | t.string "task" 14 | t.string "team_id", null: false 15 | t.string "team_domain", null: false 16 | t.uuid "user_id" 17 | t.datetime "processed_at" 18 | t.timestamps 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20160613174641_create_git_hub_organizations.rb: -------------------------------------------------------------------------------- 1 | class CreateGitHubOrganizations < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :git_hub_organizations, id: :uuid, default: -> { "uuid_generate_v4()" } do |t| 4 | t.string "name" 5 | t.string "enc_webhook_secret" 6 | t.string "default_room", default: "#general" 7 | t.string "webhook_id" 8 | t.uuid "team_id" 9 | t.timestamps 10 | end 11 | 12 | create_table :git_hub_repositories, id: :uuid, default: -> { "uuid_generate_v4()" } do |t| 13 | t.string "name" 14 | t.string "default_room", default: "#general" 15 | t.uuid "organization_id" 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20160613174641) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | enable_extension "uuid-ossp" 18 | 19 | create_table "commands", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 20 | t.string "args" 21 | t.string "application" 22 | t.string "channel_id", null: false 23 | t.string "channel_name", null: false 24 | t.string "command", null: false 25 | t.string "command_text" 26 | t.string "response_url", null: false 27 | t.string "subtask" 28 | t.string "slack_user_id" 29 | t.string "task" 30 | t.string "team_id", null: false 31 | t.string "team_domain", null: false 32 | t.uuid "user_id" 33 | t.datetime "processed_at" 34 | t.datetime "created_at", null: false 35 | t.datetime "updated_at", null: false 36 | end 37 | 38 | create_table "git_hub_organizations", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 39 | t.string "name" 40 | t.string "enc_webhook_secret" 41 | t.string "default_room", default: "#general" 42 | t.string "webhook_id" 43 | t.uuid "team_id" 44 | t.datetime "created_at", null: false 45 | t.datetime "updated_at", null: false 46 | end 47 | 48 | create_table "git_hub_repositories", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 49 | t.string "name" 50 | t.string "default_room", default: "#general" 51 | t.uuid "organization_id" 52 | t.datetime "created_at", null: false 53 | t.datetime "updated_at", null: false 54 | end 55 | 56 | create_table "slack_teams", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 57 | t.string "team_id", null: false 58 | t.string "team_domain" 59 | t.datetime "created_at", null: false 60 | t.datetime "updated_at", null: false 61 | t.string "bot_user_id" 62 | t.string "enc_bot_token" 63 | end 64 | 65 | create_table "slack_users", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 66 | t.string "slack_user_id", null: false 67 | t.string "slack_user_name", null: false 68 | t.string "github_id" 69 | t.string "github_login" 70 | t.string "enc_github_token" 71 | t.datetime "created_at", null: false 72 | t.datetime "updated_at", null: false 73 | t.uuid "team_id", null: false 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc "Reset the database to a fresh state" 3 | task delete_all_records: :environment do 4 | Command.destroy_all 5 | GitHub::Organization.destroy_all 6 | GitHub::Repository.destroy_all 7 | SlackHQ::Team.destroy_all 8 | SlackHQ::User.destroy_all 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/fixtures/api.github.com/orgs/atmos-org/hooks/configured.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Organization", 4 | "id": 7579642, 5 | "name": "web", 6 | "active": true, 7 | "events": [ 8 | "*" 9 | ], 10 | "config": { 11 | "content_type": "json", 12 | "insecure_ssl": "0", 13 | "secret": "********", 14 | "url": "https://speakerboxxx-test.atmos.org/webhooks/T123YG08V/github/atmos-org" 15 | }, 16 | "updated_at": "2016-03-28T20:34:28Z", 17 | "created_at": "2016-03-07T02:25:52Z", 18 | "url": "https://api.github.com/orgs/heroku/hooks/7579642", 19 | "ping_url": "https://api.github.com/orgs/heroku/hooks/7579642/pings" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /spec/fixtures/api.github.com/orgs/atmos-org/hooks/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "url": "https://api.github.com/orgs/atmos-org/hooks/1", 4 | "ping_url": "https://api.github.com/orgs/atmos-org/hooks/1/pings", 5 | "name": "web", 6 | "events": [ 7 | "*" 8 | ], 9 | "active": true, 10 | "config": { 11 | "url": "https://speakerboxxx-test.atmos.org/webhooks/T123YG08V/github/atmos-org", 12 | "content_type": "json" 13 | }, 14 | "updated_at": "2015-09-06T20:39:23Z", 15 | "created_at": "2015-09-06T17:26:27Z" 16 | } 17 | -------------------------------------------------------------------------------- /spec/fixtures/api.github.com/orgs/atmos-org/hooks/empty.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /spec/fixtures/api.github.com/orgs/atmos-org/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "atmos-org", 3 | "id": 19312617, 4 | "url": "https://api.github.com/orgs/atmos-org", 5 | "repos_url": "https://api.github.com/orgs/atmos-org/repos", 6 | "events_url": "https://api.github.com/orgs/atmos-org/events", 7 | "hooks_url": "https://api.github.com/orgs/atmos-org/hooks", 8 | "issues_url": "https://api.github.com/orgs/atmos-org/issues", 9 | "members_url": "https://api.github.com/orgs/atmos-org/members{/member}", 10 | "public_members_url": "https://api.github.com/orgs/atmos-org/public_members{/member}", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/19312617?v=3", 12 | "description": "", 13 | "name": "atmos.org", 14 | "company": null, 15 | "blog": "http://atmos.org", 16 | "location": "", 17 | "email": "", 18 | "public_repos": 0, 19 | "public_gists": 0, 20 | "followers": 0, 21 | "following": 0, 22 | "html_url": "https://github.com/atmos-org", 23 | "created_at": "2016-05-11T18:39:03Z", 24 | "updated_at": "2016-07-01T06:52:58Z", 25 | "type": "Organization", 26 | "total_private_repos": 3, 27 | "owned_private_repos": 3, 28 | "private_gists": 0, 29 | "disk_usage": 44593, 30 | "collaborators": 0, 31 | "billing_email": "atmos@atmos.org", 32 | "plan": { 33 | "name": "organization", 34 | "space": 976562499, 35 | "private_repos": 9999, 36 | "filled_seats": 6, 37 | "seats": 6 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/fixtures/slack.com/identity.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok":true, 3 | "user":{ 4 | "name":"Toolskai", 5 | "id":"U1RM9BNL8" 6 | }, 7 | "team":{ 8 | "id":"T092F92CG" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/webhooks/ping.json: -------------------------------------------------------------------------------- 1 | { 2 | "zen": "It's not fully shipped until it's fast.", 3 | "hook_id": 8830165, 4 | "hook": { 5 | "type": "Organization", 6 | "id": 8830165, 7 | "name": "web", 8 | "active": true, 9 | "events": [ 10 | "*" 11 | ], 12 | "config": { 13 | "content_type": "json", 14 | "secret": "********", 15 | "url": "https://250a7e07.ngrok.io/webhooks/T0YJVLHHA/github/atmos-org" 16 | }, 17 | "updated_at": "2016-06-21T06:37:15Z", 18 | "created_at": "2016-06-21T06:37:15Z", 19 | "url": "https://api.github.com/orgs/atmos-org/hooks/8830165", 20 | "ping_url": "https://api.github.com/orgs/atmos-org/hooks/8830165/pings" 21 | }, 22 | "organization": { 23 | "login": "atmos-org", 24 | "id": 19312617, 25 | "url": "https://api.github.com/orgs/atmos-org", 26 | "repos_url": "https://api.github.com/orgs/atmos-org/repos", 27 | "events_url": "https://api.github.com/orgs/atmos-org/events", 28 | "hooks_url": "https://api.github.com/orgs/atmos-org/hooks", 29 | "issues_url": "https://api.github.com/orgs/atmos-org/issues", 30 | "members_url": "https://api.github.com/orgs/atmos-org/members{/member}", 31 | "public_members_url": "https://api.github.com/orgs/atmos-org/public_members{/member}", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/19312617?v=3", 33 | "description": "" 34 | }, 35 | "sender": { 36 | "login": "atmos", 37 | "id": 38, 38 | "avatar_url": "https://avatars.githubusercontent.com/u/38?v=3", 39 | "gravatar_id": "", 40 | "url": "https://api.github.com/users/atmos", 41 | "html_url": "https://github.com/atmos", 42 | "followers_url": "https://api.github.com/users/atmos/followers", 43 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 47 | "organizations_url": "https://api.github.com/users/atmos/orgs", 48 | "repos_url": "https://api.github.com/users/atmos/repos", 49 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/atmos/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/models/command_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Command, type: :model do 4 | let(:user) { create_atmos } 5 | it "creates a command" do 6 | expect do 7 | command = user.create_command_for(command_params_for("help")) 8 | 9 | expect(command.task).to eql("help") 10 | expect(command.subtask).to eql("default") 11 | expect(command.application).to eql(nil) 12 | end.to change { Command.count }.from(0).to(1) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/models/commands/route_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Commands::Route, type: :model do 4 | let(:user) { create_atmos } 5 | let(:org) { team.organizations.create(name: "atmos-org", webhook_id: 1) } 6 | let(:team) { SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) } 7 | 8 | before do 9 | expect(org).to be_valid 10 | end 11 | 12 | it "route:info responds with :+1: if configured properly" do 13 | stub_json_request(:get, "https://api.github.com/orgs/atmos-org", 14 | github_fixture_data("orgs/atmos-org/index")) 15 | stub_json_request(:get, "https://api.github.com/orgs/atmos-org/hooks", 16 | github_fixture_data("orgs/atmos-org/hooks/configured")) 17 | 18 | command_params = command_params_for("route:info atmos-org/speakerboxxx") 19 | command = user.create_command_for(command_params) 20 | 21 | handler = command.handler 22 | expect(handler.organization_name).to eql("atmos-org") 23 | expect(handler.repository_name).to eql("speakerboxxx") 24 | 25 | handler.run 26 | 27 | expect(handler.org_hooks?).to be true 28 | response = { 29 | text: "atmos-org/speakerboxxx is configured and routing to #general.", 30 | response_type: "in_channel" 31 | } 32 | expect(handler.response).to eql(response) 33 | end 34 | 35 | it "route responds with :+1: if configured properly" do 36 | stub_json_request(:get, "https://api.github.com/orgs/atmos-org", 37 | github_fixture_data("orgs/atmos-org/index")) 38 | stub_json_request(:get, "https://api.github.com/orgs/atmos-org/hooks", 39 | github_fixture_data("orgs/atmos-org/hooks/configured")) 40 | 41 | command_string = "route atmos-org/speakerboxxx #notifications" 42 | command_params = command_params_for(command_string) 43 | command = user.create_command_for(command_params) 44 | 45 | handler = command.handler 46 | expect(handler.organization_name).to eql("atmos-org") 47 | expect(handler.repository_name).to eql("speakerboxxx") 48 | expect(handler.room_name).to eql("#notifications") 49 | 50 | expect do 51 | handler.run 52 | 53 | expect(handler.org_hooks?).to be true 54 | response = { 55 | text: "atmos-org/speakerboxxx is configured and routing to " \ 56 | "#notifications.", 57 | response_type: "in_channel" 58 | } 59 | expect(handler.response).to eql(response) 60 | end.to change { GitHub::Repository.count }.from(0).to(1) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/create_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | # rubocop:disable Metrics/LineLength 4 | RSpec.describe GitHub::EventMessages::Create, type: :model do 5 | it "creates a tagged Slack Message" do 6 | data = decoded_fixture_data("webhooks/create-tag") 7 | 8 | handler = GitHub::EventMessages::Create.new(data) 9 | 10 | response = handler.response 11 | expect(response[:channel]).to eql("#notifications") 12 | expect(response[:text]).to be_nil 13 | attachments = response[:attachments] 14 | expect(attachments.first[:color]).to eql("#4785c0") 15 | expect(attachments.first[:text]) 16 | .to eql(" atmos tagged .") 17 | expect(attachments.first[:fallback]) 18 | .to eql("[atmos-org/speakerboxxx] atmos tagged \"testing\".") 19 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 20 | end 21 | 22 | it "discards branch creation" do 23 | data = decoded_fixture_data("webhooks/create-branch") 24 | 25 | handler = GitHub::EventMessages::Create.new(data) 26 | 27 | response = handler.response 28 | expect(response).to be_nil 29 | end 30 | end 31 | # rubocop:enable Metrics/LineLength 32 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/delete_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | # rubocop:disable Metrics/LineLength 4 | RSpec.describe GitHub::EventMessages::Delete, type: :model do 5 | it "creates a push Slack Message" do 6 | data = decoded_fixture_data("webhooks/delete") 7 | 8 | handler = GitHub::EventMessages::Delete.new(data) 9 | 10 | response = handler.response 11 | expect(response[:channel]).to eql("#notifications") 12 | expect(response[:text]).to be_nil 13 | attachments = response[:attachments] 14 | expect(attachments.first[:color]).to eql("#4785c0") 15 | expect(attachments.first[:text]) 16 | .to eql(" The branch \"add-pull-requests\" was deleted by atmos.") 17 | expect(attachments.first[:fallback]) 18 | .to eql("[atmos-org/speakerboxxx] The branch \"add-pull-requests\" was deleted by atmos.") 19 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 20 | end 21 | end 22 | # rubocop:enable Metrics/LineLength 23 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/ping_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GitHub::EventMessages::Ping, type: :model do 4 | it "creates a push Slack Message" do 5 | team = SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) 6 | expect(team).to be_valid 7 | 8 | org = team.organizations.create(name: "heroku", webhook_id: 42) 9 | expect(org).to be_valid 10 | 11 | data = decoded_fixture_data("webhooks/ping") 12 | 13 | handler = GitHub::EventMessages::Ping.new(data) 14 | 15 | response = handler.response 16 | expect(response[:channel]).to eql("#notifications") 17 | expect(response[:text]).to be_nil 18 | 19 | attachments = response[:attachments] 20 | expect(attachments.first[:color]).to eql("#4785c0") 21 | expect(attachments.first[:text]).to be_nil 22 | expect(attachments.first[:title]).to eql("Webhooks enabled: :bell:") 23 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 24 | 25 | fields = attachments.first[:fields] 26 | org_cell = fields.first 27 | expect(org_cell[:title]).to eql("Organization") 28 | expect(org_cell[:value]).to eql("") # rubocop:disable Metrics/LineLength 29 | 30 | admin_cell = fields.second 31 | expect(admin_cell[:title]).to eql("Admin") 32 | 33 | expect(admin_cell[:value]).to eql("") # rubocop:disable Metrics/LineLength 34 | 35 | status_cell = fields.third 36 | expect(status_cell[:title]).to eql("Status") 37 | expect(status_cell[:value]).to eql("Enabled") 38 | 39 | zen_cell = fields.fourth 40 | expect(zen_cell[:title]).to eql("Zen") 41 | expect(zen_cell[:value]).to eql("It's not fully shipped until it's fast.") 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/pull_request_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | # rubocop:disable Metrics/LineLength 4 | RSpec.describe GitHub::EventMessages::PullRequest, type: :model do 5 | let(:org) { team.organizations.create(name: "heroku", webhook_id: 42) } 6 | let(:team) { SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) } 7 | 8 | it "returns a Slack message for pull request opened" do 9 | data = decoded_fixture_data("webhooks/pull_request-opened") 10 | 11 | handler = GitHub::EventMessages::PullRequest.new(data) 12 | response = handler.response 13 | expect(response).to_not be_nil 14 | expect(response[:channel]).to eql("#notifications") 15 | expect(response[:text]) 16 | .to eql("[atmos-org/speakerboxxx] Pull request opened by ") 17 | attachments = response[:attachments] 18 | expect(attachments.first[:fallback]).to eql("[atmos-org/speakerboxxx] Pull request #1 opened by atmos") 19 | expect(attachments.first[:color]).to eql("#36a64f") 20 | expect(attachments.first[:title]).to eql("#1 Add pull requests") 21 | expect(attachments.first[:title_link]) 22 | .to eql("https://github.com/atmos-org/speakerboxxx/pull/1") 23 | expect(attachments.first[:text]).to eql("This adds pull requests") 24 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 25 | end 26 | 27 | it "returns the right Slack message when pull request merged" do 28 | data = decoded_fixture_data("webhooks/pull_request-merged") 29 | 30 | handler = GitHub::EventMessages::PullRequest.new(data) 31 | response = handler.response 32 | expect(response).to_not be_nil 33 | expect(response[:channel]).to eql("#notifications") 34 | expect(response[:text]) 35 | .to eql("[atmos-org/speakerboxxx] Pull request merged by ") 36 | attachments = response[:attachments] 37 | expect(attachments.first[:fallback]).to eql("[atmos-org/speakerboxxx] Pull request #1 merged by atmos") 38 | expect(attachments.first[:color]).to eql("#6e5692") 39 | expect(attachments.first[:title]).to eql("#1 Add pull requests") 40 | expect(attachments.first[:title_link]) 41 | .to eql("https://github.com/atmos-org/speakerboxxx/pull/1") 42 | expect(attachments.first[:text]).to eql("This adds pull requests") 43 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 44 | end 45 | end 46 | # rubocop:enable Metrics/LineLength 47 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/push_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GitHub::EventMessages::Push, type: :model do 4 | it "creates a push Slack Message" do 5 | data = decoded_fixture_data("webhooks/push-1-commit") 6 | 7 | handler = GitHub::EventMessages::Push.new(data) 8 | 9 | response = handler.response 10 | expect(response[:channel]).to eql("#notifications") 11 | expect(response[:text]).to match("1 new commit by Corey Donohoe") 12 | attachments = response[:attachments] 13 | expect(attachments.first[:fallback]) 14 | .to eql("[speakerboxxx:master] 1 new commit by Corey Donohoe") 15 | expect(attachments.first[:color]).to eql("#4785c0") 16 | expect(attachments.first[:text]) 17 | .to include("fetch body and write to disk in local dev - Corey Donohoe") 18 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 19 | end 20 | 21 | it "uses plural form when multiple commits" do 22 | data = decoded_fixture_data("webhooks/push-2-commits") 23 | 24 | handler = GitHub::EventMessages::Push.new(data) 25 | 26 | response = handler.response 27 | expect(response[:channel]).to eql("#notifications") 28 | attachments = response[:attachments] 29 | expect(attachments.first[:fallback]) 30 | .to eql("[speakerboxxx:master] 2 new commits by Corey Donohoe") 31 | expect(attachments.first[:color]).to eql("#4785c0") 32 | expect(response[:text]).to match("2 new commits by Corey Donohoe") 33 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/models/git_hub/event_messages/unknown_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GitHub::EventMessages::Unknown, type: :model do 4 | it "handles unknown GitHub event to Slack Message" do 5 | data = decoded_fixture_data("webhooks/create-tag") 6 | 7 | handler = GitHub::EventMessages::Unknown.new(data, "team_add") 8 | 9 | response = handler.response 10 | expect(response[:channel]).to eql("#notifications") 11 | expect(response[:text]).to be_nil 12 | attachments = response[:attachments] 13 | expect(attachments.first[:color]).to eql("#4785c0") 14 | expect(attachments.first[:text]).to eql("team_add :smiley:") 15 | expect(attachments.first[:fallback]).to eql("team_add :smiley:") 16 | expect(attachments.first[:mrkdwn_in]).to eql([:text, :pretext]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/models/git_hub/organization_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GitHub::Organization, type: :model do 4 | it "creates a user successfully" do 5 | team = SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) 6 | expect(team).to be_valid 7 | 8 | org = team.organizations.create(name: "heroku", webhook_id: 42) 9 | expect(org).to be_valid 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/git_hub/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GitHub::Repository, type: :model do 4 | it "creates a user successfully" do 5 | team = SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) 6 | expect(team).to be_valid 7 | 8 | org = team.organizations.create(name: "heroku", webhook_id: 42) 9 | expect(org).to be_valid 10 | 11 | repo = org.repositories.create(name: "dashboard") 12 | expect(repo).to be_valid 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/models/slack_h_q/team_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SlackHQ::Team, type: :model do 4 | it "creates a user successfully" do 5 | team = SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) 6 | expect(team).to be_valid 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/slack_h_q/user_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SlackHQ::User, type: :model do 4 | it "creates a user successfully" do 5 | team = SlackHQ::Team.from_omniauth(slack_omniauth_hash_for_atmos) 6 | user = team.users.find_or_initialize_by(slack_user_id: "U0YJYFQ4E") 7 | expect(user).to be_valid 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= "test" 3 | require "dotenv" 4 | Dotenv.load("test") 5 | 6 | require File.expand_path("../../config/environment", __FILE__) 7 | # Prevent database truncation if the environment is production 8 | if Rails.env.production? 9 | abort("The Rails environment is running in production mode!") 10 | end 11 | 12 | require "spec_helper" 13 | require "rspec/rails" 14 | require "webmock/rspec" 15 | 16 | OmniAuth.config.test_mode = true 17 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 18 | 19 | ActiveRecord::Migration.maintain_test_schema! 20 | 21 | RSpec.configure do |config| 22 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 23 | 24 | config.use_transactional_fixtures = true 25 | 26 | config.infer_spec_type_from_file_location! 27 | 28 | config.include(SlackHelpers) 29 | config.include(WebmockHelpers) 30 | config.include(FixturesHelpers) 31 | config.include(OmniAuthHelpers) 32 | end 33 | -------------------------------------------------------------------------------- /spec/request/boomtown_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx /boomtown", type: :request do 4 | it "500s on /boomtown" do 5 | expect do 6 | get "/boomtown" 7 | end.to raise_error(CoalCar::BoomtownError) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/request/commands_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx POST /commands", type: :request do 4 | def default_params(options = {}) 5 | command_params_for("").merge( 6 | token: "secret-slack-token", 7 | user_id: "U123YG08X", 8 | user_name: "atmos" 9 | ).merge(options) 10 | end 11 | 12 | it "403s on a bad token POST to /commands" do 13 | post "/commands", params: default_params(token: "bad-slack-token") 14 | expect(status).to eql(403) 15 | end 16 | 17 | it "201s on POST to /commands" do 18 | post "/commands", params: default_params(text: "help") 19 | expect(status).to eql(201) 20 | response_body = JSON.parse(body) 21 | expect(response_body["text"]).to include("Sign in to GitHub") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/request/github_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx GET /auth/github/callback", type: :request do 4 | it "creates the GitHub login on the authenticated user when successful" do 5 | token = "xoxp-9101111159-5657146422-59735495733-3186a13efg" 6 | stub_json_request(:get, 7 | "https://slack.com/api/users.identity?token=#{token}", 8 | fixture_data("slack.com/identity")) 9 | 10 | SlackHQ::Team.create(team_id: "T092F92CG", 11 | bot_user_id: "U9MG9BRL6", 12 | bot_token: "xoxo-woop-woop") 13 | 14 | OmniAuth.config.mock_auth[:slack] = slack_omniauth_hash_for_toolskai 15 | OmniAuth.config.mock_auth[:github] = github_omniauth_hash_for_toolskai 16 | 17 | expect do 18 | get "/auth/slack" 19 | expect(status).to eql(302) 20 | uri = Addressable::URI.parse(headers["Location"]) 21 | expect(uri.host).to eql("www.example.com") 22 | expect(uri.path).to eql("/auth/slack/callback") 23 | follow_redirect! 24 | end.to change { SlackHQ::Team.count }.by(0) 25 | 26 | expect do 27 | get "/auth/github" 28 | expect(status).to eql(302) 29 | uri = Addressable::URI.parse(headers["Location"]) 30 | expect(uri.host).to eql("www.example.com") 31 | expect(uri.path).to eql("/auth/github/callback") 32 | follow_redirect! 33 | end.to change { SlackHQ::Team.count }.by(0) 34 | 35 | team = SlackHQ::Team.first 36 | user = team.users.first 37 | 38 | expect(user.slack_user_name).to eql("Toolskai") 39 | expect(user.github_login).to eql("toolskai") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/request/health_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx /health", type: :request do 4 | it "200s on /health" do 5 | get "/health" 6 | data = JSON.parse(body) 7 | expect(status).to eql(200) 8 | expect(data["name"]).to eql("speakerboxxx") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/request/installation_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx GET /install", type: :request do 4 | after { OmniAuth.config.test_mode = true } 5 | before { OmniAuth.config.test_mode = false } 6 | 7 | it "sends you to slack with appropriate scopes requested to install" do 8 | get "/install" 9 | expect(status).to eql(302) 10 | uri = Addressable::URI.parse(headers["Location"]) 11 | expect(uri.host).to eql("www.example.com") 12 | expect(uri.path).to eql("/auth/slack_install") 13 | 14 | follow_redirect! 15 | uri = Addressable::URI.parse(headers["Location"]) 16 | expect(uri.host).to eql("slack.com") 17 | expect(uri.path).to eql("/oauth/authorize") 18 | expect(uri.query_values["scope"]).to eql("identify,commands,bot") 19 | end 20 | 21 | it "sends you to slack with appropriate scopes requested to authenticate" do 22 | get "/auth/slack" 23 | expect(status).to eql(302) 24 | uri = Addressable::URI.parse(headers["Location"]) 25 | expect(uri.host).to eql("slack.com") 26 | expect(uri.path).to eql("/oauth/authorize") 27 | expect(uri.query_values["scope"]).to eql("identity.basic") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/request/pages_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx /install", type: :request do 4 | it "200s on /install" do 5 | get "/" 6 | expect(status).to eql(200) 7 | end 8 | end 9 | 10 | RSpec.describe "Speakerboxxx /support", type: :request do 11 | it "200s on /support" do 12 | get "/support" 13 | expect(status).to eql(200) 14 | expect(body).to include("mailto:atmos+speakerboxxx@atmos.org") 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/request/root_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx /", type: :request do 4 | it "200s on /" do 5 | get "/" 6 | expect(status).to eql(200) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/request/sidekiq_web_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx GET /sidekiq", type: :request do 4 | it "404s if the user is not an administrator" do 5 | token = "xoxp-9101111159-5657146422-59735495733-3186a13efg" 6 | stub_json_request(:get, 7 | "https://slack.com/api/users.identity?token=#{token}", 8 | fixture_data("slack.com/identity")) 9 | 10 | SlackHQ::Team.create(team_id: "T092F92CG", 11 | bot_user_id: "U9MG9BRL6", 12 | bot_token: "xoxo-woop-woop") 13 | 14 | OmniAuth.config.mock_auth[:slack] = slack_omniauth_hash_for_toolskai 15 | expect do 16 | get "/auth/slack" 17 | expect(status).to eql(302) 18 | uri = Addressable::URI.parse(headers["Location"]) 19 | expect(uri.host).to eql("www.example.com") 20 | expect(uri.path).to eql("/auth/slack/callback") 21 | follow_redirect! 22 | end.to change { SlackHQ::Team.count }.by(0) 23 | 24 | team = SlackHQ::Team.first 25 | user = team.users.first 26 | 27 | expect(user.slack_user_name).to eql("Toolskai") 28 | expect do 29 | get "/sidekiq" 30 | end.to raise_error(ActionController::RoutingError) 31 | end 32 | 33 | it "200s if the user is an administrator" do 34 | OmniAuth.config.mock_auth[:slack_install] = slack_omniauth_hash_for_atmos 35 | expect do 36 | get "/auth/slack_install" 37 | expect(status).to eql(302) 38 | uri = Addressable::URI.parse(headers["Location"]) 39 | expect(uri.host).to eql("www.example.com") 40 | expect(uri.path).to eql("/auth/slack_install/callback") 41 | follow_redirect! 42 | end.to change { SlackHQ::Team.count }.by(1) 43 | 44 | team = SlackHQ::Team.first 45 | user = team.users.first 46 | user.update(github_login: "atmos") 47 | 48 | expect(user.github_login).to eql("atmos") 49 | expect(user.slack_user_name).to eql("atmos") 50 | expect(team.bot_token).to eql("xoxo-hugs-n-kisses") 51 | get "/sidekiq" 52 | expect(status).to eql(200) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/request/slack_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Speakerboxxx GET /auth/slack/callback", type: :request do 4 | it "creates a user on the organization when authenticated" do 5 | token = "xoxp-9101111159-5657146422-59735495733-3186a13efg" 6 | stub_json_request(:get, 7 | "https://slack.com/api/users.identity?token=#{token}", 8 | fixture_data("slack.com/identity")) 9 | 10 | SlackHQ::Team.create(team_id: "T092F92CG", 11 | bot_user_id: "U9MG9BRL6", 12 | bot_token: "xoxo-woop-woop") 13 | 14 | OmniAuth.config.mock_auth[:slack] = slack_omniauth_hash_for_toolskai 15 | expect do 16 | get "/auth/slack" 17 | expect(status).to eql(302) 18 | uri = Addressable::URI.parse(headers["Location"]) 19 | expect(uri.host).to eql("www.example.com") 20 | expect(uri.path).to eql("/auth/slack/callback") 21 | follow_redirect! 22 | end.to change { SlackHQ::Team.count }.by(0) 23 | 24 | team = SlackHQ::Team.first 25 | user = team.users.first 26 | 27 | expect(user.slack_user_name).to eql("Toolskai") 28 | end 29 | 30 | it "creates a user and stores a token on the organization when installed" do 31 | OmniAuth.config.mock_auth[:slack_install] = slack_omniauth_hash_for_atmos 32 | expect do 33 | get "/auth/slack_install" 34 | expect(status).to eql(302) 35 | uri = Addressable::URI.parse(headers["Location"]) 36 | expect(uri.host).to eql("www.example.com") 37 | expect(uri.path).to eql("/auth/slack_install/callback") 38 | follow_redirect! 39 | end.to change { SlackHQ::Team.count }.by(1) 40 | 41 | team = SlackHQ::Team.first 42 | user = team.users.first 43 | 44 | expect(user.slack_user_name).to eql("atmos") 45 | expect(team.bot_token).to eql("xoxo-hugs-n-kisses") 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "webmock/rspec" 2 | require "sidekiq/testing" 3 | 4 | ENV["HOSTNAME"] = "speakerboxxx-test.atmos.org" 5 | ENV["SLACK_SLASH_COMMAND_TOKEN"] = "secret-slack-token" 6 | 7 | RSpec.configure do |config| 8 | config.include(WebMock::API) 9 | 10 | config.expect_with :rspec do |expectations| 11 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 12 | end 13 | 14 | config.mock_with :rspec do |mocks| 15 | mocks.verify_partial_doubles = true 16 | end 17 | 18 | config.before type: :webmock do 19 | WebMock.disable_net_connect! 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/fixtures_helpers.rb: -------------------------------------------------------------------------------- 1 | module FixturesHelpers 2 | def fixture_data(name) 3 | path = Rails.root.join("spec", "fixtures", "#{name}.json") 4 | File.read(path) 5 | end 6 | 7 | def github_fixture_data(name) 8 | fixture_data("api.github.com/#{name}") 9 | end 10 | 11 | def decoded_fixture_data(name) 12 | JSON.parse(fixture_data(name)) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/omni_auth_helpers.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Metrics/LineLength 2 | module OmniAuthHelpers 3 | def github_omniauth_hash_for_atmos 4 | user_info = { 5 | name: "Corey Donohoe", 6 | nickname: "atmos", 7 | avatar_url: "https://avatars.githubusercontent.com/u/38?v=3" 8 | } 9 | 10 | credentials = { 11 | token: SecureRandom.hex(24) 12 | } 13 | 14 | OmniAuth::AuthHash.new(provider: "github", 15 | uid: "38", 16 | info: user_info, 17 | credentials: credentials) 18 | end 19 | 20 | def github_omniauth_hash_for_toolskai 21 | user_info = { 22 | name: "Tools Helper", 23 | nickname: "toolskai", 24 | avatar_url: "https://avatars.githubusercontent.com/u/9364088?v=3" 25 | } 26 | 27 | credentials = { 28 | token: SecureRandom.hex(24) 29 | } 30 | 31 | OmniAuth::AuthHash.new(provider: "github", 32 | uid: "38", 33 | info: user_info, 34 | credentials: credentials) 35 | end 36 | 37 | # rubocop:disable Metrics/MethodLength 38 | def slack_omniauth_hash_for_atmos 39 | info = { 40 | description: nil, 41 | email: "atmos@atmos.org", 42 | first_name: "Corey", 43 | last_name: "Donohoe", 44 | image: "https://secure.gravatar.com/avatar/a86224d72ce21cd9f5bee6784d4b06c7.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0010-192.png", 45 | image_48: "https://secure.gravatar.com/avatar/a86224d72ce21cd9f5bee6784d4b06c7.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0010-48.png", 46 | is_admin: true, 47 | is_owner: true, 48 | name: "Corey Donohoe", 49 | nickname: "atmos", 50 | team: "Zero Fucks LTD", 51 | team_id: "T123YG08V", 52 | time_zone: "America/Los_Angeles", 53 | user: "atmos", 54 | user_id: "U123YG08X" 55 | } 56 | credentials = { 57 | token: SecureRandom.hex(24) 58 | } 59 | extra = { 60 | raw_info: { 61 | ok: true 62 | }, 63 | bot_info: { 64 | bot_access_token: "xoxo-hugs-n-kisses", 65 | bot_user_id: "U421FY7" 66 | } 67 | } 68 | 69 | OmniAuth::AuthHash.new(provider: "slack", 70 | uid: "U024YG08X", 71 | info: info, 72 | extra: extra, 73 | credentials: credentials) 74 | end 75 | 76 | def slack_omniauth_hash_for_toolskai 77 | info = { 78 | provider: "slack", 79 | uid: nil, 80 | info: { 81 | }, 82 | credentials: { 83 | token: "xoxp-9101111159-5657146422-59735495733-3186a13efg", 84 | expires: false 85 | }, 86 | extra: { 87 | raw_info: { 88 | ok: false, 89 | error: "missing_scope", 90 | needed: "identify", 91 | provided: "identity.basic" 92 | }, 93 | web_hook_info: { 94 | }, 95 | bot_info: { 96 | }, 97 | user_info: { 98 | ok: false, 99 | error: "missing_scope", 100 | needed: "users:read", 101 | provided: "identity.basic" 102 | }, 103 | team_info: { 104 | ok: false, 105 | error: "missing_scope", 106 | needed: "team:read", 107 | provided: "identity.basic" 108 | } 109 | } 110 | } 111 | OmniAuth::AuthHash.new(info) 112 | end 113 | # rubocop:enable Metrics/MethodLength 114 | 115 | def create_atmos 116 | slack = slack_omniauth_hash_for_atmos 117 | team = SlackHQ::Team.from_omniauth(slack) 118 | 119 | user = team.users.find_or_initialize_by( 120 | slack_user_id: slack.info.user_id, 121 | slack_user_name: slack.info.nickname 122 | ) 123 | user.save 124 | user 125 | end 126 | end 127 | # rubocop:enable Metrics/LineLength 128 | -------------------------------------------------------------------------------- /spec/support/slack_helpers.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Metrics/LineLength 2 | module SlackHelpers 3 | def command_params_for(text) 4 | { 5 | channel_id: "C99NNAY74", 6 | channel_name: "#spam", 7 | command: "/gh-notify", 8 | response_url: "https://hooks.slack.com/commands/T123YG08V/2459573/mfZPdDq", 9 | team_id: "T123YG08V", 10 | team_domain: "atmos-org", 11 | text: text 12 | } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/webmock_helpers.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Metrics/LineLength 2 | module WebmockHelpers 3 | def default_json_headers 4 | { "Content-Type" => "application/json" } 5 | end 6 | 7 | def stub_json_request(method, url, response_body, status = 200) 8 | stub_request(method, url) 9 | .to_return(status: status, body: response_body, headers: default_json_headers) 10 | end 11 | end 12 | # rubocop:enable Metrics/LineLength 13 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/tmp/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /vendor/cache/actioncable-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/actioncable-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionmailer-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/actionmailer-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionpack-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/actionpack-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionview-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/actionview-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/activejob-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/activejob-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/activemodel-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/activemodel-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/activerecord-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/activerecord-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/activesupport-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/activesupport-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/addressable-2.4.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/addressable-2.4.0.gem -------------------------------------------------------------------------------- /vendor/cache/aggregate-0.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/aggregate-0.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/arel-7.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/arel-7.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/ast-2.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/ast-2.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/atomic-1.1.99.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/atomic-1.1.99.gem -------------------------------------------------------------------------------- /vendor/cache/builder-3.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/builder-3.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/byebug-9.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/byebug-9.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/chunky_png-1.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/chunky_png-1.3.7.gem -------------------------------------------------------------------------------- /vendor/cache/coal_car-0.2.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/coal_car-0.2.6.gem -------------------------------------------------------------------------------- /vendor/cache/coffee-rails-4.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/coffee-rails-4.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/coffee-script-2.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/coffee-script-2.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/coffee-script-source-1.10.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/coffee-script-source-1.10.0.gem -------------------------------------------------------------------------------- /vendor/cache/compass-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/compass-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/compass-core-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/compass-core-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/compass-import-once-1.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/compass-import-once-1.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/compass-rails-3.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/compass-rails-3.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/concurrent-ruby-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/concurrent-ruby-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/concurrent-ruby-ext-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/concurrent-ruby-ext-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/connection_pool-2.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/connection_pool-2.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/crack-0.4.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/crack-0.4.3.gem -------------------------------------------------------------------------------- /vendor/cache/debugger-ruby_core_source-1.3.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/debugger-ruby_core_source-1.3.8.gem -------------------------------------------------------------------------------- /vendor/cache/diff-lcs-1.2.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/diff-lcs-1.2.5.gem -------------------------------------------------------------------------------- /vendor/cache/dotenv-2.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/dotenv-2.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/dotenv-rails-2.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/dotenv-rails-2.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/erubis-2.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/erubis-2.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/execjs-2.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/execjs-2.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/faraday-0.9.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/faraday-0.9.2.gem -------------------------------------------------------------------------------- /vendor/cache/faraday_middleware-0.10.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/faraday_middleware-0.10.0.gem -------------------------------------------------------------------------------- /vendor/cache/ffi-1.9.14.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/ffi-1.9.14.gem -------------------------------------------------------------------------------- /vendor/cache/finagle-thrift-1.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/finagle-thrift-1.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/gli-2.14.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/gli-2.14.0.gem -------------------------------------------------------------------------------- /vendor/cache/globalid-0.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/globalid-0.3.7.gem -------------------------------------------------------------------------------- /vendor/cache/hashdiff-0.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/hashdiff-0.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/hashie-3.4.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/hashie-3.4.6.gem -------------------------------------------------------------------------------- /vendor/cache/hetchy-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/hetchy-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/i18n-0.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/i18n-0.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/jquery-rails-4.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/jquery-rails-4.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/json-2.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/json-2.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/jwt-1.5.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/jwt-1.5.6.gem -------------------------------------------------------------------------------- /vendor/cache/librato-metrics-1.6.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/librato-metrics-1.6.1.gem -------------------------------------------------------------------------------- /vendor/cache/librato-rack-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/librato-rack-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/librato-rails-1.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/librato-rails-1.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/listen-3.0.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/listen-3.0.8.gem -------------------------------------------------------------------------------- /vendor/cache/lograge-0.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/lograge-0.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/loofah-2.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/loofah-2.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/mail-2.6.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/mail-2.6.4.gem -------------------------------------------------------------------------------- /vendor/cache/method_source-0.8.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/method_source-0.8.2.gem -------------------------------------------------------------------------------- /vendor/cache/mime-types-3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/mime-types-3.1.gem -------------------------------------------------------------------------------- /vendor/cache/mime-types-data-3.2016.0521.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/mime-types-data-3.2016.0521.gem -------------------------------------------------------------------------------- /vendor/cache/mini_portile2-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/mini_portile2-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/minitest-5.9.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/minitest-5.9.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_json-1.12.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/multi_json-1.12.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_xml-0.5.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/multi_xml-0.5.5.gem -------------------------------------------------------------------------------- /vendor/cache/multipart-post-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/multipart-post-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/nio4r-1.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/nio4r-1.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/nokogiri-1.6.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/nokogiri-1.6.8.1.gem -------------------------------------------------------------------------------- /vendor/cache/oauth2-1.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/oauth2-1.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/octokit-4.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/octokit-4.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/omniauth-1.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/omniauth-1.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/omniauth-github-1.1.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/omniauth-github-1.1.2.gem -------------------------------------------------------------------------------- /vendor/cache/omniauth-oauth2-1.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/omniauth-oauth2-1.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/omniauth-slack-2.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/omniauth-slack-2.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/parser-2.3.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/parser-2.3.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/peek-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/peek-faraday-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-faraday-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/peek-performance_bar-1.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-performance_bar-1.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/peek-pg-1.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-pg-1.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/peek-rblineprof-0.1.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-rblineprof-0.1.2.gem -------------------------------------------------------------------------------- /vendor/cache/peek-redis-1.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/peek-redis-1.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/pg-0.19.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/pg-0.19.0.gem -------------------------------------------------------------------------------- /vendor/cache/powerpack-0.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/powerpack-0.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/puma-3.6.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/puma-3.6.0.gem -------------------------------------------------------------------------------- /vendor/cache/rack-2.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rack-2.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/rack-protection-1.5.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rack-protection-1.5.3.gem -------------------------------------------------------------------------------- /vendor/cache/rack-test-0.6.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rack-test-0.6.3.gem -------------------------------------------------------------------------------- /vendor/cache/rails-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rails-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/rails-dom-testing-2.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rails-dom-testing-2.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/rails-html-sanitizer-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rails-html-sanitizer-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/railties-5.0.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/railties-5.0.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/rainbow-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rainbow-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/rake-11.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rake-11.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/rb-fsevent-0.9.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rb-fsevent-0.9.7.gem -------------------------------------------------------------------------------- /vendor/cache/rb-inotify-0.9.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rb-inotify-0.9.7.gem -------------------------------------------------------------------------------- /vendor/cache/rblineprof-0.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rblineprof-0.3.7.gem -------------------------------------------------------------------------------- /vendor/cache/rbnacl-4.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rbnacl-4.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/rbnacl-libsodium-1.0.11.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rbnacl-libsodium-1.0.11.gem -------------------------------------------------------------------------------- /vendor/cache/redis-3.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/redis-3.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-core-3.5.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rspec-core-3.5.4.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-expectations-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rspec-expectations-3.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-mocks-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rspec-mocks-3.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-rails-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rspec-rails-3.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-support-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rspec-support-3.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-0.44.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/rubocop-0.44.1.gem -------------------------------------------------------------------------------- /vendor/cache/ruby-progressbar-1.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/ruby-progressbar-1.8.1.gem -------------------------------------------------------------------------------- /vendor/cache/safe_yaml-1.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/safe_yaml-1.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/sass-3.4.22.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sass-3.4.22.gem -------------------------------------------------------------------------------- /vendor/cache/sass-rails-5.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sass-rails-5.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/sawyer-0.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sawyer-0.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/sentry-raven-2.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sentry-raven-2.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/sidekiq-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sidekiq-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/slack-ruby-client-0.7.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/slack-ruby-client-0.7.7.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-3.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sprockets-3.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-rails-3.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sprockets-rails-3.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/sucker_punch-2.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/sucker_punch-2.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/thor-0.19.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/thor-0.19.1.gem -------------------------------------------------------------------------------- /vendor/cache/thread_safe-0.3.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/thread_safe-0.3.5.gem -------------------------------------------------------------------------------- /vendor/cache/thrift-0.9.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/thrift-0.9.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/tilt-2.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/tilt-2.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/tzinfo-1.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/tzinfo-1.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/uglifier-3.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/uglifier-3.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/unicode-display_width-1.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/unicode-display_width-1.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/webmock-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/webmock-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/websocket-driver-0.6.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/websocket-driver-0.6.4.gem -------------------------------------------------------------------------------- /vendor/cache/websocket-extensions-0.1.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/websocket-extensions-0.1.2.gem -------------------------------------------------------------------------------- /vendor/cache/zipkin-tracer-0.18.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/speakerboxxx/8ea27a85871d70874c5864d2633178f190ab6169/vendor/cache/zipkin-tracer-0.18.6.gem --------------------------------------------------------------------------------