Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FYST-1089] refactor twilio service #4944

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7ebf9aa
change the structure of the credentials file (just dev for now) to ha…
jnf Nov 1, 2024
a914b96
an idea of how to evolve the twilio service, presented as a v2 for ea…
jnf Nov 1, 2024
505cdfc
tweak how the multi tenant service fetches twilio creds
jnf Nov 4, 2024
623d5dd
bring the v2 version of TwilioClient into the original object so i ca…
jnf Nov 5, 2024
ae26344
updates TwilioService specs to conform to new API
jnf Nov 5, 2024
c12385b
don't need to clear a class variable in the mock twilio object anymore
jnf Nov 5, 2024
530dcf4
instantiate TwilioService as a gyr messenger unless caller says other…
jnf Nov 5, 2024
eacfb9d
updates callsite and specs for twilio webhooks controller; this is th…
jnf Nov 5, 2024
ceee3f7
updates callsites and specs for sms sending in the bulk signup
jnf Nov 5, 2024
077654d
updates the other #get_metadata callsite and associated specs
jnf Nov 5, 2024
64d8be2
update the twilio status backfiller to the new TwilioService api. the…
jnf Nov 5, 2024
2a1a4cd
updates verification code job to use new TwilioService api. also upda…
jnf Nov 5, 2024
ee2f22b
updated TwilioService callsites in the CtcSignupMessage, but it doesn…
jnf Nov 5, 2024
bb05cb2
updates Signup to use updated TwilioService api. updates specs too. i…
jnf Nov 5, 2024
40e4dbd
updates TwilioService callsite in the send_outgoing_text_message_job …
jnf Nov 5, 2024
db5747d
updates the gyr incoming text message job/handling/specs to use the u…
jnf Nov 5, 2024
6812f1e
update code in and specs for TextMessageVerificationCodeService to us…
jnf Nov 5, 2024
4bd6e7b
updates the client login spec to work with the updated TwilioService
jnf Nov 5, 2024
288ac36
updates the login spec to work with the new TwilioService api
jnf Nov 5, 2024
64c28b9
removes the 'v2' twilio service as the changes proposed have been ref…
jnf Nov 5, 2024
4f92fea
updates the hub outbound call from to use TwilioService rather than r…
jnf Nov 7, 2024
fa01ca8
updates TwilioService, MultiTenantService, dev credentials, and assoc…
jnf Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/services/multi_tenant_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ def backtax_years(now = DateTime.now)
filing_years(now).without(current_tax_year)
end

def twilio_creds
{
account_sid: EnvironmentCredentials.dig(:twilio, :account_sid),
auth_token: EnvironmentCredentials.dig(:twilio, :auth_token),
messaging_service_id: EnvironmentCredentials.dig(:twilio, :messaging_services, service_type)
}
end

class << self
def ctc
new(:ctc)
Expand Down
172 changes: 85 additions & 87 deletions app/services/twilio_service.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
class TwilioService
FAILED_STATUSES = ["undelivered", "failed", "delivery_unknown", "twilio_error"].freeze
SUCCESSFUL_STATUSES = ["sent", "delivered"].freeze
IN_PROGRESS_STATUSES = ["accepted", "queued", "sending", nil].freeze
FAILED_STATUSES = %w(undelivered failed delivery_unknown twilio_error)
SUCCESSFUL_STATUSES = %w(sent delivered)
IN_PROGRESS_STATUSES = %w(accepted queued sending) << nil
ALL_KNOWN_STATUSES = FAILED_STATUSES + SUCCESSFUL_STATUSES + IN_PROGRESS_STATUSES
ORDERED_STATUSES = [nil, "twilio_error"] + %w[
ORDERED_STATUSES = %w(
twilio_error
queued
accepted
sending
Expand All @@ -12,104 +13,101 @@ class TwilioService
delivered
undelivered
failed
].freeze
).unshift(nil) # why do we need nil in this list, and why must it be first?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[DUST] i assume you're planning to remove this comment before merging? would prefer not to leave stuff like this around the codebase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! for sure. i literally don't know the answer but would like to. 😅


class << self
def valid_request?(request)
validator = Twilio::Security::RequestValidator.new(EnvironmentCredentials.dig(:twilio, :auth_token))
validator.validate(
request.url,
request.POST,
request.headers["X-Twilio-Signature"],
)
end
attr_reader :client, :messaging_service_sid, :auth_token

