Skip to content
This repository has been archived by the owner on Nov 13, 2021. It is now read-only.

Support multiple pinpoint configs #25

Merged
merged 13 commits into from May 27, 2020
26 changes: 13 additions & 13 deletions Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
identity-telephony (0.0.17)
identity-telephony (0.1.0)
aws-sdk-pinpoint
aws-sdk-pinpointsmsvoice
i18n
Expand Down Expand Up @@ -33,20 +33,20 @@ GEM
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
ast (2.4.0)
aws-eventstream (1.0.3)
aws-partitions (1.269.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-eventstream (1.1.0)
aws-partitions (1.320.0)
aws-sdk-core (3.96.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-pinpoint (1.32.0)
aws-sdk-pinpoint (1.39.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-pinpointsmsvoice (1.13.0)
aws-sdk-pinpointsmsvoice (1.15.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2)
builder (3.2.3)
byebug (11.0.1)
Expand All @@ -59,9 +59,9 @@ GEM
erubi (1.8.0)
ethon (0.12.0)
ffi (>= 1.3.0)
faraday (1.0.0)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
ffi (1.12.1)
ffi (1.12.2)
hashdiff (1.0.0)
highline (2.0.2)
i18n (1.6.0)
Expand Down Expand Up @@ -144,11 +144,11 @@ GEM
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.20.3)
thread_safe (0.3.6)
twilio-ruby (5.31.3)
faraday (~> 1.0.0)
twilio-ruby (5.35.0)
faraday (>= 0.9, < 2.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
typhoeus (1.3.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
Expand Down
24 changes: 14 additions & 10 deletions README.md
Expand Up @@ -18,27 +18,31 @@ Telephony.config do |c|
c.twilio.timeout = 5 # This is optional. The default is `5`
c.twilio.record_voice = false # This is optional. The default is `false`

c.pinpoint.sms.region = 'us-west-2' # This is optional, us-west-2 is the default
c.pinpoint.sms.application_id = 'fake-pinpoint-application-id-sms'
c.pinpoint.sms.shortcode = '123456'
c.pinpoint.sms.longcode_pool = ['+12223334444', '+15556667777']
c.pinpoint.add_sms_config do |sms|
sms.region = 'us-west-2' # This is optional, us-west-2 is the default
sms.application_id = 'fake-pinpoint-application-id-sms'
sms.shortcode = '123456'
end

c.pinpoint.voice.region = 'us-west-2' # This is optional, us-west-2 is the default
c.pinpoint.voice.longcode_pool = ['+12223334444', '+15556667777']
c.pinpoint.add_voice_config do |voice|
voice.region = 'us-west-2' # This is optional, us-west-2 is the default
voice.longcode_pool = ['+12223334444', '+15556667777']
end
end
```

# Error handling

If the gem encounters a problem it will raise an instance of `Telephony::TelephonyError`.
If the gem encounters a problem return a `Response` object with `success?` false and an `error` property.
This object can be used to render an error to the user like so:

```ruby

def create
Telephony.end_authentication_otp(to: to, otp: otp, expiration: expiration, channel: :sms)
rescue Telephony::TelephonyError => err
flash[:error] = error.friendly_message
response = Telephony.end_authentication_otp(to: to, otp: otp, expiration: expiration, channel: :sms)
return if response.success?

flash[:error] = response.error.friendly_message
render :new
end
```
Expand Down
57 changes: 42 additions & 15 deletions lib/telephony/configuration.rb
Expand Up @@ -14,17 +14,50 @@ module Telephony
keyword_init: true,
)

PinpointConfiguration = Struct.new(
:sms,
:voice,
keyword_init: true,
)
class PinpointConfiguration
attr_reader :sms_configs, :voice_configs

def initialize
@sms_configs = []
@voice_configs = []
end

# Adds a new SMS configuration
# @yieldparam [PinpointSmsConfiguration] sms an sms configuration object configure
def add_sms_config
raise 'missing sms configuration block' unless block_given?
sms = PinpointSmsConfiguration.new(region: 'us-west-2')
yield sms
sms_configs << sms
sms
end

# Adds a new voice configuration
# @yieldparam [PinpointVoiceConfiguration] voice a voice configuration object configure
def add_voice_config
raise 'missing voice configuration block' unless block_given?
voice = PinpointVoiceConfiguration.new(region: 'us-west-2')
yield voice
voice_configs << voice
voice
end
end

PINPOINT_CONFIGURATION_NAMES = [
:region, :access_key_id, :secret_access_key, :longcode_pool,
:region, :access_key_id, :secret_access_key,
:credential_role_arn, :credential_role_session_name, :credential_external_id
].freeze
PinpointVoiceConfiguration = Struct.new(*PINPOINT_CONFIGURATION_NAMES)
PinpointSmsConfiguration = Struct.new(:application_id, :shortcode, *PINPOINT_CONFIGURATION_NAMES)
PinpointVoiceConfiguration = Struct.new(
:longcode_pool,
*PINPOINT_CONFIGURATION_NAMES,
keyword_init: true,
)
PinpointSmsConfiguration = Struct.new(
:application_id,
:shortcode,
*PINPOINT_CONFIGURATION_NAMES,
keyword_init: true,
)

class Configuration
attr_writer :adapter
Expand All @@ -39,13 +72,7 @@ def initialize
timeout: 5,
record_voice: false,
)
pinpoint_voice = PinpointVoiceConfiguration.new(
region: 'us-west-2',
)
pinpoint_sms = PinpointSmsConfiguration.new(
region: 'us-west-2',
)
@pinpoint = PinpointConfiguration.new(voice: pinpoint_voice, sms: pinpoint_sms)
@pinpoint = PinpointConfiguration.new
end
# rubocop:enable Metrics/MethodLength

Expand Down
33 changes: 13 additions & 20 deletions lib/telephony/pinpoint/aws_credential_builder.rb
@@ -1,45 +1,38 @@
module Telephony
module Pinpoint
class AwsCredentialBuilder
def initialize(channel)
@channel = channel
attr_reader :config

# @param [Telephony::PinpointVoiceConfiguration, Telephony::PinpointSmsConfiguration] config
def initialize(config)
@config = config
end

def call
if pinpoint_config.credential_role_arn && pinpoint_config.credential_role_session_name
if config.credential_role_arn && config.credential_role_session_name
build_assumed_role_credential
elsif pinpoint_config.access_key_id && pinpoint_config.secret_access_key
elsif config.access_key_id && config.secret_access_key
build_access_key_credential
end
end

private

attr_reader :channel

def build_assumed_role_credential
Aws::AssumeRoleCredentials.new(
role_arn: pinpoint_config.credential_role_arn,
role_session_name: pinpoint_config.credential_role_session_name,
external_id: pinpoint_config.credential_external_id,
client: Aws::STS::Client.new(region: pinpoint_config.region),
role_arn: config.credential_role_arn,
role_session_name: config.credential_role_session_name,
external_id: config.credential_external_id,
client: Aws::STS::Client.new(region: config.region),
)
end

def build_access_key_credential
Aws::Credentials.new(
pinpoint_config.access_key_id,
pinpoint_config.secret_access_key,
config.access_key_id,
config.secret_access_key,
)
end

def pinpoint_config
if channel.to_sym == :sms
Telephony.config.pinpoint.sms
elsif channel.to_sym == :voice
Telephony.config.pinpoint.voice
end
end
end
end
end
114 changes: 74 additions & 40 deletions lib/telephony/pinpoint/sms_sender.rb
@@ -1,6 +1,8 @@
module Telephony
module Pinpoint
class SmsSender
ClientConfig = Struct.new(:client, :config)

ERROR_HASH = {
'DUPLICATE' => DuplicateEndpointError,
'OPT_OUT' => OptOutError,
Expand All @@ -11,45 +13,71 @@ class SmsSender
'UNKNOWN_FAILURE' => UnknownFailureError,
}.freeze

# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def send(message:, to:)
@pinpoint_response = pinpoint_client.send_messages(
application_id: Telephony.config.pinpoint.sms.application_id,
message_request: {
addresses: {
to => {
channel_type: 'SMS',
last_response = nil
client_configs.each do |client_config|
pinpoint_response = client_config.client.send_messages(
application_id: client_config.config.application_id,
message_request: {
addresses: {
to => {
channel_type: 'SMS',
},
},
},
message_configuration: {
sms_message: {
body: message,
message_type: 'TRANSACTIONAL',
origination_number: Telephony.config.pinpoint.sms.shortcode,
message_configuration: {
sms_message: {
body: message,
message_type: 'TRANSACTIONAL',
origination_number: client_config.config.shortcode,
},
},
},
},
)
response
)

response = build_response(pinpoint_response)
return response if response.success?
notify_pinpoint_failover(
error: response.error,
region: client_config.config.region,
extra: response.extra,
)
last_response = response
end
last_response
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize

private
# @api private
# An array of (client, config) pairs
# @return [Array<ClientConfig>]
def client_configs
@client_configs ||= Telephony.config.pinpoint.sms_configs.map do |sms_config|
credentials = AwsCredentialBuilder.new(sms_config).call
args = { region: sms_config.region, retry_limit: 0 }
args[:credentials] = credentials unless credentials.nil?

attr_reader :pinpoint_response
ClientConfig.new(
build_client(args),
sms_config,
)
end
end

def pinpoint_client
credentials = AwsCredentialBuilder.new(:sms).call
args = { region: Telephony.config.pinpoint.sms.region, retry_limit: 1 }
args[:credentials] = credentials unless credentials.nil?
@pinpoint_client ||= Aws::Pinpoint::Client.new(args)
# @api private
def build_client(args)
Aws::Pinpoint::Client.new(args)
end

private

# rubocop:disable Metrics/MethodLength
def response
def build_response(pinpoint_response)
message_response_result = pinpoint_response.message_response.result.values.first

Response.new(
success: success?,
error: error,
success: success?(message_response_result),
error: error(message_response_result),
extra: {
request_id: pinpoint_response.message_response.request_id,
delivery_status: message_response_result.delivery_status,
Expand All @@ -61,24 +89,30 @@ def response
end
# rubocop:enable Metrics/MethodLength

def success?
@success ||= message_response_result.delivery_status == 'SUCCESSFUL'
def success?(message_response_result)
message_response_result.delivery_status == 'SUCCESSFUL'
end

def error
return nil if success?
def error(message_response_result)
return nil if success?(message_response_result)

@error ||= begin
status_code = message_response_result.status_code
delivery_status = message_response_result.delivery_status
exception_message = "Pinpoint Error: #{delivery_status} - #{status_code}"
exception_class = ERROR_HASH[delivery_status] || TelephonyError
exception_class.new(exception_message)
end
status_code = message_response_result.status_code
delivery_status = message_response_result.delivery_status
exception_message = "Pinpoint Error: #{delivery_status} - #{status_code}"
exception_class = ERROR_HASH[delivery_status] || TelephonyError
exception_class.new(exception_message)
end

def message_response_result
@message_repsonse ||= pinpoint_response.message_response.result.values.first
def notify_pinpoint_failover(error:, region:, extra:)
response = Response.new(
success: false,
error: error,
extra: extra.merge(
failover: true,
region: region,
),
)
Telephony.config.logger.warn(response.to_h.to_json)
end
end
end
Expand Down