public async Task HandleCallback(SmsEventNotification smsEvent, CancellationToken cancellation) { // Nothing to do if (smsEvent == null || smsEvent.TenantId == null || smsEvent.TenantId == 0) // Right now we do not handle null tenant Ids, those were probably sent from identity or admin servers { return; } // Map the event to the database representation var state = smsEvent.Event switch { SmsEvent.Sent => MessageState.Sent, SmsEvent.Failed => MessageState.SendingFailed, SmsEvent.Delivered => MessageState.Delivered, SmsEvent.Undelivered => MessageState.DeliveryFailed, _ => throw new InvalidOperationException($"[Bug] Unknown {nameof(SmsEvent)} = {smsEvent.Event}"), // Future proofing }; // Update the state in the database (should we make it serializable?) var repo = _repoFactory.GetRepository(tenantId: smsEvent.TenantId.Value); // Begin serializable transaction using var trx = TransactionFactory.Serializable(TransactionScopeOption.RequiresNew); await repo.Notifications_Messages__UpdateState( id : smsEvent.MessageId, state : state, timestamp : smsEvent.Timestamp, error : smsEvent.Error, cancellation : cancellation);; trx.Complete(); }
public async Task HandleCallback(SmsEventNotification smsEvent, CancellationToken cancellation) { // Nothing to do if (smsEvent == null || smsEvent.TenantId == null || smsEvent.TenantId == 0) // Right now we do not handle null tenant Ids, those were probably sent from identity or admin servers { return; } // Map the event to the database representation var state = smsEvent.Event switch { SmsEvent.Sent => SmsState.Sent, SmsEvent.Failed => SmsState.SendingFailed, SmsEvent.Delivered => SmsState.Delivered, SmsEvent.Undelivered => SmsState.DeliveryFailed, _ => throw new InvalidOperationException($"[Bug] Unknown {nameof(SmsEvent)} = {smsEvent.Event}"), // Future proofing }; // Update the state in the database (should we make it serializable?) await _repo.Notifications_SmsMessages__UpdateState(smsEvent.TenantId.Value, smsEvent.MessageId, state, smsEvent.Timestamp, smsEvent.Error, cancellation); }
/// <summary> /// Middleware that listens to the endpoint "/api/sms-callback" for webhook events posted from Twilio. <br/> /// In order to use this middleware you must first register an implementation of <see cref="ISmsCallbackHandler"/> in the DI container. /// </summary> public static IApplicationBuilder UseTwilioCallback(this IApplicationBuilder bldr, IConfiguration config) { // Get the Twilio Section var section = config.GetSection(TwilioSectionName); var opt = section.Get <TwilioOptions>(); // This configures a callback endpoint for Twilio event webhooks if (opt?.Sms?.CallbacksEnabled ?? false) { var twilioRequestValidator = new RequestValidator(opt.AuthToken); var callbackHost = new HostString(opt.Sms.CallbackHost?.WithoutTrailingSlash()); bldr = bldr.Map("/api/sms-callback", (app) => { app.Run(async ctx => { var req = ctx.Request; var res = ctx.Response; var cancellation = ctx.RequestAborted; var handler = ctx.RequestServices.GetService <ISmsCallbackHandler>(); if (handler == null) { // Helps during configuration for making sure the endpoint is working res.StatusCode = StatusCodes.Status200OK; await res.WriteAsync($"No implementation of {nameof(ISmsCallbackHandler)} was registered.", cancellation); } else if (req.Method == "GET") { // Helps during configuration for making sure the endpoint is accessible res.StatusCode = StatusCodes.Status200OK; await res.WriteAsync("Welcome to the sms callback endpoint for Twilio webhooks!", cancellation); } else if (req.Method != "POST") { // Twilio must POST to this endpoint res.StatusCode = StatusCodes.Status405MethodNotAllowed; await res.WriteAsync($"{req.Method} method is not supported.", cancellation); } else { var requestUrl = $"{callbackHost}{req.PathBase}{req.Path}{req.QueryString}"; // var requestUrl = UriHelper.BuildAbsolute(req.Scheme, callbackHost, req.PathBase, req.Path, req.QueryString); var parameters = req.Form.ToDictionary(e => e.Key, e => req.Form[e.Key].ToString()); var signature = req.Headers["X-Twilio-Signature"]; if (signature == StringValues.Empty || !twilioRequestValidator.Validate(requestUrl, parameters, signature)) { // Call is not coming from Twilio res.StatusCode = StatusCodes.Status401Unauthorized; await res.WriteAsync("Invalid signature.", cancellation); } else { // Extract all the needed info from the webhook event var twilioStatus = req.Form["MessageStatus"].ToString(); var tenantIdString = req.Query[TwilioSmsSender.TenantIdParamName].ToString(); var messageIdString = req.Query[TwilioSmsSender.MessageIdParamName].ToString(); // Parsing and validation if (string.IsNullOrWhiteSpace(messageIdString)) { // Callback is pointless without the message_id, but we return 200 OK return; } if (!int.TryParse(messageIdString, out int messageId)) { // Message Id should be an integer res.StatusCode = StatusCodes.Status400BadRequest; await res.WriteAsync($"Could not parse message ID {messageId} into a valid integer.", cancellation); return; } int?tenantId = null; if (!string.IsNullOrWhiteSpace(tenantIdString)) { if (int.TryParse(tenantIdString, out int tenantIdInt)) { tenantId = tenantIdInt; } else { // Tenant Id (if any) should be an integer res.StatusCode = StatusCodes.Status400BadRequest; await res.WriteAsync($"Could not parse tenant ID {tenantIdString} into a valid integer.", cancellation); return; } } // https://bit.ly/32K9o1j SmsEvent type; switch (twilioStatus) { // Tracked case "sent": // Twilio sent it (treat as final after 72h) type = SmsEvent.Sent; break; case "failed": // Twilio failed to send it (no charges) type = SmsEvent.Failed; break; case "delivered": // The carrier delivered it type = SmsEvent.Delivered; break; case "undelivered": // The carrier failed to deliver it (charges apply) type = SmsEvent.Undelivered; break; // No point tracking those, TMI case "accepted": // Twilio accepted it case "queued": // Twilio assigned a from number and queued it case "sending": // Twilio started sending it // Never used case "receiving": // Only for inbound SMS case "received": // Only for inbound SMS case "read": // Only for whatsapp default: // Nothing to handle, return OK 200 res.StatusCode = StatusCodes.Status200OK; return; } // Create the event and handle it with custom behavior var smsEvent = new SmsEventNotification { MessageId = messageId, TenantId = tenantId, Event = type, Timestamp = DateTimeOffset.Now }; // Custom handler await handler.HandleCallback(smsEvent, cancellation); // Return 200 upon success res.StatusCode = StatusCodes.Status200OK; } } }); }); } return(bldr); }