babysms 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ unless Object.const_defined?(:Bandwidth)
2
+ fail '`ruby-bandwidth` gem is required to use BandwidthAdapter'
3
+ end
4
+
5
+ module BabySMS
6
+ module Adapters
7
+ class BandwidthAdapter < BabySMS::Adapter
8
+ def initialize(user_id:, api_token:, api_secret:, from:)
9
+ super(from: from)
10
+
11
+ self.client = Bandwidth::Client.new(user_id: user_id,
12
+ api_token: api_token,
13
+ api_secret: api_secret)
14
+ end
15
+
16
+ def deliver(message)
17
+ response = Bandwidth::Message.create(client,
18
+ from: from,
19
+ to: message.to,
20
+ text: message.contents)
21
+ if response[:error]
22
+ raise BabySMS::FailedDelivery.new(response[:error].to_s, adapter: self)
23
+ end
24
+
25
+ response[:id]
26
+ end
27
+
28
+ class WebHook < BabySMS::WebHook
29
+ def process(app:, report:)
30
+ validate!(app.request)
31
+ message = BabySMS::Message.new(from: app.params['from'],
32
+ to: app.params['to'],
33
+ contents: app.params['text'],
34
+ uuid: app.params['id'])
35
+ report.incoming_message(message)
36
+ [200, { 'Content-Type' => 'text/plain' }, 'ok']
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ unless Object.const_defined?(:Nexmo)
2
+ fail '`nexmo` gem is required to use NexmoAdapter'
3
+ end
4
+
5
+ module BabySMS
6
+ module Adapters
7
+ class NexmoAdapter < BabySMS::Adapter
8
+ def initialize(api_key:, api_secret:, from:)
9
+ super(from: from)
10
+
11
+ self.client = Nexmo::Client.new(api_key: api_key, api_secret: api_secret)
12
+ end
13
+
14
+ def deliver(message)
15
+ # Thanks for being weird, Nexmo. Rejects numbers starting with "+"
16
+ response = client.sms.send(from: from.gsub(/\A\+/, ''),
17
+ to: message.to,
18
+ text: message.contents)
19
+ if response.messages.first.status != '0'
20
+ raise BabySMS::FailedDelivery.new(response.messages.first.error_text,
21
+ adapter: self)
22
+ end
23
+
24
+ response.messages.first.message_id
25
+ end
26
+
27
+ class WebHook < BabySMS::WebHook
28
+ def process(app:, report:)
29
+ message = BabySMS::Message.new(from: app.params['msisdn'],
30
+ to: app.params['to'],
31
+ contents: app.params['text'],
32
+ uuid: app.params['messageId'])
33
+ report.incoming_message(message)
34
+ [200, { 'Content-Type' => 'text/plain' }, 'ok']
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ unless Object.const_defined?(:Plivo)
2
+ fail '`plivo-ruby` gem is required to use PlivoAdapter'
3
+ end
4
+
5
+ module BabySMS
6
+ module Adapters
7
+ class PlivoAdapter < BabySMS::Adapter
8
+ def initialize(auth_id:, auth_token:, from:)
9
+ super(from: from)
10
+
11
+ self.client = Plivo::RestClient.new(auth_id, auth_token)
12
+ end
13
+
14
+ def deliver(message)
15
+ response = client.messages.create(from, [message.to], message.contents)
16
+ response.message_uuid
17
+ rescue PlivoRESTError => e
18
+ raise BabySMS::FailedDelivery.new(e.message, adapter: self)
19
+ end
20
+
21
+ class WebHook < BabySMS::WebHook
22
+ def process(app:, report:)
23
+ # 'Uuid' is the uuid. Also of note: NumMedia, and MediaContentType0, MediaUrl0
24
+ message = BabySMS::Message.new(from: app.params['From'],
25
+ to: app.params['To'],
26
+ contents: app.params['Text'],
27
+ uuid: app.params['Uuid'])
28
+ report.incoming_message(message)
29
+ [200, { 'Content-Type' => 'text/plain' }, 'ok']
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ unless Object.const_defined?(:Signalwire)
2
+ fail '`signalwire-ruby` gem is required to use SignalwireAdapter'
3
+ end
4
+
5
+ require 'signalwire/sdk'
6
+
7
+ module BabySMS
8
+ module Adapters
9
+ class SignalwireAdapter < BabySMS::Adapter
10
+ def initialize(from:, project:, token:, space_url:)
11
+ super(from: from)
12
+ self.client = Signalwire::REST::Client.new(project, token, signalwire_space_url: space_url)
13
+ end
14
+
15
+ def deliver(message)
16
+ result = client.messages.create(from: from,
17
+ to: message.to,
18
+ body: message.contents,
19
+ status_callback: web_hook.end_point)
20
+ result.sid
21
+ rescue Signalwire::REST::SignalwireError => e
22
+ raise BabySMS::FailedDelivery.new(e.message, adapter: self)
23
+ end
24
+
25
+ class WebHook < BabySMS::WebHook
26
+ def process(app:, report:)
27
+ # Of note: NumMedia, and MediaContentType0, MediaUrl0
28
+ message = BabySMS::Message.new(from: app.params['from'],
29
+ to: app.params['to'],
30
+ contents: app.params['body'],
31
+ uuid: app.params['id'])
32
+ report.incoming_message(message)
33
+ [200, { 'Content-Type' => 'application/xml' }, '<Response></Response>']
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ require 'babysms/web_hook'
2
+
3
+ require 'rainbow/refinement'
4
+ require 'json'
5
+
6
+ using Rainbow
7
+
8
+ module BabySMS
9
+ module Adapters
10
+ class TestAdapter < BabySMS::Adapter
11
+ attr_accessor :verbose
12
+ attr_accessor :fails
13
+ attr_accessor :outbox
14
+
15
+ def initialize(verbose: false, fails: false, from: '+1555-555-5555')
16
+ super(from: from)
17
+
18
+ self.verbose = verbose
19
+ self.fails = fails
20
+ self.outbox = []
21
+ end
22
+
23
+ def deliver(message)
24
+ if fails
25
+ raise BabySMS::FailedDelivery.new('intentional failure', adapter: self)
26
+ end
27
+
28
+ outbox.push(message)
29
+ if verbose
30
+ terminal_output = <<~"MSG"
31
+ #{"SMS:".bright.yellow} -> #{message.to.bright.yellow}:
32
+ >> #{message.contents.white}
33
+ MSG
34
+ $stderr.puts terminal_output
35
+ end
36
+
37
+ next_message_uuid
38
+ end
39
+
40
+ private
41
+
42
+ def next_message_uuid
43
+ @message_uuid ||= 0
44
+ @message_uuid += 1
45
+ @message_uuid.to_s
46
+ end
47
+
48
+ class WebHook < BabySMS::WebHook
49
+ def process(app:, report:)
50
+ json = JSON.parse(app.request.body.read)
51
+ message = BabySMS::Message.new(to: adapter.from,
52
+ from: json["from"],
53
+ contents: json["body"])
54
+ report.incoming_message(message)
55
+
56
+ [200, { "Content-Type" => "text/plain" }, 'ok']
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ unless Object.const_defined?(:Twilio)
2
+ fail '`twilio-ruby` gem is required to use TwilioAdapter'
3
+ end
4
+
5
+ module BabySMS
6
+ module Adapters
7
+ class TwilioAdapter < BabySMS::Adapter
8
+ def initialize(account_sid:, auth_token:, from:)
9
+ super(from: from)
10
+
11
+ self.client = Twilio::REST::Client.new(account_sid, auth_token)
12
+ end
13
+
14
+ def deliver(message)
15
+ result = client.api.account.messages.create(from: from,
16
+ to: message.to,
17
+ body: message.contents,
18
+ status_callback: web_hook.end_point)
19
+ result.sid
20
+ rescue Twilio::REST::TwilioError => e
21
+ raise BabySMS::FailedDelivery.new(e.message, adapter: self)
22
+ end
23
+
24
+ class WebHook < BabySMS::WebHook
25
+ def validate!(request)
26
+ validator = Twilio::Security::RequestValidator.new(adapter.client.auth_token)
27
+ unless validator.validate(end_point, request.params,
28
+ request.headers['X-Twilio-Signature'])
29
+ fail BabySMS::Unauthorized, 'twilio signature failed'
30
+ end
31
+ end
32
+
33
+ def process(app:, report:)
34
+ validate!(app.request)
35
+
36
+ # Of note: NumMedia, and MediaContentType0, MediaUrl0
37
+ message = BabySMS::Message.new(from: app.params['From'],
38
+ to: app.params['To'],
39
+ contents: app.params['Body'],
40
+ uuid: app.params['MessageSid'])
41
+ report.incoming_message(message)
42
+ [200, { 'Content-Type' => 'application/xml' }, '<Response></Response>']
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ module BabySMS
2
+ class Error < StandardError
3
+ end
4
+
5
+ # A FailedDelivery being raised means your message didn't get sent. This can be because of a
6
+ # single error, or multiple failed attempts.
7
+ #
8
+ # A FailedDelivery may contain multiple "sub-exceptions" in #exception that can be inspected
9
+ # to determine what happened.
10
+ class FailedDelivery < BabySMS::Error
11
+ attr_reader :exceptions
12
+ attr_reader :adapter
13
+
14
+ def initialize(*args, adapter: nil)
15
+ super(*args)
16
+
17
+ @adapter = adapter
18
+ @exceptions = [self]
19
+ end
20
+
21
+ def self.multiple(exceptions)
22
+ # We don't want to give a public interface to setting exceptions
23
+ BabySMS::FailedDelivery.new("multiple exceptions").itself do |result|
24
+ result.instance_variable_set(:@exceptions, exceptions)
25
+ end
26
+ end
27
+
28
+ def multiple?
29
+ exceptions.size > 1
30
+ end
31
+ end
32
+
33
+ class WebHookError < BabySMS::Error
34
+ end
35
+
36
+ # Post from provider we don't understand
37
+ class Malformed < BabySMS::WebHookError
38
+ end
39
+
40
+ # Provider signature verification fails
41
+ class Unauthorized < BabySMS::WebHookError
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ module BabySMS
2
+
3
+ # MailMan is the link between a Message and an Adapter. It basically has a strategy for
4
+ # choosing adapters, and attempting delivery until either the message is delivered, or
5
+ # all adapters have been tried.
6
+ #
7
+ # The boring strategy is :in_order, which is good for cases where you have a primary
8
+ # adapter, and one or more fallbacks.
9
+ #
10
+ # The alternative is :random, which sort of works in a load-balancing kind of way.
11
+
12
+ class MailMan
13
+ attr_reader :adapters
14
+ attr_reader :strategy
15
+
16
+ def initialize(adapters:, strategy:)
17
+ unless respond_to?(:"next_#{strategy}_adapter", true)
18
+ fail ArgumentError, "invalid strategy: #{strategy.inspect}"
19
+ end
20
+
21
+ @adapters = adapters.dup
22
+ @strategy = strategy
23
+ end
24
+
25
+ def deliver(message)
26
+ # If the message has a "from", we need to use that adapter, even if
27
+ # others are available
28
+ if message.from
29
+ specified = BabySMS::Adapter.for_number(message.from, pool: adapters)
30
+
31
+ if specified.nil?
32
+ fail BabySMS::Error, "`from:' not associated with an adapter: #{message.from}"
33
+ end
34
+ @adapters = [specified]
35
+ end
36
+
37
+ if @adapters.empty?
38
+ fail BabySMS::Error, 'no adapter configured'
39
+ end
40
+
41
+ # We collect and return all errors leading up to the (hopefully) successful delivery
42
+ failures = []
43
+ each_adapter do |adapter|
44
+ return BabySMS::Receipt.new(message_uuid: adapter.deliver(self),
45
+ message: message,
46
+ adapter: adapter,
47
+ exceptions: failures)
48
+ rescue BabySMS::FailedDelivery => e
49
+ failures.push(e)
50
+ end
51
+
52
+ raise BabySMS::FailedDelivery.multiple(failures)
53
+ end
54
+
55
+ private
56
+
57
+ def next_in_order_adapter
58
+ adapters.shift
59
+ end
60
+
61
+ def next_random_adapter
62
+ adapters.delete(adapters.sample)
63
+ end
64
+
65
+ def next_adapter
66
+ send(:"next_#{strategy}_adapter")
67
+ end
68
+
69
+ # returns each adapter in the order specified by the strategy. Mutates adapters Array.
70
+ def each_adapter(&block)
71
+ loop do
72
+ raise StopIteration if adapters.empty?
73
+ yield next_adapter
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,58 @@
1
+ module BabySMS
2
+ class InvalidMessage < BabySMS::Error
3
+ end
4
+
5
+ class Message
6
+ attr_accessor :to
7
+ attr_accessor :from
8
+ attr_accessor :contents
9
+
10
+ def initialize(to:, from: nil, contents:, uuid: nil)
11
+ @to = Phony.normalize(to)
12
+ @from = from
13
+ @contents = contents
14
+ end
15
+
16
+ def deliver(adapters: BabySMS.adapters, strategy: BabySMS.strategy)
17
+ validate!
18
+ BabySMS::MailMan.new(adapters: adapters, strategy: strategy).deliver(self)
19
+ end
20
+
21
+ # generates and delivers a reply to a message
22
+ def reply(contents:, adapters: BabySMS.adapters, strategy: BabySMS.strategy)
23
+ Message.new(to: from, from: to, contents: contents)
24
+ .deliver(adapters: adapters, strategy: strategy)
25
+ end
26
+
27
+ private
28
+
29
+ MAX_CONTENTS_LENGTH = 1600
30
+
31
+ def validate!
32
+ validate_to!
33
+ validate_contents!
34
+ end
35
+
36
+ def validate_to!
37
+ if to.nil? || to.empty?
38
+ fail BabySMS::InvalidMessage, 'no to:'
39
+ end
40
+
41
+ unless Phony.plausible?(to)
42
+ fail BabySMS::InvalidMessage, "implausible to: #{to}"
43
+ end
44
+ end
45
+
46
+ def validate_contents!
47
+ if contents.nil? || contents.empty?
48
+ fail BabySMS::InvalidMessage, 'no contents'
49
+ end
50
+
51
+ if contents.size > MAX_CONTENTS_LENGTH
52
+ msg = "contents too long (#{contents.size} vs max of #{MAX_CONTENTS_LENGTH}); " \
53
+ "contents: `#{contents}`"
54
+ fail BabySMS::InvalidMessage, msg
55
+ end
56
+ end
57
+ end
58
+ end