stupid_sms 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.