stupid_sms 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 102162e4e03f1a19d7c0789be9ba66bc3ea0872e
4
- data.tar.gz: 3af112e48be4bdd1667acc744b38e8fa75bae4fd
3
+ metadata.gz: a0b87a32085a57c4adefbdf28a65b4498a1912b7
4
+ data.tar.gz: f2d7876d4c69253cadae3b72ec958ce3e8311016
5
5
  SHA512:
6
- metadata.gz: 9ca8187ea5be40096b1082a9da92753140bd2b1ba5807d4bab91b03da0249cb5ec2b0fce1bb4d07cf75c0b945d19099196b5bcc2d0bfc57c635bfa602da5ec1d
7
- data.tar.gz: c8092d88c92a3c9a1d2def37ff394a22b3f17a4c31a2e73b08cc576bd499338cab4fe28ffe0545824cbed6c1a1f414a1872a83f27eec7a0eee91e224e3dcbda8
6
+ metadata.gz: bbad66bf6664694aa07e9dcd83e707258d6b700fc4defe16ccdce43bc3f8e69e6b263c461e7ec1b1349589572a006f7060cdae200b1d85e6692ea335df2662a3
7
+ data.tar.gz: 38d80f91f2f0ade3834b0a49193fbb5129e40656a10adaeb44bebe16fa00c24f8307062287a0510a272cafff788cc7c46d496ff1f26b1c5d42b24bec7c269ab9
data/.gitignore CHANGED
@@ -8,6 +8,9 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
 
11
+
12
+ .byebug_history
13
+
11
14
  .env
12
15
  test.csv
13
16
  test1.csv
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # StupidSMS
2
2
 
3
- Send text SMS using Twilio.
3
+ Send bulk SMS using Twilio.
4
4
 
5
5
  ## Installation
6
6
 
@@ -23,15 +23,18 @@ Or install it yourself as:
23
23
  __Configuration__
24
24
 
25
25
  ```ruby
26
- StupidSMS.from_number = '...' # or set env var TWILIO_ACCOUNT_SID
27
- StupidSMS.auth_token = '...' # or set env var TWILIO_AUTH_TOKEN
28
- StupidSMS.account_sid = '...' # or set env var TWILIO_NUMBER
26
+ StupidSMS.configure do |config|
27
+ config.from_number = '...' # or set env var TWILIO_ACCOUNT_SID
28
+ config.auth_token = '...' # or set env var TWILIO_AUTH_TOKEN
29
+ config.account_sid = '...' # or set env var TWILIO_NUMBER
30
+ config.country_code = 'SE' # two character country code (SE is the default)
31
+ end
29
32
  ```
30
33
 
31
34
  __Send one SMS__:
32
35
 
33
36
  ```ruby
34
- StupidSMS.send_message(recipient: recipient, body: body)
37
+ StupidSMS.send(recipient: recipient, body: body)
35
38
  ```
36
39
 
37
40
  __Send SMS in bulk__:
@@ -46,18 +49,48 @@ name `first_name`.
46
49
  `file.csv`:
47
50
 
48
51
  ```csv
49
- phone, first_name
50
- +46735000000, Jacob
52
+ phone,first_name
53
+ +46735000000,Jacob
51
54
  ```
52
55
 
53
56
  ```ruby
54
57
  csv = File.read('file.csv')
55
58
  template = 'Hi %{first_name}!'
56
- StupidSMS.send_bulk_message(csv_string: csv, template: template)
59
+ StupidSMS.send_in_bulk(csv_string: csv, template: template)
57
60
  ```
58
61
 
62
+ full options:
63
+
64
+ ```ruby
65
+ csv = File.read('file.csv')
66
+ StupidSMS.send_in_bulk(
67
+ csv_string: csv,
68
+ template: 'Hello World!',
69
+ delimiter: ',',
70
+ dry_run: false,
71
+ max_threads: 5
72
+ )
73
+ ```
74
+
75
+ __CLI__
76
+
59
77
  ```bash
60
- stupid_sms --csv testfile.csv --delimiter=, --template template.txt --dry-run=true
78
+ stupid_sms --csv testfile.csv --template template.txt --dry-run=true
79
+ ```
80
+
81
+
82
+ ```
83
+ Usage: stupid_sms [options]
84
+ --csv=file.csv CSV file path (a phone header column is required)
85
+ --template=template.txt Template file path (note: you need to escape % with %%)
86
+ --delimiter=; CSV delimiter (default: ,)
87
+ --country-code=se Country code (default: se)
88
+ --max-threads=5 Max parallel threads (default: 5)
89
+ --from-number="+46735000000" Twilio from number
90
+ --account-sid=se Twilio account SID
91
+ --auth-token=XXXYYYZZZ Twilio auth token
92
+ --[no-]dry-run Dry run (default: true)
93
+ -h, --help How to use
61
94
  ```
