├── .ruby-version ├── .gitignore ├── .rspec ├── bin ├── main └── rspec ├── Gemfile ├── app ├── dl.rb └── bc.rb ├── Gemfile.lock ├── spec ├── bc_spec.rb └── spec_helper.rb └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.swp 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format <%= ENV['CI'] ? 'progress' : 'Fuubar' %> 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /bin/main: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require './app/bc' 4 | require './app/dl' 5 | 6 | DL.new.add_peoples( BC.new.client_emails ) 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'http' 4 | gem 'delighted' 5 | 6 | group :development, :test do 7 | gem 'dotenv' 8 | gem 'rspec' 9 | gem 'fuubar' 10 | end 11 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /app/dl.rb: -------------------------------------------------------------------------------- 1 | require 'delighted' 2 | 3 | if ENV['APP_ENV'] == 'development' 4 | require 'dotenv' 5 | Dotenv.load 6 | end 7 | 8 | Delighted.api_key = ENV['DELIGHTED_API_KEY'] 9 | 10 | class DL 11 | attr_reader :delay 12 | 13 | def initialize 14 | @delay = ENV['DELIGHTED_DELAY'] 15 | end 16 | 17 | def add_peoples(client_emails) 18 | client_emails.each do |client_email| 19 | Delighted::Person.create( 20 | email: client_email, 21 | delay: delay 22 | ) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | delighted (1.3.0) 6 | multi_json 7 | diff-lcs (1.2.5) 8 | dotenv (2.0.0) 9 | fuubar (2.0.0) 10 | rspec (~> 3.0) 11 | ruby-progressbar (~> 1.4) 12 | http (0.8.4) 13 | addressable (~> 2.3) 14 | http-form_data (~> 1.0.1) 15 | http_parser.rb (~> 0.6.0) 16 | http-form_data (1.0.1) 17 | http_parser.rb (0.6.0) 18 | multi_json (1.11.0) 19 | rspec (3.2.0) 20 | rspec-core (~> 3.2.0) 21 | rspec-expectations (~> 3.2.0) 22 | rspec-mocks (~> 3.2.0) 23 | rspec-core (3.2.1) 24 | rspec-support (~> 3.2.0) 25 | rspec-expectations (3.2.0) 26 | diff-lcs (>= 1.2.0, < 2.0) 27 | rspec-support (~> 3.2.0) 28 | rspec-mocks (3.2.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.2.0) 31 | rspec-support (3.2.2) 32 | ruby-progressbar (1.7.1) 33 | 34 | PLATFORMS 35 | ruby 36 | 37 | DEPENDENCIES 38 | delighted 39 | dotenv 40 | fuubar 41 | http 42 | rspec 43 | -------------------------------------------------------------------------------- /spec/bc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BC do 4 | describe '#client_emails' do 5 | let(:bc_instance) { described_class.new } 6 | 7 | before(:each) do 8 | allow(bc_instance).to receive(:client_emails_dirty) do 9 | [ 10 | ['one@number.com', nil, 'two@number.com'], 11 | [nil, nil], 12 | 'three@number.com', 13 | nil, 14 | ['one@number.com', 'two@number.com'] 15 | ] 16 | end 17 | end 18 | 19 | it 'returns array' do 20 | expect(bc_instance.client_emails.class).to eq(Array) 21 | end 22 | 23 | it 'returns compacted array' do 24 | expect(bc_instance.client_emails.include?(nil)).to eq(false) 25 | end 26 | 27 | it 'returns array with uniq elements' do 28 | expect(bc_instance.client_emails) 29 | .to eq(['one@number.com', 'two@number.com', 'three@number.com']) 30 | end 31 | 32 | it 'returns array of emails' do 33 | EMAIL_REGEX = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i 34 | 35 | bc_instance.client_emails.each do |elem| 36 | expect(elem).to match(EMAIL_REGEX) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/bc.rb: -------------------------------------------------------------------------------- 1 | require 'http' 2 | 3 | if ENV['APP_ENV'] == 'development' 4 | require 'dotenv' 5 | Dotenv.load 6 | end 7 | 8 | class BC 9 | attr_reader :user, :password, :app, :account_id 10 | 11 | def initialize 12 | @user = ENV['BC_USER'] 13 | @password = ENV['BC_PASSWORD'] 14 | @app = ENV['BC_APP'] 15 | @account_id = ENV['BC_ACCOUNT_ID'] 16 | end 17 | 18 | def client_emails 19 | client_emails_dirty.flatten.compact.uniq 20 | end 21 | 22 | private 23 | 24 | def active_projects_ids 25 | JSON.parse(projects_response).map { |elem| elem['id'] } 26 | end 27 | 28 | def projects_response 29 | http_get("https://basecamp.com/#{account_id}/api/v1/projects") 30 | end 31 | 32 | def accesses_response(project_id) 33 | http_get("https://basecamp.com/#{account_id}/api/v1/projects/#{project_id}/accesses") 34 | end 35 | 36 | def client_emails_dirty 37 | [].tap do |emails| 38 | active_projects_ids.each do |project_id| 39 | emails << JSON.parse(accesses_response(project_id)).map { |elem| elem['email_address'] if elem['is_client'] == true } 40 | end 41 | end 42 | end 43 | 44 | def http_get(url) 45 | HTTP 46 | .basic_auth(user: user, pass: password) 47 | .headers(accept: 'application/json', 'User-Agent' => app) 48 | .get(url) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Basecamp -> Delighted 2 | 3 | This is a small app for getting clients from Basecamp's active projects and putting them into Delighted survey service. 4 | 5 | ## How it works 6 | If you familiar with Ruby: 7 | 8 | 1. Clone repo to the local machine: `$ git clone git@github.com:fs/basecamp-delighted.git`. 9 | 2. Change directory: `$ cd basecamp-delighted`. 10 | 3. Resolve dependencies: `$ bundle install`. 11 | 4. Create `.env` file and populate it with key-value pairs ([example .env file](#user-content-env-example)) 12 | 5. Run app: `$ APP_ENV=development bin/main`. 13 | 6. Check Delighted service. 14 | 15 | Otherwise you can use [Heroku](http://www.heroku.com) as platform for running this app: 16 | 17 | 1. Clone repo to the local machine: `$ git clone git@github.com:fs/basecamp-delighted.git`. 18 | 2. Change directory: `$ cd basecamp-delighted`. 19 | 3. [Create account](https://id.heroku.com/signup/login) on Heroku. 20 | 4. Download and install the [Heroku Toolbet](https://toolbelt.heroku.com/). 21 | 5. If you haven't already, log in to your Heroku account and follow the prompts to create a new SSH public key: `$ heroku login`. 22 | 6. Create new app: `$ heroku apps:create app_name`. You can omit `app_name` and Heroku creates app with default name. 23 | 7. Deploy your app: `$ git push heroku master`. 24 | 8. Populate config variables on `settings` tab as it's done in the [example .env file](#user-content-env-example). 25 | 9. Run app: `$ heroku run bin/main --app app_name`. 26 | 10. Check Delighted service. 27 | 28 | ## .ENV example 29 | 30 | ```ruby 31 | # .env 32 | 33 | # Basecamp credentials 34 | BC_USER=user@example.com 35 | BC_PASSWORD=user_pass 36 | BC_APP="your_app_name (user@example.com)" 37 | BC_ACCOUNT_ID=9999999 38 | 39 | # Delighted credentials 40 | DELIGHTED_API_KEY=5SLQLx5R47peQq2F87GbP6BskEuCMDWD 41 | DELIGHTED_DELAY=3600 42 | ``` 43 | 44 | * `BC_USER/BC_PASSWORD` - credentials of Basecamp's user that have access to projects. 45 | * `BC_APP` - point here your email. Reason described [here](https://github.com/basecamp/bcx-api/#identify-your-app). 46 | * `BC_ACCOUNT_ID` - Basecamp account's id. When you logged in Basecamp, take a look at URL, it looks like: https://basecamp.com/1787128, digits after slash is account id. 47 | * `DELIGHTED_API_KEY` - API key can be found on account [API page](https://delighted.com/account/api) 48 | * `DELIGHTED_DELAY` - delay **in seconds** before surveys will be sent to just added peoples. 49 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require './app/bc' 2 | require './app/dl' 3 | 4 | RSpec.configure do |config| 5 | # rspec-expectations config goes here. You can use an alternate 6 | # assertion/expectation library such as wrong or the stdlib/minitest 7 | # assertions if you prefer. 8 | config.expect_with :rspec do |expectations| 9 | # This option will default to `true` in RSpec 4. It makes the `description` 10 | # and `failure_message` of custom matchers include text for helper methods 11 | # defined using `chain`, e.g.: 12 | # be_bigger_than(2).and_smaller_than(4).description 13 | # # => "be bigger than 2 and smaller than 4" 14 | # ...rather than: 15 | # # => "be bigger than 2" 16 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 17 | end 18 | 19 | # rspec-mocks config goes here. You can use an alternate test double 20 | # library (such as bogus or mocha) by changing the `mock_with` option here. 21 | config.mock_with :rspec do |mocks| 22 | # Prevents you from mocking or stubbing a method that does not exist on 23 | # a real object. This is generally recommended, and will default to 24 | # `true` in RSpec 4. 25 | mocks.verify_partial_doubles = true 26 | end 27 | 28 | # The settings below are suggested to provide a good initial experience 29 | # with RSpec, but feel free to customize to your heart's content. 30 | =begin 31 | # These two settings work together to allow you to limit a spec run 32 | # to individual examples or groups you care about by tagging them with 33 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 34 | # get run. 35 | config.filter_run :focus 36 | config.run_all_when_everything_filtered = true 37 | 38 | # Limits the available syntax to the non-monkey patched syntax that is 39 | # recommended. For more details, see: 40 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 41 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 42 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 43 | config.disable_monkey_patching! 44 | 45 | # This setting enables warnings. It's recommended, but in some cases may 46 | # be too noisy due to issues in dependencies. 47 | config.warnings = true 48 | 49 | # Many RSpec users commonly either run the entire suite or an individual 50 | # file, and it's useful to allow more verbose output when running an 51 | # individual spec file. 52 | if config.files_to_run.one? 53 | # Use the documentation formatter for detailed output, 54 | # unless a formatter has already been configured 55 | # (e.g. via a command-line flag). 56 | config.default_formatter = 'doc' 57 | end 58 | 59 | # Print the 10 slowest examples and example groups at the 60 | # end of the spec run, to help surface which specs are running 61 | # particularly slow. 62 | config.profile_examples = 10 63 | 64 | # Run specs in random order to surface order dependencies. If you find an 65 | # order dependency and want to debug it, you can fix the order by providing 66 | # the seed, which is printed after each run. 67 | # --seed 1234 68 | config.order = :random 69 | 70 | # Seed global randomization in this process using the `--seed` CLI option. 71 | # Setting this allows you to use `--seed` to deterministically reproduce 72 | # test failures related to randomization by passing the same `--seed` value 73 | # as the one that triggered the failure. 74 | Kernel.srand config.seed 75 | =end 76 | end 77 | --------------------------------------------------------------------------------