Add contacts, rework comms schema

This in a pretty huge change. At a high level we're adding the concept
of a 'contact' which is a person or organization that has zero or more
contact methods (email, phone). This ended up cascading a number of
changes, including critically to the publicreprt schema. In the end it
seemed safer to get to the point where I'm confident we aren't using any
of the old fields for storing reporter information (though I haven't
deleted the columns yet) so I removed the code for defining those
columns.

At this point I think it's not possible for me to regenerate the bob
schema due to the interdependencies between my various schemas, so the
migration is well-and-truly happening.
This commit is contained in:
Eli Ribble 2026-05-15 16:58:28 +00:00
parent 085935fa66
commit f1fe8b4d2b
No known key found for this signature in database
46 changed files with 1127 additions and 633 deletions

View file

@ -9,8 +9,12 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/lint"
modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model"
modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms"
querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport"
"github.com/Gleipnir-Technology/nidus-sync/lint"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
@ -68,12 +72,12 @@ func HandleTextMessage(ctx context.Context, source string, destination string, c
}
func respondText(ctx context.Context, log_id int32) error {
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
txn, err := db.BeginTxn(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")
l, err := models.FindCommsTextLog(ctx, txn, log_id)
l, err := querycomms.TextLogFromID(ctx, txn, int64(log_id))
if err != nil {
return fmt.Errorf("find comms: %w", err)
}
@ -82,19 +86,19 @@ func respondText(ctx context.Context, log_id int32) error {
return fmt.Errorf("parse source: %w", err)
}
status, err := phoneStatus(ctx, *src)
contact_phone, err := querycomms.ContactPhoneFromE164(ctx, txn, src.PhoneString())
if err != nil {
return fmt.Errorf("Failed to get phone status")
return fmt.Errorf("Failed to get contact phone")
}
body_l := strings.TrimSpace(strings.ToLower(l.Content))
// If the user isn't confirmed for sending regular texts ensure they get a reprompt
if status == enums.CommsPhonestatustypeUnconfirmed {
if contact_phone.ConfirmedMessageID == nil {
switch body_l {
case "yes":
err = setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeOkToSend)
err = querycomms.ContactPhoneUpdateConfirmedMessageID(ctx, txn, src.PhoneString(), &l.ID)
if err != nil {
return fmt.Errorf("set phone status: %w", err)
return fmt.Errorf("set phone confirmed message ID: %w", err)
}
content := "Thanks, we've confirmed your phone number. You can text STOP at any time if you change your mind"
err = sendTextCommandResponse(ctx, txn, *src, content)
@ -118,14 +122,15 @@ func respondText(ctx context.Context, log_id int32) error {
}
switch body_l {
case "stop":
err = querycomms.ContactPhoneUpdateStopMessageID(ctx, txn, src.PhoneString(), &l.ID)
if err != nil {
return fmt.Errorf("set phone stop message ID: %w", err)
}
content := "You have successfully been unsubscribed. You will not receive any more messages from this number. Reply START to resubscribe."
err = sendTextCommandResponse(ctx, txn, *src, content)
if err != nil {
log.Error().Err(err).Msg("Failed to send unsubscribe acknowledgement.")
}
lint.LogOnErrCtx(func(ctx context.Context) error {
return setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeStopped)
}, ctx, "set phone status")
return nil
case "reset conversation":
err = handleResetConversation(ctx, txn, *src)
@ -140,20 +145,23 @@ func respondText(ctx context.Context, log_id int32) error {
return nil
}
// If we've got an open public report from this phone number then we'll let the district respond
reports, err := reportsForTextRecipient(ctx, txn, *src)
// Get the list of reports that are still open for a particular text message recipient
// 'still open' is not well-defined throughout the system, but for now we'll go with
// 'not reviewed in any way'.
reports, err := querypublicreport.ReportsFromReporterPhone(ctx, txn, src.PhoneString())
if err != nil {
return fmt.Errorf("has open report: %w", err)
}
for _, report := range reports {
_, err = models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{
Created: omit.From(time.Now()),
EmailLogID: omitnull.FromPtr[int32](nil),
_, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{
Created: time.Now(),
EmailLogID: nil,
// ID
ReportID: omit.From(report.ID),
TextLogID: omitnull.From(log_id),
Type: omit.From(enums.PublicreportReportlogtypeMessageText),
UserID: omitnull.FromPtr[int32](nil),
}).One(ctx, txn)
ReportID: report.ID,
TextLogID: &log_id,
Type: modelpublicreport.Reportlogtype_MessageText,
UserID: nil,
})
if err != nil {
return fmt.Errorf("insert report log: %w", err)
}
@ -164,7 +172,8 @@ func respondText(ctx context.Context, log_id int32) error {
return nil
}
// Otherwise let the LLM handle the response
return respondTextLLM(ctx, *src)
//return respondTextLLM(ctx, *src)
return nil
}
func respondTextLLM(ctx context.Context, src types.E164) error {
@ -177,12 +186,12 @@ func respondTextLLM(ctx context.Context, src types.E164) error {
if err != nil {
return fmt.Errorf("Failed to generate next message: %w", err)
}
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
txn, err := db.BeginTxn(ctx)
if err != nil {
return fmt.Errorf("start txn: %w", err)
}
defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")
_, err = sendTextDirect(ctx, txn, enums.CommsTextoriginLLM, src.PhoneString(), next_message.Content, true, false)
_, err = sendTextDirect(ctx, txn, modelcomms.Textorigin_Llm, src.PhoneString(), next_message.Content, true, false)
if err != nil {
return fmt.Errorf("Failed to send response text: %w", err)
}
@ -201,22 +210,28 @@ func ParsePhoneNumber(input string) (*types.E164, error) {
}
func StoreSources() error {
var err error
ctx := context.TODO()
txn := db.PGInstance.PGXPool
// Magical id 1 is set in migration 00151
contact, err := querycomms.ContactFromID(ctx, txn, 1)
if err != nil {
return fmt.Errorf("contact from ID 1: %w", err)
}
for _, n := range []string{config.PhoneNumberReportStr, config.PhoneNumberSupportStr, config.VoipMSNumber} {
var err error
// Deal with Voip.ms not expecting API calls with the prefixed +1
if !strings.HasPrefix(n, "+1") {
dest, err := ParsePhoneNumber("+1" + n)
if err != nil {
return fmt.Errorf("Failed to parse +1'%s' as phone number: %w", n, err)
}
err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest)
err = EnsureInDB(ctx, txn, contact, *dest)
} else {
dest, err := ParsePhoneNumber(n)
if err != nil {
return fmt.Errorf("Failed to parse '%s' as phone number: %w", n, err)
}
err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest)
err = EnsureInDB(ctx, txn, contact, *dest)
}
if err != nil {
return fmt.Errorf("Failed to add number '%s' to DB: %w", n, err)