62
95
 
63
96
  ## Development
@@ -1,12 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/setup' # TODO: Remove!
4
3
  require 'stupid_sms'
5
4
 
5
+ # Defaults
6
6
  csv_filename = nil
7
7
  template_filename = nil
8
8
  dry_run = true
9
9
  delimiter = ','
10
+ country_code = 'se'
11
+ max_threads = StupidSMS::MAX_THREADS
12
+
13
+ # Twilio Auth
14
+ from_number = nil
15
+ auth_token = nil
16
+ account_sid = nil
10
17
 
11
18
  optparse = OptionParser.new do |parser|
12
19
  parser.on('--csv=file.csv', String, 'CSV file path (a phone header column is required)') do |value|
@@ -21,6 +28,26 @@ optparse = OptionParser.new do |parser|
21
28
  delimiter = value
22
29
  end
23
30
 
31
+ parser.on('--country-code=se', String, 'Country code (default: se)') do |value|
32
+ country_code = value
33
+ end
34
+
35
+ parser.on('--max-threads=5', Integer, "Max parallel threads (default: #{StupidSMS::MAX_THREADS})") do |value|
36
+ max_threads = value
37
+ end
38
+
39
+ parser.on('--from-number="+46735000000"', String, 'Twilio from number') do |value|
40
+ from_number = value.strip
41
+ end
42
+
43
+ parser.on('--account-sid=se', String, 'Twilio account SID') do |value|
44
+ account_sid = value.strip
45
+ end
46
+
47
+ parser.on('--auth-token=XXXYYYZZZ', String, 'Twilio auth token') do |value|
48
+ auth_token = value.strip
49
+ end
50
+
24
51
  parser.on('--[no-]dry-run', 'Dry run (default: true)') do |value|
25
52
  dry_run = value
26
53
  end
@@ -41,12 +68,20 @@ if template_filename.nil? || template_filename.strip.empty?
41
68
  raise OptionParser::MissingArgument, "'--template' 'Is required"
42
69
  end
43
70
 
71
+ StupidSMS.configure do |config|
72
+ config.from_number = from_number if from_number
73
+ config.auth_token = auth_token if auth_token
74
+ config.account_sid = account_sid if account_sid
75
+ config.country_code = country_code if country_code
76
+ end
77
+
44
78
  csv = File.read(csv_filename)
45
79
  template = File.read(template_filename)
46
80
 
