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 +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.
|