102 lines
4.0 KiB
Ruby
102 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# Copyright (C) 2015 - present Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# Utilities to send text messages via Twilio.
|
|
require "twilio-ruby"
|
|
|
|
module Canvas::Twilio
|
|
DEFAULT_COUNTRY = "US"
|
|
|
|
def self.account_sid
|
|
Rails.application.credentials.twilio_creds&.[](:account_sid)
|
|
end
|
|
|
|
def self.auth_token
|
|
Rails.application.credentials.twilio_creds&.[](:auth_token)
|
|
end
|
|
|
|
def self.client
|
|
@client ||= Twilio::REST::Client.new(account_sid, auth_token) if account_sid && auth_token
|
|
end
|
|
|
|
# Whether or not Twilio is currently enabled. Twilio is enabled when config/twilio.yml exists and specifies an
|
|
# account_sid and auth_token. Calls to deliver will fail when this returns false.
|
|
def self.enabled?
|
|
!!client
|
|
end
|
|
|
|
# Look up the ISO country code for the specified phone number. Twilio must be enabled in order for this to work.
|
|
def self.lookup_country(phone_number)
|
|
Rails.cache.fetch(["twilio_phone_number_country_2", phone_number].cache_key) do
|
|
client.lookups.v2.phone_numbers(phone_number).fetch.country_code
|
|
end
|
|
end
|
|
|
|
# Hash of ISO country codes to arrays of phone numbers that we own in those countries. This will be an empty hash if
|
|
# Twilio is not enabled.
|
|
def self.outbound_numbers
|
|
return {} unless enabled?
|
|
|
|
Rails.cache.fetch("twilio_source_phone_numbers_3", expires_in: 1.day) do
|
|
numbers = Canvas::Twilio.client.api.account.incoming_phone_numbers.stream.to_a
|
|
|
|
numbers_by_country = Hash.new { |h, k| h[k] = [] }
|
|
numbers.sort_by(&:phone_number).each do |number|
|
|
numbers_by_country[lookup_country(number.phone_number)] << number.phone_number
|
|
end
|
|
|
|
numbers_by_country.default = nil
|
|
numbers_by_country
|
|
end
|
|
end
|
|
|
|
# Synchronously send a text message. recipient_number should be a phone number in E.164 format and body should be a
|
|
# string to send. This method will take care of deciding what number to send the message from and all of the other
|
|
# assorted magic that goes into delivering text messages via Twilio.
|
|
def self.deliver(recipient_number, body, from_recipient_country: true)
|
|
raise "Twilio is not configured" unless enabled?
|
|
|
|
# Figure out what country the recipient number is in
|
|
country = from_recipient_country ? lookup_country(recipient_number) : DEFAULT_COUNTRY
|
|
|
|
# Get a list of numbers we own in that country, or numbers in the default country if we don't own any
|
|
number_map = outbound_numbers
|
|
outbound_country = number_map[country].present? ? country : DEFAULT_COUNTRY
|
|
candidates = number_map[outbound_country]
|
|
|
|
# Pick one using HRW/rendezvous hashing
|
|
outbound_number = candidates.max_by do |number|
|
|
Digest::SHA256.hexdigest([number, recipient_number].cache_key).hex
|
|
end
|
|
|
|
# Ping StatsD about sending from this number
|
|
InstStatsd::Statsd.increment("notifications.twilio.message_sent_from_number.#{outbound_country}.#{outbound_number}",
|
|
short_stat: "notifications.twilio.message_sent",
|
|
tags: { country: outbound_country, number: outbound_number })
|
|
unless country == outbound_country
|
|
InstStatsd::Statsd.increment("notifications.twilio.no_outbound_numbers_for.#{country}",
|
|
short_stat: "notifications.twilio.no_outbound_numbers",
|
|
tags: { country: })
|
|
end
|
|
|
|
# Then send the message.
|
|
client.api.account.messages.create(from: outbound_number, to: recipient_number, body:)
|
|
end
|
|
end
|