47
- StupidSMS.send_bulk_message(
81
+ StupidSMS.send_in_bulk(
48
82
  csv_string: csv,
49
83
  delimiter: delimiter,
50
84
  template: template,
51
- dry_run: dry_run
85
+ dry_run: dry_run,
86
+ max_threads: max_threads
52
87
  )
@@ -1,90 +1,74 @@
1
- require 'stupid_sms/version'
2
- require 'stupid_sms/sms_client'
1
+ # Stlib
2
+ require 'thread'
3
3
 
4
- require 'global_phone'
4
+ # Gems
5
5
  require 'honey_format'
6
6
 
7
- module StupidSMS
8
- GlobalPhone.db_path = 'global_phone.json'
7
+ # Local
8
+ require 'stupid_sms/version'
9
+ require 'stupid_sms/sms_client'
10
+ require 'stupid_sms/sms'
11
+ require 'stupid_sms/phone'
12
+ require 'stupid_sms/process_queue'
9
13
 
10
- MAX_SMS_LENGTH = 160.0
14
+ module StupidSMS
15
+ MAX_THREADS = 5
11
16
 
12
- def self.send_bulk_message(csv_string:, delimiter: ',', template:, dry_run: false)
17
+ def self.send_in_bulk(csv_string:, template:, delimiter: ',', dry_run: false, max_threads: MAX_THREADS)
13
18
  csv = HoneyFormat::CSV.new(csv_string, delimiter: delimiter)
14
19
 
15
- longest_body = 0
16
- successfully_sent_count = 0
17
- csv.each_row do |person|
18
- phone = normalize_phone(person.phone)
19
- if invalid_phone?(phone)
20
- puts "[StupidSMS ERROR] Couldn't send SMS to phone number: #{person.phone}"
21
- next
22
- end
20
+ sms_queue = Queue.new
21
+ csv.each_row { |person| sms_queue << person }
23
22
 
24
- body = template % person.to_h
25
- longest_body = body.length if body.length > longest_body
26
-
27
- send_status = if dry_run
28
- true
29
- else
30
- send_message(recipient: phone, body: body)
31
- end
32
-
33
- if send_status
34
- sms_count = (body.length / MAX_SMS_LENGTH).floor + 1 # Number of SMS sent
35
- successfully_sent_count += sms_count
36
- end
37
- end
23
+ summary = ProcessQueue.call(
24
+ sms_queue: sms_queue,
25
+ template: template,
26
+ dry_run: dry_run,
27
+ max_threads: max_threads
28
+ )
38
29
 
39
30
  puts '============================'
31
+ puts "Thread count: #{max_threads}"
40
32
  puts "Dry run: #{dry_run}"
41
- puts "Longest sms: #{longest_body}"
42
- puts "Recipients: #{csv.rows.length}"
43
- puts "Sent messages: #{successfully_sent_count}"
33
+ puts "Longest sms: #{summary.fetch(:longest_body)}"
34
+ puts "Recipients: #{summary.fetch(:recipients_count)}"
35
+ puts "Failed count: #{summary.fetch(:failed_count)}"
36
+ puts "Sent messages: #{summary.fetch(:successfully_sent_count)}"
44
37
  end
45
38
 
46
- def self.send_message(recipient:, body:)
47
- client.send_message(from: from_number, to: recipient, body: body)
48
- true
49
- rescue Twilio::REST::RequestError => e # the user has unsubscribed
50
- puts "[StupidSMS ERROR] Twilio::REST::RequestError to: #{recipient} body: #{body}"
51
- puts e
52
- false
39
+ def send(crecipient:, body:)
40
+ SMS.send(recipient: recipient, body: body)
53
41
  end
54
42
 
55
- def self.invalid_phone?(phone)
56
- GlobalPhone.validate(phone, :se) ? false : true
43
+ class << self
44
+ attr_accessor :configuration
57
45
  end
58
46
 
59
- def self.normalize_phone(phone)
60
- GlobalPhone.normalize(phone, :se)
47
+ def self.configure
48
+ self.configuration ||= Configuration.new
49
+ yield(configuration)
61
50
  end
62
51
 
63
- def self.client
64
- @client ||= SMSClient.new(account_sid: account_sid, auth_token: auth_token)
65
- end
52
+ class Configuration
53
+ attr_accessor :from_number, :account_sid, :auth_token, :country_code
66
54
 
67
- def self.from_number=(from_number)
68
- @from_number = from_number
69
- end
70
-
71
- def self.auth_token=(auth_token)
72
- @auth_token = auth_token
73
- end
74
-
75
- def self.account_sid=(account_sid)
76
- @account_sid = account_sid
77
- end
55
+ def initialize
56
+ @from_number = nil
57
+ @auth_token = nil
58
+ @account_sid = nil
59
+ @country_code = :se
60
+ end
78
61
 
79
- def self.from_number
80
- @from_number || ENV.fetch('TWILIO_NUMBER')
81
- end
62
+ def from_number
63
+ @from_number || ENV.fetch('TWILIO_NUMBER')
64
+ end
82
65
 
83
- def self.auth_token
84
- @auth_token || ENV.fetch('TWILIO_AUTH_TOKEN')
85
- end
66
+ def auth_token
67
+ @auth_token || ENV.fetch('TWILIO_AUTH_TOKEN')
68
+ end
86
69
 
87
- def self.account_sid
88
- @account_sid || ENV.fetch('TWILIO_ACCOUNT_SID')
70
+ def account_sid
71
+ @account_sid || ENV.fetch('TWILIO_ACCOUNT_SID')
72
+ end
89
73
  end
90
74
  end
@@ -0,0 +1,15 @@
1
+ require 'global_phone'
2
+
3
+ module StupidSMS
4
+ module Phone
5
+ GlobalPhone.db_path = 'global_phone.json'
6
+
7
+ def self.invalid?(phone, country_code: StupidSMS.configuration.country_code)
8
+ GlobalPhone.validate(phone, country_code) ? false : true
9
+ end
10
+
11
+ def self.normalize(phone, country_code: StupidSMS.configuration.country_code)
12
+ GlobalPhone.normalize(phone, country_code)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,97 @@
1
+ module StupidSMS
2
+ class ProcessQueue
3
+ MAX_SMS_LENGTH = 160.0
4
+
5
+ def self.call(**args)
6
+ new(**args).call
7
+ end
8
+
9
+ def initialize(sms_queue:, template:, dry_run:, max_threads:)
10
+ @sms_queue = sms_queue
11
+ @template = template
12
+ @dry_run = dry_run
13
+ @max_threads = Integer(max_threads)
14
+
15
+ # Stats
16
+ @recipients_count = @sms_queue.length
17
+ end
18
+
19
+ def call
20
+ threads = @max_threads.times.map do
21
+ Thread.new do
22
+ # We need one client per Thread since the Twilio client is not thread safe
23
+ client = SMSClient.new
24
+
25
+ results = { send_count: 0, longest_body: 0, failed_count: 0 }
26
+ until @sms_queue.empty?
27
+ # TODO: Consider capturing all errors and only log them
28
+ sms_result = process_sms(
29
+ client: client,
30
+ person: @sms_queue.pop
31
+ )
32
+
33
+ results[:send_count] += sms_result.fetch(:send_count)
34
+ results[:failed_count] += 1 unless sms_result.fetch(:success)
35
+
36
+ body_length = sms_result.fetch(:length)
37
+ if body_length > results[:longest_body]
38
+ results[:longest_body] = body_length
39
+ end
40
+ end
41
+
42
+ results
43
+ end
44
+ end
45
+
46
+ threads.map(&:join) # Wait for each thread
47
+ calculate_summary(threads: threads)
48
+ end
49
+
50
+ private
51
+
52
+ def process_sms(client:, person:)
53
+ failed_result = { send_count: 0, length: 0, success: false }
54
+
55
+ phone = Phone.normalize(person.phone)
56
+ if Phone.invalid?(phone)
57
+ puts "[StupidSMS ERROR] Invalid phone number: #{person.phone}"
58
+ return failed_result
59
+ end
60
+
61
+ body = @template % person.to_h
62
+
63
+ send_status = if @dry_run
64
+ true
65
+ else
66
+ SMS.send(client: client, recipient: phone, body: body)
67
+ end
68
+
69
+ if send_status
70
+ sms_count = (body.length / (MAX_SMS_LENGTH - 1)).floor + 1 # Number of SMS sent
71
+ return { send_count: sms_count, length: body.length, success: true }
72
+ end
73
+
74
+ failed_result
75
+ end
76
+
77
+ def calculate_summary(threads:)
78
+ longest_body = 0
79
+ successfully_sent_count = 0
80
+ failed_count = 0
81
+
82
+ threads.map do |thread|
83
+ result = thread.value
84
+ longest_body = result[:longest_body] if result.fetch(:longest_body) > longest_body
85
+ successfully_sent_count += result.fetch(:send_count)
86
+ failed_count += result.fetch(:failed_count)
87
+ end
88
+
89
+ {
90
+ longest_body: longest_body,
91
+ successfully_sent_count: successfully_sent_count,
92
+ failed_count: failed_count,
93
+ recipients_count: @recipients_count
94
+ }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ module StupidSMS
2
+ module SMS
3
+ def self.send(client: SMSClient.new, recipient:, body:)
4
+ from_number = StupidSMS.configuration.from_number
5
+ client.send_message(from: from_number, to: recipient, body: body)
6
+ true
7
+ rescue Twilio::REST::RequestError => e # the user has unsubscribed
8
+ puts "[StupidSMS ERROR] Twilio::REST::RequestError to: #{recipient} body: #{body}"
9
+ puts e
10
+ false
11
+ end
12
+ end
13
+ end
@@ -2,7 +2,7 @@ require 'twilio-ruby'
2
2
 
3
3
  module StupidSMS
4
4
  class SMSClient
5
- def initialize(account_sid:, auth_token:)
5
+ def initialize(account_sid: StupidSMS.configuration.account_sid, auth_token: StupidSMS.configuration.auth_token)
6
6
  @client = Twilio::REST::Client.new(account_sid, auth_token)
7
7
  end
8
8
 
@@ -1,3 +1,3 @@
1
1
  module StupidSMS
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{Send SMS with ease.}
13
13
  spec.description = %q{Send SMS with ease, either one by one or in bulk.}
14
- spec.homepage = 'https://github.com/young-skilled/stupid_sms'
14
+ spec.homepage = 'https://github.com/buren/stupid_sms'
15
15
  spec.license = 'MIT'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stupid_sms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacob Burenstam
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-24 00:00:00.000000000 Z
11
+ date: 2017-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: twilio-ruby
@@ -112,10 +112,13 @@ files:
112
112
  - exe/stupid_sms
113
113
  - global_phone.json
114
114
  - lib/stupid_sms.rb
115
+ - lib/stupid_sms/phone.rb
116
+ - lib/stupid_sms/process_queue.rb
117
+ - lib/stupid_sms/sms.rb
115
118
  - lib/stupid_sms/sms_client.rb
116
119
  - lib/stupid_sms/version.rb
117
120
  - stupid_sms.gemspec
118
- homepage: https://github.com/young-skilled/stupid_sms
121
+ homepage: https://github.com/buren/stupid_sms
119
122
  licenses:
120
123
  - MIT
121
124
  metadata: {}
@@ -135,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
138
  version: '0'
136
139
  requirements: []
137
140
  rubyforge_project:
138
- rubygems_version: 2.5.1
141
+ rubygems_version: 2.6.11
139
142
  signing_key:
140
143
  specification_version: 4
141
144
  summary: Send SMS with ease.