def fetch_attachment(url)
begin
response = Net::HTTP.get_response(URI(url)) # first we get a redirect from Twilio to S3
response = Net::HTTP.get_response(URI(response['location'])) # then we get a redirect from S3 to S3
response = Net::HTTP.get_response(URI(response['location'])) # finally we should get a 200 OK with the file
filename_from_s3 = response['content-disposition'].split('"').last # S3 gives us the original filename
{
filename: filename_from_s3,
body: response.body,
}
rescue ArgumentError => e
Rails.logger.error("Error getting attachment from Twilio: #{url}: #{response&.code}: #{response&.to_hash}")
{
filename: "unknown-file",
body: nil
}
end
end
def initialize(service_type)
creds = MultiTenantService.new(service_type).twilio_creds
@messaging_service_sid = creds[:messaging_service_id]
@auth_token = creds[:auth_token]
@client = Twilio::REST::Client.new(creds[:account_sid], auth_token)
end

def parse_attachments(params)
num_media = params["NumMedia"].to_i
def send_text_message(to:, body:, status_callback: nil, outgoing_text_message: nil)
arguments = {
messaging_service_sid: ENV['MESSAGING_SERVICE_SID'] || messaging_service_sid, # why do we check the environment for this??
to: to,
body: body
}
arguments[:status_callback] = status_callback if status_callback.present?

(0..(num_media - 1)).map do |i|
content_type = params["MediaContentType#{i}"]
attachment = fetch_attachment(params["MediaUrl#{i}"])
DatadogApi.increment("twilio.outgoing_text_messages.sent")

if FileTypeAllowedValidator.mime_types(Document).include?(content_type) && !attachment[:body].empty?
{
content_type: params["MediaContentType#{i}"],
filename: attachment[:filename],
body: attachment[:body]
}
else
{
content_type: "text/plain;charset=UTF-8",
filename: "invalid-#{attachment[:filename]}.txt",
body: <<~TEXT
Unusable file with unknown or unsupported file type.
File name: #{attachment[:filename]}
File type: #{content_type}
File size: #{attachment[:body].size} bytes
TEXT
}
end
client.messages.create(**arguments)
rescue Twilio::REST::RestError => e
status_key =
if outgoing_text_message.is_a?(OutgoingMessageStatus)
:delivery_status
else
:twilio_status
end
end
outgoing_text_message&.update(status_key => "twilio_error")

def client
@@_client ||= Twilio::REST::Client.new(
EnvironmentCredentials.dig(:twilio, :account_sid),
EnvironmentCredentials.dig(:twilio, :auth_token)
)
unless e.code == 21211 # Invalid 'To' Phone Number https://www.twilio.com/docs/api/errors/21211
raise # should we include the original exception here (e)??
end

def send_text_message(to:, body:, status_callback: nil, outgoing_text_message: nil)
arguments = {
messaging_service_sid: ENV['MESSAGING_SERVICE_SID'] || EnvironmentCredentials.dig(:twilio, :messaging_service_sid),
to: to,
body: body
}
arguments[:status_callback] = status_callback if status_callback.present?
nil
end

DatadogApi.increment("twilio.outgoing_text_messages.sent")
def get_metadata(phone_number:)
client.lookups.v2.phone_numbers(phone_number).fetch(fields: 'line_type_intelligence').line_type_intelligence
rescue Twilio::REST::RestError
{}
end

client.messages.create(**arguments)
rescue Twilio::REST::RestError => e
status_key =
if outgoing_text_message.is_a?(OutgoingMessageStatus)
:delivery_status
else
:twilio_status
end
outgoing_text_message&.update(status_key => "twilio_error")
def valid_request?(request)
validator = Twilio::Security::RequestValidator.new(auth_token)
validator.validate(
request.url,
request.POST,
request.headers["X-Twilio-Signature"],
)
end

unless e.code == 21211 # Invalid 'To' Phone Number https://www.twilio.com/docs/api/errors/21211
raise
end
def fetch_attachment(url)
response = Net::HTTP.get_response(URI(url)) # first we get a redirect from Twilio to S3
response = Net::HTTP.get_response(URI(response['location'])) # then we get a redirect from S3 to S3
response = Net::HTTP.get_response(URI(response['location'])) # finally we should get a 200 OK with the file
filename_from_s3 = response['content-disposition'].split('"').last # S3 gives us the original filename
{
filename: filename_from_s3,
body: response.body,
}
rescue ArgumentError => e
Rails.logger.error("Error getting attachment from Twilio: #{url}: #{response&.code}: #{response&.to_hash}")
{
filename: "unknown-file",
body: nil
}
end

nil
end
def parse_attachments(params)
num_media = params["NumMedia"].to_i

