package text import ( "context" "fmt" "time" "github.com/rs/zerolog/log" "source.gleipnir.technology/Gleipnir/nidus-sync/comms/text" "source.gleipnir.technology/Gleipnir/nidus-sync/config" "source.gleipnir.technology/Gleipnir/nidus-sync/db" "source.gleipnir.technology/Gleipnir/nidus-sync/db/enums" modelcomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/model" modelpublic "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/public/model" modelpublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/publicreport/model" querycomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/comms" querypublic "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/public" querypublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/publicreport" "source.gleipnir.technology/Gleipnir/nidus-sync/lint" "source.gleipnir.technology/Gleipnir/nidus-sync/platform/background" "source.gleipnir.technology/Gleipnir/nidus-sync/platform/event" "source.gleipnir.technology/Gleipnir/nidus-sync/platform/types" ) func ensureInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { logs, err := querycomms.TextLogWelcomeFromDestination(ctx, txn, dst.PhoneString()) if err != nil { return fmt.Errorf("Failed to query text logs: %w", err) } if len(logs) > 0 { return nil } return sendInitialText(ctx, txn, dst) } func resendInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { phone, err := querycomms.ContactPhoneFromE164(ctx, txn, dst.PhoneString()) if err != nil { return fmt.Errorf("Failed to find phone %s: %w", dst, err) } err = querycomms.ContactPhoneUpdateStopMessageID(ctx, txn, phone.E164, nil) if err != nil { return fmt.Errorf("Failed to clear subscription on phone %s: %w", dst, err) } return nil } func sendInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" _, err := sendTextDirect(ctx, txn, modelcomms.Textorigin_WebsiteAction, dst.PhoneString(), content, false, true) if err != nil { return fmt.Errorf("send text: %w", err) } return nil } // Begin the process of sending the text message, but only get as far as adding it to // the database, then let the backend finish sending. func sendTextBegin(ctx context.Context, txn db.Ex, user_id *int32, report_id *int32, destination types.E164, content string, type_ modelcomms.Textjobtype) (*int32, error) { job, err := querycomms.TextJobInsert(ctx, txn, modelcomms.TextJob{ Content: content, CreatorID: user_id, Created: time.Now(), Destination: destination.PhoneString(), //ID: ReportID: report_id, Source: modelcomms.Textjobsource_Rmo, Type: type_, }) if err != nil { return nil, fmt.Errorf("Failed to add delayed text job: %w", err) } err = background.NewTextSend(ctx, txn, job.ID) if err != nil { return nil, fmt.Errorf("new background job: %w", err) } return &job.ID, nil } func sendTextCommandResponse(ctx context.Context, txn db.Ex, dst types.E164, content string) error { _, err := sendTextDirect(ctx, txn, modelcomms.Textorigin_CommandResponse, dst.PhoneString(), content, false, false) return err } func sendTextComplete(ctx context.Context, job modelcomms.TextJob) error { txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback") dst, err := ParsePhoneNumber(job.Destination) if err != nil { return fmt.Errorf("parse phone: %w", err) } var origin modelcomms.Textorigin switch job.Type { case modelcomms.Textjobtype_ReportConfirmation: origin = modelcomms.Textorigin_WebsiteAction case modelcomms.Textjobtype_ReportMessage: origin = modelcomms.Textorigin_District default: return fmt.Errorf("incomplete switch: %s", string(job.Type)) } status, err := phoneStatus(ctx, *dst) if err != nil { return fmt.Errorf("Failed to check if subscribed: %w", err) } log.Debug().Str("phone status", string(status)).Str("destination", job.Destination).Send() switch status { case enums.CommsPhonestatustypeUnconfirmed: err := ensureInitialText(ctx, txn, *dst) if err != nil { return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) } return nil //case enums.CommsPhonestatustypeOkToSend: // allow to drop through case enums.CommsPhonestatustypeStopped: lint.LogOnErrCtx(func(ctx context.Context) error { return resendInitialText(ctx, txn, *dst) }, ctx, "resend initial text") return nil } text_log, err := sendTextDirect(ctx, txn, origin, job.Destination, job.Content, true, false) if err != nil { return fmt.Errorf("send text direct: %w", err) } err = querycomms.TextJobComplete(ctx, txn, int64(job.ID)) if err != nil { return fmt.Errorf("update job: %w", err) } if job.ReportID != nil { creator_id := *job.CreatorID report_id := *job.ReportID log.Debug().Int32("creator", creator_id).Int32("report_id", report_id).Msg("Creating report entries for text message") querypublic.ReportTextInsert(ctx, txn, modelpublic.ReportText{ CreatorID: creator_id, ReportID: report_id, TextLogID: text_log.ID, }) if err != nil { return fmt.Errorf("insert report_text: %w", err) } _, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{ Created: time.Now(), EmailLogID: nil, // ID ReportID: report_id, TextLogID: &text_log.ID, Type: modelpublicreport.Reportlogtype_MessageText, UserID: &creator_id, }) if err != nil { return fmt.Errorf("insert report log: %w", err) } report, err := querypublicreport.ReportFromID(ctx, txn, int64(report_id)) if err != nil { return fmt.Errorf("find public report: %w", err) } event.Updated(event.TypeRMOPublicReport, report.OrganizationID, report.PublicID) } else { log.Debug().Msg("no report info on text") } if err := txn.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } return nil } // Send a text message and save the appropriate database records. // Send immediately using the current goroutine func sendTextDirect(ctx context.Context, txn db.Ex, origin modelcomms.Textorigin, destination, content string, is_visible_to_llm, is_welcome bool) (modelcomms.TextLog, error) { text_log, err := querycomms.TextLogInsert(ctx, txn, modelcomms.TextLog{ Content: content, Created: time.Now(), Destination: destination, IsVisibleToLlm: is_visible_to_llm, IsWelcome: is_welcome, Origin: origin, Source: config.PhoneNumberReportStr, TwilioSid: nil, TwilioStatus: "", }) if err != nil { return modelcomms.TextLog{}, fmt.Errorf("insert text log: %w", err) } pid, err := text.SendText(ctx, config.VoipMSNumber, destination, content) if err != nil { return modelcomms.TextLog{}, fmt.Errorf("send text: %w", err) } err = querycomms.TextLogUpdate(ctx, txn, int64(text_log.ID), pid, "created") if err != nil { return modelcomms.TextLog{}, fmt.Errorf("update %w", err) } return text_log, nil }