diff --git a/comms/text/db.go b/comms/text/db.go index 9801e1ca..791dac38 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -15,6 +15,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/types/pgtypes" @@ -55,7 +56,7 @@ func ensureInDB(ctx context.Context, destination string) (err error) { if err.Error() == "sql: no rows in result set" { _, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{ E164: omit.From(destination), - IsSubscribed: omit.From(false), + IsSubscribed: omitnull.FromPtr[bool](nil), }).One(ctx, db.PGInstance.BobDB) if err != nil { return fmt.Errorf("Failed to insert new phone contact: %w", err) @@ -81,16 +82,6 @@ func insertTextLog(ctx context.Context, content string, destination string, sour return err } -func isSubscribed(ctx context.Context, destination string) (bool, error) { - phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) - if err != nil { - if err.Error() == "sql: no rows in result set" { - return false, nil - } - return false, fmt.Errorf("Failed to find phone number %s: %w", destination, err) - } - return phone.IsSubscribed, nil -} func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { if m == nil || len(m) == 0 { diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go index c43f6b89..12328338 100644 --- a/comms/text/report-subscription.go +++ b/comms/text/report-subscription.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" + //"github.com/Gleipnir-Technology/nidus-sync/db/enums" + //"github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/nyaruka/phonenumbers" //"github.com/rs/zerolog/log" ) @@ -43,29 +44,32 @@ func (j jobReportSubscription) source() string { } func sendReportSubscription(ctx context.Context, job Job) error { - j, ok := job.(jobReportSubscription) - if !ok { - return fmt.Errorf("job is not for report subscription confirmation") - } + /* + j, ok := job.(jobReportSubscription) + if !ok { + return fmt.Errorf("job is not for report subscription confirmation") + } - sub, err := isSubscribed(ctx, job.destination()) - if err != nil { - return fmt.Errorf("Failed to check if subscribed: %w", err) - } - if !sub { - err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) + sub, err := isSubscribed(ctx, job.destination()) if err != nil { - return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + return fmt.Errorf("Failed to check if subscribed: %w", err) } - } else { - err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) - if err != nil { - return fmt.Errorf("Failed to delay report subscription message: %w", err) + if !sub { + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + } else { + err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) + if err != nil { + return fmt.Errorf("Failed to delay report subscription message: %w", err) + } + err := ensureInitialText(ctx, j.source(), j.destination()) + if err != nil { + return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) + } } - err := ensureInitialText(ctx, j.source(), j.destination()) - if err != nil { - return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) - } - } + return nil + */ return nil } diff --git a/db/dberrors/user_.bob.go b/db/dberrors/user_.bob.go index d000ac1f..7f08ae62 100644 --- a/db/dberrors/user_.bob.go +++ b/db/dberrors/user_.bob.go @@ -10,8 +10,17 @@ var UserErrors = &userErrors{ columns: []string{"id"}, s: "user__pkey", }, + + ErrUniqueUserUsernameUnique: &UniqueConstraintError{ + schema: "", + table: "user_", + columns: []string{"username"}, + s: "user_username_unique", + }, } type userErrors struct { ErrUniqueUser_Pkey *UniqueConstraintError + + ErrUniqueUserUsernameUnique *UniqueConstraintError } diff --git a/db/dbinfo/comms.phone.bob.go b/db/dbinfo/comms.phone.bob.go index 2919387c..be6039b2 100644 --- a/db/dbinfo/comms.phone.bob.go +++ b/db/dbinfo/comms.phone.bob.go @@ -27,9 +27,9 @@ var CommsPhones = Table[ IsSubscribed: column{ Name: "is_subscribed", DBType: "boolean", - Default: "", + Default: "NULL", Comment: "", - Nullable: false, + Nullable: true, Generated: false, AutoIncr: false, }, diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index b72cfaf4..1c1f25ef 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -142,6 +142,23 @@ var Users = Table[ Where: "", Include: []string{}, }, + UserUsernameUnique: index{ + Type: "btree", + Name: "user_username_unique", + Columns: []indexColumn{ + { + Name: "username", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, }, PrimaryKey: &constraint{ Name: "user__pkey", @@ -159,6 +176,13 @@ var Users = Table[ ForeignColumns: []string{"id"}, }, }, + Uniques: userUniques{ + UserUsernameUnique: constraint{ + Name: "user_username_unique", + Columns: []string{"username"}, + Comment: "", + }, + }, Comment: "", } @@ -185,12 +209,13 @@ func (c userColumns) AsSlice() []column { } type userIndexes struct { - UserPkey index + UserPkey index + UserUsernameUnique index } func (i userIndexes) AsSlice() []index { return []index{ - i.UserPkey, + i.UserPkey, i.UserUsernameUnique, } } @@ -204,10 +229,14 @@ func (f userForeignKeys) AsSlice() []foreignKey { } } -type userUniques struct{} +type userUniques struct { + UserUsernameUnique constraint +} func (u userUniques) AsSlice() []constraint { - return []constraint{} + return []constraint{ + u.UserUsernameUnique, + } } type userChecks struct{} diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 6e8d4391..45b2b33f 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -293,7 +293,7 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla o := &CommsPhoneTemplate{f: f, alreadyPersisted: true} o.E164 = func() string { return m.E164 } - o.IsSubscribed = func() bool { return m.IsSubscribed } + o.IsSubscribed = func() null.Val[bool] { return m.IsSubscribed } ctx := context.Background() if len(m.R.DestinationTextJobs) > 0 { diff --git a/db/factory/comms.phone.bob.go b/db/factory/comms.phone.bob.go index cefbb124..abcd554d 100644 --- a/db/factory/comms.phone.bob.go +++ b/db/factory/comms.phone.bob.go @@ -8,7 +8,9 @@ import ( "testing" models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" "github.com/stephenafamo/bob" ) @@ -35,7 +37,7 @@ func (mods CommsPhoneModSlice) Apply(ctx context.Context, n *CommsPhoneTemplate) // all columns are optional and should be set by mods type CommsPhoneTemplate struct { E164 func() string - IsSubscribed func() bool + IsSubscribed func() null.Val[bool] r commsPhoneR f *Factory @@ -123,7 +125,7 @@ func (o CommsPhoneTemplate) BuildSetter() *models.CommsPhoneSetter { } if o.IsSubscribed != nil { val := o.IsSubscribed() - m.IsSubscribed = omit.From(val) + m.IsSubscribed = omitnull.FromNull(val) } return m @@ -177,10 +179,6 @@ func ensureCreatableCommsPhone(m *models.CommsPhoneSetter) { val := random_string(nil) m.E164 = omit.From(val) } - if !(m.IsSubscribed.IsValue()) { - val := random_bool(nil) - m.IsSubscribed = omit.From(val) - } } // insertOptRels creates and inserts any optional the relationships on *models.CommsPhone @@ -378,14 +376,14 @@ func (m commsPhoneMods) RandomE164(f *faker.Faker) CommsPhoneMod { } // Set the model columns to this value -func (m commsPhoneMods) IsSubscribed(val bool) CommsPhoneMod { +func (m commsPhoneMods) IsSubscribed(val null.Val[bool]) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { - o.IsSubscribed = func() bool { return val } + o.IsSubscribed = func() null.Val[bool] { return val } }) } // Set the Column from the function -func (m commsPhoneMods) IsSubscribedFunc(f func() bool) CommsPhoneMod { +func (m commsPhoneMods) IsSubscribedFunc(f func() null.Val[bool]) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { o.IsSubscribed = f }) @@ -400,10 +398,32 @@ func (m commsPhoneMods) UnsetIsSubscribed() CommsPhoneMod { // Generates a random value for the column using the given faker // if faker is nil, a default faker is used +// The generated value is sometimes null func (m commsPhoneMods) RandomIsSubscribed(f *faker.Faker) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { - o.IsSubscribed = func() bool { - return random_bool(f) + o.IsSubscribed = func() null.Val[bool] { + if f == nil { + f = &defaultFaker + } + + val := random_bool(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m commsPhoneMods) RandomIsSubscribedNotNull(f *faker.Faker) CommsPhoneMod { + return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { + o.IsSubscribed = func() null.Val[bool] { + if f == nil { + f = &defaultFaker + } + + val := random_bool(f) + return null.From(val) } }) } diff --git a/db/migrations/00042_text_subscribe_nullable.sql b/db/migrations/00042_text_subscribe_nullable.sql new file mode 100644 index 00000000..778c7577 --- /dev/null +++ b/db/migrations/00042_text_subscribe_nullable.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE comms.phone ALTER COLUMN is_subscribed DROP NOT NULL; diff --git a/db/migrations/00043_username_unique.sql b/db/migrations/00043_username_unique.sql new file mode 100644 index 00000000..48225744 --- /dev/null +++ b/db/migrations/00043_username_unique.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE user_ ADD CONSTRAINT user_username_unique UNIQUE (username); diff --git a/db/models/comms.phone.bob.go b/db/models/comms.phone.bob.go index 6af2d6e7..56a665c0 100644 --- a/db/models/comms.phone.bob.go +++ b/db/models/comms.phone.bob.go @@ -8,7 +8,9 @@ import ( "fmt" "io" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/bob/dialect/psql/dialect" @@ -23,8 +25,8 @@ import ( // CommsPhone is an object representing the database table. type CommsPhone struct { - E164 string `db:"e164,pk" ` - IsSubscribed bool `db:"is_subscribed" ` + E164 string `db:"e164,pk" ` + IsSubscribed null.Val[bool] `db:"is_subscribed" ` R commsPhoneR `db:"-" ` @@ -78,8 +80,8 @@ func (commsPhoneColumns) AliasedAs(alias string) commsPhoneColumns { // All values are optional, and do not have to be set // Generated columns are not included type CommsPhoneSetter struct { - E164 omit.Val[string] `db:"e164,pk" ` - IsSubscribed omit.Val[bool] `db:"is_subscribed" ` + E164 omit.Val[string] `db:"e164,pk" ` + IsSubscribed omitnull.Val[bool] `db:"is_subscribed" ` } func (s CommsPhoneSetter) SetColumns() []string { @@ -87,7 +89,7 @@ func (s CommsPhoneSetter) SetColumns() []string { if s.E164.IsValue() { vals = append(vals, "e164") } - if s.IsSubscribed.IsValue() { + if !s.IsSubscribed.IsUnset() { vals = append(vals, "is_subscribed") } return vals @@ -97,8 +99,8 @@ func (s CommsPhoneSetter) Overwrite(t *CommsPhone) { if s.E164.IsValue() { t.E164 = s.E164.MustGet() } - if s.IsSubscribed.IsValue() { - t.IsSubscribed = s.IsSubscribed.MustGet() + if !s.IsSubscribed.IsUnset() { + t.IsSubscribed = s.IsSubscribed.MustGetNull() } } @@ -115,8 +117,8 @@ func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) { vals[0] = psql.Raw("DEFAULT") } - if s.IsSubscribed.IsValue() { - vals[1] = psql.Arg(s.IsSubscribed.MustGet()) + if !s.IsSubscribed.IsUnset() { + vals[1] = psql.Arg(s.IsSubscribed.MustGetNull()) } else { vals[1] = psql.Raw("DEFAULT") } @@ -139,7 +141,7 @@ func (s CommsPhoneSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if s.IsSubscribed.IsValue() { + if !s.IsSubscribed.IsUnset() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "is_subscribed")...), psql.Arg(s.IsSubscribed), @@ -650,7 +652,7 @@ func (commsPhone0 *CommsPhone) AttachSourceTextLogs(ctx context.Context, exec bo type commsPhoneWhere[Q psql.Filterable] struct { E164 psql.WhereMod[Q, string] - IsSubscribed psql.WhereMod[Q, bool] + IsSubscribed psql.WhereNullMod[Q, bool] } func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] { @@ -660,7 +662,7 @@ func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] { func buildCommsPhoneWhere[Q psql.Filterable](cols commsPhoneColumns) commsPhoneWhere[Q] { return commsPhoneWhere[Q]{ E164: psql.Where[Q, string](cols.E164), - IsSubscribed: psql.Where[Q, bool](cols.IsSubscribed), + IsSubscribed: psql.WhereNull[Q, bool](cols.IsSubscribed), } } diff --git a/platform/text.go b/platform/text.go index 2157dae2..e857a702 100644 --- a/platform/text.go +++ b/platform/text.go @@ -11,7 +11,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/llm" - "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" ) @@ -66,12 +66,16 @@ func splitPhoneSource(s string) (string, string) { } -func isSubscribed(ctx context.Context, src string) (bool, error) { +func isSubscribed(ctx context.Context, src string) (*bool, error) { phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) if err != nil { - return false, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) + return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) } - return phone.IsSubscribed, nil + if phone.IsSubscribed.IsNull() { + return nil, nil + } + result := phone.IsSubscribed.MustGet() + return &result, nil } func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { @@ -80,11 +84,16 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { return fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) } phone.Update(ctx, db.PGInstance.BobDB, &models.CommsPhoneSetter{ - IsSubscribed: omit.From(is_subscribed), + IsSubscribed: omitnull.From(is_subscribed), }) + log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed") return nil } +func handleWaitingTextJobs(ctx context.Context, src string) { + log.Info().Str("src", src).Msg("Pretend handle waiting jobs") + +} func HandleTextMessage(from string, to string, body string) { ctx := context.Background() type_, src := splitPhoneSource(from) @@ -98,18 +107,25 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Msg("Failed to handle message") return } - if !subscribed { + // We don't know if they're subscribed or not. + if subscribed == nil { body_l := strings.TrimSpace(strings.ToLower(body)) - if body_l == "stop" { + switch body_l { + case "stop": setSubscribed(ctx, src, false) - return - } - err = text.SendInitialReprompt(ctx, dst, src) - if err != nil { - log.Error().Err(err).Msg("Failed to resend initial prompt.") + case "yes": + setSubscribed(ctx, src, true) + handleWaitingTextJobs(ctx, src) + default: + err = text.SendInitialReprompt(ctx, dst, src) + if err != nil { + log.Error().Err(err).Msg("Failed to resend initial prompt.") + } } return } + if !(*subscribed) { + } previous_messages, err := loadPreviousMessages(ctx, dst, src) if err != nil { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages")