Created
July 13, 2018 16:58
-
-
Save eldavido/69b1e5a809cc1198ce01a28d081c181c to your computer and use it in GitHub Desktop.
Twilio client from interval
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Http.Extensions; | |
using Microsoft.Extensions.Logging; | |
using Newtonsoft.Json.Linq; | |
using Twilio.Security; | |
namespace interval.Support.Twilio { | |
public class TwilioClient { | |
private readonly ILogger<TwilioClient> _log; | |
private readonly string _accountSid; | |
private readonly string _authToken; | |
private readonly Uri _baseUrl; | |
private const string TwilioSigHeaderKey = "X-Twilio-Signature"; | |
private readonly RequestValidator _rv; | |
public TwilioClient(ILoggerFactory lf, string sid, string authToken, string baseUrl) { | |
_log = lf.CreateLogger<TwilioClient>(); | |
_accountSid = sid; | |
_authToken = authToken; | |
_baseUrl = new Uri(baseUrl); | |
_rv = new RequestValidator(authToken); | |
} | |
public async Task<TwilioNumber?> AllocateNumberForSmsConnection() { | |
using (var tc = Client) { | |
// Find a new number | |
var searchRq = await tc.GetAsync($"AvailablePhoneNumbers/US/Local.json?SmsEnabled=true") | |
.ConfigureAwait(false); | |
var searchRsp = await searchRq.Content.ReadAsStringAsync(); | |
// Parse the response | |
var twilioResp = JObject.Parse(searchRsp); | |
var numbers = twilioResp.GetValue("available_phone_numbers").ToArray(); | |
if (numbers.Length < 1) { | |
throw new Exception("No numbers returned from Twilio"); | |
} | |
var desiredNumber = ((JObject)numbers[0]).GetValue("phone_number").ToString(); | |
_log.LogInformation($"Attempting to allocate {desiredNumber} from Twilio"); | |
// Provision the new number | |
var provisionParams = new Dictionary<string, string> { | |
{ "PhoneNumber", desiredNumber }, | |
{ "SmsUrl", new Uri(_baseUrl, "/twilio").ToString() }, | |
}; | |
var provisionRq = await tc.PostAsync($"IncomingPhoneNumbers/Local.json", | |
Dict2Form(provisionParams)).ConfigureAwait(false); | |
var provisionRsp = await provisionRq.Content.ReadAsStringAsync(); | |
var rsp = JObject.Parse(provisionRsp); | |
var sid = (string) rsp.GetValue("sid"); | |
var provisionedNumber = (string) rsp.GetValue("phone_number"); | |
if (provisionedNumber != desiredNumber) { | |
_log.LogWarning($"Provisioned number {provisionedNumber} was not desired number {desiredNumber}"); | |
} | |
_log.LogInformation($"Provisioned {provisionedNumber} from twilio. sc={provisionRq.StatusCode}, rsp={provisionRsp}"); | |
return new TwilioNumber(sid, new NanpPstnEndpoint(provisionedNumber)); | |
} | |
} | |
public async Task<bool> ReleaseNumber(TwilioNumber n) { | |
using (var tc = Client) { | |
_log.LogInformation($"De-provisioning twilio number. sid={n.Sid}, pstn={n.Endpoint}"); | |
var rsp = await tc.DeleteAsync($"IncomingPhoneNumbers/{n.Sid}"); | |
_log.LogInformation($"Response from twilio: {rsp.StatusCode}"); | |
return rsp.StatusCode == HttpStatusCode.NoContent; | |
} | |
} | |
public async Task SendSmsAsync(SmsMessage msg) { | |
using (var tc = Client) { | |
var d = new Dictionary<string, string> { | |
{ "To", msg.Recipient.NormalizedNumber }, | |
{ "From", msg.Sender.NormalizedNumber }, | |
{ "Body", msg.Body } }; | |
await tc.PostAsync($"Messages.json", Dict2Form(d)).ConfigureAwait(false); | |
} | |
} | |
public bool RequestHasValidSignature(HttpRequest rq, Dictionary<string, string> postParams) { | |
if (!rq.Headers.ContainsKey(TwilioSigHeaderKey)) { | |
return false; | |
} | |
var twilioSignature = rq.Headers[TwilioSigHeaderKey]; | |
// NOTE(DA) Test gate: sid=test. Safeguard: this will never work in prod (no outgoing sends) | |
// so is likely to be caught quickly, in the unlikely event this is misdeployed to prod. | |
// NOTE(DA) (2) Hardcode https here so that the hook delivery always arrives to us as https. | |
// However, it may get rewritten by a reverse proxy, so we hardcode https here. | |
var path = string.Concat("https://", rq.Host.ToUriComponent(), rq.PathBase.ToUriComponent(), | |
rq.Path.ToUriComponent()); | |
return (_accountSid == "test" && postParams.ContainsKey("bypass_twilio_check")) || | |
_rv.Validate(path, postParams, twilioSignature); | |
} | |
private HttpClient Client { | |
get { | |
var tc = new HttpClient { BaseAddress = new Uri($"https://api.twilio.com/2010-04-01/Accounts/{_accountSid}/") }; | |
tc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", | |
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_accountSid}:{_authToken}"))); | |
return tc; | |
} | |
} | |
private FormUrlEncodedContent Dict2Form(Dictionary<string, string> d) { | |
var kvps = d.Select(x => new KeyValuePair<string, string>(x.Key, x.Value)); | |
return new FormUrlEncodedContent(kvps); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment