From 0af2e3b1d72fca818b31958b5182e88f4a4e6f1f Mon Sep 17 00:00:00 2001 From: Thomas Ryan Date: Fri, 4 Aug 2023 17:30:54 -0700 Subject: [PATCH] Added suppor for reporting of failed numbers in support of #379. --- .../20230805002438_MessageSuccess.Designer.cs | 105 ++++++++++++++++ .../20230805002438_MessageSuccess.cs | 29 +++++ .../MessagingContextModelSnapshot.cs | 3 + Messaging/Program.cs | 114 +++++++++++++++--- 4 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 Messaging/Migrations/20230805002438_MessageSuccess.Designer.cs create mode 100644 Messaging/Migrations/20230805002438_MessageSuccess.cs diff --git a/Messaging/Migrations/20230805002438_MessageSuccess.Designer.cs b/Messaging/Migrations/20230805002438_MessageSuccess.Designer.cs new file mode 100644 index 00000000..1059bb1a --- /dev/null +++ b/Messaging/Migrations/20230805002438_MessageSuccess.Designer.cs @@ -0,0 +1,105 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Models; + +#nullable disable + +namespace Messaging.Migrations +{ + [DbContext(typeof(MessagingContext))] + [Migration("20230805002438_MessageSuccess")] + partial class MessageSuccess + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + + modelBuilder.Entity("Models.ClientRegistration", b => + { + b.Property("ClientRegistrationId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AsDialed") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CallbackUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateRegistered") + .HasColumnType("TEXT"); + + b.Property("RegisteredUpstream") + .HasColumnType("INTEGER"); + + b.Property("UpstreamStatusDescription") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ClientRegistrationId"); + + b.ToTable("ClientRegistrations"); + }); + + modelBuilder.Entity("Models.MessageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateReceivedUTC") + .HasColumnType("TEXT"); + + b.Property("From") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MediaURLs") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MessageSource") + .HasColumnType("INTEGER"); + + b.Property("MessageType") + .HasColumnType("INTEGER"); + + b.Property("RawRequest") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RawResponse") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Succeeded") + .HasColumnType("INTEGER"); + + b.Property("To") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Messaging/Migrations/20230805002438_MessageSuccess.cs b/Messaging/Migrations/20230805002438_MessageSuccess.cs new file mode 100644 index 00000000..f0d78f90 --- /dev/null +++ b/Messaging/Migrations/20230805002438_MessageSuccess.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Messaging.Migrations +{ + /// + public partial class MessageSuccess : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Succeeded", + table: "Messages", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Succeeded", + table: "Messages"); + } + } +} diff --git a/Messaging/Migrations/MessagingContextModelSnapshot.cs b/Messaging/Migrations/MessagingContextModelSnapshot.cs index ea84bb2b..64861455 100644 --- a/Messaging/Migrations/MessagingContextModelSnapshot.cs +++ b/Messaging/Migrations/MessagingContextModelSnapshot.cs @@ -85,6 +85,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("Succeeded") + .HasColumnType("INTEGER"); + b.Property("To") .IsRequired() .HasColumnType("TEXT"); diff --git a/Messaging/Program.cs b/Messaging/Program.cs index 2d264e8e..772f6b4b 100644 --- a/Messaging/Program.cs +++ b/Messaging/Program.cs @@ -645,6 +645,30 @@ }) .RequireAuthorization().WithOpenApi(x => new(x) { Summary = "View all sent and received messages.", Description = "This is intended to help you debug problems with message sending and delivery so you can see if it's this API or the upstream vendor that is causing problems." }); + app.MapGet("/message/all/failed", async Task, NotFound, BadRequest>> (MessagingContext db, DateTime start, DateTime end) => + { + try + { + var messages = await db.Messages.Where(x => !x.Succeeded && x.DateReceivedUTC > start && x.DateReceivedUTC <= end).OrderByDescending(x => x.DateReceivedUTC).ToArrayAsync(); + + if (messages is not null && messages.Any()) + { + return TypedResults.Ok(messages); + } + else + { + return TypedResults.NotFound($"No failed messages have been recorded between {start} and {end}."); + } + } + catch (Exception ex) + { + Log.Error(ex.Message); + Log.Error(ex.StackTrace ?? "No stack trace found."); + return TypedResults.BadRequest(ex.Message); + } + }) + .RequireAuthorization().WithOpenApi(x => new(x) { Summary = "View all sent and received messages.", Description = "This is intended to help you debug problems with message sending and delivery so you can see if it's this API or the upstream vendor that is causing problems." }); + app.MapPost("/message/send", async Task, BadRequest>> ([Microsoft.AspNetCore.Mvc.FromBody] SendMessageRequest message, bool? test, MessagingContext db) => { var toForward = new toForwardOutbound @@ -683,7 +707,7 @@ MessageSource = MessageSource.Outgoing, MessageType = message?.MediaURLs.Length > 0 ? MessageType.MMS : MessageType.SMS, RawRequest = System.Text.Json.JsonSerializer.Serialize(message), - RawResponse = $"MSISDN {message?.MSISDN} could not be parsed as valid NANP (North American Numbering Plan) number." + RawResponse = $"MSISDN {message?.MSISDN} could not be parsed as valid NANP (North American Numbering Plan) number.", }; db.Messages.Add(record); @@ -795,7 +819,8 @@ MessageSource = MessageSource.Outgoing, MessageType = MessageType.MMS, RawRequest = System.Text.Json.JsonSerializer.Serialize(multipartContent), - RawResponse = MMSResponse + RawResponse = MMSResponse, + Succeeded = true }; db.Messages.Add(record); @@ -859,7 +884,8 @@ MessageSource = MessageSource.Outgoing, MessageType = MessageType.SMS, RawRequest = System.Text.Json.JsonSerializer.Serialize(toForward), - RawResponse = System.Text.Json.JsonSerializer.Serialize(sendMessage) + RawResponse = System.Text.Json.JsonSerializer.Serialize(sendMessage), + Succeeded = true }; db.Messages.Add(record); @@ -992,6 +1018,14 @@ MessageType = MessageType.MMS, }; + MessageRecord messageRecord = new MessageRecord + { + RawRequest = incomingRequest, + DateReceivedUTC = DateTime.UtcNow, + MessageSource = MessageSource.Incoming, + MessageType = MessageType.MMS, + }; + if (!string.IsNullOrWhiteSpace(msisdn)) { bool checkFrom = PhoneNumbersNA.PhoneNumber.TryParse(msisdn, out var fromPhoneNumber); @@ -1065,6 +1099,14 @@ else { Log.Error($"Failed to parse To {to} from incoming request {incomingRequest} please file a ticket with the message provider."); + + messageRecord.Content = toForward.Content; + messageRecord.From = toForward.From; + messageRecord.RawResponse = $"Failed to parse To {to} from incoming request {incomingRequest} please file a ticket with the message provider."; + + db.Messages.Add(messageRecord); + await db.SaveChangesAsync(); + return TypedResults.BadRequest($"To {to}{fullrecipientlist} could not be parsed as valid NANP (North American Numbering Plan) numbers. {incomingRequest}"); } } @@ -1101,6 +1143,7 @@ } } toForward.MediaURLs = mediaURLs.ToArray(); + messageRecord.MediaURLs = string.Join(',', toForward.MediaURLs); // We already know that it's good. _ = PhoneNumbersNA.PhoneNumber.TryParse(toForward.To, out var toRegisteredNumber); @@ -1115,41 +1158,51 @@ var response = await client.CallbackUrl.PostJsonAsync(toForward); Log.Information(await response.GetStringAsync()); Log.Information(System.Text.Json.JsonSerializer.Serialize(toForward)); + messageRecord.RawResponse = System.Text.Json.JsonSerializer.Serialize(toForward); + messageRecord.Succeeded = true; } catch (FlurlHttpException ex) { - Log.Error(await ex.GetResponseStringAsync()); + string error = await ex.GetResponseStringAsync(); + Log.Error(error); Log.Error(System.Text.Json.JsonSerializer.Serialize(client)); Log.Error(System.Text.Json.JsonSerializer.Serialize(toForward)); Log.Error($"Failed to forward message to {toForward.To}"); + messageRecord.RawResponse = $"Failed to forward message to {toForward.To} {error}"; } - MessageRecord record = toForward.ToMessageRecord(incomingRequest, System.Text.Json.JsonSerializer.Serialize(toForward)); - try { - await db.Messages.AddAsync(record); + await db.Messages.AddAsync(messageRecord); await db.SaveChangesAsync(); } catch (Exception ex) { - return TypedResults.BadRequest($"Failed to save incoming message to the database. {ex.Message} {System.Text.Json.JsonSerializer.Serialize(record)}"); + return TypedResults.BadRequest($"Failed to save incoming message to the database. {ex.Message} {System.Text.Json.JsonSerializer.Serialize(messageRecord)}"); } - Log.Information(System.Text.Json.JsonSerializer.Serialize(record)); + Log.Information(System.Text.Json.JsonSerializer.Serialize(messageRecord)); return TypedResults.Ok("The incoming message was received and forwarded to the client."); } else { Log.Warning($"{toForward.To} is not registered as a client."); + + messageRecord.To = toForward.To; + messageRecord.From = toForward.From; + messageRecord.RawResponse = $"{toForward.To} is not registered as a client."; + db.Messages.Add(messageRecord); + await db.SaveChangesAsync(); + return TypedResults.BadRequest($"{toForward.To} is not registered as a client."); } } catch (Exception ex) { Log.Error(ex.Message); - Log.Error(ex.StackTrace ?? "No stacktrace found."); + Log.Error(ex.StackTrace ?? "No stack trace found."); Log.Information("Failed to read form data by field name."); + return TypedResults.BadRequest(ex.Message); } @@ -1190,6 +1243,14 @@ MessageType = MessageType.SMS, }; + MessageRecord messageRecord = new MessageRecord + { + RawRequest = incomingRequest, + DateReceivedUTC = DateTime.UtcNow, + MessageSource = MessageSource.Incoming, + MessageType = MessageType.SMS, + }; + if (!string.IsNullOrWhiteSpace(msisdn)) { bool checkFrom = PhoneNumbersNA.PhoneNumber.TryParse(msisdn, out var fromPhoneNumber); @@ -1263,6 +1324,14 @@ else { Log.Error($"Failed to parse To {to} from incoming request {incomingRequest} please file a ticket with the message provider."); + + messageRecord.Content = toForward.Content; + messageRecord.From = toForward.From; + messageRecord.RawResponse = $"Failed to parse To {to} from incoming request {incomingRequest} please file a ticket with the message provider."; + + db.Messages.Add(messageRecord); + await db.SaveChangesAsync(); + return TypedResults.BadRequest($"To {to}{fullrecipientlist} could not be parsed as valid NANP (North American Numbering Plan) numbers. {incomingRequest}"); } } @@ -1280,40 +1349,48 @@ var response = await client.CallbackUrl.PostJsonAsync(toForward); Log.Information(await response.GetStringAsync()); Log.Information(System.Text.Json.JsonSerializer.Serialize(toForward)); + messageRecord.RawResponse = System.Text.Json.JsonSerializer.Serialize(toForward); + messageRecord.Succeeded = true; } catch (FlurlHttpException ex) { - Log.Error(await ex.GetResponseStringAsync()); + string error = await ex.GetResponseStringAsync(); + Log.Error(error); Log.Error(System.Text.Json.JsonSerializer.Serialize(client)); Log.Error(System.Text.Json.JsonSerializer.Serialize(toForward)); Log.Error($"Failed to forward message to {toForward.To}"); + messageRecord.RawResponse = $"Failed to forward message to {toForward.To} {error}"; } - - MessageRecord record = toForward.ToMessageRecord(incomingRequest, System.Text.Json.JsonSerializer.Serialize(toForward)); - try { - await db.Messages.AddAsync(record); + await db.Messages.AddAsync(messageRecord); await db.SaveChangesAsync(); } catch (Exception ex) { - return TypedResults.BadRequest($"Failed to save incoming message to the database. {ex.Message} {System.Text.Json.JsonSerializer.Serialize(record)}"); + return TypedResults.BadRequest($"Failed to save incoming message to the database. {ex.Message} {System.Text.Json.JsonSerializer.Serialize(messageRecord)}"); } - Log.Information(System.Text.Json.JsonSerializer.Serialize(record)); + Log.Information(System.Text.Json.JsonSerializer.Serialize(messageRecord)); return TypedResults.Ok("The incoming message was received and forwarded to the client."); } else { Log.Warning($"{toForward.To} is not registered as a client."); + + messageRecord.To = toForward.To; + messageRecord.From = toForward.From; + messageRecord.RawResponse = $"{toForward.To} is not registered as a client."; + db.Messages.Add(messageRecord); + await db.SaveChangesAsync(); + return TypedResults.BadRequest($"{toForward.To} is not registered as a client."); } } catch (Exception ex) { Log.Error(ex.Message); - Log.Error(ex.StackTrace ?? "No stacktrace found."); + Log.Error(ex.StackTrace ?? "No stack trace found."); Log.Information("Failed to read form data by field name."); return TypedResults.BadRequest("Failed to read form data by field name."); } @@ -1490,6 +1567,7 @@ public class MessageRecord public DateTime DateReceivedUTC { get; set; } = DateTime.UtcNow; public string RawRequest { get; set; } = string.Empty; public string RawResponse { get; set; } = string.Empty; + public bool Succeeded { get; set; } = false; } // Format forward to client apps as JSON.