From 0d74619caca232983b428c9ac986b50b7cc19454 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sat, 30 Dec 2023 15:51:42 +0530 Subject: [PATCH] Make providing `name` in subscriber creation optional and assign internally. Closes #1630. --- cmd/subscribers.go | 20 ++--------- cmd/utils.go | 9 +++++ frontend/src/views/SubscriberForm.vue | 12 ------- internal/subimporter/importer.go | 51 +++++++++++++++------------ 4 files changed, 41 insertions(+), 51 deletions(-) diff --git a/cmd/subscribers.go b/cmd/subscribers.go index ed5074175..087f3b543 100644 --- a/cmd/subscribers.go +++ b/cmd/subscribers.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/knadh/listmonk/internal/subimporter" "github.com/knadh/listmonk/models" "github.com/labstack/echo/v4" ) @@ -183,12 +184,7 @@ loop: func handleCreateSubscriber(c echo.Context) error { var ( app = c.Get("app").(*App) - req struct { - models.Subscriber - Lists []int `json:"lists"` - ListUUIDs []string `json:"list_uuids"` - PreconfirmSubs bool `json:"preconfirm_subscriptions"` - } + req subimporter.SubReq ) // Get and validate fields. @@ -197,20 +193,10 @@ func handleCreateSubscriber(c echo.Context) error { } // Validate fields. - if len(req.Email) > 1000 { - return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail")) - } - - em, err := app.importer.SanitizeEmail(req.Email) + req, err := app.importer.ValidateFields(req) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - req.Email = em - - req.Name = strings.TrimSpace(req.Name) - if len(req.Name) == 0 || len(req.Name) > stdInputMaxLen { - return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName")) - } // Insert the subscriber into the DB. sub, _, err := app.core.InsertSubscriber(req.Subscriber, req.Lists, req.ListUUIDs, req.PreconfirmSubs) diff --git a/cmd/utils.go b/cmd/utils.go index 65b9df8f1..763ae9374 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -104,3 +104,12 @@ func strSliceContains(str string, sl []string) bool { func trimNullBytes(b []byte) string { return string(bytes.Trim(b, "\x00")) } + +func titleCase(input string) string { + parts := strings.Fields(input) + for n, p := range parts { + parts[n] = strings.Title(p) + } + + return strings.Join(parts, " ") +} diff --git a/frontend/src/views/SubscriberForm.vue b/frontend/src/views/SubscriberForm.vue index c42adb1e1..f0f73157c 100644 --- a/frontend/src/views/SubscriberForm.vue +++ b/frontend/src/views/SubscriberForm.vue @@ -222,18 +222,6 @@ export default Vue.extend({ }, onSubmit() { - // If there is no name, auto-generate one from the e-mail. - if (!this.form.name) { - let name = ''; - [name] = this.form.email.toLowerCase().split('@'); - - if (name.includes('.')) { - this.form.name = name.split('.').map((c) => this.$utils.titleCase(c)).join(' '); - } else { - this.form.name = this.$utils.titleCase(name); - } - } - if (this.isEditing) { this.updateSubscriber(); return; diff --git a/internal/subimporter/importer.go b/internal/subimporter/importer.go index 943e58905..167d3ceab 100644 --- a/internal/subimporter/importer.go +++ b/internal/subimporter/importer.go @@ -103,9 +103,9 @@ type Status struct { // SubReq is a wrapper over the Subscriber model. type SubReq struct { models.Subscriber - Lists pq.Int64Array `json:"lists"` - ListUUIDs pq.StringArray `json:"list_uuids"` - PreconfirmSubs bool `json:"preconfirm_subscriptions"` + Lists []int `json:"lists"` + ListUUIDs []string `json:"list_uuids"` + PreconfirmSubs bool `json:"preconfirm_subscriptions"` } type importStatusTpl struct { @@ -263,11 +263,11 @@ func (s *Session) Start() { total = 0 cur = 0 - listIDs = make(pq.Int64Array, len(s.opt.ListIDs)) + listIDs = make([]int, len(s.opt.ListIDs)) ) for i, v := range s.opt.ListIDs { - listIDs[i] = int64(v) + listIDs[i] = v } for sub := range s.subQueue { @@ -294,7 +294,7 @@ func (s *Session) Start() { } if s.opt.Mode == ModeSubscribe { - _, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.opt.SubStatus, s.opt.Overwrite) + _, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, pq.Array(listIDs), s.opt.SubStatus, s.opt.Overwrite) } else if s.opt.Mode == ModeBlocklist { _, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs) } @@ -324,7 +324,7 @@ func (s *Session) Start() { if cur == 0 { s.im.setStatus(StatusFinished) s.log.Printf("imported finished") - if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil { + if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil { s.log.Printf("error updating lists date: %v", err) } s.im.sendNotif(StatusFinished) @@ -343,7 +343,7 @@ func (s *Session) Start() { s.im.incrementImportCount(cur) s.im.setStatus(StatusFinished) s.log.Printf("imported finished") - if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil { + if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil { s.log.Printf("error updating lists date: %v", err) } s.im.sendNotif(StatusFinished) @@ -486,15 +486,11 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error { } hdrKeys := s.mapCSVHeaders(csvHdr, csvHeaders) - // email, and name are required headers. + // email is a required header. if _, ok := hdrKeys["email"]; !ok { s.log.Printf("'email' column not found in '%s'", srcPath) return errors.New("'email' column not found") } - if _, ok := hdrKeys["name"]; !ok { - s.log.Printf("'name' column not found in '%s'", srcPath) - return errors.New("'name' column not found") - } var ( lnHdr = len(hdrKeys) @@ -541,9 +537,12 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error { sub := SubReq{} sub.Email = row["email"] - sub.Name = row["name"] - sub, err = s.im.validateFields(sub) + if v, ok := row["name"]; ok { + sub.Name = v + } + + sub, err = s.im.ValidateFields(sub) if err != nil { s.log.Printf("skipping line %d: %s: %v", i, sub.Email, err) continue @@ -630,23 +629,31 @@ func (im *Importer) SanitizeEmail(email string) (string, error) { return em.Address, nil } -// validateFields validates incoming subscriber field values and returns sanitized fields. -func (im *Importer) validateFields(s SubReq) (SubReq, error) { +// ValidateFields validates incoming subscriber field values and returns sanitized fields. +func (im *Importer) ValidateFields(s SubReq) (SubReq, error) { if len(s.Email) > 1000 { return s, errors.New(im.i18n.T("subscribers.invalidEmail")) } - s.Name = strings.TrimSpace(s.Name) - if len(s.Name) == 0 || len(s.Name) > stdInputMaxLen { - return s, errors.New(im.i18n.T("subscribers.invalidName")) - } - em, err := im.SanitizeEmail(s.Email) if err != nil { return s, err } s.Email = strings.ToLower(em) + // If there's no name, use the name part of the e-mail. + s.Name = strings.TrimSpace(s.Name) + if len(s.Name) == 0 { + name := strings.ToLower(strings.Split(s.Email, "@")[0]) + + parts := strings.Fields(strings.ReplaceAll(name, ".", " ")) + for n, p := range parts { + parts[n] = strings.Title(p) + } + + s.Name = strings.Join(parts, " ") + } + return s, nil }