babysms 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.0
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in babysms.gemspec
6
+ gemspec
@@ -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.
@@ -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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,2 @@
1
+ require 'babysms/web_application'
2
+ run BabySMS::WebApplication.new
@@ -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