def get_metadata(phone_number:)
client.lookups.v2.phone_numbers(phone_number).fetch(fields: 'line_type_intelligence').line_type_intelligence
(0...num_media).map do |i|
content_type = params["MediaContentType#{i}"]
attachment = fetch_attachment(params["MediaUrl#{i}"])

rescue Twilio::REST::RestError
{}
if FileTypeAllowedValidator.mime_types(Document).include?(content_type) && !attachment[:body].empty?
{
content_type: params["MediaContentType#{i}"],
filename: attachment[:filename],
body: attachment[:body]
}
else
{
content_type: "text/plain;charset=UTF-8",
filename: "invalid-#{attachment[:filename]}.txt",
body: <<~TEXT
Unusable file with unknown or unsupported file type.
File name: #{attachment[:filename]}
File type: #{content_type}
File size: #{attachment[:body].size} bytes
TEXT
}
end
end
end
end
112 changes: 112 additions & 0 deletions app/services/twilio_service_v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
class TwilioServiceV2
FAILED_STATUSES = %w(undelivered failed delivery_unknown twilio_error)
SUCCESSFUL_STATUSES = %w(sent delivered)
IN_PROGRESS_STATUSES = %w(accepted queued sending) << nil
ALL_KNOWN_STATUSES = FAILED_STATUSES + SUCCESSFUL_STATUSES + IN_PROGRESS_STATUSES
ORDERED_STATUSES = %w(
twilio_error
queued
accepted
sending
sent
delivery_unknown
delivered
undelivered
failed
).unshift(nil) # why do we need nil in this list, and why must it be first?

attr_reader :client, :messaging_service_sid

def initialize(service_type)
mts = MultiTenantService.new(service_type)
@messaging_service_sid = mts.twilio_creds[:messaging_service_id]
@client = Twilio::REST::Client.new(mts.twilio_creds[:account_sid], mts.twilio_creds[:auth_token])
end

def send_text_message(to:, body:, status_callback: nil, outgoing_text_message: nil)
arguments = {
messaging_service_sid: ENV['MESSAGING_SERVICE_SID'] || messaging_service_sid, # why do we check the environment for this??
to: to,
body: body
}
arguments[:status_callback] = status_callback if status_callback.present?

DatadogApi.increment("twilio.outgoing_text_messages.sent")

client.messages.create(**arguments)
rescue Twilio::REST::RestError => e
status_key =
if outgoing_text_message.is_a?(OutgoingMessageStatus)
:delivery_status
else
:twilio_status
end
outgoing_text_message&.update(status_key => "twilio_error")

unless e.code == 21211 # Invalid 'To' Phone Number https://www.twilio.com/docs/api/errors/21211
raise # should we include the original exception here (e)??
end

nil
end

def get_metadata(phone_number:)
client.lookups.v2.phone_numbers(phone_number).fetch(fields: 'line_type_intelligence').line_type_intelligence
rescue Twilio::REST::RestError
{}
end

def valid_request?(request)
validator = Twilio::Security::RequestValidator.new(EnvironmentCredentials.dig(:twilio, :auth_token))
validator.validate(
request.url,
request.POST,
request.headers["X-Twilio-Signature"],
)
end

def fetch_attachment(url)
response = Net::HTTP.get_response(URI(url)) # first we get a redirect from Twilio to S3
response = Net::HTTP.get_response(URI(response['location'])) # then we get a redirect from S3 to S3
response = Net::HTTP.get_response(URI(response['location'])) # finally we should get a 200 OK with the file
filename_from_s3 = response['content-disposition'].split('"').last # S3 gives us the original filename
{
filename: filename_from_s3,
body: response.body,
}
rescue ArgumentError => e
Rails.logger.error("Error getting attachment from Twilio: #{url}: #{response&.code}: #{response&.to_hash}")
{
filename: "unknown-file",
body: nil
}
end

def parse_attachments(params)
num_media = params["NumMedia"].to_i

(0...num_media).map do |i|
content_type = params["MediaContentType#{i}"]
attachment = fetch_attachment(params["MediaUrl#{i}"])

if FileTypeAllowedValidator.mime_types(Document).include?(content_type) && !attachment[:body].empty?
{
content_type: params["MediaContentType#{i}"],
filename: attachment[:filename],
body: attachment[:body]
}
else
{
content_type: "text/plain;charset=UTF-8",
filename: "invalid-#{attachment[:filename]}.txt",
body: <<~TEXT
Unusable file with unknown or unsupported file type.
File name: #{attachment[:filename]}
File type: #{content_type}
File size: #{attachment[:body].size} bytes
TEXT
}
end
end
end
end
2 changes: 1 addition & 1 deletion config/credentials/development.yml.enc

Large diffs are not rendered by default.

Loading