├── .gitignore ├── .rspec ├── COPYING ├── Capfile ├── Gemfile ├── Gemfile.lock ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── config.ru ├── config ├── deploy.rb └── deploy │ ├── production.rb │ └── staging.rb ├── data ├── regions.yml ├── sms_prices.yml └── words.csv ├── lib ├── crowdring.rb ├── crowdring │ ├── aggregate_campaign.rb │ ├── asks │ │ ├── ask.rb │ │ ├── join_ask.rb │ │ ├── offline_ask.rb │ │ ├── send_sms_ask.rb │ │ ├── text_ask.rb │ │ └── voicemail_ask.rb │ ├── assigned_phone_number.rb │ ├── batch_send_sms.rb │ ├── campaign.rb │ ├── campaign_export_csv.haml │ ├── campaign_stats.rb │ ├── constraint.rb │ ├── crowdring.rb │ ├── csv_fields.rb │ ├── filtered_message.rb │ ├── high_charts_builder.rb │ ├── ivr.rb │ ├── message.rb │ ├── number_pool.rb │ ├── outgoing_sms.rb │ ├── patches.rb │ ├── phone_number_fields.rb │ ├── price_estimate.rb │ ├── regions.rb │ ├── ring.rb │ ├── ring_observer.rb │ ├── ringer.rb │ ├── short_code.rb │ ├── sms_prices.rb │ ├── tag.rb │ ├── tag_filter.rb │ ├── telephony_services │ │ ├── caching_service.rb │ │ ├── composite_service.rb │ │ ├── kookoo_service.rb │ │ ├── logging_service.rb │ │ ├── netcore_service.rb │ │ ├── nexmo_service.rb │ │ ├── plivo_service.rb │ │ ├── routo_service.rb │ │ ├── telephony_service.rb │ │ ├── tropo_service.rb │ │ ├── twilio_service.rb │ │ └── voxeo_service.rb │ ├── text.rb │ ├── time_service.rb │ └── voicemail.rb ├── public │ ├── crowdringlogo.png │ ├── favicon.ico │ ├── javascript │ │ ├── 1.12-pusher.min.js │ │ ├── application.coffee │ │ ├── ask_new.coffee │ │ ├── campaign_new.coffee │ │ ├── catcomplete.js │ │ ├── charCount.js │ │ ├── chosen.jquery.min.js │ │ ├── coffee-script-1.3.1.min.js │ │ ├── embedded.js │ │ ├── highcharts.js │ │ ├── ivr.coffee │ │ ├── jquery-1.8.1.min.js │ │ ├── jquery-1.9.1.min.js │ │ ├── jquery-ui-1.9.0.min.js │ │ ├── jquery.cookie.js │ │ └── less-1.3.0.min.js │ └── stylesheets │ │ ├── application.less │ │ ├── chosen-sprite.png │ │ ├── chosen.css │ │ ├── embedded.css │ │ ├── ivr.less │ │ └── max480.less ├── utils │ ├── password_generator.rb │ └── smtp-tls.rb └── views │ ├── aggregate_campaign_edit.haml │ ├── aggregate_campaign_new.haml │ ├── aggregate_campaign_preview.haml │ ├── ask.haml │ ├── asks │ ├── join_ask.haml │ ├── offline_ask.haml │ ├── send_sms_ask.haml │ ├── text_ask.haml │ └── voicemail_ask.haml │ ├── assign_unsubscribe_number.haml │ ├── assign_voice_number.haml │ ├── auth │ ├── change_password.haml │ ├── edit.haml │ ├── index.haml │ ├── login.haml │ ├── newuser.haml │ ├── reset_password.haml │ ├── show.haml │ └── signup.haml │ ├── campaign.haml │ ├── campaign_add_new_ask.haml │ ├── campaign_assign_voice_number.haml │ ├── campaign_edit_ask.haml │ ├── campaign_edit_goal.haml │ ├── campaign_export_csv.haml │ ├── campaign_ivr_detail.haml │ ├── campaign_ivr_key_option_template.haml │ ├── campaign_new.haml │ ├── campaign_new_missed_call.haml │ ├── campaign_new_sms_back.haml │ ├── campaign_preview.haml │ ├── campaign_progress.haml │ ├── campaign_progress_embedded.haml │ ├── campaigns.haml │ ├── export_csv.haml │ ├── filtered_message_template.haml │ ├── index.haml │ ├── kookoo.haml │ ├── layout.haml │ ├── message.haml │ ├── not_found.haml │ ├── price_estimate.haml │ ├── select_region.haml │ ├── unsubscribe_number.haml │ ├── user_header.haml │ └── voice_numbers.haml └── spec ├── aggregate_campaign_spec.rb ├── ask_spec.rb ├── assigned_phone_number_spec.rb ├── campaign_spec.rb ├── crowdring_spec.rb ├── factories.rb ├── filtered_message_spec.rb ├── ivr_spec.rb ├── message_spec.rb ├── number_pool_spec.rb ├── ringer_spec.rb ├── server_spec.rb ├── sms_prices_spec.rb ├── spec_helper.rb ├── tag_filter_spec.rb ├── tag_spec.rb └── telephony_services ├── composite_service_spec.rb ├── kookoo_service_spec.rb ├── netcore_service_spec.rb ├── nexmo_spec.rb ├── plivo_service_spec.rb ├── routo_service_spec.rb ├── tropo_service_spec.rb ├── twilio_service_spec.rb └── voxeo_service_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | logfile 3 | .DS_Store 4 | dump.rdb 5 | UTF-8 6 | log_file.log 7 | lib/public/javascript/jquery-cookie 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order rand:3455 -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and Setup Up Stages 2 | require 'capistrano/setup' 3 | 4 | # Includes default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Includes tasks from other gems included in your Gemfile 8 | # 9 | # For documentation on these, see for example: 10 | # 11 | # https://github.com/capistrano/rvm 12 | # https://github.com/capistrano/rbenv 13 | # https://github.com/capistrano/chruby 14 | # https://github.com/capistrano/bundler 15 | # https://github.com/capistrano/rails 16 | # 17 | # require 'capistrano/rvm' 18 | # require 'capistrano/rbenv' 19 | # require 'capistrano/chruby' 20 | # require 'capistrano/bundler' 21 | # require 'capistrano/rails/assets' 22 | # require 'capistrano/rails/migrations' 23 | 24 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 25 | Dir.glob('lib/capistrano/tasks/*.cap').each { |r| import r } 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby '1.9.3' 4 | 5 | gem 'sinatra' 6 | gem 'sinatra-contrib' 7 | gem 'sinatra-authentication' 8 | gem 'rack-ssl' 9 | gem 'foreman' 10 | gem 'datamapper' 11 | gem 'dm-postgres-adapter',:require => true 12 | gem 'dm-observer' 13 | 14 | gem 'pusher' 15 | gem 'thin' 16 | gem 'rack-flash3' 17 | gem 'facets' 18 | 19 | gem 'phonie', git: 'git://github.com/wmoxam/phonie.git' 20 | 21 | gem 'tropo-webapi-ruby' 22 | gem 'tropo-provisioning' 23 | gem 'nexmo' 24 | gem 'plivo' 25 | gem 'twilio-ruby' 26 | 27 | group :test do 28 | gem 'dm-rspec' 29 | gem 'factory_girl' 30 | gem 'rspec' 31 | gem 'fakeweb' 32 | gem 'pusher-fake' 33 | gem 'capybara' 34 | gem 'shoulda-matchers' 35 | end 36 | 37 | gem 'resque' 38 | gem 'rake' 39 | gem 'haml' 40 | gem 'lazy_high_charts', git: 'git://github.com/michelson/lazy_high_charts.git' 41 | gem 'statsd-ruby', require: 'statsd' 42 | gem 'racksh' 43 | gem 'sinatra-jsonp' 44 | gem "actionpack", "~> 3.2.11" 45 | gem "googlecharts", git: 'git://github.com/mattetti/googlecharts.git' 46 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/mattetti/googlecharts.git 3 | revision: b0f9d44cf8c8dcfc521661e02036b73306041e75 4 | specs: 5 | googlecharts (1.6.8) 6 | 7 | GIT 8 | remote: git://github.com/michelson/lazy_high_charts.git 9 | revision: c2b17d09f5abb1f5aa29206eaf42ebe59c9503da 10 | specs: 11 | lazy_high_charts (1.3.3) 12 | bundler (~> 1.0) 13 | hash-deep-merge 14 | 15 | GIT 16 | remote: git://github.com/wmoxam/phonie.git 17 | revision: f56672121cfd237d03691120eacf5725a722deba 18 | specs: 19 | phonie (1.0.3) 20 | 21 | GEM 22 | remote: https://rubygems.org/ 23 | specs: 24 | actionpack (3.2.12) 25 | activemodel (= 3.2.12) 26 | activesupport (= 3.2.12) 27 | builder (~> 3.0.0) 28 | erubis (~> 2.7.0) 29 | journey (~> 1.0.4) 30 | rack (~> 1.4.5) 31 | rack-cache (~> 1.2) 32 | rack-test (~> 0.6.1) 33 | sprockets (~> 2.2.1) 34 | activemodel (3.2.12) 35 | activesupport (= 3.2.12) 36 | builder (~> 3.0.0) 37 | activesupport (3.2.12) 38 | i18n (~> 0.6) 39 | multi_json (~> 1.0) 40 | addressable (2.2.8) 41 | backports (3.1.1) 42 | bcrypt-ruby (3.0.1) 43 | bourne (1.1.2) 44 | mocha (= 0.10.5) 45 | builder (3.0.4) 46 | capybara (2.0.2) 47 | mime-types (>= 1.16) 48 | nokogiri (>= 1.3.3) 49 | rack (>= 1.0.0) 50 | rack-test (>= 0.5.4) 51 | selenium-webdriver (~> 2.0) 52 | xpath (~> 1.0.0) 53 | childprocess (0.3.9) 54 | ffi (~> 1.0, >= 1.0.11) 55 | daemons (1.1.9) 56 | data_objects (0.10.12) 57 | addressable (~> 2.1) 58 | datamapper (1.2.0) 59 | dm-aggregates (~> 1.2.0) 60 | dm-constraints (~> 1.2.0) 61 | dm-core (~> 1.2.0) 62 | dm-migrations (~> 1.2.0) 63 | dm-serializer (~> 1.2.0) 64 | dm-timestamps (~> 1.2.0) 65 | dm-transactions (~> 1.2.0) 66 | dm-types (~> 1.2.0) 67 | dm-validations (~> 1.2.0) 68 | diff-lcs (1.2.1) 69 | dm-aggregates (1.2.0) 70 | dm-core (~> 1.2.0) 71 | dm-constraints (1.2.0) 72 | dm-core (~> 1.2.0) 73 | dm-core (1.2.0) 74 | addressable (~> 2.2.6) 75 | dm-do-adapter (1.2.0) 76 | data_objects (~> 0.10.6) 77 | dm-core (~> 1.2.0) 78 | dm-migrations (1.2.0) 79 | dm-core (~> 1.2.0) 80 | dm-observer (1.2.0) 81 | dm-core (~> 1.2.0) 82 | dm-postgres-adapter (1.2.0) 83 | dm-do-adapter (~> 1.2.0) 84 | do_postgres (~> 0.10.6) 85 | dm-rspec (0.2.4) 86 | dm-core 87 | dm-validations 88 | dm-serializer (1.2.2) 89 | dm-core (~> 1.2.0) 90 | fastercsv (~> 1.5) 91 | json (~> 1.6) 92 | json_pure (~> 1.6) 93 | multi_json (~> 1.0) 94 | dm-timestamps (1.2.0) 95 | dm-core (~> 1.2.0) 96 | dm-transactions (1.2.0) 97 | dm-core (~> 1.2.0) 98 | dm-types (1.2.2) 99 | bcrypt-ruby (~> 3.0) 100 | dm-core (~> 1.2.0) 101 | fastercsv (~> 1.5) 102 | json (~> 1.6) 103 | multi_json (~> 1.0) 104 | stringex (~> 1.4) 105 | uuidtools (~> 2.1) 106 | dm-validations (1.2.0) 107 | dm-core (~> 1.2.0) 108 | do_postgres (0.10.12) 109 | data_objects (= 0.10.12) 110 | em-websocket (0.3.8) 111 | addressable (>= 2.1.1) 112 | eventmachine (>= 0.12.9) 113 | erubis (2.7.0) 114 | eventmachine (1.0.3) 115 | facets (2.9.3) 116 | factory_girl (4.2.0) 117 | activesupport (>= 3.0.0) 118 | fakeweb (1.3.0) 119 | fastercsv (1.5.5) 120 | ffi (1.4.0) 121 | foreman (0.62.0) 122 | thor (>= 0.13.6) 123 | haml (4.0.0) 124 | tilt 125 | hash-deep-merge (0.1.1) 126 | hashie (2.0.2) 127 | hike (1.2.1) 128 | i18n (0.6.4) 129 | journey (1.0.4) 130 | json (1.7.7) 131 | json_pure (1.7.7) 132 | jwt (0.1.6) 133 | multi_json (>= 1.0) 134 | metaclass (0.0.1) 135 | mime-types (1.21) 136 | mocha (0.10.5) 137 | metaclass (~> 0.0.1) 138 | multi_json (1.3.7) 139 | nexmo (1.1.0) 140 | nokogiri (1.5.6) 141 | plivo (0.2.15) 142 | builder (>= 2.1.2) 143 | json (>= 1.6.6) 144 | rest-client (>= 1.6.7) 145 | pusher (0.11.3) 146 | multi_json (~> 1.0) 147 | signature (~> 0.1.6) 148 | pusher-fake (0.2.0) 149 | em-websocket (= 0.3.8) 150 | multi_json (= 1.3.7) 151 | thin (= 1.5.0) 152 | rack (1.4.5) 153 | rack-cache (1.2) 154 | rack (>= 0.4) 155 | rack-flash (0.1.2) 156 | rack 157 | rack-flash3 (1.0.3) 158 | rack 159 | rack 160 | rack-protection (1.4.0) 161 | rack 162 | rack-ssl (1.3.3) 163 | rack 164 | rack-test (0.6.2) 165 | rack (>= 1.0) 166 | racksh (1.0.0) 167 | rack (>= 1.0) 168 | rack-test (>= 0.5) 169 | rake (10.0.3) 170 | redis (3.0.3) 171 | redis-namespace (1.2.1) 172 | redis (~> 3.0.0) 173 | resque (1.23.1) 174 | multi_json (~> 1.0) 175 | redis-namespace (~> 1.0) 176 | sinatra (>= 0.9.2) 177 | vegas (~> 0.1.2) 178 | rest-client (1.6.7) 179 | mime-types (>= 1.16) 180 | rspec (2.13.0) 181 | rspec-core (~> 2.13.0) 182 | rspec-expectations (~> 2.13.0) 183 | rspec-mocks (~> 2.13.0) 184 | rspec-core (2.13.1) 185 | rspec-expectations (2.13.0) 186 | diff-lcs (>= 1.1.3, < 2.0) 187 | rspec-mocks (2.13.0) 188 | rubyzip (0.9.9) 189 | rufus-tokyo (1.0.7) 190 | selenium-webdriver (2.31.0) 191 | childprocess (>= 0.2.5) 192 | multi_json (~> 1.0) 193 | rubyzip 194 | websocket (~> 1.0.4) 195 | shoulda-matchers (1.4.2) 196 | activesupport (>= 3.0.0) 197 | bourne (~> 1.1.2) 198 | signature (0.1.6) 199 | sinatra (1.3.5) 200 | rack (~> 1.4) 201 | rack-protection (~> 1.3) 202 | tilt (~> 1.3, >= 1.3.3) 203 | sinatra-authentication (0.4.1) 204 | dm-core 205 | dm-migrations 206 | dm-timestamps 207 | dm-validations 208 | rack-flash 209 | rufus-tokyo 210 | sinatra 211 | sinbook 212 | sinatra-contrib (1.3.2) 213 | backports (>= 2.0) 214 | eventmachine 215 | rack-protection 216 | rack-test 217 | sinatra (~> 1.3.0) 218 | tilt (~> 1.3) 219 | sinatra-jsonp (0.4.1) 220 | multi_json (~> 1.3.7) 221 | sinatra (~> 1.0) 222 | sinbook (0.1.9) 223 | yajl-ruby 224 | sprockets (2.2.2) 225 | hike (~> 1.2) 226 | multi_json (~> 1.0) 227 | rack (~> 1.0) 228 | tilt (~> 1.1, != 1.3.0) 229 | statsd-ruby (1.2.0) 230 | stringex (1.5.1) 231 | thin (1.5.0) 232 | daemons (>= 1.0.9) 233 | eventmachine (>= 0.12.6) 234 | rack (>= 1.0.0) 235 | thor (0.17.0) 236 | tilt (1.3.5) 237 | tropo-provisioning (0.0.27) 238 | activesupport 239 | hashie (>= 0.2.1) 240 | i18n 241 | tropo-webapi-ruby (0.1.11) 242 | hashie (>= 0.2.0) 243 | json_pure (>= 1.2.0) 244 | twilio-ruby (3.9.0) 245 | builder (>= 2.1.2) 246 | jwt (>= 0.1.2) 247 | multi_json (>= 1.3.0) 248 | uuidtools (2.1.3) 249 | vegas (0.1.11) 250 | rack (>= 1.0.0) 251 | websocket (1.0.7) 252 | xpath (1.0.0) 253 | nokogiri (~> 1.3) 254 | yajl-ruby (1.1.0) 255 | 256 | PLATFORMS 257 | ruby 258 | 259 | DEPENDENCIES 260 | actionpack (~> 3.2.11) 261 | capybara 262 | datamapper 263 | dm-observer 264 | dm-postgres-adapter 265 | dm-rspec 266 | facets 267 | factory_girl 268 | fakeweb 269 | foreman 270 | googlecharts! 271 | haml 272 | lazy_high_charts! 273 | nexmo 274 | phonie! 275 | plivo 276 | pusher 277 | pusher-fake 278 | rack-flash3 279 | rack-ssl 280 | racksh 281 | rake 282 | resque 283 | rspec 284 | shoulda-matchers 285 | sinatra 286 | sinatra-authentication 287 | sinatra-contrib 288 | sinatra-jsonp 289 | statsd-ruby 290 | thin 291 | tropo-provisioning 292 | tropo-webapi-ruby 293 | twilio-ruby 294 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec thin start -p $PORT -l - 2 | resque: bundle exec rake environment resque:work VVERBOSE=1 TERM_CHILD=1 QUEUE=send_sms 3 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bundle exec thin start -p $PORT -l - 2 | redis: redis-server 3 | resque: bundle exec rake environment resque:work VVERBOSE=1 TERM_CHILD=1 QUEUE=send_sms 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##crowdring 2 | ============== 3 | + Authored by: Nathan Herzing and Willa Wang. Lead: Manu Kabahizi. CopyLeft: The Rules 4 | 5 | + Crowdring, originally from https://github.com/mbelinsky/Crowdring 6 | 7 | + Pre Install 8 | 9 | + Install Redis 10 | 11 | + download redis from [Here](http://redis.io/) 12 | 13 | + run `tar xvf redis.version.tar` 14 | 15 | + `cd redis.version/` 16 | 17 | + run `make install` 18 | 19 | + Install Ruby 1.9.3 20 | 21 | + Install database 22 | 23 | + defaults to PostgreSQL ie. postgres://localhost/crowdring_#{ENVIRONMENT} 24 | 25 | + create database `crowdring_development`, `crowdring_test`, `crowdring_production` locally. 26 | 27 | + To Install. 28 | 29 | + `git clone git@github.com:therules/CrowdRing.git` 30 | 31 | + `cd crowdring` 32 | 33 | + `bundle` 34 | 35 | + `rake db:migrate` 36 | 37 | 38 | + To Run 39 | 40 | + `foreman start -f Procfile.dev` 41 | 42 | + Sign in as frodo@crowdring.org, password `gAnd0lf` 43 | 44 | + Supported Services 45 | 46 | + Plivo, Twilio, Tropo, KooKoo, Voxeo, Nexmo, Routo, Netcore. 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift 'lib' 2 | require 'rake' 3 | require 'resque/tasks' 4 | 5 | task :environment, :env do |cmd, args| 6 | ENV["RACK_ENV"] ||= args[:env] || "development" 7 | require 'crowdring' 8 | end 9 | 10 | 11 | namespace :db do 12 | desc "Create database, env symbol are in [development, test, production]." 13 | task :migrate, :env do |cmd, args| 14 | env = args[:env] || "development" 15 | Rake::Task['environment'].invoke(env) 16 | 17 | DataMapper.repository.auto_migrate! 18 | end 19 | 20 | task :update, :env do |cmd, args| 21 | env = args[:env] || "development" 22 | Rake::Task['environment'].invoke(env) 23 | 24 | DataMapper.repository.auto_upgrade! 25 | 26 | User.set( 27 | email: ENV["ADMIN_EMAIL"] || 'frodo@crowdring.org', 28 | password: ENV["ADMIN_PASSWORD"] || 'gAnd0lf', 29 | password_confirmation: ENV["ADMIN_PASSWORD"] || 'gAnd0lf') 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift 'lib' 2 | require 'crowdring' 3 | 4 | run Crowdring::Server -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for Capistrano 3.1 2 | lock '3.1.0' 3 | 4 | set :application, 'my_app_name' 5 | set :repo_url, 'git@example.com:me/my_repo.git' 6 | 7 | # Default branch is :master 8 | # ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp } 9 | 10 | # Default deploy_to directory is /var/www/my_app 11 | # set :deploy_to, '/var/www/my_app' 12 | 13 | # Default value for :scm is :git 14 | # set :scm, :git 15 | 16 | # Default value for :format is :pretty 17 | # set :format, :pretty 18 | 19 | # Default value for :log_level is :debug 20 | # set :log_level, :debug 21 | 22 | # Default value for :pty is false 23 | # set :pty, true 24 | 25 | # Default value for :linked_files is [] 26 | # set :linked_files, %w{config/database.yml} 27 | 28 | # Default value for linked_dirs is [] 29 | # set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system} 30 | 31 | # Default value for default_env is {} 32 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 33 | 34 | # Default value for keep_releases is 5 35 | # set :keep_releases, 5 36 | 37 | namespace :deploy do 38 | 39 | desc 'Restart application' 40 | task :restart do 41 | on roles(:app), in: :sequence, wait: 5 do 42 | # Your restart mechanism here, for example: 43 | # execute :touch, release_path.join('tmp/restart.txt') 44 | end 45 | end 46 | 47 | after :publishing, :restart 48 | 49 | after :restart, :clear_cache do 50 | on roles(:web), in: :groups, limit: 3, wait: 10 do 51 | # Here we can do anything such as: 52 | # within release_path do 53 | # execute :rake, 'cache:clear' 54 | # end 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # Simple Role Syntax 2 | # ================== 3 | # Supports bulk-adding hosts to roles, the primary 4 | # server in each group is considered to be the first 5 | # unless any hosts have the primary property set. 6 | # Don't declare `role :all`, it's a meta role 7 | role :app, %w{deploy@example.com} 8 | role :web, %w{deploy@example.com} 9 | role :db, %w{deploy@example.com} 10 | 11 | # Extended Server Syntax 12 | # ====================== 13 | # This can be used to drop a more detailed server 14 | # definition into the server list. The second argument 15 | # something that quacks like a hash can be used to set 16 | # extended properties on the server. 17 | server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value 18 | 19 | # you can set custom ssh options 20 | # it's possible to pass any option but you need to keep in mind that net/ssh understand limited list of options 21 | # you can see them in [net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start) 22 | # set it globally 23 | # set :ssh_options, { 24 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 25 | # forward_agent: false, 26 | # auth_methods: %w(password) 27 | # } 28 | # and/or per server 29 | # server 'example.com', 30 | # user: 'user_name', 31 | # roles: %w{web app}, 32 | # ssh_options: { 33 | # user: 'user_name', # overrides user setting above 34 | # keys: %w(/home/user_name/.ssh/id_rsa), 35 | # forward_agent: false, 36 | # auth_methods: %w(publickey password) 37 | # # password: 'please use keys' 38 | # } 39 | # setting per server overrides global ssh_options 40 | -------------------------------------------------------------------------------- /config/deploy/staging.rb: -------------------------------------------------------------------------------- 1 | # Simple Role Syntax 2 | # ================== 3 | # Supports bulk-adding hosts to roles, the primary 4 | # server in each group is considered to be the first 5 | # unless any hosts have the primary property set. 6 | # Don't declare `role :all`, it's a meta role 7 | role :app, %w{deploy@example.com} 8 | role :web, %w{deploy@example.com} 9 | role :db, %w{deploy@example.com} 10 | 11 | # Extended Server Syntax 12 | # ====================== 13 | # This can be used to drop a more detailed server 14 | # definition into the server list. The second argument 15 | # something that quacks like a hash can be used to set 16 | # extended properties on the server. 17 | server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value 18 | 19 | # you can set custom ssh options 20 | # it's possible to pass any option but you need to keep in mind that net/ssh understand limited list of options 21 | # you can see them in [net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start) 22 | # set it globally 23 | # set :ssh_options, { 24 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 25 | # forward_agent: false, 26 | # auth_methods: %w(password) 27 | # } 28 | # and/or per server 29 | # server 'example.com', 30 | # user: 'user_name', 31 | # roles: %w{web app}, 32 | # ssh_options: { 33 | # user: 'user_name', # overrides user setting above 34 | # keys: %w(/home/user_name/.ssh/id_rsa), 35 | # forward_agent: false, 36 | # auth_methods: %w(publickey password) 37 | # # password: 'please use keys' 38 | # } 39 | # setting per server overrides global ssh_options 40 | -------------------------------------------------------------------------------- /data/sms_prices.yml: -------------------------------------------------------------------------------- 1 | nexmo: 2 | ZA: .071 3 | US: .007 4 | routo: 5 | BR: .058 6 | twilio: 7 | US: .01 8 | tropo: 9 | US: .01 10 | plivo: 11 | US: .008 12 | -------------------------------------------------------------------------------- /lib/crowdring.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'sinatra/base' 3 | require 'sinatra/reloader' 4 | require 'rack/ssl' 5 | require 'data_mapper' 6 | require 'dm-observer' 7 | require 'sinatra-authentication' 8 | require 'pusher' 9 | require 'rack-flash' 10 | require 'facets/module/mattr' 11 | require 'phonie' 12 | require 'resque' 13 | require 'haml' 14 | require 'statsd' 15 | require 'json' 16 | require 'logger' 17 | require 'crowdring/time_service' 18 | require 'action_view' 19 | require 'lazy_high_charts' 20 | require 'twilio-ruby' 21 | require "sinatra/json" 22 | require "sinatra/jsonp" 23 | require 'googlecharts' 24 | 25 | require 'crowdring/telephony_services/telephony_service' 26 | require 'crowdring/telephony_services/twilio_service' 27 | require 'crowdring/telephony_services/kookoo_service' 28 | require 'crowdring/telephony_services/tropo_service' 29 | require 'crowdring/telephony_services/nexmo_service' 30 | require 'crowdring/telephony_services/logging_service' 31 | require 'crowdring/telephony_services/routo_service' 32 | require 'crowdring/telephony_services/voxeo_service' 33 | require 'crowdring/telephony_services/netcore_service' 34 | require 'crowdring/telephony_services/plivo_service' 35 | require 'crowdring/telephony_services/caching_service' 36 | require 'crowdring/telephony_services/composite_service' 37 | require 'crowdring/batch_send_sms' 38 | 39 | require 'crowdring/ivr' 40 | require 'crowdring/phone_number_fields' 41 | require 'crowdring/campaign' 42 | require 'crowdring/ringer' 43 | require 'crowdring/ring' 44 | require 'crowdring/text' 45 | require 'crowdring/voicemail' 46 | require 'crowdring/ring_observer' 47 | require 'crowdring/assigned_phone_number' 48 | require 'crowdring/tag' 49 | require 'crowdring/tag_filter' 50 | require 'crowdring/aggregate_campaign' 51 | 52 | require 'crowdring/constraint' 53 | require 'crowdring/asks/ask.rb' 54 | require 'crowdring/asks/offline_ask.rb' 55 | require 'crowdring/asks/join_ask.rb' 56 | require 'crowdring/asks/send_sms_ask.rb' 57 | require 'crowdring/asks/text_ask.rb' 58 | require 'crowdring/asks/voicemail_ask.rb' 59 | 60 | require 'crowdring/filtered_message' 61 | require 'crowdring/message' 62 | require 'crowdring/csv_fields' 63 | require 'crowdring/sms_prices' 64 | require 'crowdring/price_estimate' 65 | require 'crowdring/outgoing_sms' 66 | 67 | require 'crowdring/high_charts_builder' 68 | require 'crowdring/campaign_stats' 69 | require 'crowdring/regions' 70 | require 'crowdring/short_code' 71 | require 'crowdring/number_pool' 72 | 73 | require 'utils/password_generator' 74 | require 'crowdring/patches' 75 | require 'crowdring/crowdring' 76 | 77 | Phonie::Phone.default_country_code = '1' 78 | -------------------------------------------------------------------------------- /lib/crowdring/aggregate_campaign.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class AggregateCampaign 3 | include DataMapper::Resource 4 | 5 | property :name, String, required: true, unique: true, key: true 6 | 7 | has n, :campaigns, through: Resource, constraint: :skip 8 | 9 | def campaigns=(campaigns) 10 | campaigns = campaigns.map {|c| Campaign.get(c)} if campaigns.first.is_a?(Fixnum) || campaigns.first.is_a?(String) 11 | super campaigns 12 | end 13 | 14 | def ringer_count 15 | campaigns.sum :ringer_count 16 | end 17 | 18 | def campaign_summary 19 | return 'no campaigns' if campaigns.empty? 20 | full_summary = campaigns.map(&:title).join(', ') 21 | full_summary = full_summary[0..80] + '...' if full_summary.length > 80 22 | full_summary 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/crowdring/asks/ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Ask 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :title, String, unique: true, required: true, length: 0..100 7 | property :type, Discriminator 8 | property :created_at, DateTime 9 | property :prompt, String, length: 250, lazy: false, required: false 10 | 11 | belongs_to :message, required: false 12 | belongs_to :triggered_ask, 'Ask', required: false 13 | 14 | before :create do 15 | message.save if message 16 | end 17 | 18 | after :create do 19 | recipient_tag 20 | respondent_tag 21 | end 22 | 23 | after :destroy do 24 | recipient_tag.destroy 25 | respondent_tag.destroy 26 | end 27 | 28 | def handle?(type, ringer) 29 | ringer.tagged?(recipient_tag) 30 | end 31 | 32 | def recipient_tag 33 | Tag.from_str("ask_recipient:#{id}") 34 | end 35 | 36 | def respondent_tag 37 | Tag.from_str("ask_respondent:#{id}") 38 | end 39 | 40 | def respond(ringer, sms_number) 41 | ringer.tag(respondent_tag) 42 | 43 | triggered_ask.trigger_for(ringer, sms_number) if triggered_ask 44 | 45 | [{cmd: :reject}] 46 | end 47 | 48 | def potential_recipients(ringers) 49 | ringers.reject {|r| r.tagged?(recipient_tag) } 50 | end 51 | 52 | def recipients(ringers=Ringer.subscribed) 53 | ringers.select {|r| r.tagged?(recipient_tag)} 54 | end 55 | 56 | def respondents(ringers=Ringer.subscribed) 57 | ringers.select {|r| r.tagged?(respondent_tag)} 58 | end 59 | 60 | def trigger_for(ringer, sms_number) 61 | ringer.tag(recipient_tag) 62 | message.send_message(to: ringer, from: sms_number) if message 63 | end 64 | 65 | def trigger(ringers, sms_number) 66 | ringers.each {|ringer| trigger_for(ringer, sms_number) } 67 | end 68 | 69 | def initial_price_estimate(ringers=Ringer.all, sms_number) 70 | return 0.0 if message.nil? 71 | 72 | smss = ringers.map do |ringer| 73 | text = message.for(ringer, sms_number) 74 | text && OutgoingSMS.new(from: sms_number, to: ringer.phone_number, text: text) 75 | end 76 | 77 | PriceEstimate.new(smss.compact) 78 | end 79 | 80 | def all_errors 81 | allerrors = [errors] 82 | allerrors << message.errors unless message.nil? 83 | allerrors.flatten 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/crowdring/asks/join_ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class JoinAsk < Ask 3 | validates_presence_of :message 4 | def handle?(type, ringer) 5 | type == :voice && super(type, ringer) 6 | end 7 | 8 | def self.typesym 9 | :join_ask 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/crowdring/asks/offline_ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class OfflineAsk < Ask 3 | def title 4 | 'Offline Ask' 5 | end 6 | 7 | def handle?(type, ringer) 8 | type == :voice 9 | end 10 | 11 | def recipient_tag 12 | Tag.from_str("ask_recipient:#{id}", hidden: true) 13 | end 14 | 15 | def respondent_tag 16 | Tag.from_str("ask_respondent:#{id}", hidden: true) 17 | end 18 | 19 | def self.typesym 20 | :offline_ask 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/crowdring/asks/send_sms_ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class SendSMSAsk < Ask 3 | validates_presence_of :message 4 | 5 | def handle?(type, ringer) 6 | false 7 | end 8 | 9 | def self.typesym 10 | :send_sms_ask 11 | end 12 | 13 | def self.readable_name 14 | 'Send a text message' 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/crowdring/asks/text_ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class TextAsk < Ask 3 | has n, :texts, through: Resource, constraint: :destroy 4 | validates_presence_of :message 5 | def handle?(type, ringer) 6 | type == :sms && super(type, ringer) 7 | end 8 | 9 | def text(ringer, text, sms_number) 10 | email = find_email(text.message) 11 | if email 12 | ringer.email = email.to_s 13 | ringer.save 14 | end 15 | texts << text 16 | self.save 17 | 18 | respond(ringer, sms_number) 19 | end 20 | 21 | def self.typesym 22 | :text_ask 23 | end 24 | 25 | def self.readable_name 26 | 'Recieve a text message' 27 | end 28 | 29 | def find_email(message) 30 | pattern = Regexp.new(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b/i) 31 | email = pattern.match(message) 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/crowdring/asks/voicemail_ask.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class VoicemailAsk < Ask 3 | 4 | has n, :voicemails, through: Resource, constraint: :destroy 5 | validates_presence_of :message 6 | 7 | def handle?(type, ringer) 8 | type == :voice && super(type, ringer) 9 | end 10 | 11 | def respond(ringer, sms_number) 12 | voicemail = voicemails.create(ringer: ringer) 13 | super(ringer, sms_number) 14 | [{cmd: :record, prompt: prompt, voicemail: voicemail}] 15 | end 16 | 17 | def self.typesym 18 | :voicemail_ask 19 | end 20 | 21 | def self.readable_name 22 | 'Receive a voice message' 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/crowdring/assigned_phone_number.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module AssignedPhoneNumberFields 3 | def self.included(base) 4 | base.class_eval do 5 | include DataMapper::Resource 6 | 7 | property :id, DataMapper::Property::Serial 8 | property :phone_number, DataMapper::Property::String, key: true 9 | property :raw_number, DataMapper::Property::String 10 | validates_uniqueness_of :phone_number 11 | 12 | def phone_number=(number) 13 | self.raw_number = number 14 | super (Phonie::Phone.parse(number) || number).to_s 15 | end 16 | 17 | def self.from(number) 18 | norm_number = Phonie::Phone.parse(number).to_s 19 | self.first(phone_number: norm_number) 20 | end 21 | end 22 | end 23 | end 24 | 25 | class AssignedVoiceNumber 26 | include AssignedPhoneNumberFields 27 | include PhoneNumberFields 28 | 29 | property :type, Discriminator 30 | property :description, String, default: '[No description given]' 31 | 32 | belongs_to :campaign, required: false 33 | 34 | validates_with_method :phone_number, :valid_phone_number? 35 | 36 | def description=(text) 37 | super unless text.empty? 38 | end 39 | end 40 | 41 | class AssignedCampaignVoiceNumber < AssignedVoiceNumber 42 | validates_presence_of :campaign 43 | validates_length_of :description, max: 64 44 | 45 | after :create do 46 | tag 47 | end 48 | 49 | after :destroy do 50 | tag.destroy 51 | end 52 | 53 | def tag 54 | Tag.from_str("rang:#{id}") 55 | end 56 | 57 | 58 | def ring(ringer) 59 | ringer.tag(tag) 60 | campaign.ring(ringer) 61 | end 62 | end 63 | 64 | class AssignedUnsubscribeVoiceNumber < AssignedVoiceNumber 65 | def ring(ringer) 66 | ringer.unsubscribe 67 | end 68 | end 69 | 70 | class AssignedSMSNumber 71 | include AssignedPhoneNumberFields 72 | include PhoneNumberFields 73 | belongs_to :campaign 74 | 75 | def text(ringer, message) 76 | campaign.text(ringer, message) 77 | end 78 | end 79 | 80 | class AssignedPhoneNumber 81 | def self.from(type, number) 82 | case type 83 | when :voice 84 | AssignedVoiceNumber.from(number) 85 | when :sms 86 | AssignedSMSNumber.from(number) 87 | end 88 | end 89 | 90 | def self.handle(type, request) 91 | number = from(type, request.to) 92 | ringer = Ringer.from(request.from) 93 | ringer.subscribe 94 | 95 | case number 96 | when AssignedVoiceNumber 97 | number.ring(ringer) 98 | [{cmd: :ivr, to: ringer.phone_number, text: "#{ivr(number).read_text}", campaign_id: "#{number.campaign.id}"}] if ivr?(number) 99 | when AssignedSMSNumber 100 | number.text(ringer, request.message) 101 | end 102 | end 103 | 104 | def self.ivr?(number) 105 | campaign = number.campaign 106 | number.class == Crowdring::AssignedCampaignVoiceNumber && !campaign.ivrs.empty? && campaign.ivrs.last.activated 107 | end 108 | 109 | def self.ivr(number) 110 | campaign = number.campaign 111 | campaign.ivrs.last 112 | end 113 | end 114 | end -------------------------------------------------------------------------------- /lib/crowdring/batch_send_sms.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class TwilioBatchSendSms 3 | @queue = :send_sms 4 | 5 | def self.perform(params, from, msg, to_numbers) 6 | service = TwilioService.new(params['account_sid'], params['auth_token']) 7 | to_numbers.each do |to| 8 | begin 9 | service.send_sms from: from, to: to, msg: msg 10 | rescue Twilio::REST::RequestError => e 11 | p "Send sms failed with RequestError: #{e.message}" 12 | rescue Twilio::REST::ServerError => e 13 | p "Send sms failed with ServerError: #{e.message}" 14 | end 15 | end 16 | end 17 | end 18 | 19 | class TropoBatchSendSms 20 | @queue = :send_sms 21 | 22 | def self.perform(params, from, msg, to_numbers) 23 | service = TropoService.new(params['msg_token'], params['app_id'], params['username'], params['password']) 24 | to_numbers.each {|to| service.send_sms from: from, to: to, msg: msg } 25 | end 26 | end 27 | 28 | class NexmoBatchSendSms 29 | @queue = :send_sms 30 | 31 | def self.perform(params, from, msg, to_numbers) 32 | service = NexmoService.new(params['key'], params['secret']) 33 | to_numbers.each {|to| service.send_sms from: from, to: to, msg: msg } 34 | end 35 | end 36 | 37 | class RoutoBatchSendSms 38 | @queue = :send_sms 39 | 40 | def self.perform(params, from, msg, to_numbers) 41 | service = RoutoService.new(params['username'], params['password'], params['number']) 42 | to_numbers.each {|to| service.send_sms to: to, msg: msg } 43 | end 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /lib/crowdring/campaign.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Campaign 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :title, String, required: true, length: 0..64, unique: true, 7 | messages: { presence: 'Non-empty title required', 8 | length: 'Title must be fewer than 64 letters in length' } 9 | property :created_at, DateTime 10 | property :goal, Integer, default: 777 11 | 12 | property :ringer_count, Integer, default: 0 13 | 14 | has n, :rings, constraint: :destroy 15 | 16 | has n, :asks, through: Resource, constraint: :destroy 17 | has n, :ivrs, through: Resource, constraint: :destroy 18 | 19 | has n, :voice_numbers, 'AssignedCampaignVoiceNumber', constraint: :destroy 20 | has 1, :sms_number, 'AssignedSMSNumber', constraint: :destroy 21 | 22 | has n, :aggregate_campaigns, through: Resource, constraint: :skip 23 | 24 | validates_presence_of :goal 25 | validates_with_method :voice_numbers, :at_least_one_assigned_number? 26 | validates_with_method :goal, :valid_range? 27 | 28 | before :create do 29 | asks << OfflineAsk.create(title: "Offline Ask - #{title}") 30 | end 31 | 32 | after :create do 33 | tag 34 | end 35 | 36 | after :destroy do 37 | tag.destroy 38 | end 39 | 40 | def sms_number=(number) 41 | number = AssignedSMSNumber.new(phone_number: number) if number.is_a? String 42 | super number 43 | end 44 | 45 | def country 46 | sms_number.country.name 47 | end 48 | 49 | def ringers 50 | rings.all.ringer 51 | end 52 | 53 | def ringers_from(assigned_number) 54 | ringers.select {|r| r.tagged?(assigned_number.tag) } 55 | end 56 | 57 | def tag 58 | Tag.from_str("campaign:#{id}") 59 | end 60 | 61 | def ring(ringer) 62 | update! ringer_count: ringer_count + 1 unless ringers.include?(ringer) 63 | return unless rings.create(ringer: ringer).saved? 64 | ringer.tag(tag) 65 | ask = asks.reverse.find {|ask| ask.handle?(:voice, ringer) } 66 | ask.respond(ringer, sms_number.raw_number) if ask 67 | end 68 | 69 | def text(ringer, message) 70 | text = Text.create(ringer: ringer, message: message) 71 | ask = asks.reverse.find {|ask| ask.handle?(:sms, ringer) } 72 | ask.text(ringer, text, sms_number.raw_number) if ask 73 | end 74 | 75 | def unique_rings(assigned_number=nil) 76 | ringers_to_rings = rings.all.reduce({}) do |res, ring| 77 | res.merge(res.key?(ring.ringer_id) ? {} : {ring.ringer_id => ring}) 78 | end 79 | if assigned_number 80 | (ringers_to_rings.select {|_,ring| ring.ringer.tagged?(assigned_number.tag) }).values 81 | else 82 | ringers_to_rings.values 83 | end 84 | end 85 | 86 | def triggered_ask?(ask) 87 | !(asks.find{|n| n.triggered_ask && n.triggered_ask == ask}.nil? && ask != asks.first) 88 | end 89 | 90 | def all_errors 91 | allerrors = [errors] 92 | allerrors << voice_numbers.map(&:errors) 93 | allerrors << sms_number.errors if sms_number 94 | allerrors << asks.map(&:errors) 95 | allerrors.flatten 96 | end 97 | 98 | def slug 99 | title.gsub(/\s/, '_').gsub(/[^a-zA-Z_]/, '').downcase 100 | end 101 | 102 | private 103 | 104 | def valid_range? 105 | (0...9999999999).include?(goal) 106 | end 107 | 108 | def at_least_one_assigned_number? 109 | @voice_numbers && !@voice_numbers.empty? ? true : [false, 'Must assign at least one number'] 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /lib/crowdring/campaign_export_csv.haml: -------------------------------------------------------------------------------- 1 | .rounded-box 2 | %form{action: '/csv', method: 'get'} 3 | =haml :export_csv 4 | -------------------------------------------------------------------------------- /lib/crowdring/campaign_stats.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class CampaignStats 3 | @@stats = {} 4 | 5 | def initialize(campaign) 6 | @campaign = campaign 7 | end 8 | 9 | def self.calculate(campaign, stat, *args) 10 | @@stats[stat].new(campaign).calculate(*args) 11 | end 12 | 13 | def self.register(id) 14 | @@stats[id] = self 15 | end 16 | end 17 | 18 | class MemberTotal < CampaignStats 19 | def calculate(assigned_number=nil) 20 | unique_rings = @campaign.unique_rings(assigned_number) 21 | datapoints = unique_rings.each_with_index.map {|ring, index| [ring.created_at.strftime('%Q').to_i, index + 1] } 22 | datapoints.unshift([@campaign.created_at.strftime('%Q').to_i, 0]) 23 | datapoints << [DateTime.now.strftime('%Q').to_i, unique_rings.size] 24 | end 25 | 26 | register :member_total 27 | end 28 | 29 | class NewMembersPerDay < CampaignStats 30 | def calculate 31 | datapoints = CampaignStats.calculate(@campaign, :member_total) 32 | datapoints.each_cons(2).map do |dps| 33 | thisPoint = dps.first 34 | nextPoint = dps.last 35 | [(thisPoint[0] + nextPoint[0])/2, ((nextPoint[1] - thisPoint[1]).to_f / (nextPoint[0] - thisPoint[0]).to_f) * 1000*60*60*24] 36 | end 37 | end 38 | 39 | register :new_members_per_day 40 | end 41 | end -------------------------------------------------------------------------------- /lib/crowdring/constraint.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Constraint 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :type, Discriminator 7 | 8 | belongs_to :tag 9 | 10 | def self.from_str(str) 11 | if str[0] == '!' 12 | HasNotConstraint.new(tag:Tag.from_str(str[1..-1])) 13 | else 14 | HasConstraint.new(tag:Tag.from_str(str)) 15 | end 16 | end 17 | end 18 | 19 | class HasConstraint < Constraint 20 | def satisfied_by?(item) 21 | item.tags.include? tag 22 | end 23 | 24 | def to_readable 25 | "#{tag.readable_s}" 26 | end 27 | 28 | def to_s 29 | "#{tag}" 30 | end 31 | end 32 | 33 | class HasNotConstraint < Constraint 34 | def satisfied_by?(item) 35 | not item.tags.include? tag 36 | end 37 | 38 | def to_readable 39 | "has not #{tag.readable_s}" 40 | end 41 | 42 | def to_s 43 | "!#{tag}" 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/crowdring/csv_fields.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class CsvField 3 | @fields = [] 4 | @default_fields = [] 5 | 6 | attr_reader :id, :display_name 7 | 8 | def initialize(id, display_name) 9 | @id = id 10 | @display_name = display_name 11 | end 12 | 13 | def default? 14 | CsvField.default? self.id 15 | end 16 | 17 | class << self 18 | 19 | def add_field(id, display_name, opts={}) 20 | field = CsvField.new(id, display_name) 21 | @fields << field 22 | @default_fields << field if opts[:default] 23 | end 24 | 25 | def all_fields 26 | @fields 27 | end 28 | 29 | def default_fields 30 | @default_fields 31 | end 32 | 33 | def default?(id) 34 | @default_fields.map(&:id).include?(id) 35 | end 36 | 37 | def from_id(id) 38 | @fields.find {|f| f.id == id.to_s } 39 | end 40 | end 41 | 42 | add_field 'phone_number', 'Phone Number', default: true 43 | add_field 'created_at', 'Support Date', default: true 44 | add_field 'country_code', 'Country Code' 45 | add_field 'area_code', 'Area Code' 46 | add_field 'country_abbreviation', 'Country Abbreviation' 47 | add_field 'country_name', 'Country' 48 | add_field 'campaign_support', 'Joined Campaign' 49 | add_field 'email', 'Email' 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/crowdring/filtered_message.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class FilteredMessage 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :message_text, Text, lazy: false, required: true 7 | property :priority, Integer 8 | 9 | has 1, :tag_filter, through: Resource, constraint: :destroy 10 | 11 | def constraints=(constraints) 12 | self.tag_filter = TagFilter.create(constraints: constraints) 13 | end 14 | 15 | def accept?(ringer, sms_number) 16 | tag_filter.accept?(ringer) && ringer.subscribed? 17 | end 18 | 19 | def send_message(params) 20 | if accept?(params[:to], params[:from]) 21 | CompositeService.instance.send_sms( 22 | from: params[:from], to: params[:to].phone_number, 23 | msg: message_text) 24 | true 25 | else 26 | false 27 | end 28 | end 29 | 30 | def local?(phone_number, sms_number) 31 | ringer_number = Phonie::Phone.parse(phone_number) 32 | sms_number = ShortCode.shortcode?(sms_number) ? ShortCode.new : Phonie::Phone.parse(sms_number) 33 | ringer_number.country == sms_number.country 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/crowdring/high_charts_builder.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module HighChartsBuilder 3 | def basic_stats(campaign) 4 | LazyHighCharts::HighChart.new('graph') do |f| 5 | f.options[:chart][:zoomType] = 'x' 6 | f.options[:title] = {text: ''} 7 | f.options[:plotOptions][:area] = {marker: {enabled: false}} 8 | f.options[:plotOptions][:line] = {marker: {enabled: false}} 9 | f.options[:xAxis] = {ordinal: false, type: 'datetime'} 10 | f.options[:tooltip] = {yDecimals: 0} 11 | f.options[:yAxis] = [ 12 | {title: {text: 'Total Calls'}, opposite: true, labels: {style: { color: 'green'}}}] 13 | 14 | f.options[:legend] = {enabled: true, verticalAlign: 'top'} 15 | 16 | f.series(name: 'Total Calls', data: CampaignStats.calculate(campaign, :member_total), step: true, yAxis: 0, color: 'rgba(67, 142, 204, .5)') 17 | colors = ['rgba(191, 59, 72, .5)', 'rgba(59, 59, 191, .5)', 'rgba(81, 191, 59, .5)'] 18 | campaign.voice_numbers.each_with_index do |num, i| 19 | f.series(name: "#{num.description}", data: CampaignStats.calculate(campaign, :member_total, num), step: true, yAxis: 0, color: colors[i % colors.count]) 20 | end 21 | end 22 | end 23 | module_function :basic_stats 24 | end 25 | end -------------------------------------------------------------------------------- /lib/crowdring/ivr.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class KeyOption 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :press, String, required: true 7 | property :for, String, required: true 8 | property :ringer_count, Integer, required: false, default: 0 9 | 10 | validates_with_method :press, :valid_keys? 11 | 12 | def increment 13 | update! ringer_count: ringer_count + 1 14 | end 15 | 16 | def to_s 17 | "Press #{press} for #{self.for} has #{ringer_count}" 18 | end 19 | 20 | private 21 | 22 | def valid_keys? 23 | key_pool.include?(press) ? true : [false, "Invalid press key"] 24 | end 25 | 26 | def key_pool 27 | (0..9).to_a.map{|a| a.to_s} << "*" << "#" << "+" 28 | end 29 | end 30 | 31 | class Ivr 32 | include DataMapper::Resource 33 | 34 | property :id, Serial 35 | property :activated, Boolean, default: true 36 | property :read_text, Text, lazy: false 37 | property :question, Text, lazy: false 38 | 39 | has n, :key_options, "KeyOption", through: Resource, constraint: :destroy 40 | 41 | validates_presence_of :key_options 42 | 43 | before :create do 44 | set_read_text 45 | end 46 | 47 | def deactivate 48 | update(activated: false) 49 | end 50 | 51 | def key_options=(keyoption) 52 | return super key_options unless keyoption.is_a? Hash 53 | keyoption = keyoption.values 54 | keyoption.each do |ko| 55 | new_ivr = KeyOption.create(press: ko["press"], for: ko["for"]) 56 | key_options << new_ivr if new_ivr.save 57 | end 58 | end 59 | 60 | def valid_keys 61 | key_options.each {|k| k.press.to_i} 62 | end 63 | 64 | def set_read_text 65 | text_to_read = key_options.map do |option| 66 | "press #{option.press} for #{option.for}" 67 | end.join(' ') 68 | self.read_text = @question + ' ' + text_to_read 69 | true 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/crowdring/message.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Message 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | 7 | has n, :filtered_messages, through: Resource, constraint: :destroy 8 | 9 | validates_presence_of :filtered_messages 10 | 11 | validates_with_method :filtered_messages, :non_empty_filters? 12 | validates_with_method :filtered_messages, :at_least_one_message? 13 | 14 | 15 | def filtered_messages=(messages) 16 | return super messages unless messages.is_a? Hash 17 | messages = messages.values 18 | messages.each_with_index.each {|m, i| filtered_messages.new(m.merge({priority: i})) } 19 | end 20 | 21 | def default_message=(message) 22 | return if message.empty? 23 | default_filtered_message.destroy if default_filtered_message 24 | filtered_messages.new(tag_filter: TagFilter.create, message_text: message, priority: 100) 25 | end 26 | 27 | def send_message(params) 28 | prioritized_messages.find {|fm| fm.send_message(params) } 29 | end 30 | 31 | def for(ringer, sms_number) 32 | prioritized_messages.find {|fm| fm.accept?(ringer, sms_number) } 33 | end 34 | 35 | def default_message 36 | if default_filtered_message.nil? 37 | nil 38 | else 39 | default_filtered_message.message_text 40 | end 41 | end 42 | 43 | def nondefault_messages 44 | prioritized_messages.select{|msg| msg.priority != 100} 45 | end 46 | 47 | private 48 | 49 | def default_filtered_message 50 | filtered_messages.first(priority: 100) 51 | end 52 | 53 | def prioritized_messages 54 | filtered_messages.all.sort {|a,b| a.priority <=> b.priority } 55 | end 56 | 57 | def non_empty_filters? 58 | nondefault_messages.each do |m| 59 | if m.tag_filter.nil? || m.tag_filter.constraints.empty? 60 | return [false, 'All filtered messages must provide at least one constraint'] 61 | end 62 | end 63 | true 64 | end 65 | 66 | def at_least_one_message? 67 | filtered_messages.empty? ? [false, 'At least one message required'] : true 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/crowdring/number_pool.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module NumberPool 3 | module_function 4 | 5 | def available_summary(type=:voice) 6 | NumPool.new.summary(type) 7 | end 8 | 9 | def find_single_number(opts, type=:voice) 10 | NumPool.new.find_number(opts, type) 11 | end 12 | 13 | def find_numbers(opts, type=:voice) 14 | NumPool.new.find_numbers(opts, type) 15 | end 16 | 17 | def available_voice_with_sms 18 | available_summary(:voice).reject {|n| find_single_number({country: n[:country]}, :sms).nil? } 19 | end 20 | 21 | private 22 | 23 | class NumPool 24 | attr_accessor :voice_numbers, :sms_numbers 25 | 26 | def initialize 27 | used_voice_numbers = AssignedVoiceNumber.all.map(&:raw_number) 28 | 29 | avail_voice_numbers = CompositeService.instance.voice_numbers - used_voice_numbers 30 | @voice_numbers = avail_voice_numbers 31 | 32 | used_sms_numbers = AssignedSMSNumber.all.map(&:raw_number) 33 | avail_sms_numbers = CompositeService.instance.sms_numbers - used_sms_numbers 34 | @sms_numbers = avail_sms_numbers 35 | end 36 | 37 | def summary(type) 38 | summary = summary_with_numbers(type) 39 | summary.map {|entry| entry.delete(:numbers); entry} 40 | end 41 | 42 | 43 | def summary_with_numbers(type) 44 | numbers = numbers_of_type(type) 45 | region_summary = numbers.reduce({}) do |summary, raw_number| 46 | number = Phonie::Phone.parse(raw_number) || ShortCode.parse(raw_number) 47 | 48 | country = number.country.name 49 | regions = Regions.strs_for(number).join(', ') 50 | key = country + regions 51 | 52 | unless summary.key?(key) 53 | summary[key] = {country: country, count: 0} 54 | summary[key][:region] = regions unless regions.empty? 55 | summary[key][:numbers] = [] 56 | end 57 | summary[key][:numbers] << raw_number 58 | summary[key][:count] += 1 59 | summary 60 | end 61 | 62 | Hash[region_summary.sort].values 63 | end 64 | 65 | def find_numbers(opts, type) 66 | avail_numbers = summary_with_numbers(type) 67 | found_numbers = [] 68 | opts.each do |opt| 69 | region = find_matching(opt, avail_numbers) 70 | found_number = region && region[:numbers].first 71 | found_numbers << found_number 72 | region[:numbers].delete(found_number) if region 73 | end 74 | found_numbers 75 | end 76 | 77 | 78 | def find_number(opts, type) 79 | region = find_matching(opts, summary_with_numbers(type)) 80 | region && region[:numbers].first 81 | end 82 | 83 | private 84 | 85 | def numbers_of_type(type) 86 | case type 87 | when :voice 88 | @voice_numbers 89 | when :sms 90 | @sms_numbers 91 | end 92 | end 93 | 94 | def find_matching(opts, numbers) 95 | numbers.find do |summary| 96 | if opts[:region] 97 | summary[:country] == opts[:country] && summary[:region] && summary[:region] == opts[:region] 98 | else 99 | summary[:country] == opts[:country] 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end -------------------------------------------------------------------------------- /lib/crowdring/outgoing_sms.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class OutgoingSMS 3 | def initialize(opts) 4 | @from = opts[:from] 5 | @to = opts[:to] 6 | @text = opts[:text] 7 | end 8 | 9 | def price 10 | SMSPrices.price_for(CompositeService.instance.service_for(:sms, @from), Phonie::Phone.parse(@to)) 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/crowdring/patches.rb: -------------------------------------------------------------------------------- 1 | require 'net/smtp' 2 | require 'utils/smtp-tls' 3 | 4 | module Sinatra 5 | module Helpers 6 | def login_required 7 | return true if current_user.class != GuestUser 8 | 9 | if request.xhr? 10 | redirect 403 11 | else 12 | session[:return_to] = request.fullpath 13 | redirect '/login' 14 | end 15 | 16 | false 17 | end 18 | 19 | def send_password_email(to, created_by, password) 20 | message = < 22 | Subject: Welcome to Crowdring! 23 | 24 | Hello #{to}, 25 | 26 | #{created_by} created a Crowdring account for you. Login at https://campaign.crowdring.org 27 | 28 | Your username is #{to} 29 | Your password is #{password} 30 | 31 | Happy campaigning! 32 | The Crowdring Team 33 | MESSAGE_END 34 | 35 | Net::SMTP.start(ENV["SMTP_HOST"], ENV["SMTP_PORT"], 36 | ENV["SMTP_DOMAIN"], ENV["SMTP_USER"], ENV["SMTP_PASSWORD"], :login) do |smtp| 37 | smtp.send_message message, ENV["SMTP_USER"], to 38 | end 39 | end 40 | 41 | 42 | def send_reset_password_email(to, password) 43 | message = < 45 | Subject: Your Crowdring password has been reset. 46 | 47 | Hello #{to}, 48 | 49 | Your Crowdring password has been reset by you or some trickster. Login at https://campaign.crowdring.org 50 | 51 | Your new password is #{password} 52 | 53 | Happy campaigning! 54 | The Crowdring Team 55 | MESSAGE_END 56 | 57 | Net::SMTP.start(ENV["SMTP_HOST"], ENV["SMTP_PORT"], 58 | ENV["SMTP_DOMAIN"], ENV["SMTP_USER"], ENV["SMTP_PASSWORD"], :login) do |smtp| 59 | smtp.send_message message, ENV["SMTP_USER"], to 60 | end 61 | end 62 | end 63 | end 64 | 65 | # sinatra-authentication incorrectly uses Time as the type of :created_at 66 | class DmUser 67 | property :created_at, DateTime 68 | end 69 | 70 | module Sinatra 71 | module SinatraAuthentication 72 | class << self 73 | alias_method :orig_registered, :registered 74 | 75 | 76 | def registered(app) 77 | orig_registered(app) 78 | 79 | app.get '/newuser' do 80 | haml get_view_as_string("newuser.haml"), layout: use_layout? 81 | end 82 | 83 | app.post '/newuser' do 84 | if params[:email] != params[:email_confirmation] 85 | flash[:errors] = "Email and confirmation email do not match." 86 | redirect '/newuser?' + hash_to_query_string(params) 87 | else 88 | password = PasswordGenerator.generate 89 | @user = User.set(email: params[:email], password: password, password_confirmation: password) 90 | if @user.valid && @user.id 91 | send_password_email(params[:email], current_user.email, password) 92 | flash[:notice] = "Account created." 93 | redirect '/users' 94 | else 95 | flash[:errors] = "#{@user.errors}" 96 | redirect '/newuser?' + hash_to_query_string(params) 97 | end 98 | end 99 | end 100 | 101 | app.get '/changepassword' do 102 | haml get_view_as_string("change_password.haml") 103 | end 104 | 105 | app.post '/changepassword' do 106 | user = current_user 107 | if User.authenticate(user.email, params[:current_password]) 108 | if user.update(params[:user]) 109 | flash[:notice] = "Password successfully updated." 110 | redirect to('/') 111 | else 112 | flash[:errors] = "#{user.errors}" 113 | redirect '/changepassword' 114 | end 115 | else 116 | flash[:errors] = "Original password is incorrect." 117 | redirect '/changepassword' 118 | end 119 | end 120 | 121 | app.get '/resetpassword' do 122 | haml get_view_as_string("reset_password.haml") 123 | end 124 | 125 | app.post '/resetpassword' do 126 | user = DmUser.first(email: params[:email]) 127 | if user 128 | password = PasswordGenerator.generate 129 | if user.update(password: password, password_confirmation: password) 130 | send_reset_password_email(params[:email], password) 131 | flash[:notice] = "New password emailed to #{params[:email]}" 132 | redirect '/' 133 | else 134 | flash[:errors] = "#{user.errors}" 135 | redirect '/resetpassword' 136 | end 137 | else 138 | flash[:errors] = "Failed to reset password for #{params[:email]}" 139 | redirect '/resetpassword' 140 | end 141 | end 142 | 143 | end 144 | 145 | end 146 | end 147 | end -------------------------------------------------------------------------------- /lib/crowdring/phone_number_fields.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module PhoneNumberFields 3 | def pretty_phone_number 4 | return phone_number unless Phonie::Phone.valid? phone_number 5 | number = Phonie::Phone.parse phone_number 6 | Phonie::Phone.n1_length = (country_code == '91') ? 4 : 3 7 | number.format "+%c (%a) %f-%l" + " [" + country.char_3_code + "]" 8 | end 9 | 10 | def country_code 11 | Phonie::Phone.parse(phone_number).country_code 12 | end 13 | 14 | def country_abbreviation 15 | country.char_3_code 16 | end 17 | 18 | def country_name 19 | country.name 20 | end 21 | 22 | def area_code 23 | Phonie::Phone.parse(phone_number).area_code 24 | end 25 | 26 | def country 27 | short_code = ShortCode.parse(phone_number) 28 | short_code ? short_code.country : number.country 29 | end 30 | 31 | def number 32 | Phonie::Phone.parse phone_number 33 | end 34 | 35 | module_function 36 | 37 | def pretty_number(number_str) 38 | PrettyNumber.new(number_str).pretty_phone_number 39 | end 40 | 41 | private 42 | 43 | class PrettyNumber < Struct.new(:phone_number) 44 | include PhoneNumberFields 45 | end 46 | 47 | def valid_phone_number? 48 | if Phonie::Phone.valid? @phone_number 49 | true 50 | else 51 | [false, 'Phone number does not appear to be valid'] 52 | end 53 | end 54 | end 55 | end -------------------------------------------------------------------------------- /lib/crowdring/price_estimate.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class PriceEstimate 3 | attr_accessor :total_price, :unpriceable_items, :total_item_count 4 | 5 | def initialize(priced_items) 6 | @total_price = 0.0 7 | @unpriceable_items = [] 8 | @total_item_count = priced_items.count 9 | 10 | priced_items.each do |item| 11 | price = item.price 12 | if price 13 | @total_price += price 14 | else 15 | @unpriceable_items << item 16 | end 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/crowdring/regions.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module Regions 3 | @@region_hash = nil 4 | module_function 5 | 6 | def region_hash 7 | return @@region_hash unless @@region_hash.nil? 8 | data_file = File.join(File.dirname(__FILE__), '../..', 'data', 'regions.yml') 9 | @@region_hash = YAML.load(File.read(data_file)) 10 | @@region_hash 11 | end 12 | 13 | def tags_for(number) 14 | strs_for(number).map { |region| Tag.from_str('region:' + region)} 15 | end 16 | 17 | def strs_for(number) 18 | regions = region_hash[number.country.name.downcase] 19 | if regions && number.respond_to?(:area_code) 20 | regions[number.area_code.to_i] || [] 21 | else 22 | [] 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/crowdring/ring.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Ring 3 | include DataMapper::Resource 4 | include PhoneNumberFields 5 | 6 | property :id, Serial 7 | property :created_at, DateTime 8 | 9 | belongs_to :campaign 10 | belongs_to :ringer 11 | 12 | validates_with_method :ringer, :nonredundant_ring? 13 | 14 | def nonredundant_ring? 15 | five_seconds_ago = Time.now - 5 16 | not Ring.first(ringer: @ringer, :created_at.gt => five_seconds_ago) 17 | end 18 | 19 | def phone_number 20 | ringer.phone_number 21 | end 22 | 23 | def campaign_support 24 | campaign.title 25 | end 26 | 27 | def email 28 | ringer.email 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/crowdring/ring_observer.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class RingObserver 3 | include DataMapper::Observer 4 | 5 | observe Ring 6 | 7 | after :create do |r| 8 | if r.campaign.rings.all(ringer: r).count == 1 9 | RingObserver.statsd_increment("members_joined", r.campaign.slug) 10 | else 11 | RingObserver.statsd_increment("members_responded", r.campaign.slug) 12 | end 13 | RingObserver.push_notify(r) 14 | end 15 | 16 | def self.statsd_increment(stat, slug=nil) 17 | Crowdring.statsd.increment "#{stat}.count" 18 | Crowdring.statsd.increment "campaigns.#{slug}.#{stat}.count" if slug 19 | end 20 | 21 | def self.push_notify(m) 22 | data = { number: m.ringer.pretty_phone_number, 23 | ringer_count: m.campaign.ringers.count, 24 | ring_count: m.campaign.rings.count, 25 | goal: m.campaign.goal } 26 | 27 | begin 28 | Pusher.trigger(m.campaign.id.to_s, 'new', data) 29 | rescue SocketError 30 | p "SocketError: Failed to send message to Pusher" 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/crowdring/ringer.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class RingerTagging 3 | include DataMapper::Resource 4 | 5 | belongs_to :ringer, key: true 6 | belongs_to :tag, key: true 7 | end 8 | 9 | 10 | class Ringer 11 | include DataMapper::Resource 12 | include PhoneNumberFields 13 | 14 | property :id, Serial 15 | property :phone_number, String, unique: true 16 | property :created_at, DateTime 17 | property :subscribed, Boolean, default: true 18 | property :email, String, required: false 19 | 20 | has n, :ringer_taggings, constraint: :destroy 21 | has n, :tags, through: :ringer_taggings, constraint: :skip 22 | 23 | has n, :rings, constraint: :destroy 24 | 25 | validates_presence_of :phone_number 26 | # validates_with_method :phone_number, :valid_phone_number? 27 | 28 | after :create, :add_tags 29 | 30 | def self.unsubscribed 31 | all(subscribed: false) 32 | end 33 | 34 | def self.subscribed 35 | all(subscribed: true) 36 | end 37 | 38 | def phone_number=(number) 39 | super Phonie::Phone.parse(number).to_s 40 | end 41 | 42 | def self.from(number) 43 | norm_number = Phonie::Phone.parse(number).to_s 44 | self.first(phone_number: norm_number) || self.create(phone_number: norm_number) 45 | end 46 | 47 | def add_tags 48 | tags << Tag.from_str('area code:' + area_code) 49 | tags << Tag.from_str('country:' + country_name) 50 | tags.concat(Regions.tags_for(number)) 51 | save 52 | end 53 | 54 | def tag(tag) 55 | unless tags.include?(tag) 56 | tags << tag 57 | save 58 | end 59 | end 60 | 61 | def tagged?(tag) 62 | tags.include?(tag) 63 | end 64 | 65 | def unsubscribe 66 | update(subscribed: false) 67 | end 68 | 69 | def subscribe 70 | update(subscribed: true) 71 | end 72 | 73 | def subscribed? 74 | subscribed 75 | end 76 | 77 | def unsubscribed? 78 | !subscribed 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /lib/crowdring/short_code.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class ShortCode 3 | def self.parse(number) 4 | ShortCode.new if shortcode?(number) 5 | end 6 | 7 | def self.shortcode?(number) 8 | number == ENV['ROUTO_NUMBER'] 9 | end 10 | 11 | def country 12 | Phonie::Country.load 13 | Phonie::Country.find_by_name('Brazil') 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/crowdring/sms_prices.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | module SMSPrices 3 | @@prices = nil 4 | 5 | module_function 6 | 7 | def default_prices 8 | return @@prices unless @@prices.nil? 9 | 10 | data_file = File.join(File.dirname(__FILE__), '../..', 'data', 'sms_prices.yml') 11 | @@prices = YAML.load(File.read(data_file)) 12 | end 13 | 14 | def price_for(service, number, opts={}) 15 | prices = opts[:prices] || default_prices 16 | if prices[service] 17 | prices[service][number.country.char_3_code] 18 | end 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/crowdring/tag.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Tag 3 | include DataMapper::Resource 4 | @readable = {} 5 | 6 | property :group, String, key: true 7 | property :value, String, key: true 8 | property :hidden, Boolean, default: false 9 | 10 | has n, :ringer_taggings, constraint: :destroy 11 | has n, :constraints, constraint: :destroy 12 | 13 | before :save do |tag| 14 | tag.group = tag.group.downcase 15 | tag.value = tag.value.downcase 16 | end 17 | 18 | # str of format "type:value" 19 | def self.from_str(str, opts={hidden: false}) 20 | group, value = str.split(':') 21 | group = (group || '').downcase 22 | value = (value || '').downcase 23 | Tag.first(group: group, value: value) || Tag.create(group: group, value: value, hidden: opts[:hidden]) 24 | end 25 | 26 | def self.visible 27 | all(hidden: false) 28 | end 29 | 30 | def to_s 31 | "#{group}:#{value}" 32 | end 33 | 34 | def readable_group 35 | Tag.readable_group(self) 36 | end 37 | 38 | def readable_value 39 | Tag.readable_value(self) 40 | end 41 | 42 | def readable_s 43 | "#{readable_group}:#{readable_value}" 44 | end 45 | 46 | def self.readable_group(tag) 47 | @readable[tag.group] ? @readable[tag.group][:group] : tag.group 48 | end 49 | 50 | def self.readable_value(tag) 51 | @readable[tag.group] ? @readable[tag.group][:value].(tag.value) : tag.value 52 | end 53 | 54 | def self.register(group, readable_group, &block) 55 | @readable[group] = {group: readable_group, value: block} 56 | end 57 | 58 | register('rang', 'Missed Called') {|value| AssignedVoiceNumber.first(id: value).pretty_phone_number } 59 | register('campaign', 'Campaign Support') {|value| Campaign.get(value).title } 60 | register('ask_recipient', 'Received Ask') {|value| Ask.get(value).title } 61 | register('ask_respondent', 'Responded to Ask') {|value| Ask.get(value).title } 62 | end 63 | 64 | end -------------------------------------------------------------------------------- /lib/crowdring/tag_filter.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class TagFilter 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | 7 | has n, :constraints, constraint: :destroy 8 | 9 | def filter(items) 10 | items.select do |item| 11 | accept? item 12 | end 13 | end 14 | 15 | def constraints=(constraints) 16 | return if constraints.nil? 17 | 18 | constraints = constraints.map {|str| Constraint.from_str(str) } if constraints.first.is_a?(String) 19 | super constraints 20 | end 21 | 22 | def accept?(item) 23 | constraints.reduce(true) {|acc, con| acc and con.satisfied_by? item } 24 | end 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/caching_service.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class CachingService 3 | 4 | def initialize(service, seconds_to_expiry=300) 5 | @service = service 6 | @seconds_to_expiry = seconds_to_expiry 7 | refetch 8 | end 9 | 10 | def numbers 11 | refetch if expired? 12 | @numbers 13 | end 14 | 15 | def method_missing(method, *args, &block) 16 | @service.send(method, *args, &block) 17 | end 18 | 19 | private 20 | 21 | def expired? 22 | Time.now - @most_recent_updated > @seconds_to_expiry 23 | end 24 | 25 | def refetch 26 | @most_recent_updated = Time.now 27 | @numbers = @service.numbers 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/composite_service.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Crowdring 4 | 5 | class NoServiceError < StandardError; end 6 | 7 | class CompositeService 8 | include Singleton 9 | 10 | def initialize 11 | @services = {} 12 | @credentials = {} 13 | end 14 | 15 | def add(name, service, opts={}) 16 | opts = {cache: true}.merge opts 17 | @services[name] = opts[:cache] ? CachingService.new(service) : service 18 | @credentials[name] = opts[:credentials] 19 | end 20 | 21 | def credentials_for(name) 22 | @credentials[name] 23 | end 24 | 25 | def get(name) 26 | @services[name] 27 | end 28 | 29 | def reset 30 | @services = {} 31 | end 32 | 33 | def voice_numbers 34 | @services.values.select(&:voice?).map(&:numbers).flatten 35 | end 36 | 37 | def sms_numbers 38 | @services.values.select(&:sms?).map(&:numbers).flatten 39 | end 40 | 41 | def send_sms(params) 42 | service_name = service_for(:sms, params[:from]) 43 | service = @services[service_name] 44 | service.send_sms(params) 45 | end 46 | 47 | def broadcast(from, msg, to_numbers) 48 | return if to_numbers.empty? 49 | 50 | service_name = service_for(:sms, from) 51 | service = @services[service_name] 52 | 53 | to_numbers.each_slice(10) do |numbers| 54 | service.broadcast(from, msg, numbers) 55 | end 56 | end 57 | 58 | def service_for(type, number) 59 | service = @services.each {|name, service| return name if supports_number(service, number) && service.send("#{type}?") } 60 | raise NoServiceError, "No service handler for #{number}" 61 | end 62 | 63 | private 64 | 65 | def supports_number(service, number) 66 | service.numbers.include? number || service.number.include?(Phonie::Phone.parse(number).to_s) 67 | end 68 | end 69 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/kookoo_service.rb: -------------------------------------------------------------------------------- 1 | require 'builder' 2 | require 'net/http' 3 | 4 | module Crowdring 5 | class KooKooRequest 6 | attr_reader :from, :to 7 | 8 | def initialize(request) 9 | @to = request.GET['called_number'] 10 | @from = request.GET['cid'] 11 | end 12 | 13 | def callback? 14 | false 15 | end 16 | end 17 | 18 | class KooKooService < TelephonyService 19 | supports :voice, :sms 20 | request_handler KooKooRequest 21 | 22 | def initialize(api_key) 23 | @api_key = api_key 24 | @number = ['+911130715351'] 25 | end 26 | 27 | def build_response(from, commands) 28 | response = '' 29 | builder = Builder::XmlMarkup.new(indent: 2, target:response) 30 | builder.instruct! :xml 31 | builder.response do |r| 32 | commands.each do |c| 33 | case c[:cmd] 34 | when :reject 35 | r.hangup{} 36 | when :ivr 37 | p c 38 | r.hangup{} 39 | r.dial{"#{format_number(c[:to])}"} 40 | end 41 | end 42 | end 43 | response 44 | end 45 | 46 | 47 | def numbers 48 | @number 49 | end 50 | 51 | def send_sms(params) 52 | uri = URI('http://www.kookoo.in/outbound/outbound_sms.php') 53 | params = { message: params[:msg], phone_no: params[:to], api_key: @api_key } 54 | uri.query = URI.encode_www_form(params) 55 | res = Net::HTTP.get_response(uri) 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/logging_service.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class LoggingRequest 3 | attr_reader :from, :to, :message 4 | 5 | def initialize(request) 6 | @to = request.GET['to'] 7 | @from = request.GET['from'] 8 | @message = request.GET['msg'] if request.GET['msg'] 9 | end 10 | 11 | def callback? 12 | false 13 | end 14 | end 15 | 16 | class LoggingService < TelephonyService 17 | supports :voice, :sms 18 | request_handler LoggingRequest 19 | 20 | attr_reader :last_sms, :last_broadcast 21 | 22 | def initialize(numbers, opts={}) 23 | @numbers = numbers 24 | @do_output = opts[:output] 25 | end 26 | 27 | def build_response(from, commands) 28 | @last_response = "Reponse: From: #{from}, Commands: #{commands}" 29 | p @last_response if @do_output 30 | @last_response 31 | end 32 | 33 | def numbers 34 | @numbers 35 | end 36 | 37 | def send_sms(params) 38 | @last_sms = params 39 | p "Send SMS: #{params}" if @do_output 40 | end 41 | 42 | def broadcast(from, msg, to_numbers) 43 | @last_broadcast = {from: from, msg: msg, to_numbers: to_numbers } 44 | p "Broadcast: from: #{from}, msg: '#{msg}', to: #{to_numbers}" if @do_output 45 | end 46 | end 47 | 48 | class VoiceLoggingService < TelephonyService 49 | supports :voice 50 | request_handler LoggingRequest 51 | 52 | def initialize(numbers, opts={}) 53 | @numbers = numbers 54 | @do_output = opts[:output] 55 | end 56 | 57 | def build_response(from, commands) 58 | response = "" 59 | builder = Builder::XmlMarkup.new(indent: 2, target: response) 60 | builder.instruct! :xml 61 | builder.response do |r| 62 | commands.each do |c| 63 | case c[:cmd] 64 | when :sendsms 65 | r.sendsms c[:msg], to: c[:to] 66 | r.hangup{} 67 | when :reject 68 | r.playtext "I love kookoo" 69 | r.hangup{} 70 | end 71 | end 72 | end 73 | p response if @do_output 74 | end 75 | 76 | def numbers 77 | @numbers 78 | end 79 | 80 | 81 | end 82 | 83 | class SMSLoggingService < TelephonyService 84 | supports :sms 85 | 86 | def initialize(numbers, opts={}) 87 | @numbers = numbers 88 | @do_output = opts[:output] 89 | end 90 | 91 | def build_response(from, commands) 92 | @last_response = "Response: From: #{from}, Commands: #{commands}" 93 | p @last_response if @do_output 94 | @last_response 95 | end 96 | 97 | def numbers 98 | @numbers 99 | end 100 | 101 | def send_sms(params) 102 | @last_sms = params 103 | p "Send SMS: #{params}" if @do_output 104 | end 105 | 106 | def broadcast(from, msg, to_numbers) 107 | @last_broadcast = {from: from, msg: msg, to_numbers: to_numbers } 108 | p "Broadcast: from: #{from}, msg: '#{msg}', to: #{to_numbers}" if @do_output 109 | end 110 | end 111 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/netcore_service.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class NetcoreRequest 3 | attr_reader :from, :to 4 | 5 | def initialize(request, to) 6 | @to = to 7 | @from = request.GET['msisdn'] 8 | end 9 | 10 | def callback? 11 | false 12 | end 13 | end 14 | 15 | class NetcoreService < TelephonyService 16 | supports :voice, :sms 17 | request_handler(NetcoreRequest) {|inst| [inst.numbers.first]} 18 | 19 | def initialize(feedid, from, password) 20 | @feedid = feedid 21 | @from = from 22 | @password = password 23 | end 24 | 25 | def numbers 26 | ['+91'+ @from] 27 | end 28 | 29 | def send_sms(params) 30 | uri = URI('https://bulkpush.mytoday.com/BulkSms/SingleMsgApi') 31 | encoded_params = encode_params(params) 32 | uri.query = encoded_params 33 | response = send_request(uri) 34 | end 35 | 36 | private 37 | 38 | def encode_params(params) 39 | to = params[:to].sub('+', '') 40 | message = params[:text] 41 | request_params = { feedid: @feedid, password: @password, text: message, username: @from, to: to} 42 | URI.encode_www_form(request_params) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/nexmo_service.rb: -------------------------------------------------------------------------------- 1 | require 'nexmo' 2 | 3 | module Crowdring 4 | class NexmoRequest 5 | attr_reader :from, :to, :message 6 | 7 | def initialize(request) 8 | @from = request.GET['msisdn'] 9 | @to = request.GET['to'] 10 | @message = request.GET['text'] 11 | end 12 | 13 | def callback? 14 | false 15 | end 16 | end 17 | 18 | class NexmoService < TelephonyService 19 | supports :sms 20 | request_handler NexmoRequest 21 | 22 | def initialize(key, secret) 23 | @key = key 24 | @secret = secret 25 | @client = Nexmo::Client.new(key, secret) 26 | end 27 | 28 | def numbers 29 | @client.get_account_numbers(size:100).object["numbers"].map{|n| n["msisdn"]} 30 | end 31 | 32 | def build_response(from, commands) 33 | '' 34 | end 35 | 36 | def send_sms(params) 37 | @client.send_message to: params[:to], 38 | from: params[:from], 39 | text: params[:msg] 40 | end 41 | 42 | def broadcast(from, msg, to_numbers) 43 | params = {key: @key, secret: @secret} 44 | Resque.enqueue(NexmoBatchSendSms, params, from, msg, to_numbers) 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/plivo_service.rb: -------------------------------------------------------------------------------- 1 | require 'plivo' 2 | 3 | module Crowdring 4 | class PlivoRequest 5 | attr_reader :from, :to, :message 6 | 7 | def initialize(request) 8 | @from = request.POST['From'] 9 | @to = request.POST['To'] 10 | end 11 | 12 | def callback? 13 | false 14 | end 15 | end 16 | 17 | class PlivoService < TelephonyService 18 | supports :voice 19 | request_handler PlivoRequest 20 | 21 | def initialize(auth_id, auth_token) 22 | @rest_api = Plivo::RestAPI.new(auth_id, auth_token) 23 | end 24 | 25 | def build_response(from, commands) 26 | response = Plivo::Response.new 27 | commands.map do |c| 28 | case c[:cmd] 29 | when :reject 30 | response.addHangup(reason: 'busy') 31 | when :ivr 32 | c_id = c[:campaign_id] 33 | params = {to: "#{format_number(c[:to])}", from: "#{from}", 34 | answer_url: "#{ENV['SERVER_NAME']}/ivrs/plivo_hangup", 35 | answer_method: 'GET', 36 | hangup_url: "#{ENV['SERVER_NAME']}/ivrs/#{c_id}/trigger", 37 | hangup_method: 'POST'} 38 | res = @rest_api.make_call(params) 39 | response.addHangup(reason: 'busy') 40 | when :record 41 | response.addSpeak("#{c[:prompt]}") 42 | response.addRecord(action: "#{c[:voicemail].plivo_callback}", callbackUrl: "#{c[:voicemail].plivo_callback}") 43 | end 44 | end 45 | response.to_xml() 46 | end 47 | 48 | 49 | def numbers 50 | @rest_api.get_numbers[1]['objects'].map {|o| o['number']} 51 | end 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/routo_service.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class RoutoService < TelephonyService 3 | supports :sms 4 | 5 | def initialize(username, password, number) 6 | @user_name = username 7 | @password = password 8 | @number = number 9 | end 10 | 11 | def send_sms(params) 12 | uri = URI('https://smsc5.routotelecom.com/SMSsend') 13 | back_up_uri = URI('https://smsc6.routotelecom.com/SMSsend') 14 | 15 | encoded_params = encode_params(params) 16 | uri.query = encoded_params 17 | response = send_request(uri) 18 | if ['failed', 'sys_error', 'bad_operator'].include? response.body 19 | back_up_uri.query = encoded_params 20 | send_request(back_up_uri) 21 | end 22 | end 23 | 24 | def numbers 25 | [@number] 26 | end 27 | 28 | def broadcast 29 | params = {key: @username, secret: @password} 30 | Resque.enqueue(RoutoBatchSendSms, params, from, msg, to_numbers) 31 | end 32 | 33 | private 34 | 35 | def encode_params(params) 36 | to = params[:to].sub('+','') 37 | from = params[:from].sub('+','') 38 | message = params[:msg] 39 | 40 | params={ number: to, user: @user_name, pass: @password, message: message, ownnum: from} 41 | URI.encode_www_form(params) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/telephony_service.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class TelephonyService 3 | def sms? 4 | false 5 | end 6 | 7 | def voice? 8 | false 9 | end 10 | 11 | def self.supports(*types) 12 | types.each {|type| define_method("#{type}?") { true }} 13 | end 14 | 15 | def self.request_handler(klass, &extra_params) 16 | fun_block = extra_params ? proc {|request| klass.new(request, *extra_params.(self))} : proc {|request| klass.new(request)} 17 | define_method(:transform_request, fun_block) 18 | end 19 | 20 | def send_request(uri) 21 | http = Net::HTTP.new(uri.host, uri.port) 22 | http.use_ssl = true 23 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 24 | request = Net::HTTP::Get.new(uri.request_uri) 25 | http.request(request) 26 | end 27 | 28 | def format_number(num) 29 | num[0] == '+' ? num[1..-1]: num 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/tropo_service.rb: -------------------------------------------------------------------------------- 1 | require 'tropo-webapi-ruby' 2 | require 'net/http' 3 | require 'tropo-provisioning' 4 | 5 | module Crowdring 6 | class TropoRequest 7 | attr_reader :from, :to, :msg 8 | 9 | def initialize(request) 10 | session = Tropo::Generator.parse(request.body.read).session 11 | @is_callback = session.parameters? 12 | if @is_callback 13 | @from = session.parameters.from 14 | @to = session.parameters.to 15 | @msg = session.parameters.msg 16 | else 17 | @from = session.from.name 18 | @to = session.to.name 19 | end 20 | end 21 | 22 | def callback? 23 | @is_callback 24 | end 25 | end 26 | 27 | class TropoService < TelephonyService 28 | supports :voice, :sms 29 | request_handler TropoRequest 30 | 31 | def initialize(msg_token, app_id, username, password) 32 | @msg_token = msg_token 33 | @app_id = app_id 34 | @username = username 35 | @password = password 36 | end 37 | 38 | def process_callback(request) 39 | build_response(request.from, [{cmd: :sendsms, to: request.to, msg: request.msg}]) 40 | end 41 | 42 | def build_response(from, commands) 43 | response = Tropo::Generator.new do 44 | commands.each do |c| 45 | case c[:cmd] 46 | when :sendsms 47 | message(to: c[:to], network: 'SMS', channel: 'TEXT') do 48 | say c[:msg] 49 | end 50 | when :reject 51 | reject 52 | end 53 | end 54 | end 55 | response.response 56 | end 57 | 58 | def numbers 59 | provisioning = TropoProvisioning.new(@username, @password) 60 | numbers = provisioning.addresses(@app_id).select {|a| a.type == 'number' && a.smsEnabled } 61 | numbers.map(&:number) 62 | end 63 | 64 | def send_sms(params) 65 | uri = URI('http://api.tropo.com/1.0/sessions') 66 | params = { action: 'create', token: @msg_token, from: params[:from], to: params[:to], msg: params[:msg] } 67 | uri.query = URI.encode_www_form(params) 68 | res = Net::HTTP.get_response(uri) 69 | end 70 | 71 | def broadcast(from, msg, to_numbers) 72 | params = {msg_token: @msg_token, app_id: @app_id, 73 | username: @username, password: @password} 74 | Resque.enqueue(TropoBatchSendSms, params, from, msg, to_numbers) 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/twilio_service.rb: -------------------------------------------------------------------------------- 1 | 2 | module Crowdring 3 | class TwilioRequest 4 | attr_reader :from, :to, :message 5 | 6 | def initialize(request) 7 | params = request.POST 8 | @from = params['From'] 9 | @to = params['To'] 10 | @message = params['Body'] if params['Body'] 11 | end 12 | 13 | def callback? 14 | false 15 | end 16 | end 17 | 18 | class TwilioService < TelephonyService 19 | supports :voice, :sms 20 | request_handler TwilioRequest 21 | 22 | def initialize(account_sid, auth_token) 23 | @account_sid = account_sid 24 | @auth_token = auth_token 25 | @client = Twilio::REST::Client.new account_sid, auth_token 26 | end 27 | 28 | def build_response(from, commands) 29 | response = Twilio::TwiML::Response.new do |r| 30 | commands.each do |c| 31 | case c[:cmd] 32 | when :sendsms 33 | r.Sms c[:msg], from: from, to: c[:to] 34 | when :reject 35 | r.Reject reason: 'busy' 36 | end 37 | end 38 | end 39 | response.text 40 | end 41 | 42 | def numbers 43 | @client.account.incoming_phone_numbers.list.map(&:phone_number) 44 | end 45 | 46 | def send_sms(params) 47 | @client.account.sms.messages.create( 48 | to: params[:to], 49 | from: params[:from], 50 | body: params[:msg] 51 | ) 52 | end 53 | 54 | def broadcast(from, msg, to_numbers) 55 | params = {account_sid: @account_sid, auth_token: @auth_token} 56 | Resque.enqueue(TwilioBatchSendSms, params, from, msg, to_numbers) 57 | end 58 | 59 | end 60 | end -------------------------------------------------------------------------------- /lib/crowdring/telephony_services/voxeo_service.rb: -------------------------------------------------------------------------------- 1 | require 'tropo-provisioning' 2 | 3 | module Crowdring 4 | class VoxeoRequest 5 | attr_reader :from, :to, :msg 6 | 7 | def initialize(request) 8 | @from = request.GET['callerID'] 9 | @to = request.GET['calledID'] 10 | end 11 | 12 | def callback? 13 | false 14 | end 15 | end 16 | 17 | class VoxeoService < TelephonyService 18 | supports :voice 19 | request_handler VoxeoRequest 20 | 21 | def initialize(app_id, username, password) 22 | @app_id = app_id 23 | @username = username 24 | @password = password 25 | end 26 | 27 | def build_response(from, commands) 28 | response = '' 29 | response + commands.map do |c| 30 | case c[:cmd] 31 | when :reject 32 | '' 33 | when :record 34 | prompt = c[:prompt] || 'Please leave a message' 35 | "" 36 | end 37 | end.join('') + '' 38 | end 39 | 40 | def numbers 41 | provisioning = TropoProvisioning.new(@username, @password) 42 | numbers = provisioning.addresses(@app_id).select {|a| a.type == 'number' } 43 | numbers.map(&:number) 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/crowdring/text.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Text 3 | include DataMapper::Resource 4 | include PhoneNumberFields 5 | 6 | property :id, Serial 7 | property :created_at, DateTime 8 | property :message, Text, lazy: false 9 | 10 | belongs_to :ringer 11 | 12 | def phone_number 13 | ringer.phone_number 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/crowdring/time_service.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | module Crowdring 3 | class TimeService 4 | $LOG = Logger.new('log_file.log') 5 | 6 | def initialize(name, service) 7 | @name = name 8 | @service = service 9 | end 10 | 11 | def method_missing(method, *args, &block) 12 | start_time = Time.now 13 | result = @service.send(method, *args, &block) 14 | end_time = Time.now 15 | $LOG.debug("#{@name}: #{method}, #{end_time - start_time}s") 16 | result 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/crowdring/voicemail.rb: -------------------------------------------------------------------------------- 1 | module Crowdring 2 | class Voicemail 3 | include DataMapper::Resource 4 | 5 | property :id, Serial 6 | property :filename, String, required: false, length: 250 7 | 8 | belongs_to :ringer 9 | 10 | def filename 11 | super || "ftp://#{ENV['FTP_USER']}:#{ENV['FTP_PASSWORD']}@#{ENV['FTP_HOST']}/voicemails/#{id}.wav" 12 | end 13 | 14 | def plivo_callback 15 | "#{ENV['SERVER_NAME']}/voicemails/#{id}/plivo" 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/public/crowdringlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/therules/CrowdRing/2b84a243b0c89068c113a75b77657f91789b394e/lib/public/crowdringlogo.png -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/therules/CrowdRing/2b84a243b0c89068c113a75b77657f91789b394e/lib/public/favicon.ico -------------------------------------------------------------------------------- /lib/public/javascript/application.coffee: -------------------------------------------------------------------------------- 1 | new_ringer = (data) -> 2 | $("#campaign-ringers .total_count").text( 3 | data.ring_count + " Total Ring" + (if data.ring_count != 1 then "s" else "")) 4 | $("#campaign-ringers .unique_count").text( 5 | data.ringer_count + " Unique Ringer" + (if data.ringer_count != 1 then "s" else "")) 6 | $("#campaign-ringers .counts").effect("highlight", {color: '#63DB00'}, 500) 7 | $(".all-label .ui-button-text").text('All ' + data.ringer_count) 8 | 9 | delete_last = -> 10 | if $('#ringers-numbers li').length > 10 11 | $('#ringers-numbers li').last().remove() 12 | 13 | $("
  • #{data.number}
  • ").hide() 14 | .css('opacity', 0.0) 15 | .prependTo("ul.ringers") 16 | .slideDown(250) 17 | .animate({opacity: 1.0}, 250, delete_last) 18 | 19 | $('#progress-inner').css('width', "#{5 + (data.ringer_count / data.goal )* 100}%") 20 | $('#progress-inner .count').html(data.ringer_count) 21 | 22 | setupBroadcastTextArea = -> 23 | character_limit = 160 24 | $('#broadcast-text-area').bind('input', -> 25 | if $.trim($(this).val()) == "" || $(this).val().length > character_limit 26 | $('#broadcastbutton').attr('disabled', 'disabled') 27 | else 28 | $('#broadcastbutton').removeAttr('disabled')) 29 | 30 | 31 | setupTabs = -> 32 | $( "#tabs" ).tabs 33 | show: (event, ui) -> 34 | effect: "blind" 35 | selected_tab = $("#tabs").tabs("option","active") 36 | $.cookie("activated", selected_tab) 37 | active: $.cookie('activated') 38 | 39 | 40 | 41 | 42 | setupFilters = (buttons) -> 43 | $(buttons).buttonset() 44 | $("#{buttons} :radio").change -> 45 | context = $(buttons).parent().parent() 46 | $("#filter-options", context).slideUp() 47 | clicked = $(this) 48 | id = $(this).attr('id') 49 | id = id.substr(0, id.length-1) 50 | if $("##{id}-options").length == 1 51 | $("#filter-options", context).html($("##{id}-options").html()).slideDown() 52 | 53 | $("#filter-options :checkbox", context).change(-> 54 | str = 'country:' + $("#filter-options :checked", context).map((_, c) -> c.value).toArray().join('|') 55 | clicked.val(str) 56 | ) 57 | 58 | loadCampaign = (pusher, campaign, prev_channel) -> 59 | if prev_channel? 60 | pusher.unsubscribe(prev_channel) 61 | 62 | $("#campaign").empty() 63 | $("select.campaign-select").val(campaign) 64 | $("select").trigger("liszt:updated") 65 | 66 | channel_name = null 67 | if campaign != "" 68 | $.get("/campaign/#{campaign}", 69 | (data) -> 70 | $("#campaign").hide() 71 | .html(data) 72 | .slideDown(200) 73 | setupBroadcastTextArea() 74 | setupFilters('#broadcast-filter') 75 | setupFilters('#export-filter') 76 | setupTabs() 77 | ).error(-> window.location.replace '/') 78 | 79 | channel_name = campaign 80 | channel = pusher.subscribe(channel_name) 81 | channel.bind 'new', new_ringer 82 | window.onhashchange = -> loadCampaign(pusher, document.location.hash[1..], channel_name) 83 | 84 | 85 | tagFor = (tagItem, id) -> 86 | $("
    #{tagItem.label}
    ") 87 | removeFilter = (btn) -> 88 | btn.parent().remove() 89 | 90 | removeTag = (btn) -> 91 | btn.parent().remove() 92 | 93 | addTag = (parent, item, id) -> 94 | newTag = tagFor(item, id) 95 | $('button', newTag).click -> 96 | removeTag($(this)) 97 | newTag.appendTo($('#tag-filters', parent)) 98 | $('.tag-name', parent).val('') 99 | 100 | newFilterMessage = -> 101 | id = $('.filtered-message-template').length 102 | newDiv = $('#original-filtered-message-template-container div:first-child').clone() 103 | $('textarea[name="MESSAGE"]', newDiv).attr('name', "ask[message][filtered_messages][#{id}][message_text]") 104 | $('input[name="CONSTRAINT_TYPE"]', newDiv).attr('name', "constraint_type#{id}") 105 | 106 | $('input[id="HAS_ID"]', newDiv).attr('id', "has_#{id}") 107 | $('input[id="HAS_NOT_ID"]', newDiv).attr('id', "has_not_#{id}") 108 | $('label[for="HAS_ID"]', newDiv).attr('for', "has_#{id}") 109 | $('label[for="HAS_NOT_ID"]', newDiv).attr('for', "has_not_#{id}") 110 | $('#filtered-messages').append(newDiv) 111 | 112 | $('#remove-filter-button', newDiv).click -> 113 | removeFilter($(this)) 114 | 115 | fillTags(newDiv, id) 116 | $('.counter', newDiv).remove() 117 | 118 | 119 | 120 | fillTags = (div, id) -> 121 | $.getJSON '/tags/grouped_tags.json', (data) -> 122 | $.each data, (key, value) -> 123 | $('.tag-name', div) 124 | .append($("") 125 | .attr("label", key)) 126 | $.each value, (_, item) -> 127 | $('.tag-name optgroup:last', div) 128 | .append($("") 129 | .attr("value", item.value) 130 | .text(item.visible_label)) 131 | $('.tag-name', div).chosen() 132 | $('.tag-name', div).change (evt) -> 133 | selected = $(':selected', $(this)) 134 | constraints = $("input[name='constraint_type#{id}']:checked").val() 135 | label = (if constraints == 'has' then '' else 'Has not ') + selected.parent().attr('label') 136 | value = (if constraints == 'has' then '' else '!') + $(this).val() 137 | addTag $(this).parent(), {label: "#{label} : #{selected.text()}", value: "#{value}"}, id 138 | 139 | $ -> 140 | $('select.campaign-select').chosen() 141 | setTimeout((->$('.notice').slideUp('medium')), 3000) 142 | 143 | pusher = new Pusher(window.pusher_key) 144 | $("#campaign").empty() 145 | window.onhashchange = -> loadCampaign(pusher, document.location.hash[1..], null) 146 | window.onhashchange() 147 | $("select.campaign-select").change (evt) -> 148 | document.location.hash = $(this).val() 149 | 150 | 151 | $('#filtered-messages .filtered-message-template').each (index) -> fillTags($(this), index) 152 | 153 | window.removeTag = (btn) -> removeTag(btn) 154 | window.removeFilter = (btn) -> removeFilter(btn) 155 | window.addFilter = -> newFilterMessage() 156 | 157 | -------------------------------------------------------------------------------- /lib/public/javascript/ask_new.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('select.ask-type').change (evt) -> 3 | $('#voice-prompt').remove() 4 | if $(this).val() == 'voicemail_ask' 5 | $('#selected-ask-type').append("

    What should the ringer hear when they call?

    6 |