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:
parent
83b49df39d
commit
8853809713
8
admin.go
8
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)))
|
||||
}
|
||||
|
|
90
campaigns.go
90
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.")
|
||||
}
|
||||
|
|
9
go.mod
9
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
|
||||
)
|
||||
|
|
21
go.sum
21
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=
|
||||
|
|
|
@ -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.`))
|
||||
|
|
14
import.go
14
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()})
|
||||
}
|
||||
|
|
|
@ -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")))
|
||||
}
|
45
install.go
45
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)
|
||||
|
|
18
lists.go
18
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))
|
||||
}
|
||||
|
|
331
main.go
331
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)
|
||||
}
|
||||
|
|
32
media.go
32
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})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
48
public.go
48
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."))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
16
templates.go
16
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})
|
||||
|
|
64
utils.go
64
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) {
|
||||
|
|
Loading…
Reference in New Issue