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 +4 -4
- data/.gitignore +3 -0
- data/README.md +42 -9
- data/exe/stupid_sms +38 -3
- data/lib/stupid_sms.rb +49 -65
- data/lib/stupid_sms/phone.rb +15 -0
- data/lib/stupid_sms/process_queue.rb +97 -0
- data/lib/stupid_sms/sms.rb +13 -0
- data/lib/stupid_sms/sms_client.rb +1 -1
- data/lib/stupid_sms/version.rb +1 -1
- data/stupid_sms.gemspec +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0b87a32085a57c4adefbdf28a65b4498a1912b7
|
4
|
+
data.tar.gz: f2d7876d4c69253cadae3b72ec958ce3e8311016
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bbad66bf6664694aa07e9dcd83e707258d6b700fc4defe16ccdce43bc3f8e69e6b263c461e7ec1b1349589572a006f7060cdae200b1d85e6692ea335df2662a3
|
7
|
+
data.tar.gz: 38d80f91f2f0ade3834b0a49193fbb5129e40656a10adaeb44bebe16fa00c24f8307062287a0510a272cafff788cc7c46d496ff1f26b1c5d42b24bec7c269ab9
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# StupidSMS
|
2
2
|
|
3
|
-
Send
|
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.
|
27
|
-
|
28
|
-
|
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.
|
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,
|
50
|
-
+46735000000,
|
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.
|
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 --
|
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
|
data/exe/stupid_sms
CHANGED
@@ -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.
|
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
|
)
|
data/lib/stupid_sms.rb
CHANGED
@@ -1,90 +1,74 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
# Stlib
|
2
|
+
require 'thread'
|
3
3
|
|
4
|
-
|
4
|
+
# Gems
|
5
5
|
require 'honey_format'
|
6
6
|
|
7
|
-
|
8
|
-
|
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
|
-
|
14
|
+
module StupidSMS
|
15
|
+
MAX_THREADS = 5
|
11
16
|
|
12
|
-
def self.
|
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
|
-
|
16
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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: #{
|
43
|
-
puts "
|
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
|
47
|
-
|
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
|
-
|
56
|
-
|
43
|
+
class << self
|
44
|
+
attr_accessor :configuration
|
57
45
|
end
|
58
46
|
|
59
|
-
def self.
|
60
|
-
|
47
|
+
def self.configure
|
48
|
+
self.configuration ||= Configuration.new
|
49
|
+
yield(configuration)
|
61
50
|
end
|
62
51
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
52
|
+
class Configuration
|
53
|
+
attr_accessor :from_number, :account_sid, :auth_token, :country_code
|
66
54
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
62
|
+
def from_number
|
63
|
+
@from_number || ENV.fetch('TWILIO_NUMBER')
|
64
|
+
end
|
82
65
|
|
83
|
-
|
84
|
-
|
85
|
-
|
66
|
+
def auth_token
|
67
|
+
@auth_token || ENV.fetch('TWILIO_AUTH_TOKEN')
|
68
|
+
end
|
86
69
|
|
87
|
-
|
88
|
-
|
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
|
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
|
|
data/lib/stupid_sms/version.rb
CHANGED
data/stupid_sms.gemspec
CHANGED
@@ -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/
|
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.
|
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:
|
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/
|
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.
|
141
|
+
rubygems_version: 2.6.11
|
139
142
|
signing_key:
|
140
143
|
specification_version: 4
|
141
144
|
summary: Send SMS with ease.
|