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.
This commit is contained in:
Kailash Nadh 2020-03-08 00:03:22 +05:30
parent 83b49df39d
commit 8853809713
16 changed files with 572 additions and 565 deletions

View file

@ -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)))
}

View file

@ -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.")
}

9
go.mod
View file

@ -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
)

21
go.sum
View file

@ -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=

View file

@ -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.`))

View file

@ -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()})
}

310
init.go Normal file
View file

@ -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")))
}

View file

@ -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)

View file

@ -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))
}

331
main.go
View file

@ -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)
}

View file

@ -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})
}

View file

@ -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
}

View file

@ -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."))

View file

@ -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
}

View file

@ -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})

View file

@ -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) {