babysms 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop-airbnb.yml +1716 -0
- data/.rubocop.yml +75 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/Rakefile +6 -0
- data/babysms.gemspec +51 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config.ru +2 -0
- data/lib/babysms.rb +52 -0
- data/lib/babysms/adapter.rb +60 -0
- data/lib/babysms/adapters/bandwidth_adapter.rb +41 -0
- data/lib/babysms/adapters/nexmo_adapter.rb +39 -0
- data/lib/babysms/adapters/plivo_adapter.rb +34 -0
- data/lib/babysms/adapters/signalwire_adapter.rb +38 -0
- data/lib/babysms/adapters/test_adapter.rb +61 -0
- data/lib/babysms/adapters/twilio_adapter.rb +47 -0
- data/lib/babysms/errors.rb +43 -0
- data/lib/babysms/mail_man.rb +77 -0
- data/lib/babysms/message.rb +58 -0
- data/lib/babysms/receipt.rb +19 -0
- data/lib/babysms/report.rb +15 -0
- data/lib/babysms/version.rb +3 -0
- data/lib/babysms/web_application.rb +87 -0
- data/lib/babysms/web_hook.rb +21 -0
- metadata +243 -0
data/.rubocop.yml
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- .rubocop-airbnb.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
TargetRubyVersion: 2.6.3
|
6
|
+
|
7
|
+
Rails:
|
8
|
+
Enabled: true
|
9
|
+
|
10
|
+
Layout/EmptyLines:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
# Restore Rubocop default
|
14
|
+
Layout/DotPosition:
|
15
|
+
EnforcedStyle: leading
|
16
|
+
|
17
|
+
# The inbuilt style does not accommodate for:
|
18
|
+
#
|
19
|
+
# var = begin
|
20
|
+
# something
|
21
|
+
# rescue
|
22
|
+
# something
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# without indenting the entire block and looking dumb.
|
26
|
+
|
27
|
+
Layout/RescueEnsureAlignment:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
# In every instance where I've removed these extra lines, the result was less readable.
|
31
|
+
Layout/EmptyLinesAroundBlockBody:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Layout/EmptyLinesAroundModuleBody:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
# Restore Rubocop default
|
38
|
+
Layout/MultilineMethodCallIndentation:
|
39
|
+
EnforcedStyle: aligned
|
40
|
+
|
41
|
+
Layout/CaseIndentation:
|
42
|
+
EnforcedStyle: end
|
43
|
+
|
44
|
+
Layout/EndAlignment:
|
45
|
+
EnforcedStyleAlignWith: variable
|
46
|
+
|
47
|
+
# Allow both %x() and backticks
|
48
|
+
Style/CommandLiteral:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
# $? and $: are more idiomatic than importing English and using $MAGIC_GLOBALS.
|
52
|
+
# If it's more esoteric, comment.
|
53
|
+
Style/SpecialGlobalVars:
|
54
|
+
Enabled: false
|
55
|
+
|
56
|
+
# Direct opposite of Prettier, allow either.
|
57
|
+
Style/TrailingCommaInHashLiteral:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
# Direct opposite of Prettier, allow either
|
61
|
+
Style/TrailingCommaInArrayLiteral:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
# Allow top-level mixins. Rightfully used in stubs/tasks
|
65
|
+
Style/MixinUsage:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
# Generated code/stubs as well as the rest of the code use what's
|
69
|
+
# appropriate according to the context
|
70
|
+
Style/PercentLiteralDelimiters:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
# "warn" and $stderr.puts have different connotations
|
74
|
+
Style/StderrPuts:
|
75
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Mike A. Owens
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
# BabySMS
|
2
|
+
|
3
|
+
BabySMS is a Ruby interface to a decent collection of SMS service providers. With it, you can
|
4
|
+
fire and forget SMS messages, or script entire two-way SMS conversations. All without tying
|
5
|
+
yourself to a single provider.
|
6
|
+
|
7
|
+
The interface stays the same, So you can comparison-shop for prices or reliability.
|
8
|
+
|
9
|
+
BabySMS allows multiple services to be used at once for failover, or random selection of a
|
10
|
+
provider for load-balancing (against throttling.)
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add these lines to your application's `Gemfile`:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'YourAPIProviderGem' # see below
|
19
|
+
gem 'babysms', '~> 0.5'
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Basic
|
25
|
+
```ruby
|
26
|
+
BabySMS.adapter = BabySMS::Adapters::SomeAdapter.new(...)
|
27
|
+
|
28
|
+
BabySMS::Message.new(to: '+1 555-555-5555', contents: 'Hello, World!').deliver
|
29
|
+
```
|
30
|
+
|
31
|
+
If you need just basic outgoing functionality, that's all there is to it.
|
32
|
+
|
33
|
+
### Multiple Adapters for Reliability
|
34
|
+
```ruby
|
35
|
+
BabySMS.adapters = [
|
36
|
+
BabySMS::Adapters::HooliAdapter.new(...),
|
37
|
+
BabySMS::Adapters::PiedPiperAdapter.new(...)
|
38
|
+
]
|
39
|
+
|
40
|
+
BabySMS.strategy = :in_order
|
41
|
+
BabySMS::Message.new(...).deliver
|
42
|
+
```
|
43
|
+
|
44
|
+
This will try `HooliAdapter` first, but if it fails, `PiedPiperAdapter`. Changing the `strategy`
|
45
|
+
to `:random` will go through the list randomly, providing something akin to load-balancing.
|
46
|
+
|
47
|
+
## Sending Messages
|
48
|
+
|
49
|
+
`BabySMS::Message#deliver` can directly accept an `adapters` keyword argument that overrides the
|
50
|
+
global adapters list in `BabySMS.adapter[s]`.
|
51
|
+
|
52
|
+
BabySMS will attempt delivery with each adapter until one succeeds, or will raise
|
53
|
+
`BabySMS::FailedDelivery` once exhausted.
|
54
|
+
|
55
|
+
`#deliver` returns a `BabySMS::Receipt`. This allows you to inspect (and log) adapters that failed
|
56
|
+
along the way to delivery. Use `#exceptions?` to discover if something is failing, and
|
57
|
+
`#exceptions` to see the list of failed deliveries before the successful one.
|
58
|
+
|
59
|
+
## Receiving Messages
|
60
|
+
|
61
|
+
*Note: This functionality is currently being implemented.*
|
62
|
+
|
63
|
+
The API providers report incoming messages to you by accessing a publicly-available URL. This means
|
64
|
+
you have to have a web server running on the open internet to receive and process incoming messages
|
65
|
+
|
66
|
+
`BabySMS::WebApplication` is a Rack (Sinatra) application that does just that.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# config.ru
|
70
|
+
require 'babysms'
|
71
|
+
require 'babysms/web_application'
|
72
|
+
|
73
|
+
# Replace this with your service provider as listed below
|
74
|
+
BabySMS.adapter = BabySMS::TestAdapter.new
|
75
|
+
|
76
|
+
# Publicly-accessible URL
|
77
|
+
BabySMS.web_hook_root = "https://example.com/_babysms"
|
78
|
+
|
79
|
+
BabySMS.inbox = BabySMS::Inbox.new do
|
80
|
+
receive do |message|
|
81
|
+
puts "Incoming message from #{message.from}:\n"
|
82
|
+
puts " > #{message.contents}"
|
83
|
+
|
84
|
+
# Replies with same number/adapter, regardless of strategy
|
85
|
+
message.reply(contents: "Thank you for your message.")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
run BabySMS::WebApplication.new
|
90
|
+
```
|
91
|
+
|
92
|
+
You'll need to secure the web-application against the general public, allowing only your SMS
|
93
|
+
providers to access them.
|
94
|
+
|
95
|
+
*TODO: Explain exactly how to do that with Rack*
|
96
|
+
|
97
|
+
## Rails Integration
|
98
|
+
|
99
|
+
*TODO: Write about mounting BabySMS::WebApplication in routes.rb, sending messages via ActiveJob,
|
100
|
+
etc*
|
101
|
+
|
102
|
+
## Adapters
|
103
|
+
|
104
|
+
Each adapter uses the terminology of the service provider for configuration. E.g., `api_key`
|
105
|
+
vs. `auth_token`, etc. Other than `TestAdapter`, they all share a common `from` initialization
|
106
|
+
parameter to specify the number you're sending from.
|
107
|
+
|
108
|
+
You can configure multiple adapters (even instances of the same adapter class with different
|
109
|
+
configurations) by assigning `BabySMS.adapters` or using the `adapters:` argument to
|
110
|
+
`Message#deliver`.
|
111
|
+
|
112
|
+
Adapters require external gems to interface with their respective services. You will have to
|
113
|
+
require these gems yourself, and *before* `babysms`: BabySMS enables functionality by feature-
|
114
|
+
detecting these gems.
|
115
|
+
|
116
|
+
Putting it before `babysms` in your `Gemfile` should work.
|
117
|
+
|
118
|
+
|
119
|
+
### TestAdapter
|
120
|
+
|
121
|
+
The built-in testing adapter. It stores outgoing texts in its `outbox`. It prints colorful lines
|
122
|
+
to stderr when a message is sent for development purposes. It is the default adapter.
|
123
|
+
|
124
|
+
`verbose:` controls if it displays the outgoing SMS to stderr. `fails:` determines if deliveries
|
125
|
+
will fail.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
BabySMS::Adapters::TestAdapter.new(verbose: true, fails: false, from: '15555555555')
|
129
|
+
```
|
130
|
+
|
131
|
+
|
132
|
+
### BandwidthAdapter
|
133
|
+
|
134
|
+
Requires the `ruby-bandwidth` gem.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
BabySMS::Adapters::BandwidthAdapter.new(user_id:, api_token:, api_secret:, from:)
|
138
|
+
```
|
139
|
+
|
140
|
+
|
141
|
+
### TwilioAdapter
|
142
|
+
|
143
|
+
Requires the `twilio-ruby` gem.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
BabySMS::Adapters::TwilioAdapter.new(account_sid:, auth_token:, from:)
|
147
|
+
```
|
148
|
+
|
149
|
+
|
150
|
+
### NexmoAdapter
|
151
|
+
|
152
|
+
Requires the `nexmo` gem.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
BabySMS::Adapters::NexmoAdapter.new(api_key:, api_secret:, from:)
|
156
|
+
```
|
157
|
+
|
158
|
+
|
159
|
+
### PlivoAdapter
|
160
|
+
|
161
|
+
Requires the `plivo-ruby` gem.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
BabySMS::Adapters::PlivoAdapter.new(auth_id:, auth_token:, from:)
|
165
|
+
```
|
166
|
+
|
167
|
+
|
168
|
+
### SignalwireAdapter
|
169
|
+
|
170
|
+
Requires the `signalwire` gem.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
# Notice the capitalization, to be consistent with their SDK naming
|
174
|
+
BabySMS::Adapters::SignalwireAdapter.new(from:, project:, token:, space_url:)
|
175
|
+
```
|
176
|
+
|
177
|
+
## Credits
|
178
|
+
|
179
|
+
BabySMS was written by Mike A. Owens <mike@filespanker.com>
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
The gem is available as open source under the terms of the
|
184
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/babysms.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "babysms/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "babysms"
|
7
|
+
spec.version = BabySMS::VERSION
|
8
|
+
spec.authors = ["Mike A. Owens"]
|
9
|
+
spec.email = ["mike@filespanker.com"]
|
10
|
+
|
11
|
+
spec.summary = "Simple Rails interface to a SMS APIs"
|
12
|
+
spec.description = "Functionality to send SMS via Twilio, Nexmo, etc."
|
13
|
+
spec.homepage = "https://github.com/mieko/babysms"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/mieko/babysms"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/mieko/babysms/CHANGELOG.md"
|
22
|
+
else
|
23
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
24
|
+
"public gem pushes."
|
25
|
+
end
|
26
|
+
|
27
|
+
# Specify which files should be added to the gem when it is released.
|
28
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
29
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
30
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
31
|
+
end
|
32
|
+
|
33
|
+
spec.bindir = "exe"
|
34
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
35
|
+
spec.require_paths = ["lib"]
|
36
|
+
|
37
|
+
spec.add_dependency "activesupport", ">= 5.2.1"
|
38
|
+
spec.add_dependency "rainbow", ">= 3.0"
|
39
|
+
spec.add_dependency "phony", ">= 0.14"
|
40
|
+
spec.add_dependency "sinatra", "~> 2.0"
|
41
|
+
|
42
|
+
spec.add_development_dependency "bundler", ">= 1.17"
|
43
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
44
|
+
spec.add_development_dependency "rspec", ">= 3.0"
|
45
|
+
|
46
|
+
spec.add_development_dependency "rack-test", "~> 1.1.0"
|
47
|
+
spec.add_development_dependency "twilio-ruby", ">= 5.18.0"
|
48
|
+
spec.add_development_dependency "nexmo", ">= 5.5.0"
|
49
|
+
spec.add_development_dependency "ruby-bandwidth", ">= 1.0.20"
|
50
|
+
spec.add_development_dependency "plivo", ">= 4.1.6"
|
51
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "babysms"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/config.ru
ADDED
data/lib/babysms.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'babysms/version'
|
2
|
+
require 'babysms/adapter'
|
3
|
+
require 'babysms/receipt'
|
4
|
+
require 'babysms/errors'
|
5
|
+
require 'babysms/mail_man'
|
6
|
+
require 'babysms/message'
|
7
|
+
|
8
|
+
require 'babysms/adapters/test_adapter'
|
9
|
+
require 'babysms/adapters/bandwidth_adapter' if Object.const_defined?(:Bandwidth)
|
10
|
+
require 'babysms/adapters/nexmo_adapter' if Object.const_defined?(:Nexmo)
|
11
|
+
require 'babysms/adapters/plivo_adapter' if Object.const_defined?(:Plivo)
|
12
|
+
require 'babysms/adapters/twilio_adapter' if Object.const_defined?(:Twilio)
|
13
|
+
require 'babysms/adapters/signalwire_adapter' if Object.const_defined?(:Signalwire)
|
14
|
+
|
15
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
16
|
+
require 'phony'
|
17
|
+
|
18
|
+
module BabySMS
|
19
|
+
mattr_accessor :web_hook_root, default: "http://example.com/web_hooks/"
|
20
|
+
mattr_accessor :strategy, default: :in_order
|
21
|
+
mattr_accessor :adapters, default: [BabySMS::Adapters::TestAdapter.new(verbose: true)]
|
22
|
+
|
23
|
+
# Shorthand to set a list of one adapter in the simple case
|
24
|
+
def self.adapter=(adapter)
|
25
|
+
self.adapters = [adapter]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.adapter
|
29
|
+
fail "can't use #adapters= with multiple adapters" unless adapters.size == 1
|
30
|
+
adapters.first
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.available_adapters(test: false, cache: true)
|
34
|
+
# Allow cache-busting
|
35
|
+
@found_adapters = nil if !cache
|
36
|
+
|
37
|
+
@found_adapters ||= begin
|
38
|
+
BabySMS::Adapters.constants.map do |constant|
|
39
|
+
cls = BabySMS::Adapters.const_get(constant)
|
40
|
+
if cls.is_a?(Class) && cls.ancestors.include?(BabySMS::Adapter)
|
41
|
+
cls
|
42
|
+
end
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
if test
|
47
|
+
@found_adapters
|
48
|
+
else
|
49
|
+
@found_adapters - [BabySMS::Adapters::TestAdapter]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module BabySMS
|
2
|
+
class Adapter
|
3
|
+
attr_reader :from
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(from:)
|
7
|
+
@from = Phony.normalize(from)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Locates an adapter instance associated with a phone number
|
11
|
+
def self.for_number(number, pool: BabySMS.adapters)
|
12
|
+
number = Phony.normalize(number)
|
13
|
+
pool.find { |adapter| adapter.from == number }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.for_adapter_id(adapter_id, pool: BabySMS.adapters)
|
17
|
+
pool.find { |adapter| adapter.adapter_id == adapter_id }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.adapter_name
|
21
|
+
bare_name = name.split('::').last
|
22
|
+
bare_name.gsub(/Adapter\z/, '').downcase
|
23
|
+
end
|
24
|
+
|
25
|
+
# e.g., "twilio"
|
26
|
+
def adapter_name
|
27
|
+
self.class.adapter_name
|
28
|
+
end
|
29
|
+
|
30
|
+
# e.g., "15555555555@twilio"
|
31
|
+
def adapter_id
|
32
|
+
"#{from}@#{adapter_name}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# if the adapter supports web hooks, it'll have a nested class
|
36
|
+
# called "WebHook". This returns the class
|
37
|
+
def web_hook_class
|
38
|
+
self.class.const_get(:WebHook)
|
39
|
+
end
|
40
|
+
|
41
|
+
def web_hook?
|
42
|
+
!!web_hook_class
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an instance of the web hook handler, if it exists
|
46
|
+
def web_hook
|
47
|
+
unless instance_variable_defined?(:@web_hook)
|
48
|
+
@web_hook = if (cls = web_hook_class)
|
49
|
+
cls.new(self)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@web_hook
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
attr_writer :client
|
59
|
+
end
|
60
|
+
end
|