From 88538097134e5ca0720836c48c9f53cd20f6d476 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sun, 8 Mar 2020 00:03:22 +0530 Subject: [PATCH] Refactor and cleanup initialization. - Clean up main.go (by moving init to init.go) and improve composition comprehension. - Refactor app context and init struct and field names. - Update package dependencies in initialisation. --- admin.go | 8 +- campaigns.go | 90 ++++++------- go.mod | 9 +- go.sum | 21 +++ handlers.go | 8 +- import.go | 14 +- init.go | 310 ++++++++++++++++++++++++++++++++++++++++++++ install.go | 45 ++++--- lists.go | 18 +-- main.go | 331 +++++++---------------------------------------- media.go | 32 ++--- notifications.go | 31 +---- public.go | 48 +++---- subscribers.go | 92 ++++++------- templates.go | 16 +-- utils.go | 64 --------- 16 files changed, 572 insertions(+), 565 deletions(-) create mode 100644 init.go diff --git a/admin.go b/admin.go index 7a33a1c..cf832a5 100644 --- a/admin.go +++ b/admin.go @@ -27,9 +27,9 @@ func handleGetConfigScript(c echo.Context) error { var ( app = c.Get("app").(*App) out = configScript{ - RootURL: app.Constants.RootURL, - FromEmail: app.Constants.FromEmail, - Messengers: app.Manager.GetMessengerNames(), + RootURL: app.constants.RootURL, + FromEmail: app.constants.FromEmail, + Messengers: app.manager.GetMessengerNames(), } b = bytes.Buffer{} @@ -48,7 +48,7 @@ func handleGetDashboardStats(c echo.Context) error { out dashboardStats ) - if err := app.Queries.GetDashboardStats.Get(&out); err != nil { + if err := app.queries.GetDashboardStats.Get(&out); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err))) } diff --git a/campaigns.go b/campaigns.go index 69590d4..8f2d1a1 100644 --- a/campaigns.go +++ b/campaigns.go @@ -86,9 +86,9 @@ func handleGetCampaigns(c echo.Context) error { query = string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&"))) } - err := app.Queries.QueryCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit) + err := app.queries.QueryCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit) if err != nil { - app.Logger.Printf("error fetching campaigns: %v", err) + app.log.Printf("error fetching campaigns: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err))) } @@ -112,8 +112,8 @@ func handleGetCampaigns(c echo.Context) error { } // Lazy load stats. - if err := out.Results.LoadStats(app.Queries.GetCampaignStats); err != nil { - app.Logger.Printf("error fetching campaign stats: %v", err) + if err := out.Results.LoadStats(app.queries.GetCampaignStats); err != nil { + app.log.Printf("error fetching campaign stats: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign stats: %v", pqErrMsg(err))) } @@ -144,25 +144,25 @@ func handlePreviewCampaign(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") } - err := app.Queries.GetCampaignForPreview.Get(camp, id) + err := app.queries.GetCampaignForPreview.Get(camp, id) if err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") } - app.Logger.Printf("error fetching campaign: %v", err) + app.log.Printf("error fetching campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) } var sub models.Subscriber // Get a random subscriber from the campaign. - if err := app.Queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil { + if err := app.queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil { if err == sql.ErrNoRows { // There's no subscriber. Mock one. sub = dummySubscriber } else { - app.Logger.Printf("error fetching subscriber: %v", err) + app.log.Printf("error fetching subscriber: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err))) } @@ -173,16 +173,16 @@ func handlePreviewCampaign(c echo.Context) error { camp.Body = body } - if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil { - app.Logger.Printf("error compiling template: %v", err) + if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil { + app.log.Printf("error compiling template: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err)) } // Render the message body. - m := app.Manager.NewMessage(camp, &sub) + m := app.manager.NewMessage(camp, &sub) if err := m.Render(); err != nil { - app.Logger.Printf("error rendering message: %v", err) + app.log.Printf("error rendering message: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error rendering message: %v", err)) } @@ -218,20 +218,20 @@ func handleCreateCampaign(c echo.Context) error { o = c } - if !app.Manager.HasMessenger(o.MessengerID) { + if !app.manager.HasMessenger(o.MessengerID) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown messenger %s", o.MessengerID)) } uu, err := uuid.NewV4() if err != nil { - app.Logger.Printf("error generating UUID: %v", err) + app.log.Printf("error generating UUID: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID") } // Insert and read ID. var newID int - if err := app.Queries.CreateCampaign.Get(&newID, + if err := app.queries.CreateCampaign.Get(&newID, uu, o.Type, o.Name, @@ -250,7 +250,7 @@ func handleCreateCampaign(c echo.Context) error { "There aren't any subscribers in the target lists to create the campaign.") } - app.Logger.Printf("error creating campaign: %v", err) + app.log.Printf("error creating campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err))) } @@ -274,12 +274,12 @@ func handleUpdateCampaign(c echo.Context) error { } var cm models.Campaign - if err := app.Queries.GetCampaign.Get(&cm, id); err != nil { + if err := app.queries.GetCampaign.Get(&cm, id); err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") } - app.Logger.Printf("error fetching campaign: %v", err) + app.log.Printf("error fetching campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) } @@ -301,7 +301,7 @@ func handleUpdateCampaign(c echo.Context) error { o = c } - res, err := app.Queries.UpdateCampaign.Exec(cm.ID, + res, err := app.queries.UpdateCampaign.Exec(cm.ID, o.Name, o.Subject, o.FromEmail, @@ -313,7 +313,7 @@ func handleUpdateCampaign(c echo.Context) error { o.TemplateID, o.ListIDs) if err != nil { - app.Logger.Printf("error updating campaign: %v", err) + app.log.Printf("error updating campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err))) } @@ -337,12 +337,12 @@ func handleUpdateCampaignStatus(c echo.Context) error { } var cm models.Campaign - if err := app.Queries.GetCampaign.Get(&cm, id); err != nil { + if err := app.queries.GetCampaign.Get(&cm, id); err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") } - app.Logger.Printf("error fetching campaign: %v", err) + app.log.Printf("error fetching campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) } @@ -385,9 +385,9 @@ func handleUpdateCampaignStatus(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, errMsg) } - res, err := app.Queries.UpdateCampaignStatus.Exec(cm.ID, o.Status) + res, err := app.queries.UpdateCampaignStatus.Exec(cm.ID, o.Status) if err != nil { - app.Logger.Printf("error updating campaign status: %v", err) + app.log.Printf("error updating campaign status: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err))) } @@ -412,12 +412,12 @@ func handleDeleteCampaign(c echo.Context) error { } var cm models.Campaign - if err := app.Queries.GetCampaign.Get(&cm, id); err != nil { + if err := app.queries.GetCampaign.Get(&cm, id); err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") } - app.Logger.Printf("error fetching campaign: %v", err) + app.log.Printf("error fetching campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) } @@ -429,8 +429,8 @@ func handleDeleteCampaign(c echo.Context) error { "Only campaigns that haven't been started can be deleted.") } - if _, err := app.Queries.DeleteCampaign.Exec(cm.ID); err != nil { - app.Logger.Printf("error deleting campaign: %v", err) + if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil { + app.log.Printf("error deleting campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err))) } @@ -445,12 +445,12 @@ func handleGetRunningCampaignStats(c echo.Context) error { out []campaignStats ) - if err := app.Queries.GetCampaignStatus.Select(&out, models.CampaignStatusRunning); err != nil { + if err := app.queries.GetCampaignStatus.Select(&out, models.CampaignStatusRunning); err != nil { if err == sql.ErrNoRows { return c.JSON(http.StatusOK, okResp{[]struct{}{}}) } - app.Logger.Printf("error fetching campaign stats: %v", err) + app.log.Printf("error fetching campaign stats: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err))) } else if len(out) == 0 { @@ -509,8 +509,8 @@ func handleTestCampaign(c echo.Context) error { req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i])) } var subs models.Subscribers - if err := app.Queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil { - app.Logger.Printf("error fetching subscribers: %v", err) + if err := app.queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil { + app.log.Printf("error fetching subscribers: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err))) } else if len(subs) == 0 { @@ -519,12 +519,12 @@ func handleTestCampaign(c echo.Context) error { // The campaign. var camp models.Campaign - if err := app.Queries.GetCampaignForPreview.Get(&camp, campID); err != nil { + if err := app.queries.GetCampaignForPreview.Get(&camp, campID); err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") } - app.Logger.Printf("error fetching campaign: %v", err) + app.log.Printf("error fetching campaign: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) } @@ -549,20 +549,20 @@ func handleTestCampaign(c echo.Context) error { // sendTestMessage takes a campaign and a subsriber and sends out a sample campaign message. func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error { - if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil { - app.Logger.Printf("error compiling template: %v", err) + if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil { + app.log.Printf("error compiling template: %v", err) return fmt.Errorf("Error compiling template: %v", err) } // Render the message body. - m := app.Manager.NewMessage(camp, sub) + m := app.manager.NewMessage(camp, sub) if err := m.Render(); err != nil { - app.Logger.Printf("error rendering message: %v", err) + app.log.Printf("error rendering message: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error rendering message: %v", err)) } - if err := app.Messenger.Push(camp.FromEmail, []string{sub.Email}, camp.Subject, m.Body, nil); err != nil { + if err := app.messenger.Push(camp.FromEmail, []string{sub.Email}, camp.Subject, m.Body, nil); err != nil { return err } @@ -572,7 +572,7 @@ func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) er // validateCampaignFields validates incoming campaign field values. func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) { if c.FromEmail == "" { - c.FromEmail = app.Constants.FromEmail + c.FromEmail = app.constants.FromEmail } else if !regexFromAddress.Match([]byte(c.FromEmail)) { if !govalidator.IsEmail(c.FromEmail) { return c, errors.New("invalid `from_email`") @@ -598,7 +598,7 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) { } camp := models.Campaign{Body: c.Body, TemplateBody: tplTag} - if err := c.CompileTemplate(app.Manager.TemplateFuncs(&camp)); err != nil { + if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil { return c, fmt.Errorf("Error compiling campaign body: %v", err) } @@ -621,9 +621,9 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) { // Fetch double opt-in lists from the given list IDs. var lists []models.List - err := app.Queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil) + err := app.queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil) if err != nil { - app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) + app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) return o, echo.NewHTTPError(http.StatusInternalServerError, "Error fetching opt-in lists.") } @@ -648,11 +648,11 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) { // Prepare sample opt-in message for the campaign. var b bytes.Buffer - if err := app.NotifTpls.ExecuteTemplate(&b, "optin-campaign", struct { + if err := app.notifTpls.ExecuteTemplate(&b, "optin-campaign", struct { Lists []models.List OptinURLAttr template.HTMLAttr }{lists, optinURLAttr}); err != nil { - app.Logger.Printf("error compiling 'optin-campaign' template: %v", err) + app.log.Printf("error compiling 'optin-campaign' template: %v", err) return o, echo.NewHTTPError(http.StatusInternalServerError, "Error compiling opt-in campaign template.") } diff --git a/go.mod b/go.mod index 64162fd..1ffb196 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/jinzhu/gorm v1.9.1 github.com/jmoiron/sqlx v1.2.0 github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b - github.com/knadh/goyesql v2.0.0+incompatible - github.com/knadh/koanf v0.4.4 + github.com/knadh/goyesql/v2 v2.1.1 + github.com/knadh/koanf v0.8.1 github.com/knadh/stuffbin v1.0.0 github.com/kr/pretty v0.1.0 // indirect github.com/labstack/echo v3.3.10+incompatible @@ -16,14 +16,11 @@ require ( github.com/lib/pq v1.0.0 github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect - github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727 + github.com/rhnvrm/simples3 v0.5.0 github.com/spf13/pflag v1.0.3 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect - golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect - golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 // indirect - google.golang.org/appengine v1.4.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b ) diff --git a/go.sum b/go.sum index fcbf307..a85dd11 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0= github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= @@ -14,6 +17,7 @@ github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaL github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA= @@ -24,8 +28,12 @@ github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8 h1:HVq7nA5uWjpo93WsWjv github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY= github.com/knadh/goyesql v2.0.0+incompatible h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk= github.com/knadh/goyesql v2.0.0+incompatible/go.mod h1:W0tSzU8l7lYH1Fihj+bdQzkzOwvirrsMNHwkuY22qoY= +github.com/knadh/goyesql/v2 v2.1.1 h1:Orp5ldaxPM4ozKHfu1m7p6iolJFXDGOpF3/jyOgO6ls= +github.com/knadh/goyesql/v2 v2.1.1/go.mod h1:pMzCA130/ZhEIoMmSmbEFXor3A2dxl5L+JllAc/l64s= github.com/knadh/koanf v0.4.4 h1:Pg+eR7wuJtCGHLeip31K20eJojjZ3lXE8ILQQGj2PTM= github.com/knadh/koanf v0.4.4/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs= +github.com/knadh/koanf v0.8.1 h1:4VLACWqrkWRQIup3ooq6lOnaSbOJSNO+YVXnJn/NPZ8= +github.com/knadh/koanf v0.8.1/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w= github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M= github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -53,6 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727 h1:2josYcx2gm3CT0WMqi0jBagvg50V3UMWlYN/CnBEbSI= github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727/go.mod h1:iphavgjkW1uvoIiqLUX6D42XuuI9Cr+B/63xw3gb9qA= +github.com/rhnvrm/simples3 v0.5.0 h1:X+WX0hqoKScdoJAw/G3GArfZ6Ygsn8q+6MdocTMKXOw= +github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -64,16 +74,27 @@ github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QI github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w= golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24 h1:R8bzl0244nw47n1xKs1MUMAaTNgjavKcN/aX2Ss3+Fo= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/handlers.go b/handlers.go index 0112842..8874245 100644 --- a/handlers.go +++ b/handlers.go @@ -35,7 +35,7 @@ type pagination struct { var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") // registerHandlers registers HTTP handlers. -func registerHandlers(e *echo.Echo) { +func registerHTTPHandlers(e *echo.Echo) { e.GET("/", handleIndexPage) e.GET("/api/config.js", handleGetConfigScript) e.GET("/api/dashboard/stats", handleGetDashboardStats) @@ -128,7 +128,7 @@ func registerHandlers(e *echo.Echo) { func handleIndexPage(c echo.Context) error { app := c.Get("app").(*App) - b, err := app.FS.Read("/frontend/index.html") + b, err := app.fs.Read("/frontend/index.html") if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } @@ -161,8 +161,8 @@ func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc ) var exists bool - if err := app.Queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil { - app.Logger.Printf("error checking subscriber existence: %v", err) + if err := app.queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil { + app.log.Printf("error checking subscriber existence: %v", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error", "", `Error processing request. Please retry.`)) diff --git a/import.go b/import.go index 0430d99..c91fd85 100644 --- a/import.go +++ b/import.go @@ -25,7 +25,7 @@ func handleImportSubscribers(c echo.Context) error { app := c.Get("app").(*App) // Is an import already running? - if app.Importer.GetStats().Status == subimporter.StatusImporting { + if app.importer.GetStats().Status == subimporter.StatusImporting { return echo.NewHTTPError(http.StatusBadRequest, "An import is already running. Wait for it to finish or stop it before trying again.") } @@ -71,7 +71,7 @@ func handleImportSubscribers(c echo.Context) error { } // Start the importer session. - impSess, err := app.Importer.NewSession(file.Filename, r.Mode, r.ListIDs) + impSess, err := app.importer.NewSession(file.Filename, r.Mode, r.ListIDs) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error starting import session: %v", err)) @@ -95,14 +95,14 @@ func handleImportSubscribers(c echo.Context) error { go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0])) } - return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()}) + return c.JSON(http.StatusOK, okResp{app.importer.GetStats()}) } // handleGetImportSubscribers returns import statistics. func handleGetImportSubscribers(c echo.Context) error { var ( app = c.Get("app").(*App) - s = app.Importer.GetStats() + s = app.importer.GetStats() ) return c.JSON(http.StatusOK, okResp{s}) } @@ -110,7 +110,7 @@ func handleGetImportSubscribers(c echo.Context) error { // handleGetImportSubscriberStats returns import statistics. func handleGetImportSubscriberStats(c echo.Context) error { app := c.Get("app").(*App) - return c.JSON(http.StatusOK, okResp{string(app.Importer.GetLogs())}) + return c.JSON(http.StatusOK, okResp{string(app.importer.GetLogs())}) } // handleStopImportSubscribers sends a stop signal to the importer. @@ -118,6 +118,6 @@ func handleGetImportSubscriberStats(c echo.Context) error { // is finished, it's state is cleared. func handleStopImportSubscribers(c echo.Context) error { app := c.Get("app").(*App) - app.Importer.Stop() - return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()}) + app.importer.Stop() + return c.JSON(http.StatusOK, okResp{app.importer.GetStats()}) } diff --git a/init.go b/init.go new file mode 100644 index 0000000..a8d28d4 --- /dev/null +++ b/init.go @@ -0,0 +1,310 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "os" + "path/filepath" + "strings" + "time" + + _ "github.com/jinzhu/gorm/dialects/postgres" + "github.com/jmoiron/sqlx" + "github.com/knadh/goyesql/v2" + goyesqlx "github.com/knadh/goyesql/v2/sqlx" + "github.com/knadh/koanf/maps" + "github.com/knadh/listmonk/manager" + "github.com/knadh/listmonk/media" + "github.com/knadh/listmonk/media/providers/filesystem" + "github.com/knadh/listmonk/media/providers/s3" + "github.com/knadh/listmonk/messenger" + "github.com/knadh/listmonk/subimporter" + "github.com/knadh/stuffbin" + "github.com/labstack/echo" +) + +const ( + queryFilePath = "queries.sql" +) + +// initFileSystem initializes the stuffbin FileSystem to provide +// access to bunded static assets to the app. +func initFS() stuffbin.FileSystem { + // Get the executable's path. + path, err := os.Executable() + if err != nil { + log.Fatalf("error getting executable path: %v", err) + } + + fs, err := stuffbin.UnStuff(path) + if err == nil { + return fs + } + + // Running in local mode. Load the required static assets into + // the in-memory stuffbin.FileSystem. + lo.Printf("unable to initialize embedded filesystem: %v", err) + lo.Printf("using local filesystem for static assets") + files := []string{ + "config.toml.sample", + "queries.sql", + "schema.sql", + "email-templates", + "public", + + // The frontend app's static assets are aliased to /frontend + // so that they are accessible at localhost:port/frontend/static/ ... + "frontend/build:/frontend", + } + + fs, err = stuffbin.NewLocalFS("/", files...) + if err != nil { + lo.Fatalf("failed to initialize local file for assets: %v", err) + } + + return fs +} + +// initDB initializes the main DB connection pool and parse and loads the app's +// SQL queries into a prepared query map. +func initDB() *sqlx.DB { + var dbCfg dbConf + if err := ko.Unmarshal("db", &dbCfg); err != nil { + log.Fatalf("error loading db config: %v", err) + } + db, err := connectDB(dbCfg) + if err != nil { + lo.Fatalf("error connecting to DB: %v", err) + } + + return db +} + +// initQueries loads named SQL queries from the queries file and optionally +// prepares them. +func initQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem, prepareQueries bool) (goyesql.Queries, *Queries) { + // Load SQL queries. + qB, err := fs.Read(sqlFile) + if err != nil { + lo.Fatalf("error reading SQL file %s: %v", sqlFile, err) + } + qMap, err := goyesql.ParseBytes(qB) + if err != nil { + lo.Fatalf("error parsing SQL queries: %v", err) + } + + if !prepareQueries { + return qMap, nil + } + + // Prepare queries. + var q Queries + if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil { + lo.Fatalf("error preparing SQL queries: %v", err) + } + return qMap, &q +} + +// constants contains static, constant config values required by the app. +type constants struct { + RootURL string `koanf:"root"` + LogoURL string `koanf:"logo_url"` + FaviconURL string `koanf:"favicon_url"` + FromEmail string `koanf:"from_email"` + NotifyEmails []string `koanf:"notify_emails"` + Privacy struct { + AllowBlacklist bool `koanf:"allow_blacklist"` + AllowExport bool `koanf:"allow_export"` + AllowWipe bool `koanf:"allow_wipe"` + Exportable map[string]bool `koanf:"-"` + } `koanf:"privacy"` + + UnsubURL string + LinkTrackURL string + ViewTrackURL string + OptinURL string +} + +func initConstants() *constants { + // Read constants. + var c constants + if err := ko.Unmarshal("app", &c); err != nil { + log.Fatalf("error loading app config: %v", err) + } + if err := ko.Unmarshal("privacy", &c.Privacy); err != nil { + log.Fatalf("error loading app config: %v", err) + } + c.RootURL = strings.TrimRight(c.RootURL, "/") + c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable")) + + // Static URLS. + // url.com/subscription/{campaign_uuid}/{subscriber_uuid} + c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", c.RootURL) + + // url.com/subscription/optin/{subscriber_uuid} + c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", c.RootURL) + + // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid} + c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", c.RootURL) + + // url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png + c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL) + + return &c +} + +// initCampaignManager initializes the campaign manager. +func initCampaignManager(app *App) *manager.Manager { + campNotifCB := func(subject string, data interface{}) error { + return sendNotification(app.constants.NotifyEmails, subject, notifTplCampaign, data, app) + } + return manager.New(manager.Config{ + Concurrency: ko.Int("app.concurrency"), + MaxSendErrors: ko.Int("app.max_send_errors"), + FromEmail: app.constants.FromEmail, + UnsubURL: app.constants.UnsubURL, + OptinURL: app.constants.OptinURL, + LinkTrackURL: app.constants.LinkTrackURL, + ViewTrackURL: app.constants.ViewTrackURL, + }, newManagerDB(app.queries), campNotifCB, lo) + +} + +// initImporter initializes the bulk subscriber importer. +func initImporter(app *App) *subimporter.Importer { + return subimporter.New(app.queries.UpsertSubscriber.Stmt, + app.queries.UpsertBlacklistSubscriber.Stmt, + app.queries.UpdateListsDate.Stmt, + app.db.DB, + func(subject string, data interface{}) error { + go sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data, app) + return nil + }) +} + +// initMessengers initializes various messaging backends. +func initMessengers(r *manager.Manager) messenger.Messenger { + // Load SMTP configurations for the default e-mail Messenger. + var ( + mapKeys = ko.MapKeys("smtp") + srv = make([]messenger.Server, 0, len(mapKeys)) + ) + + for _, name := range mapKeys { + if !ko.Bool(fmt.Sprintf("smtp.%s.enabled", name)) { + lo.Printf("skipped SMTP: %s", name) + continue + } + + var s messenger.Server + if err := ko.Unmarshal("smtp."+name, &s); err != nil { + lo.Fatalf("error loading SMTP: %v", err) + } + s.Name = name + s.SendTimeout *= time.Millisecond + srv = append(srv, s) + + lo.Printf("loaded SMTP: %s (%s@%s)", s.Name, s.Username, s.Host) + } + + // Initialize the default e-mail messenger. + msgr, err := messenger.NewEmailer(srv...) + if err != nil { + lo.Fatalf("error loading e-mail messenger: %v", err) + } + if err := r.AddMessenger(msgr); err != nil { + lo.Printf("error registering messenger %s", err) + } + + return msgr +} + +// initMediaStore initializes Upload manager with a custom backend. +func initMediaStore() media.Store { + switch provider := ko.String("upload.provider"); provider { + case "s3": + var opts s3.Opts + ko.Unmarshal("upload.s3", &opts) + uplder, err := s3.NewS3Store(opts) + if err != nil { + lo.Fatalf("error initializing s3 upload provider %s", err) + } + return uplder + + case "filesystem": + var opts filesystem.Opts + ko.Unmarshal("upload.filesystem", &opts) + opts.UploadPath = filepath.Clean(opts.UploadPath) + opts.UploadURI = filepath.Clean(opts.UploadURI) + uplder, err := filesystem.NewDiskStore(opts) + if err != nil { + lo.Fatalf("error initializing filesystem upload provider %s", err) + } + return uplder + + default: + lo.Fatalf("unknown provider. please select one of either filesystem or s3") + } + return nil +} + +// initNotifTemplates compiles and returns e-mail notification templates that are +// used for sending ad-hoc notifications to admins and subscribers. +func initNotifTemplates(path string, fs stuffbin.FileSystem, cs *constants) *template.Template { + // Register utility functions that the e-mail templates can use. + funcs := template.FuncMap{ + "RootURL": func() string { + return cs.RootURL + }, + "LogoURL": func() string { + return cs.LogoURL + }} + + tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/email-templates/*.html") + if err != nil { + lo.Fatalf("error parsing e-mail notif templates: %v", err) + } + return tpl +} + +// initHTTPServer sets up and runs the app's main HTTP server and blocks forever. +func initHTTPServer(app *App) { + // Initialize the HTTP server. + var srv = echo.New() + srv.HideBanner = true + + // Register app (*App) to be injected into all HTTP handlers. + srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + c.Set("app", app) + return next(c) + } + }) + + // Parse and load user facing templates. + tpl, err := stuffbin.ParseTemplatesGlob(nil, app.fs, "/public/templates/*.html") + if err != nil { + lo.Fatalf("error parsing public templates: %v", err) + } + srv.Renderer = &tplRenderer{ + templates: tpl, + RootURL: app.constants.RootURL, + LogoURL: app.constants.LogoURL, + FaviconURL: app.constants.FaviconURL} + + // Initialize the static file server. + fSrv := app.fs.FileServer() + srv.GET("/public/*", echo.WrapHandler(fSrv)) + srv.GET("/frontend/*", echo.WrapHandler(fSrv)) + if ko.String("upload.provider") == "filesystem" { + srv.Static(ko.String("upload.filesystem.upload_uri"), + ko.String("upload.filesystem.upload_path")) + } + + // Register all HTTP handlers. + registerHTTPHandlers(srv) + + // Start the server. + srv.Logger.Fatal(srv.Start(ko.String("app.address"))) +} diff --git a/install.go b/install.go index 22b3bf7..ddebc23 100644 --- a/install.go +++ b/install.go @@ -10,14 +10,17 @@ import ( "github.com/gofrs/uuid" "github.com/jmoiron/sqlx" - "github.com/knadh/goyesql" + goyesqlx "github.com/knadh/goyesql/v2/sqlx" "github.com/knadh/listmonk/models" + "github.com/knadh/stuffbin" "github.com/lib/pq" ) // install runs the first time setup of creating and // migrating the database and creating the super user. -func install(app *App, qMap goyesql.Queries, prompt bool) { +func install(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) { + qMap, _ := initQueries(queryFilePath, db, fs, false) + fmt.Println("") fmt.Println("** First time installation **") fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **", @@ -28,7 +31,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { var ok string fmt.Print("Continue (y/n)? ") if _, err := fmt.Scanf("%s", &ok); err != nil { - logger.Fatalf("Error reading value from terminal: %v", err) + lo.Fatalf("Error reading value from terminal: %v", err) } if strings.ToLower(ok) != "y" { fmt.Println("Installation cancelled.") @@ -37,15 +40,15 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { } // Migrate the tables. - err := installMigrate(app.DB, app) + err := installMigrate(db, fs) if err != nil { - logger.Fatalf("Error migrating DB schema: %v", err) + lo.Fatalf("Error migrating DB schema: %v", err) } // Load the queries. var q Queries - if err := scanQueriesToStruct(&q, qMap, app.DB.Unsafe()); err != nil { - logger.Fatalf("error loading SQL queries: %v", err) + if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil { + lo.Fatalf("error loading SQL queries: %v", err) } // Sample list. @@ -60,7 +63,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { models.ListOptinSingle, pq.StringArray{"test"}, ); err != nil { - logger.Fatalf("Error creating list: %v", err) + lo.Fatalf("Error creating list: %v", err) } if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()), @@ -69,7 +72,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { models.ListOptinDouble, pq.StringArray{"test"}, ); err != nil { - logger.Fatalf("Error creating list: %v", err) + lo.Fatalf("Error creating list: %v", err) } // Sample subscriber. @@ -80,7 +83,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { `{"type": "known", "good": true, "city": "Bengaluru"}`, pq.Int64Array{int64(defList)}, ); err != nil { - logger.Fatalf("Error creating subscriber: %v", err) + lo.Fatalf("Error creating subscriber: %v", err) } if _, err := q.UpsertSubscriber.Exec( uuid.Must(uuid.NewV4()), @@ -89,7 +92,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { `{"type": "unknown", "good": true, "city": "Bengaluru"}`, pq.Int64Array{int64(optinList)}, ); err != nil { - logger.Fatalf("Error creating subscriber: %v", err) + lo.Fatalf("Error creating subscriber: %v", err) } // Default template. @@ -103,10 +106,10 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { "Default template", string(tplBody), ); err != nil { - logger.Fatalf("error creating default template: %v", err) + lo.Fatalf("error creating default template: %v", err) } if _, err := q.SetDefaultTemplate.Exec(tplID); err != nil { - logger.Fatalf("error setting default template: %v", err) + lo.Fatalf("error setting default template: %v", err) } // Sample campaign. @@ -126,17 +129,17 @@ func install(app *App, qMap goyesql.Queries, prompt bool) { 1, pq.Int64Array{1}, ); err != nil { - logger.Fatalf("error creating sample campaign: %v", err) + lo.Fatalf("error creating sample campaign: %v", err) } - logger.Printf("Setup complete") - logger.Printf(`Run the program and access the dashboard at %s`, ko.String("app.address")) + lo.Printf("Setup complete") + lo.Printf(`Run the program and access the dashboard at %s`, ko.MustString("app.address")) } // installMigrate executes the SQL schema and creates the necessary tables and types. -func installMigrate(db *sqlx.DB, app *App) error { - q, err := app.FS.Read("/schema.sql") +func installMigrate(db *sqlx.DB, fs stuffbin.FileSystem) error { + q, err := fs.Read("/schema.sql") if err != nil { return err } @@ -156,11 +159,7 @@ func newConfigFile() error { // Initialize the static file system into which all // required static assets (.sql, .js files etc.) are loaded. - fs, err := initFileSystem(os.Args[0]) - if err != nil { - return err - } - + fs := initFS() b, err := fs.Read("config.toml.sample") if err != nil { return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err) diff --git a/lists.go b/lists.go index e5ea778..c1a4cf4 100644 --- a/lists.go +++ b/lists.go @@ -37,9 +37,9 @@ func handleGetLists(c echo.Context) error { single = true } - err := app.Queries.GetLists.Select(&out.Results, listID, pg.Offset, pg.Limit) + err := app.queries.GetLists.Select(&out.Results, listID, pg.Offset, pg.Limit) if err != nil { - app.Logger.Printf("error fetching lists: %v", err) + app.log.Printf("error fetching lists: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err))) } @@ -87,20 +87,20 @@ func handleCreateList(c echo.Context) error { uu, err := uuid.NewV4() if err != nil { - app.Logger.Printf("error generating UUID: %v", err) + app.log.Printf("error generating UUID: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID") } // Insert and read ID. var newID int o.UUID = uu.String() - if err := app.Queries.CreateList.Get(&newID, + if err := app.queries.CreateList.Get(&newID, o.UUID, o.Name, o.Type, o.Optin, pq.StringArray(normalizeTags(o.Tags))); err != nil { - app.Logger.Printf("error creating list: %v", err) + app.log.Printf("error creating list: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error creating list: %s", pqErrMsg(err))) } @@ -128,10 +128,10 @@ func handleUpdateList(c echo.Context) error { return err } - res, err := app.Queries.UpdateList.Exec(id, + res, err := app.queries.UpdateList.Exec(id, o.Name, o.Type, o.Optin, pq.StringArray(normalizeTags(o.Tags))) if err != nil { - app.Logger.Printf("error updating list: %v", err) + app.log.Printf("error updating list: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error updating list: %s", pqErrMsg(err))) } @@ -165,8 +165,8 @@ func handleDeleteLists(c echo.Context) error { ids = append(ids, id) } - if _, err := app.Queries.DeleteLists.Exec(ids); err != nil { - app.Logger.Printf("error deleting lists: %v", err) + if _, err := app.queries.DeleteLists.Exec(ids); err != nil { + app.log.Printf("error deleting lists: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error deleting: %v", err)) } diff --git a/main.go b/main.go index cc38fc6..cbb0c03 100644 --- a/main.go +++ b/main.go @@ -5,68 +5,41 @@ import ( "html/template" "log" "os" - "path/filepath" "strings" "time" - _ "github.com/jinzhu/gorm/dialects/postgres" "github.com/jmoiron/sqlx" - "github.com/knadh/goyesql" "github.com/knadh/koanf" - "github.com/knadh/koanf/maps" "github.com/knadh/koanf/parsers/toml" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/posflag" "github.com/knadh/listmonk/manager" "github.com/knadh/listmonk/media" - "github.com/knadh/listmonk/media/providers/filesystem" - "github.com/knadh/listmonk/media/providers/s3" "github.com/knadh/listmonk/messenger" "github.com/knadh/listmonk/subimporter" "github.com/knadh/stuffbin" - "github.com/labstack/echo" flag "github.com/spf13/pflag" ) -type constants struct { - RootURL string `koanf:"root"` - LogoURL string `koanf:"logo_url"` - FaviconURL string `koanf:"favicon_url"` - UnsubURL string - LinkTrackURL string - ViewTrackURL string - OptinURL string - FromEmail string `koanf:"from_email"` - NotifyEmails []string `koanf:"notify_emails"` - Privacy privacyOptions `koanf:"privacy"` -} - -type privacyOptions struct { - AllowBlacklist bool `koanf:"allow_blacklist"` - AllowExport bool `koanf:"allow_export"` - AllowWipe bool `koanf:"allow_wipe"` - Exportable map[string]bool `koanf:"-"` -} - // App contains the "global" components that are // passed around, especially through HTTP handlers. type App struct { - Constants *constants - DB *sqlx.DB - Queries *Queries - Importer *subimporter.Importer - Manager *manager.Manager - FS stuffbin.FileSystem - Logger *log.Logger - NotifTpls *template.Template - Messenger messenger.Messenger - Media media.Store + fs stuffbin.FileSystem + db *sqlx.DB + queries *Queries + constants *constants + manager *manager.Manager + importer *subimporter.Importer + messenger messenger.Messenger + media media.Store + notifTpls *template.Template + log *log.Logger } var ( // Global logger. - logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) + lo = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) // Global configuration reader. ko = koanf.New(".") @@ -75,14 +48,14 @@ var ( ) func init() { - // Register --help handler. f := flag.NewFlagSet("config", flag.ContinueOnError) f.Usage = func() { + // Register --help handler. fmt.Println(f.FlagUsages()) os.Exit(0) } - // Setup the default configuration. + // Register the commandline flags. f.StringSlice("config", []string{"config.toml"}, "Path to one or more config files (will be merged in order)") f.Bool("install", false, "Run first time installation") @@ -90,9 +63,8 @@ func init() { f.Bool("new-config", false, "Generate sample config file") f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install") - // Process flags. if err := f.Parse(os.Args[1:]); err != nil { - logger.Fatalf("error loading flags: %v", err) + lo.Fatalf("error loading flags: %v", err) } // Display version. @@ -104,277 +76,72 @@ func init() { // Generate new config. if ok, _ := f.GetBool("new-config"); ok { if err := newConfigFile(); err != nil { - logger.Println(err) + lo.Println(err) os.Exit(1) } - logger.Println("generated config.toml. Edit and run --install") + lo.Println("generated config.toml. Edit and run --install") os.Exit(0) } // Load config files. cFiles, _ := f.GetStringSlice("config") for _, f := range cFiles { - logger.Printf("reading config: %s", f) + lo.Printf("reading config: %s", f) if err := ko.Load(file.Provider(f), toml.Parser()); err != nil { if os.IsNotExist(err) { - logger.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.") + lo.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.") } - logger.Fatalf("error loadng config from file: %v.", err) + lo.Fatalf("error loadng config from file: %v.", err) } } + // Load environment variables and merge into the loaded config. if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string { return strings.Replace(strings.ToLower( strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1) }), nil); err != nil { - logger.Fatalf("error loading config from env: %v", err) + lo.Fatalf("error loading config from env: %v", err) } if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil { - logger.Fatalf("error loading config: %v", err) + lo.Fatalf("error loading config: %v", err) } } -// initFileSystem initializes the stuffbin FileSystem to provide -// access to bunded static assets to the app. -func initFileSystem(binPath string) (stuffbin.FileSystem, error) { - fs, err := stuffbin.UnStuff(binPath) - if err == nil { - return fs, nil - } - - // Running in local mode. Load the required static assets into - // the in-memory stuffbin.FileSystem. - logger.Printf("unable to initialize embedded filesystem: %v", err) - logger.Printf("using local filesystem for static assets") - files := []string{ - "config.toml.sample", - "queries.sql", - "schema.sql", - "email-templates", - "public", - - // The frontend app's static assets are aliased to /frontend - // so that they are accessible at localhost:port/frontend/static/ ... - "frontend/build:/frontend", - } - - fs, err = stuffbin.NewLocalFS("/", files...) - if err != nil { - return nil, fmt.Errorf("failed to initialize local file for assets: %v", err) - } - - return fs, nil -} - -// initMessengers initializes various messaging backends. -func initMessengers(r *manager.Manager) messenger.Messenger { - // Load SMTP configurations for the default e-mail Messenger. - var ( - mapKeys = ko.MapKeys("smtp") - srv = make([]messenger.Server, 0, len(mapKeys)) - ) - - for _, name := range mapKeys { - if !ko.Bool(fmt.Sprintf("smtp.%s.enabled", name)) { - logger.Printf("skipped SMTP: %s", name) - continue - } - - var s messenger.Server - if err := ko.Unmarshal("smtp."+name, &s); err != nil { - logger.Fatalf("error loading SMTP: %v", err) - } - s.Name = name - s.SendTimeout *= time.Millisecond - srv = append(srv, s) - - logger.Printf("loaded SMTP: %s (%s@%s)", s.Name, s.Username, s.Host) - } - - msgr, err := messenger.NewEmailer(srv...) - if err != nil { - logger.Fatalf("error loading e-mail messenger: %v", err) - } - if err := r.AddMessenger(msgr); err != nil { - logger.Printf("error registering messenger %s", err) - } - - return msgr -} - -// initMediaStore initializes Upload manager with a custom backend. -func initMediaStore() media.Store { - switch provider := ko.String("upload.provider"); provider { - case "s3": - var opts s3.Opts - ko.Unmarshal("upload.s3", &opts) - uplder, err := s3.NewS3Store(opts) - if err != nil { - logger.Fatalf("error initializing s3 upload provider %s", err) - } - return uplder - case "filesystem": - var opts filesystem.Opts - ko.Unmarshal("upload.filesystem", &opts) - opts.UploadPath = filepath.Clean(opts.UploadPath) - opts.UploadURI = filepath.Clean(opts.UploadURI) - uplder, err := filesystem.NewDiskStore(opts) - if err != nil { - logger.Fatalf("error initializing filesystem upload provider %s", err) - } - return uplder - default: - logger.Fatalf("unknown provider. please select one of either filesystem or s3") - } - return nil -} - func main() { - // Connect to the DB. - var dbCfg dbConf - if err := ko.Unmarshal("db", &dbCfg); err != nil { - log.Fatalf("error loading db config: %v", err) - } - db, err := connectDB(dbCfg) - if err != nil { - logger.Fatalf("error connecting to DB: %v", err) - } + // Initialize the DB and the filesystem that are required by the installer + // and the app. + var ( + fs = initFS() + db = initDB() + ) defer db.Close() - var c constants - if err := ko.Unmarshal("app", &c); err != nil { - log.Fatalf("error loading app config: %v", err) - } - if err := ko.Unmarshal("privacy", &c.Privacy); err != nil { - log.Fatalf("error loading app config: %v", err) - } - c.RootURL = strings.TrimRight(c.RootURL, "/") - c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable")) - - // Initialize the static file system into which all - // required static assets (.sql, .js files etc.) are loaded. - fs, err := initFileSystem(os.Args[0]) - if err != nil { - logger.Fatal(err) - } - - // Initialize the app context that's passed around. - app := &App{ - Constants: &c, - DB: db, - Logger: logger, - FS: fs, - } - - // Load SQL queries. - qB, err := fs.Read("/queries.sql") - if err != nil { - logger.Fatalf("error reading queries.sql: %v", err) - } - qMap, err := goyesql.ParseBytes(qB) - if err != nil { - logger.Fatalf("error parsing SQL queries: %v", err) - } - - // Run the first time installation. + // Installer mode? This runs before the SQL queries are loaded and prepared + // as the installer needs to work on an empty DB. if ko.Bool("install") { - install(app, qMap, !ko.Bool("yes")) + install(db, fs, !ko.Bool("yes")) return } - // Map queries to the query container. - q := &Queries{} - if err := scanQueriesToStruct(q, qMap, db.Unsafe()); err != nil { - logger.Fatalf("no SQL queries loaded: %v", err) + // Initialize the main app controller that wraps all of the app's + // components. This is passed around HTTP handlers. + app := &App{ + fs: fs, + db: db, + constants: initConstants(), + media: initMediaStore(), + log: lo, } - app.Queries = q + _, app.queries = initQueries(queryFilePath, db, fs, true) + app.manager = initCampaignManager(app) + app.importer = initImporter(app) + app.messenger = initMessengers(app.manager) + app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.constants) - // Initialize the bulk subscriber importer. - importNotifCB := func(subject string, data interface{}) error { - go sendNotification(app.Constants.NotifyEmails, subject, notifTplImport, data, app) - return nil - } - app.Importer = subimporter.New(q.UpsertSubscriber.Stmt, - q.UpsertBlacklistSubscriber.Stmt, - q.UpdateListsDate.Stmt, - db.DB, - importNotifCB) + // Start the campaign workers. + go app.manager.Run(time.Second * 5) + app.manager.SpawnWorkers() - // Prepare notification e-mail templates. - notifTpls, err := compileNotifTpls("/email-templates/*.html", fs, app) - if err != nil { - logger.Fatalf("error loading e-mail notification templates: %v", err) - } - app.NotifTpls = notifTpls - - // Static URLS. - // url.com/subscription/{campaign_uuid}/{subscriber_uuid} - c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", app.Constants.RootURL) - - // url.com/subscription/optin/{subscriber_uuid} - c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", app.Constants.RootURL) - - // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid} - c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", app.Constants.RootURL) - - // url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png - c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", app.Constants.RootURL) - - // Initialize the campaign manager. - campNotifCB := func(subject string, data interface{}) error { - return sendNotification(app.Constants.NotifyEmails, subject, notifTplCampaign, data, app) - } - m := manager.New(manager.Config{ - Concurrency: ko.Int("app.concurrency"), - MaxSendErrors: ko.Int("app.max_send_errors"), - FromEmail: app.Constants.FromEmail, - UnsubURL: c.UnsubURL, - OptinURL: c.OptinURL, - LinkTrackURL: c.LinkTrackURL, - ViewTrackURL: c.ViewTrackURL, - }, newManagerDB(q), campNotifCB, logger) - app.Manager = m - - // Add messengers. - app.Messenger = initMessengers(app.Manager) - - // Add uploader - app.Media = initMediaStore() - - // Initialize the workers that push out messages. - go m.Run(time.Second * 5) - m.SpawnWorkers() - - // Initialize the HTTP server. - var srv = echo.New() - srv.HideBanner = true - - // Register app (*App) to be injected into all HTTP handlers. - srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Set("app", app) - return next(c) - } - }) - - // Parse user facing templates. - tpl, err := stuffbin.ParseTemplatesGlob(nil, fs, "/public/templates/*.html") - if err != nil { - logger.Fatalf("error parsing public templates: %v", err) - } - srv.Renderer = &tplRenderer{ - templates: tpl, - RootURL: c.RootURL, - LogoURL: c.LogoURL, - FaviconURL: c.FaviconURL} - - // Register HTTP handlers and static file servers. - fSrv := app.FS.FileServer() - srv.GET("/public/*", echo.WrapHandler(fSrv)) - srv.GET("/frontend/*", echo.WrapHandler(fSrv)) - if ko.String("upload.provider") == "filesystem" { - srv.Static(ko.String("upload.filesystem.upload_uri"), ko.String("upload.filesystem.upload_path")) - } - registerHandlers(srv) - srv.Logger.Fatal(srv.Start(ko.String("app.address"))) + // Start and run the app server. + initHTTPServer(app) } diff --git a/media.go b/media.go index b4d3951..c36ba40 100644 --- a/media.go +++ b/media.go @@ -54,9 +54,9 @@ func handleUploadMedia(c echo.Context) error { defer src.Close() // Upload the file. - fName, err = app.Media.Put(fName, typ, src) + fName, err = app.media.Put(fName, typ, src) if err != nil { - app.Logger.Printf("error uploading file: %v", err) + app.log.Printf("error uploading file: %v", err) cleanUp = true return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error uploading file: %s", err)) @@ -66,8 +66,8 @@ func handleUploadMedia(c echo.Context) error { // If any of the subroutines in this function fail, // the uploaded image should be removed. if cleanUp { - app.Media.Delete(fName) - app.Media.Delete(thumbPrefix + fName) + app.media.Delete(fName) + app.media.Delete(thumbPrefix + fName) } }() @@ -75,30 +75,30 @@ func handleUploadMedia(c echo.Context) error { thumbFile, err := createThumbnail(file) if err != nil { cleanUp = true - app.Logger.Printf("error resizing image: %v", err) + app.log.Printf("error resizing image: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error resizing image: %s", err)) } // Upload thumbnail. - thumbfName, err := app.Media.Put(thumbPrefix+fName, typ, thumbFile) + thumbfName, err := app.media.Put(thumbPrefix+fName, typ, thumbFile) if err != nil { cleanUp = true - app.Logger.Printf("error saving thumbnail: %v", err) + app.log.Printf("error saving thumbnail: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error saving thumbnail: %s", err)) } uu, err := uuid.NewV4() if err != nil { - app.Logger.Printf("error generating UUID: %v", err) + app.log.Printf("error generating UUID: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID") } // Write to the DB. - if _, err := app.Queries.InsertMedia.Exec(uu, fName, thumbfName, 0, 0); err != nil { + if _, err := app.queries.InsertMedia.Exec(uu, fName, thumbfName, 0, 0); err != nil { cleanUp = true - app.Logger.Printf("error inserting uploaded file to db: %v", err) + app.log.Printf("error inserting uploaded file to db: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err))) } @@ -112,14 +112,14 @@ func handleGetMedia(c echo.Context) error { out []media.Media ) - if err := app.Queries.GetMedia.Select(&out); err != nil { + if err := app.queries.GetMedia.Select(&out); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err))) } for i := 0; i < len(out); i++ { - out[i].URI = app.Media.Get(out[i].Filename) - out[i].ThumbURI = app.Media.Get(thumbPrefix + out[i].Filename) + out[i].URI = app.media.Get(out[i].Filename) + out[i].ThumbURI = app.media.Get(thumbPrefix + out[i].Filename) } return c.JSON(http.StatusOK, okResp{out}) @@ -137,12 +137,12 @@ func handleDeleteMedia(c echo.Context) error { } var m media.Media - if err := app.Queries.DeleteMedia.Get(&m, id); err != nil { + if err := app.queries.DeleteMedia.Get(&m, id); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error deleting media: %s", pqErrMsg(err))) } - app.Media.Delete(m.Filename) - app.Media.Delete(thumbPrefix + m.Filename) + app.media.Delete(m.Filename) + app.media.Delete(thumbPrefix + m.Filename) return c.JSON(http.StatusOK, okResp{true}) } diff --git a/notifications.go b/notifications.go index 85b8523..9b70daa 100644 --- a/notifications.go +++ b/notifications.go @@ -2,9 +2,6 @@ package main import ( "bytes" - "html/template" - - "github.com/knadh/stuffbin" ) const ( @@ -24,39 +21,19 @@ type notifData struct { // sendNotification sends out an e-mail notification to admins. func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error { var b bytes.Buffer - if err := app.NotifTpls.ExecuteTemplate(&b, tplName, data); err != nil { - app.Logger.Printf("error compiling notification template '%s': %v", tplName, err) + if err := app.notifTpls.ExecuteTemplate(&b, tplName, data); err != nil { + app.log.Printf("error compiling notification template '%s': %v", tplName, err) return err } - err := app.Messenger.Push(app.Constants.FromEmail, + err := app.messenger.Push(app.constants.FromEmail, toEmails, subject, b.Bytes(), nil) if err != nil { - app.Logger.Printf("error sending admin notification (%s): %v", subject, err) + app.log.Printf("error sending admin notification (%s): %v", subject, err) return err } return nil } - -// compileNotifTpls compiles and returns e-mail notification templates that are -// used for sending ad-hoc notifications to admins and subscribers. -func compileNotifTpls(path string, fs stuffbin.FileSystem, app *App) (*template.Template, error) { - // Register utility functions that the e-mail templates can use. - funcs := template.FuncMap{ - "RootURL": func() string { - return app.Constants.RootURL - }, - "LogoURL": func() string { - return app.Constants.LogoURL - }} - - tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/email-templates/*.html") - if err != nil { - return nil, err - } - - return tpl, err -} diff --git a/public.go b/public.go index 14d87b9..afa6c34 100644 --- a/public.go +++ b/public.go @@ -97,19 +97,19 @@ func handleSubscriptionPage(c echo.Context) error { ) out.SubUUID = subUUID out.Title = "Unsubscribe from mailing list" - out.AllowBlacklist = app.Constants.Privacy.AllowBlacklist - out.AllowExport = app.Constants.Privacy.AllowExport - out.AllowWipe = app.Constants.Privacy.AllowWipe + out.AllowBlacklist = app.constants.Privacy.AllowBlacklist + out.AllowExport = app.constants.Privacy.AllowExport + out.AllowWipe = app.constants.Privacy.AllowWipe // Unsubscribe. if unsub { // Is blacklisting allowed? - if !app.Constants.Privacy.AllowBlacklist { + if !app.constants.Privacy.AllowBlacklist { blacklist = false } - if _, err := app.Queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil { - app.Logger.Printf("error unsubscribing: %v", err) + if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil { + app.log.Printf("error unsubscribing: %v", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error", "", `Error processing request. Please retry.`)) @@ -152,9 +152,9 @@ func handleOptinPage(c echo.Context) error { } // Get the list of subscription lists where the subscriber hasn't confirmed. - if err := app.Queries.GetSubscriberLists.Select(&out.Lists, 0, subUUID, + if err := app.queries.GetSubscriberLists.Select(&out.Lists, 0, subUUID, nil, pq.StringArray(out.ListUUIDs), models.SubscriptionStatusUnconfirmed, nil); err != nil { - app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) + app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error", "", `Error fetching lists. Please retry.`)) } @@ -168,8 +168,8 @@ func handleOptinPage(c echo.Context) error { // Confirm. if confirm { - if _, err := app.Queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil { - app.Logger.Printf("error unsubscribing: %v", err) + if _, err := app.queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil { + app.log.Printf("error unsubscribing: %v", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error", "", `Error processing request. Please retry.`)) @@ -233,9 +233,9 @@ func handleLinkRedirect(c echo.Context) error { ) var url string - if err := app.Queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil { + if err := app.queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil { if err != sql.ErrNoRows { - app.Logger.Printf("error fetching redirect link: %s", err) + app.log.Printf("error fetching redirect link: %s", err) } return c.Render(http.StatusInternalServerError, tplMessage, @@ -255,8 +255,8 @@ func handleRegisterCampaignView(c echo.Context) error { campUUID = c.Param("campUUID") subUUID = c.Param("subUUID") ) - if _, err := app.Queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil { - app.Logger.Printf("error registering campaign view: %s", err) + if _, err := app.queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil { + app.log.Printf("error registering campaign view: %s", err) } c.Response().Header().Set("Cache-Control", "no-cache") return c.Blob(http.StatusOK, "image/png", pixelPNG) @@ -272,7 +272,7 @@ func handleSelfExportSubscriberData(c echo.Context) error { subUUID = c.Param("subUUID") ) // Is export allowed? - if !app.Constants.Privacy.AllowExport { + if !app.constants.Privacy.AllowExport { return c.Render(http.StatusBadRequest, tplMessage, makeMsgTpl("Invalid request", "", "The feature is not available.")) } @@ -280,9 +280,9 @@ func handleSelfExportSubscriberData(c echo.Context) error { // Get the subscriber's data. A single query that gets the profile, // list subscriptions, campaign views, and link clicks. Names of // private lists are replaced with "Private list". - data, b, err := exportSubscriberData(0, subUUID, app.Constants.Privacy.Exportable, app) + data, b, err := exportSubscriberData(0, subUUID, app.constants.Privacy.Exportable, app) if err != nil { - app.Logger.Printf("error exporting subscriber data: %s", err) + app.log.Printf("error exporting subscriber data: %s", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error processing request", "", "There was an error processing your request. Please try later.")) @@ -290,8 +290,8 @@ func handleSelfExportSubscriberData(c echo.Context) error { // Send the data out to the subscriber as an atachment. var msg bytes.Buffer - if err := app.NotifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil { - app.Logger.Printf("error compiling notification template '%s': %v", + if err := app.notifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil { + app.log.Printf("error compiling notification template '%s': %v", notifSubscriberData, err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error preparing data", "", @@ -299,7 +299,7 @@ func handleSelfExportSubscriberData(c echo.Context) error { } const fname = "profile.json" - if err := app.Messenger.Push(app.Constants.FromEmail, + if err := app.messenger.Push(app.constants.FromEmail, []string{data.Email}, "Your profile data", msg.Bytes(), @@ -311,7 +311,7 @@ func handleSelfExportSubscriberData(c echo.Context) error { }, }, ); err != nil { - app.Logger.Printf("error e-mailing subscriber profile: %s", err) + app.log.Printf("error e-mailing subscriber profile: %s", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error e-mailing data", "", "There was an error e-mailing your data. Please try later.")) @@ -331,14 +331,14 @@ func handleWipeSubscriberData(c echo.Context) error { ) // Is wiping allowed? - if !app.Constants.Privacy.AllowExport { + if !app.constants.Privacy.AllowExport { return c.Render(http.StatusBadRequest, tplMessage, makeMsgTpl("Invalid request", "", "The feature is not available.")) } - if _, err := app.Queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil { - app.Logger.Printf("error wiping subscriber data: %s", err) + if _, err := app.queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil { + app.log.Printf("error wiping subscriber data: %s", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl("Error processing request", "", "There was an error processing your request. Please try later.")) diff --git a/subscribers.go b/subscribers.go index e7f6746..ab6e38f 100644 --- a/subscribers.go +++ b/subscribers.go @@ -74,17 +74,17 @@ func handleGetSubscriber(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.") } - err := app.Queries.GetSubscriber.Select(&out, id, nil) + err := app.queries.GetSubscriber.Select(&out, id, nil) if err != nil { - app.Logger.Printf("error fetching subscriber: %v", err) + app.log.Printf("error fetching subscriber: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err))) } if len(out) == 0 { return echo.NewHTTPError(http.StatusBadRequest, "Subscriber not found.") } - if err := out.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil { - app.Logger.Printf("error loading subscriber lists: %v", err) + if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil { + app.log.Printf("error loading subscriber lists: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, "Error loading subscriber lists.") } @@ -119,12 +119,12 @@ func handleQuerySubscribers(c echo.Context) error { cond = " AND " + query } - stmt := fmt.Sprintf(app.Queries.QuerySubscribers, cond) + stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond) // Create a readonly transaction to prevent mutations. - tx, err := app.DB.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true}) + tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true}) if err != nil { - app.Logger.Printf("error preparing subscriber query: %v", err) + app.log.Printf("error preparing subscriber query: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error preparing subscriber query: %v", pqErrMsg(err))) } @@ -137,8 +137,8 @@ func handleQuerySubscribers(c echo.Context) error { } // Lazy load lists for each subscriber. - if err := out.Results.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil { - app.Logger.Printf("error fetching subscriber lists: %v", err) + if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil { + app.log.Printf("error fetching subscriber lists: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching subscriber lists: %v", pqErrMsg(err))) } @@ -211,14 +211,14 @@ func handleUpdateSubscriber(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid length for `name`.") } - _, err := app.Queries.UpdateSubscriber.Exec(req.ID, + _, err := app.queries.UpdateSubscriber.Exec(req.ID, strings.ToLower(strings.TrimSpace(req.Email)), strings.TrimSpace(req.Name), req.Status, req.Attribs, req.Lists) if err != nil { - app.Logger.Printf("error updating subscriber: %v", err) + app.log.Printf("error updating subscriber: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err))) } @@ -239,9 +239,9 @@ func handleGetSubscriberSendOptin(c echo.Context) error { } // Fetch the subscriber. - err := app.Queries.GetSubscriber.Select(&out, id, nil) + err := app.queries.GetSubscriber.Select(&out, id, nil) if err != nil { - app.Logger.Printf("error fetching subscriber: %v", err) + app.log.Printf("error fetching subscriber: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err))) } @@ -287,8 +287,8 @@ func handleBlacklistSubscribers(c echo.Context) error { IDs = req.SubscriberIDs } - if _, err := app.Queries.BlacklistSubscribers.Exec(IDs); err != nil { - app.Logger.Printf("error blacklisting subscribers: %v", err) + if _, err := app.queries.BlacklistSubscribers.Exec(IDs); err != nil { + app.log.Printf("error blacklisting subscribers: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error blacklisting: %v", err)) } @@ -335,17 +335,17 @@ func handleManageSubscriberLists(c echo.Context) error { var err error switch req.Action { case "add": - _, err = app.Queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs) + _, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs) case "remove": - _, err = app.Queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs) + _, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs) case "unsubscribe": - _, err = app.Queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs) + _, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs) default: return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.") } if err != nil { - app.Logger.Printf("error updating subscriptions: %v", err) + app.log.Printf("error updating subscriptions: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error processing lists: %v", err)) } @@ -383,8 +383,8 @@ func handleDeleteSubscribers(c echo.Context) error { IDs = i } - if _, err := app.Queries.DeleteSubscribers.Exec(IDs, nil); err != nil { - app.Logger.Printf("error deleting subscribers: %v", err) + if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil { + app.log.Printf("error deleting subscribers: %v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error deleting subscribers: %v", err)) } @@ -404,11 +404,11 @@ func handleDeleteSubscribersByQuery(c echo.Context) error { return err } - err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), - app.Queries.DeleteSubscribersByQuery, - req.ListIDs, app.DB) + err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), + app.queries.DeleteSubscribersByQuery, + req.ListIDs, app.db) if err != nil { - app.Logger.Printf("error querying subscribers: %v", err) + app.log.Printf("error querying subscribers: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error: %v", err)) } @@ -428,11 +428,11 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error { return err } - err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), - app.Queries.BlacklistSubscribersByQuery, - req.ListIDs, app.DB) + err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), + app.queries.BlacklistSubscribersByQuery, + req.ListIDs, app.db) if err != nil { - app.Logger.Printf("error blacklisting subscribers: %v", err) + app.log.Printf("error blacklisting subscribers: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error: %v", err)) } @@ -459,19 +459,19 @@ func handleManageSubscriberListsByQuery(c echo.Context) error { var stmt string switch req.Action { case "add": - stmt = app.Queries.AddSubscribersToListsByQuery + stmt = app.queries.AddSubscribersToListsByQuery case "remove": - stmt = app.Queries.DeleteSubscriptionsByQuery + stmt = app.queries.DeleteSubscriptionsByQuery case "unsubscribe": - stmt = app.Queries.UnsubscribeSubscribersFromListsByQuery + stmt = app.queries.UnsubscribeSubscribersFromListsByQuery default: return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.") } - err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), - stmt, req.ListIDs, app.DB, req.TargetListIDs) + err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query), + stmt, req.ListIDs, app.db, req.TargetListIDs) if err != nil { - app.Logger.Printf("error updating subscriptions: %v", err) + app.log.Printf("error updating subscriptions: %v", err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error: %v", err)) } @@ -496,9 +496,9 @@ func handleExportSubscriberData(c echo.Context) error { // Get the subscriber's data. A single query that gets the profile, // list subscriptions, campaign views, and link clicks. Names of // private lists are replaced with "Private list". - _, b, err := exportSubscriberData(id, "", app.Constants.Privacy.Exportable, app) + _, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app) if err != nil { - app.Logger.Printf("error exporting subscriber data: %s", err) + app.log.Printf("error exporting subscriber data: %s", err) return echo.NewHTTPError(http.StatusBadRequest, "Error exporting subscriber data.") } @@ -516,7 +516,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) { } req.UUID = uu.String() - err = app.Queries.InsertSubscriber.Get(&req.ID, + err = app.queries.InsertSubscriber.Get(&req.ID, req.UUID, req.Email, strings.TrimSpace(req.Name), @@ -529,7 +529,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) { return 0, echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.") } - app.Logger.Printf("error inserting subscriber: %v", err) + app.log.Printf("error inserting subscriber: %v", err) return 0, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error inserting subscriber: %v", err)) } @@ -556,8 +556,8 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool, if subUUID != "" { uu = subUUID } - if err := app.Queries.ExportSubscriberData.Get(&data, id, uu); err != nil { - app.Logger.Printf("error fetching subscriber export data: %v", err) + if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil { + app.log.Printf("error fetching subscriber export data: %v", err) return data, nil, err } @@ -578,7 +578,7 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool, // Marshal the data into an indented payload. b, err := json.MarshalIndent(data, "", " ") if err != nil { - app.Logger.Printf("error marshalling subscriber export data: %v", err) + app.log.Printf("error marshalling subscriber export data: %v", err) return data, nil, err } return data, b, nil @@ -591,9 +591,9 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err // Fetch double opt-in lists from the given list IDs. // Get the list of subscription lists where the subscriber hasn't confirmed. - if err := app.Queries.GetSubscriberLists.Select(&lists, sub.ID, nil, + if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil, pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, nil); err != nil { - app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) + app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) return err } @@ -610,13 +610,13 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err for _, l := range out.Lists { qListIDs.Add("l", l.UUID) } - out.OptinURL = fmt.Sprintf(app.Constants.OptinURL, sub.UUID, qListIDs.Encode()) + out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode()) // Send the e-mail. if err := sendNotification([]string{sub.Email}, "Confirm subscription", notifSubscriberOptin, out, app); err != nil { - app.Logger.Printf("error e-mailing subscriber profile: %s", err) + app.log.Printf("error e-mailing subscriber profile: %s", err) return err } diff --git a/templates.go b/templates.go index 79eda7f..d5c26d9 100644 --- a/templates.go +++ b/templates.go @@ -48,7 +48,7 @@ func handleGetTemplates(c echo.Context) error { single = true } - err := app.Queries.GetTemplates.Select(&out, id, noBody) + err := app.queries.GetTemplates.Select(&out, id, noBody) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err))) @@ -87,7 +87,7 @@ func handlePreviewTemplate(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") } - err := app.Queries.GetTemplates.Select(&tpls, id, false) + err := app.queries.GetTemplates.Select(&tpls, id, false) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err))) @@ -109,12 +109,12 @@ func handlePreviewTemplate(c echo.Context) error { Body: dummyTpl, } - if err := camp.CompileTemplate(app.Manager.TemplateFuncs(&camp)); err != nil { + if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err)) } // Render the message body. - m := app.Manager.NewMessage(&camp, &dummySubscriber) + m := app.manager.NewMessage(&camp, &dummySubscriber) if err := m.Render(); err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error rendering message: %v", err)) @@ -140,7 +140,7 @@ func handleCreateTemplate(c echo.Context) error { // Insert and read ID. var newID int - if err := app.Queries.CreateTemplate.Get(&newID, + if err := app.queries.CreateTemplate.Get(&newID, o.Name, o.Body); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, @@ -174,7 +174,7 @@ func handleUpdateTemplate(c echo.Context) error { } // TODO: PASSWORD HASHING. - res, err := app.Queries.UpdateTemplate.Exec(o.ID, o.Name, o.Body) + res, err := app.queries.UpdateTemplate.Exec(o.ID, o.Name, o.Body) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error updating template: %s", pqErrMsg(err))) @@ -198,7 +198,7 @@ func handleTemplateSetDefault(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") } - _, err := app.Queries.SetDefaultTemplate.Exec(id) + _, err := app.queries.SetDefaultTemplate.Exec(id) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error updating template: %s", pqErrMsg(err))) @@ -221,7 +221,7 @@ func handleDeleteTemplate(c echo.Context) error { } var delID int - err := app.Queries.DeleteTemplate.Get(&delID, id) + err := app.queries.DeleteTemplate.Get(&delID, id) if err != nil { if err == sql.ErrNoRows { return c.JSON(http.StatusOK, okResp{true}) diff --git a/utils.go b/utils.go index 40f2ffc..b752871 100644 --- a/utils.go +++ b/utils.go @@ -7,14 +7,11 @@ import ( "log" "mime/multipart" "net/http" - "reflect" "regexp" "strconv" "strings" "github.com/disintegration/imaging" - "github.com/jmoiron/sqlx" - "github.com/knadh/goyesql" "github.com/labstack/echo" "github.com/lib/pq" ) @@ -26,67 +23,6 @@ var ( tagRegexpSpaces = regexp.MustCompile(`[\s]+`) ) -// ScanToStruct prepares a given set of Queries and assigns the resulting -// *sql.Stmt statements to the fields of a given struct, matching based on the name -// in the `query` tag in the struct field names. -func scanQueriesToStruct(obj interface{}, q goyesql.Queries, db *sqlx.DB) error { - ob := reflect.ValueOf(obj) - if ob.Kind() == reflect.Ptr { - ob = ob.Elem() - } - - if ob.Kind() != reflect.Struct { - return fmt.Errorf("Failed to apply SQL statements to struct. Non struct type: %T", ob) - } - - // Go through every field in the struct and look for it in the Args map. - for i := 0; i < ob.NumField(); i++ { - f := ob.Field(i) - - if f.IsValid() { - if tag := ob.Type().Field(i).Tag.Get("query"); tag != "" && tag != "-" { - // Extract the value of the `query` tag. - var ( - tg = strings.Split(tag, ",") - name string - ) - if len(tg) == 2 { - if tg[0] != "-" && tg[0] != "" { - name = tg[0] - } - } else { - name = tg[0] - } - - // Query name found in the field tag is not in the map. - if _, ok := q[name]; !ok { - return fmt.Errorf("query '%s' not found in query map", name) - } - - if !f.CanSet() { - return fmt.Errorf("query field '%s' is unexported", ob.Type().Field(i).Name) - } - - switch f.Type().String() { - case "string": - // Unprepared SQL query. - f.Set(reflect.ValueOf(q[name].Query)) - case "*sqlx.Stmt": - // Prepared query. - stmt, err := db.Preparex(q[name].Query) - if err != nil { - return fmt.Errorf("Error preparing query '%s': %v", name, err) - } - - f.Set(reflect.ValueOf(stmt)) - } - } - } - } - - return nil -} - // validateMIME is a helper function to validate uploaded file's MIME type // against the slice of MIME types is given. func validateMIME(typ string, mimes []string) (ok bool) {