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 (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
out = configScript{
|
out = configScript{
|
||||||
RootURL: app.Constants.RootURL,
|
RootURL: app.constants.RootURL,
|
||||||
FromEmail: app.Constants.FromEmail,
|
FromEmail: app.constants.FromEmail,
|
||||||
Messengers: app.Manager.GetMessengerNames(),
|
Messengers: app.manager.GetMessengerNames(),
|
||||||
}
|
}
|
||||||
|
|
||||||
b = bytes.Buffer{}
|
b = bytes.Buffer{}
|
||||||
|
@ -48,7 +48,7 @@ func handleGetDashboardStats(c echo.Context) error {
|
||||||
out dashboardStats
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err)))
|
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("&")))
|
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 {
|
if err != nil {
|
||||||
app.Logger.Printf("error fetching campaigns: %v", err)
|
app.log.Printf("error fetching campaigns: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -112,8 +112,8 @@ func handleGetCampaigns(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy load stats.
|
// Lazy load stats.
|
||||||
if err := out.Results.LoadStats(app.Queries.GetCampaignStats); err != nil {
|
if err := out.Results.LoadStats(app.queries.GetCampaignStats); err != nil {
|
||||||
app.Logger.Printf("error fetching campaign stats: %v", err)
|
app.log.Printf("error fetching campaign stats: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign stats: %v", pqErrMsg(err)))
|
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.")
|
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 != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var sub models.Subscriber
|
var sub models.Subscriber
|
||||||
// Get a random subscriber from the campaign.
|
// 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 {
|
if err == sql.ErrNoRows {
|
||||||
// There's no subscriber. Mock one.
|
// There's no subscriber. Mock one.
|
||||||
sub = dummySubscriber
|
sub = dummySubscriber
|
||||||
} else {
|
} else {
|
||||||
app.Logger.Printf("error fetching subscriber: %v", err)
|
app.log.Printf("error fetching subscriber: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -173,16 +173,16 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
camp.Body = body
|
camp.Body = body
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil {
|
if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
|
||||||
app.Logger.Printf("error compiling template: %v", err)
|
app.log.Printf("error compiling template: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error compiling template: %v", err))
|
fmt.Sprintf("Error compiling template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the message body.
|
// Render the message body.
|
||||||
m := app.Manager.NewMessage(camp, &sub)
|
m := app.manager.NewMessage(camp, &sub)
|
||||||
if err := m.Render(); err != nil {
|
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,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error rendering message: %v", err))
|
fmt.Sprintf("Error rendering message: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -218,20 +218,20 @@ func handleCreateCampaign(c echo.Context) error {
|
||||||
o = c
|
o = c
|
||||||
}
|
}
|
||||||
|
|
||||||
if !app.Manager.HasMessenger(o.MessengerID) {
|
if !app.manager.HasMessenger(o.MessengerID) {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Unknown messenger %s", o.MessengerID))
|
fmt.Sprintf("Unknown messenger %s", o.MessengerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
uu, err := uuid.NewV4()
|
uu, err := uuid.NewV4()
|
||||||
if err != nil {
|
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")
|
return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert and read ID.
|
// Insert and read ID.
|
||||||
var newID int
|
var newID int
|
||||||
if err := app.Queries.CreateCampaign.Get(&newID,
|
if err := app.queries.CreateCampaign.Get(&newID,
|
||||||
uu,
|
uu,
|
||||||
o.Type,
|
o.Type,
|
||||||
o.Name,
|
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.")
|
"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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err)))
|
fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -274,12 +274,12 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cm models.Campaign
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -301,7 +301,7 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
o = c
|
o = c
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := app.Queries.UpdateCampaign.Exec(cm.ID,
|
res, err := app.queries.UpdateCampaign.Exec(cm.ID,
|
||||||
o.Name,
|
o.Name,
|
||||||
o.Subject,
|
o.Subject,
|
||||||
o.FromEmail,
|
o.FromEmail,
|
||||||
|
@ -313,7 +313,7 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
o.ListIDs)
|
o.ListIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error updating campaign: %v", err)
|
app.log.Printf("error updating campaign: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -337,12 +337,12 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cm models.Campaign
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -385,9 +385,9 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, errMsg)
|
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 {
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -412,12 +412,12 @@ func handleDeleteCampaign(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cm models.Campaign
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
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.")
|
"Only campaigns that haven't been started can be deleted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.DeleteCampaign.Exec(cm.ID); err != nil {
|
if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil {
|
||||||
app.Logger.Printf("error deleting campaign: %v", err)
|
app.log.Printf("error deleting campaign: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err)))
|
fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -445,12 +445,12 @@ func handleGetRunningCampaignStats(c echo.Context) error {
|
||||||
out []campaignStats
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return c.JSON(http.StatusOK, okResp{[]struct{}{}})
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err)))
|
||||||
} else if len(out) == 0 {
|
} 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]))
|
req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i]))
|
||||||
}
|
}
|
||||||
var subs models.Subscribers
|
var subs models.Subscribers
|
||||||
if err := app.Queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
|
if err := app.queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
|
||||||
app.Logger.Printf("error fetching subscribers: %v", err)
|
app.log.Printf("error fetching subscribers: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
|
||||||
} else if len(subs) == 0 {
|
} else if len(subs) == 0 {
|
||||||
|
@ -519,12 +519,12 @@ func handleTestCampaign(c echo.Context) error {
|
||||||
|
|
||||||
// The campaign.
|
// The campaign.
|
||||||
var camp models.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 {
|
if err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
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.
|
// 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 {
|
func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error {
|
||||||
if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil {
|
if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
|
||||||
app.Logger.Printf("error compiling template: %v", err)
|
app.log.Printf("error compiling template: %v", err)
|
||||||
return fmt.Errorf("Error compiling template: %v", err)
|
return fmt.Errorf("Error compiling template: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the message body.
|
// Render the message body.
|
||||||
m := app.Manager.NewMessage(camp, sub)
|
m := app.manager.NewMessage(camp, sub)
|
||||||
if err := m.Render(); err != nil {
|
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,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error rendering message: %v", err))
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,7 +572,7 @@ func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) er
|
||||||
// validateCampaignFields validates incoming campaign field values.
|
// validateCampaignFields validates incoming campaign field values.
|
||||||
func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
|
func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
|
||||||
if c.FromEmail == "" {
|
if c.FromEmail == "" {
|
||||||
c.FromEmail = app.Constants.FromEmail
|
c.FromEmail = app.constants.FromEmail
|
||||||
} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
|
} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
|
||||||
if !govalidator.IsEmail(c.FromEmail) {
|
if !govalidator.IsEmail(c.FromEmail) {
|
||||||
return c, errors.New("invalid `from_email`")
|
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}
|
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)
|
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.
|
// Fetch double opt-in lists from the given list IDs.
|
||||||
var lists []models.List
|
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 {
|
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,
|
return o, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
"Error fetching opt-in lists.")
|
"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.
|
// Prepare sample opt-in message for the campaign.
|
||||||
var b bytes.Buffer
|
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
|
Lists []models.List
|
||||||
OptinURLAttr template.HTMLAttr
|
OptinURLAttr template.HTMLAttr
|
||||||
}{lists, optinURLAttr}); err != nil {
|
}{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,
|
return o, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
"Error compiling opt-in campaign template.")
|
"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/jinzhu/gorm v1.9.1
|
||||||
github.com/jmoiron/sqlx v1.2.0
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b
|
github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b
|
||||||
github.com/knadh/goyesql v2.0.0+incompatible
|
github.com/knadh/goyesql/v2 v2.1.1
|
||||||
github.com/knadh/koanf v0.4.4
|
github.com/knadh/koanf v0.8.1
|
||||||
github.com/knadh/stuffbin v1.0.0
|
github.com/knadh/stuffbin v1.0.0
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/labstack/echo v3.3.10+incompatible
|
github.com/labstack/echo v3.3.10+incompatible
|
||||||
|
@ -16,14 +16,11 @@ require (
|
||||||
github.com/lib/pq v1.0.0
|
github.com/lib/pq v1.0.0
|
||||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.4 // 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/spf13/pflag v1.0.3
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // 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/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/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b
|
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/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 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0=
|
||||||
github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU=
|
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 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
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=
|
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 h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
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.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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
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=
|
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/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 h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk=
|
||||||
github.com/knadh/goyesql v2.0.0+incompatible/go.mod h1:W0tSzU8l7lYH1Fihj+bdQzkzOwvirrsMNHwkuY22qoY=
|
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 h1:Pg+eR7wuJtCGHLeip31K20eJojjZ3lXE8ILQQGj2PTM=
|
||||||
github.com/knadh/koanf v0.4.4/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
|
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 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
|
||||||
github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
|
github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
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/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 h1:2josYcx2gm3CT0WMqi0jBagvg50V3UMWlYN/CnBEbSI=
|
||||||
github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727/go.mod h1:iphavgjkW1uvoIiqLUX6D42XuuI9Cr+B/63xw3gb9qA=
|
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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
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=
|
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=
|
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 h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
|
||||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
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 h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
|
||||||
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
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 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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 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-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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
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}$")
|
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.
|
// registerHandlers registers HTTP handlers.
|
||||||
func registerHandlers(e *echo.Echo) {
|
func registerHTTPHandlers(e *echo.Echo) {
|
||||||
e.GET("/", handleIndexPage)
|
e.GET("/", handleIndexPage)
|
||||||
e.GET("/api/config.js", handleGetConfigScript)
|
e.GET("/api/config.js", handleGetConfigScript)
|
||||||
e.GET("/api/dashboard/stats", handleGetDashboardStats)
|
e.GET("/api/dashboard/stats", handleGetDashboardStats)
|
||||||
|
@ -128,7 +128,7 @@ func registerHandlers(e *echo.Echo) {
|
||||||
func handleIndexPage(c echo.Context) error {
|
func handleIndexPage(c echo.Context) error {
|
||||||
app := c.Get("app").(*App)
|
app := c.Get("app").(*App)
|
||||||
|
|
||||||
b, err := app.FS.Read("/frontend/index.html")
|
b, err := app.fs.Read("/frontend/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -161,8 +161,8 @@ func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
var exists bool
|
var exists bool
|
||||||
if err := app.Queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
|
if err := app.queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
|
||||||
app.Logger.Printf("error checking subscriber existence: %v", err)
|
app.log.Printf("error checking subscriber existence: %v", err)
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error", "",
|
makeMsgTpl("Error", "",
|
||||||
`Error processing request. Please retry.`))
|
`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)
|
app := c.Get("app").(*App)
|
||||||
|
|
||||||
// Is an import already running?
|
// Is an import already running?
|
||||||
if app.Importer.GetStats().Status == subimporter.StatusImporting {
|
if app.importer.GetStats().Status == subimporter.StatusImporting {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
"An import is already running. Wait for it to finish or stop it before trying again.")
|
"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.
|
// 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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error starting import session: %v", err))
|
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]))
|
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.
|
// handleGetImportSubscribers returns import statistics.
|
||||||
func handleGetImportSubscribers(c echo.Context) error {
|
func handleGetImportSubscribers(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
s = app.Importer.GetStats()
|
s = app.importer.GetStats()
|
||||||
)
|
)
|
||||||
return c.JSON(http.StatusOK, okResp{s})
|
return c.JSON(http.StatusOK, okResp{s})
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func handleGetImportSubscribers(c echo.Context) error {
|
||||||
// handleGetImportSubscriberStats returns import statistics.
|
// handleGetImportSubscriberStats returns import statistics.
|
||||||
func handleGetImportSubscriberStats(c echo.Context) error {
|
func handleGetImportSubscriberStats(c echo.Context) error {
|
||||||
app := c.Get("app").(*App)
|
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.
|
// 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.
|
// is finished, it's state is cleared.
|
||||||
func handleStopImportSubscribers(c echo.Context) error {
|
func handleStopImportSubscribers(c echo.Context) error {
|
||||||
app := c.Get("app").(*App)
|
app := c.Get("app").(*App)
|
||||||
app.Importer.Stop()
|
app.importer.Stop()
|
||||||
return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()})
|
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/gofrs/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/goyesql"
|
goyesqlx "github.com/knadh/goyesql/v2/sqlx"
|
||||||
"github.com/knadh/listmonk/models"
|
"github.com/knadh/listmonk/models"
|
||||||
|
"github.com/knadh/stuffbin"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// install runs the first time setup of creating and
|
// install runs the first time setup of creating and
|
||||||
// migrating the database and creating the super user.
|
// 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("")
|
||||||
fmt.Println("** First time installation **")
|
fmt.Println("** First time installation **")
|
||||||
fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
|
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
|
var ok string
|
||||||
fmt.Print("Continue (y/n)? ")
|
fmt.Print("Continue (y/n)? ")
|
||||||
if _, err := fmt.Scanf("%s", &ok); err != nil {
|
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" {
|
if strings.ToLower(ok) != "y" {
|
||||||
fmt.Println("Installation cancelled.")
|
fmt.Println("Installation cancelled.")
|
||||||
|
@ -37,15 +40,15 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate the tables.
|
// Migrate the tables.
|
||||||
err := installMigrate(app.DB, app)
|
err := installMigrate(db, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Error migrating DB schema: %v", err)
|
lo.Fatalf("Error migrating DB schema: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the queries.
|
// Load the queries.
|
||||||
var q Queries
|
var q Queries
|
||||||
if err := scanQueriesToStruct(&q, qMap, app.DB.Unsafe()); err != nil {
|
if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
|
||||||
logger.Fatalf("error loading SQL queries: %v", err)
|
lo.Fatalf("error loading SQL queries: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample list.
|
// Sample list.
|
||||||
|
@ -60,7 +63,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
models.ListOptinSingle,
|
models.ListOptinSingle,
|
||||||
pq.StringArray{"test"},
|
pq.StringArray{"test"},
|
||||||
); err != nil {
|
); 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()),
|
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,
|
models.ListOptinDouble,
|
||||||
pq.StringArray{"test"},
|
pq.StringArray{"test"},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Fatalf("Error creating list: %v", err)
|
lo.Fatalf("Error creating list: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample subscriber.
|
// Sample subscriber.
|
||||||
|
@ -80,7 +83,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
`{"type": "known", "good": true, "city": "Bengaluru"}`,
|
`{"type": "known", "good": true, "city": "Bengaluru"}`,
|
||||||
pq.Int64Array{int64(defList)},
|
pq.Int64Array{int64(defList)},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Fatalf("Error creating subscriber: %v", err)
|
lo.Fatalf("Error creating subscriber: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := q.UpsertSubscriber.Exec(
|
if _, err := q.UpsertSubscriber.Exec(
|
||||||
uuid.Must(uuid.NewV4()),
|
uuid.Must(uuid.NewV4()),
|
||||||
|
@ -89,7 +92,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
|
`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
|
||||||
pq.Int64Array{int64(optinList)},
|
pq.Int64Array{int64(optinList)},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Fatalf("Error creating subscriber: %v", err)
|
lo.Fatalf("Error creating subscriber: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default template.
|
// Default template.
|
||||||
|
@ -103,10 +106,10 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
"Default template",
|
"Default template",
|
||||||
string(tplBody),
|
string(tplBody),
|
||||||
); err != nil {
|
); 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 {
|
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.
|
// Sample campaign.
|
||||||
|
@ -126,17 +129,17 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
|
||||||
1,
|
1,
|
||||||
pq.Int64Array{1},
|
pq.Int64Array{1},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Fatalf("error creating sample campaign: %v", err)
|
lo.Fatalf("error creating sample campaign: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("Setup complete")
|
lo.Printf("Setup complete")
|
||||||
logger.Printf(`Run the program and access the dashboard at %s`, ko.String("app.address"))
|
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.
|
// installMigrate executes the SQL schema and creates the necessary tables and types.
|
||||||
func installMigrate(db *sqlx.DB, app *App) error {
|
func installMigrate(db *sqlx.DB, fs stuffbin.FileSystem) error {
|
||||||
q, err := app.FS.Read("/schema.sql")
|
q, err := fs.Read("/schema.sql")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -156,11 +159,7 @@ func newConfigFile() error {
|
||||||
|
|
||||||
// Initialize the static file system into which all
|
// Initialize the static file system into which all
|
||||||
// required static assets (.sql, .js files etc.) are loaded.
|
// required static assets (.sql, .js files etc.) are loaded.
|
||||||
fs, err := initFileSystem(os.Args[0])
|
fs := initFS()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := fs.Read("config.toml.sample")
|
b, err := fs.Read("config.toml.sample")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
|
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
|
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 {
|
if err != nil {
|
||||||
app.Logger.Printf("error fetching lists: %v", err)
|
app.log.Printf("error fetching lists: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -87,20 +87,20 @@ func handleCreateList(c echo.Context) error {
|
||||||
|
|
||||||
uu, err := uuid.NewV4()
|
uu, err := uuid.NewV4()
|
||||||
if err != nil {
|
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")
|
return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert and read ID.
|
// Insert and read ID.
|
||||||
var newID int
|
var newID int
|
||||||
o.UUID = uu.String()
|
o.UUID = uu.String()
|
||||||
if err := app.Queries.CreateList.Get(&newID,
|
if err := app.queries.CreateList.Get(&newID,
|
||||||
o.UUID,
|
o.UUID,
|
||||||
o.Name,
|
o.Name,
|
||||||
o.Type,
|
o.Type,
|
||||||
o.Optin,
|
o.Optin,
|
||||||
pq.StringArray(normalizeTags(o.Tags))); err != nil {
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error creating list: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error creating list: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,10 @@ func handleUpdateList(c echo.Context) error {
|
||||||
return err
|
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)))
|
o.Name, o.Type, o.Optin, pq.StringArray(normalizeTags(o.Tags)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error updating list: %v", err)
|
app.log.Printf("error updating list: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error updating list: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error updating list: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -165,8 +165,8 @@ func handleDeleteLists(c echo.Context) error {
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.DeleteLists.Exec(ids); err != nil {
|
if _, err := app.queries.DeleteLists.Exec(ids); err != nil {
|
||||||
app.Logger.Printf("error deleting lists: %v", err)
|
app.log.Printf("error deleting lists: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error deleting: %v", err))
|
fmt.Sprintf("Error deleting: %v", err))
|
||||||
}
|
}
|
||||||
|
|
331
main.go
331
main.go
|
@ -5,68 +5,41 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/goyesql"
|
|
||||||
"github.com/knadh/koanf"
|
"github.com/knadh/koanf"
|
||||||
"github.com/knadh/koanf/maps"
|
|
||||||
"github.com/knadh/koanf/parsers/toml"
|
"github.com/knadh/koanf/parsers/toml"
|
||||||
"github.com/knadh/koanf/providers/env"
|
"github.com/knadh/koanf/providers/env"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/providers/posflag"
|
"github.com/knadh/koanf/providers/posflag"
|
||||||
"github.com/knadh/listmonk/manager"
|
"github.com/knadh/listmonk/manager"
|
||||||
"github.com/knadh/listmonk/media"
|
"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/messenger"
|
||||||
"github.com/knadh/listmonk/subimporter"
|
"github.com/knadh/listmonk/subimporter"
|
||||||
"github.com/knadh/stuffbin"
|
"github.com/knadh/stuffbin"
|
||||||
"github.com/labstack/echo"
|
|
||||||
flag "github.com/spf13/pflag"
|
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
|
// App contains the "global" components that are
|
||||||
// passed around, especially through HTTP handlers.
|
// passed around, especially through HTTP handlers.
|
||||||
type App struct {
|
type App struct {
|
||||||
Constants *constants
|
fs stuffbin.FileSystem
|
||||||
DB *sqlx.DB
|
db *sqlx.DB
|
||||||
Queries *Queries
|
queries *Queries
|
||||||
Importer *subimporter.Importer
|
constants *constants
|
||||||
Manager *manager.Manager
|
manager *manager.Manager
|
||||||
FS stuffbin.FileSystem
|
importer *subimporter.Importer
|
||||||
Logger *log.Logger
|
messenger messenger.Messenger
|
||||||
NotifTpls *template.Template
|
media media.Store
|
||||||
Messenger messenger.Messenger
|
notifTpls *template.Template
|
||||||
Media media.Store
|
log *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Global logger.
|
// 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.
|
// Global configuration reader.
|
||||||
ko = koanf.New(".")
|
ko = koanf.New(".")
|
||||||
|
@ -75,14 +48,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Register --help handler.
|
|
||||||
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
||||||
f.Usage = func() {
|
f.Usage = func() {
|
||||||
|
// Register --help handler.
|
||||||
fmt.Println(f.FlagUsages())
|
fmt.Println(f.FlagUsages())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the default configuration.
|
// Register the commandline flags.
|
||||||
f.StringSlice("config", []string{"config.toml"},
|
f.StringSlice("config", []string{"config.toml"},
|
||||||
"Path to one or more config files (will be merged in order)")
|
"Path to one or more config files (will be merged in order)")
|
||||||
f.Bool("install", false, "Run first time installation")
|
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("new-config", false, "Generate sample config file")
|
||||||
f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install")
|
f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install")
|
||||||
|
|
||||||
// Process flags.
|
|
||||||
if err := f.Parse(os.Args[1:]); err != nil {
|
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.
|
// Display version.
|
||||||
|
@ -104,277 +76,72 @@ func init() {
|
||||||
// Generate new config.
|
// Generate new config.
|
||||||
if ok, _ := f.GetBool("new-config"); ok {
|
if ok, _ := f.GetBool("new-config"); ok {
|
||||||
if err := newConfigFile(); err != nil {
|
if err := newConfigFile(); err != nil {
|
||||||
logger.Println(err)
|
lo.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.Println("generated config.toml. Edit and run --install")
|
lo.Println("generated config.toml. Edit and run --install")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config files.
|
// Load config files.
|
||||||
cFiles, _ := f.GetStringSlice("config")
|
cFiles, _ := f.GetStringSlice("config")
|
||||||
for _, f := range cFiles {
|
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 err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
|
||||||
if os.IsNotExist(err) {
|
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.
|
// Load environment variables and merge into the loaded config.
|
||||||
if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
|
if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
|
||||||
return strings.Replace(strings.ToLower(
|
return strings.Replace(strings.ToLower(
|
||||||
strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
|
strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
|
||||||
}), nil); err != nil {
|
}), 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 {
|
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() {
|
func main() {
|
||||||
// Connect to the DB.
|
// Initialize the DB and the filesystem that are required by the installer
|
||||||
var dbCfg dbConf
|
// and the app.
|
||||||
if err := ko.Unmarshal("db", &dbCfg); err != nil {
|
var (
|
||||||
log.Fatalf("error loading db config: %v", err)
|
fs = initFS()
|
||||||
}
|
db = initDB()
|
||||||
db, err := connectDB(dbCfg)
|
)
|
||||||
if err != nil {
|
|
||||||
logger.Fatalf("error connecting to DB: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
var c constants
|
// Installer mode? This runs before the SQL queries are loaded and prepared
|
||||||
if err := ko.Unmarshal("app", &c); err != nil {
|
// as the installer needs to work on an empty DB.
|
||||||
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.
|
|
||||||
if ko.Bool("install") {
|
if ko.Bool("install") {
|
||||||
install(app, qMap, !ko.Bool("yes"))
|
install(db, fs, !ko.Bool("yes"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map queries to the query container.
|
// Initialize the main app controller that wraps all of the app's
|
||||||
q := &Queries{}
|
// components. This is passed around HTTP handlers.
|
||||||
if err := scanQueriesToStruct(q, qMap, db.Unsafe()); err != nil {
|
app := &App{
|
||||||
logger.Fatalf("no SQL queries loaded: %v", err)
|
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.
|
// Start the campaign workers.
|
||||||
importNotifCB := func(subject string, data interface{}) error {
|
go app.manager.Run(time.Second * 5)
|
||||||
go sendNotification(app.Constants.NotifyEmails, subject, notifTplImport, data, app)
|
app.manager.SpawnWorkers()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
app.Importer = subimporter.New(q.UpsertSubscriber.Stmt,
|
|
||||||
q.UpsertBlacklistSubscriber.Stmt,
|
|
||||||
q.UpdateListsDate.Stmt,
|
|
||||||
db.DB,
|
|
||||||
importNotifCB)
|
|
||||||
|
|
||||||
// Prepare notification e-mail templates.
|
// Start and run the app server.
|
||||||
notifTpls, err := compileNotifTpls("/email-templates/*.html", fs, app)
|
initHTTPServer(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")))
|
|
||||||
}
|
}
|
||||||
|
|
32
media.go
32
media.go
|
@ -54,9 +54,9 @@ func handleUploadMedia(c echo.Context) error {
|
||||||
defer src.Close()
|
defer src.Close()
|
||||||
|
|
||||||
// Upload the file.
|
// Upload the file.
|
||||||
fName, err = app.Media.Put(fName, typ, src)
|
fName, err = app.media.Put(fName, typ, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error uploading file: %v", err)
|
app.log.Printf("error uploading file: %v", err)
|
||||||
cleanUp = true
|
cleanUp = true
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error uploading file: %s", err))
|
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,
|
// If any of the subroutines in this function fail,
|
||||||
// the uploaded image should be removed.
|
// the uploaded image should be removed.
|
||||||
if cleanUp {
|
if cleanUp {
|
||||||
app.Media.Delete(fName)
|
app.media.Delete(fName)
|
||||||
app.Media.Delete(thumbPrefix + fName)
|
app.media.Delete(thumbPrefix + fName)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -75,30 +75,30 @@ func handleUploadMedia(c echo.Context) error {
|
||||||
thumbFile, err := createThumbnail(file)
|
thumbFile, err := createThumbnail(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanUp = true
|
cleanUp = true
|
||||||
app.Logger.Printf("error resizing image: %v", err)
|
app.log.Printf("error resizing image: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error resizing image: %s", err))
|
fmt.Sprintf("Error resizing image: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload thumbnail.
|
// Upload thumbnail.
|
||||||
thumbfName, err := app.Media.Put(thumbPrefix+fName, typ, thumbFile)
|
thumbfName, err := app.media.Put(thumbPrefix+fName, typ, thumbFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanUp = true
|
cleanUp = true
|
||||||
app.Logger.Printf("error saving thumbnail: %v", err)
|
app.log.Printf("error saving thumbnail: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error saving thumbnail: %s", err))
|
fmt.Sprintf("Error saving thumbnail: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
uu, err := uuid.NewV4()
|
uu, err := uuid.NewV4()
|
||||||
if err != nil {
|
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")
|
return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to the DB.
|
// 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
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -112,14 +112,14 @@ func handleGetMedia(c echo.Context) error {
|
||||||
out []media.Media
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(out); i++ {
|
for i := 0; i < len(out); i++ {
|
||||||
out[i].URI = app.Media.Get(out[i].Filename)
|
out[i].URI = app.media.Get(out[i].Filename)
|
||||||
out[i].ThumbURI = app.Media.Get(thumbPrefix + out[i].Filename)
|
out[i].ThumbURI = app.media.Get(thumbPrefix + out[i].Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, okResp{out})
|
return c.JSON(http.StatusOK, okResp{out})
|
||||||
|
@ -137,12 +137,12 @@ func handleDeleteMedia(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var m media.Media
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error deleting media: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error deleting media: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Media.Delete(m.Filename)
|
app.media.Delete(m.Filename)
|
||||||
app.Media.Delete(thumbPrefix + m.Filename)
|
app.media.Delete(thumbPrefix + m.Filename)
|
||||||
return c.JSON(http.StatusOK, okResp{true})
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"html/template"
|
|
||||||
|
|
||||||
"github.com/knadh/stuffbin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -24,39 +21,19 @@ type notifData struct {
|
||||||
// sendNotification sends out an e-mail notification to admins.
|
// sendNotification sends out an e-mail notification to admins.
|
||||||
func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error {
|
func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if err := app.NotifTpls.ExecuteTemplate(&b, tplName, data); err != nil {
|
if err := app.notifTpls.ExecuteTemplate(&b, tplName, data); err != nil {
|
||||||
app.Logger.Printf("error compiling notification template '%s': %v", tplName, err)
|
app.log.Printf("error compiling notification template '%s': %v", tplName, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Messenger.Push(app.Constants.FromEmail,
|
err := app.messenger.Push(app.constants.FromEmail,
|
||||||
toEmails,
|
toEmails,
|
||||||
subject,
|
subject,
|
||||||
b.Bytes(),
|
b.Bytes(),
|
||||||
nil)
|
nil)
|
||||||
if err != 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 err
|
||||||
}
|
}
|
||||||
return nil
|
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.SubUUID = subUUID
|
||||||
out.Title = "Unsubscribe from mailing list"
|
out.Title = "Unsubscribe from mailing list"
|
||||||
out.AllowBlacklist = app.Constants.Privacy.AllowBlacklist
|
out.AllowBlacklist = app.constants.Privacy.AllowBlacklist
|
||||||
out.AllowExport = app.Constants.Privacy.AllowExport
|
out.AllowExport = app.constants.Privacy.AllowExport
|
||||||
out.AllowWipe = app.Constants.Privacy.AllowWipe
|
out.AllowWipe = app.constants.Privacy.AllowWipe
|
||||||
|
|
||||||
// Unsubscribe.
|
// Unsubscribe.
|
||||||
if unsub {
|
if unsub {
|
||||||
// Is blacklisting allowed?
|
// Is blacklisting allowed?
|
||||||
if !app.Constants.Privacy.AllowBlacklist {
|
if !app.constants.Privacy.AllowBlacklist {
|
||||||
blacklist = false
|
blacklist = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
|
if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
|
||||||
app.Logger.Printf("error unsubscribing: %v", err)
|
app.log.Printf("error unsubscribing: %v", err)
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error", "",
|
makeMsgTpl("Error", "",
|
||||||
`Error processing request. Please retry.`))
|
`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.
|
// 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 {
|
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,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error", "", `Error fetching lists. Please retry.`))
|
makeMsgTpl("Error", "", `Error fetching lists. Please retry.`))
|
||||||
}
|
}
|
||||||
|
@ -168,8 +168,8 @@ func handleOptinPage(c echo.Context) error {
|
||||||
|
|
||||||
// Confirm.
|
// Confirm.
|
||||||
if confirm {
|
if confirm {
|
||||||
if _, err := app.Queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil {
|
if _, err := app.queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil {
|
||||||
app.Logger.Printf("error unsubscribing: %v", err)
|
app.log.Printf("error unsubscribing: %v", err)
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error", "",
|
makeMsgTpl("Error", "",
|
||||||
`Error processing request. Please retry.`))
|
`Error processing request. Please retry.`))
|
||||||
|
@ -233,9 +233,9 @@ func handleLinkRedirect(c echo.Context) error {
|
||||||
)
|
)
|
||||||
|
|
||||||
var url string
|
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 {
|
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,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
|
@ -255,8 +255,8 @@ func handleRegisterCampaignView(c echo.Context) error {
|
||||||
campUUID = c.Param("campUUID")
|
campUUID = c.Param("campUUID")
|
||||||
subUUID = c.Param("subUUID")
|
subUUID = c.Param("subUUID")
|
||||||
)
|
)
|
||||||
if _, err := app.Queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {
|
if _, err := app.queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {
|
||||||
app.Logger.Printf("error registering campaign view: %s", err)
|
app.log.Printf("error registering campaign view: %s", err)
|
||||||
}
|
}
|
||||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||||
return c.Blob(http.StatusOK, "image/png", pixelPNG)
|
return c.Blob(http.StatusOK, "image/png", pixelPNG)
|
||||||
|
@ -272,7 +272,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
|
||||||
subUUID = c.Param("subUUID")
|
subUUID = c.Param("subUUID")
|
||||||
)
|
)
|
||||||
// Is export allowed?
|
// Is export allowed?
|
||||||
if !app.Constants.Privacy.AllowExport {
|
if !app.constants.Privacy.AllowExport {
|
||||||
return c.Render(http.StatusBadRequest, tplMessage,
|
return c.Render(http.StatusBadRequest, tplMessage,
|
||||||
makeMsgTpl("Invalid request", "", "The feature is not available."))
|
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,
|
// Get the subscriber's data. A single query that gets the profile,
|
||||||
// list subscriptions, campaign views, and link clicks. Names of
|
// list subscriptions, campaign views, and link clicks. Names of
|
||||||
// private lists are replaced with "Private list".
|
// 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 {
|
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,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error processing request", "",
|
makeMsgTpl("Error processing request", "",
|
||||||
"There was an error processing your request. Please try later."))
|
"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.
|
// Send the data out to the subscriber as an atachment.
|
||||||
var msg bytes.Buffer
|
var msg bytes.Buffer
|
||||||
if err := app.NotifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
|
if err := app.notifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
|
||||||
app.Logger.Printf("error compiling notification template '%s': %v",
|
app.log.Printf("error compiling notification template '%s': %v",
|
||||||
notifSubscriberData, err)
|
notifSubscriberData, err)
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error preparing data", "",
|
makeMsgTpl("Error preparing data", "",
|
||||||
|
@ -299,7 +299,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fname = "profile.json"
|
const fname = "profile.json"
|
||||||
if err := app.Messenger.Push(app.Constants.FromEmail,
|
if err := app.messenger.Push(app.constants.FromEmail,
|
||||||
[]string{data.Email},
|
[]string{data.Email},
|
||||||
"Your profile data",
|
"Your profile data",
|
||||||
msg.Bytes(),
|
msg.Bytes(),
|
||||||
|
@ -311,7 +311,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
); err != nil {
|
); 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,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error e-mailing data", "",
|
makeMsgTpl("Error e-mailing data", "",
|
||||||
"There was an error e-mailing your data. Please try later."))
|
"There was an error e-mailing your data. Please try later."))
|
||||||
|
@ -331,14 +331,14 @@ func handleWipeSubscriberData(c echo.Context) error {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Is wiping allowed?
|
// Is wiping allowed?
|
||||||
if !app.Constants.Privacy.AllowExport {
|
if !app.constants.Privacy.AllowExport {
|
||||||
return c.Render(http.StatusBadRequest, tplMessage,
|
return c.Render(http.StatusBadRequest, tplMessage,
|
||||||
makeMsgTpl("Invalid request", "",
|
makeMsgTpl("Invalid request", "",
|
||||||
"The feature is not available."))
|
"The feature is not available."))
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil {
|
if _, err := app.queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil {
|
||||||
app.Logger.Printf("error wiping subscriber data: %s", err)
|
app.log.Printf("error wiping subscriber data: %s", err)
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl("Error processing request", "",
|
makeMsgTpl("Error processing request", "",
|
||||||
"There was an error processing your request. Please try later."))
|
"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.")
|
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 {
|
if err != nil {
|
||||||
app.Logger.Printf("error fetching subscriber: %v", err)
|
app.log.Printf("error fetching subscriber: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Subscriber not found.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Subscriber not found.")
|
||||||
}
|
}
|
||||||
if err := out.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil {
|
if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
|
||||||
app.Logger.Printf("error loading subscriber lists: %v", err)
|
app.log.Printf("error loading subscriber lists: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
"Error loading subscriber lists.")
|
"Error loading subscriber lists.")
|
||||||
}
|
}
|
||||||
|
@ -119,12 +119,12 @@ func handleQuerySubscribers(c echo.Context) error {
|
||||||
cond = " AND " + query
|
cond = " AND " + query
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt := fmt.Sprintf(app.Queries.QuerySubscribers, cond)
|
stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond)
|
||||||
|
|
||||||
// Create a readonly transaction to prevent mutations.
|
// 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 {
|
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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error preparing subscriber query: %v", pqErrMsg(err)))
|
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.
|
// Lazy load lists for each subscriber.
|
||||||
if err := out.Results.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil {
|
if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
|
||||||
app.Logger.Printf("error fetching subscriber lists: %v", err)
|
app.log.Printf("error fetching subscriber lists: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching subscriber lists: %v", pqErrMsg(err)))
|
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`.")
|
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.ToLower(strings.TrimSpace(req.Email)),
|
||||||
strings.TrimSpace(req.Name),
|
strings.TrimSpace(req.Name),
|
||||||
req.Status,
|
req.Status,
|
||||||
req.Attribs,
|
req.Attribs,
|
||||||
req.Lists)
|
req.Lists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error updating subscriber: %v", err)
|
app.log.Printf("error updating subscriber: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err)))
|
fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -239,9 +239,9 @@ func handleGetSubscriberSendOptin(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the subscriber.
|
// Fetch the subscriber.
|
||||||
err := app.Queries.GetSubscriber.Select(&out, id, nil)
|
err := app.queries.GetSubscriber.Select(&out, id, nil)
|
||||||
if err != 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,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
@ -287,8 +287,8 @@ func handleBlacklistSubscribers(c echo.Context) error {
|
||||||
IDs = req.SubscriberIDs
|
IDs = req.SubscriberIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.BlacklistSubscribers.Exec(IDs); err != nil {
|
if _, err := app.queries.BlacklistSubscribers.Exec(IDs); err != nil {
|
||||||
app.Logger.Printf("error blacklisting subscribers: %v", err)
|
app.log.Printf("error blacklisting subscribers: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error blacklisting: %v", err))
|
fmt.Sprintf("Error blacklisting: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -335,17 +335,17 @@ func handleManageSubscriberLists(c echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
case "add":
|
case "add":
|
||||||
_, err = app.Queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
|
_, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
|
||||||
case "remove":
|
case "remove":
|
||||||
_, err = app.Queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
|
_, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
|
||||||
case "unsubscribe":
|
case "unsubscribe":
|
||||||
_, err = app.Queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
|
_, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
|
||||||
default:
|
default:
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error updating subscriptions: %v", err)
|
app.log.Printf("error updating subscriptions: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error processing lists: %v", err))
|
fmt.Sprintf("Error processing lists: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -383,8 +383,8 @@ func handleDeleteSubscribers(c echo.Context) error {
|
||||||
IDs = i
|
IDs = i
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := app.Queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
|
if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
|
||||||
app.Logger.Printf("error deleting subscribers: %v", err)
|
app.log.Printf("error deleting subscribers: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error deleting subscribers: %v", err))
|
fmt.Sprintf("Error deleting subscribers: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -404,11 +404,11 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
||||||
app.Queries.DeleteSubscribersByQuery,
|
app.queries.DeleteSubscribersByQuery,
|
||||||
req.ListIDs, app.DB)
|
req.ListIDs, app.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error querying subscribers: %v", err)
|
app.log.Printf("error querying subscribers: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error: %v", err))
|
fmt.Sprintf("Error: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -428,11 +428,11 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
||||||
app.Queries.BlacklistSubscribersByQuery,
|
app.queries.BlacklistSubscribersByQuery,
|
||||||
req.ListIDs, app.DB)
|
req.ListIDs, app.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error blacklisting subscribers: %v", err)
|
app.log.Printf("error blacklisting subscribers: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error: %v", err))
|
fmt.Sprintf("Error: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -459,19 +459,19 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
|
||||||
var stmt string
|
var stmt string
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
case "add":
|
case "add":
|
||||||
stmt = app.Queries.AddSubscribersToListsByQuery
|
stmt = app.queries.AddSubscribersToListsByQuery
|
||||||
case "remove":
|
case "remove":
|
||||||
stmt = app.Queries.DeleteSubscriptionsByQuery
|
stmt = app.queries.DeleteSubscriptionsByQuery
|
||||||
case "unsubscribe":
|
case "unsubscribe":
|
||||||
stmt = app.Queries.UnsubscribeSubscribersFromListsByQuery
|
stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
|
||||||
default:
|
default:
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
||||||
stmt, req.ListIDs, app.DB, req.TargetListIDs)
|
stmt, req.ListIDs, app.db, req.TargetListIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Printf("error updating subscriptions: %v", err)
|
app.log.Printf("error updating subscriptions: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error: %v", err))
|
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,
|
// Get the subscriber's data. A single query that gets the profile,
|
||||||
// list subscriptions, campaign views, and link clicks. Names of
|
// list subscriptions, campaign views, and link clicks. Names of
|
||||||
// private lists are replaced with "Private list".
|
// 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 {
|
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,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
"Error exporting subscriber data.")
|
"Error exporting subscriber data.")
|
||||||
}
|
}
|
||||||
|
@ -516,7 +516,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) {
|
||||||
}
|
}
|
||||||
req.UUID = uu.String()
|
req.UUID = uu.String()
|
||||||
|
|
||||||
err = app.Queries.InsertSubscriber.Get(&req.ID,
|
err = app.queries.InsertSubscriber.Get(&req.ID,
|
||||||
req.UUID,
|
req.UUID,
|
||||||
req.Email,
|
req.Email,
|
||||||
strings.TrimSpace(req.Name),
|
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.")
|
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,
|
return 0, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error inserting subscriber: %v", err))
|
fmt.Sprintf("Error inserting subscriber: %v", err))
|
||||||
}
|
}
|
||||||
|
@ -556,8 +556,8 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
|
||||||
if subUUID != "" {
|
if subUUID != "" {
|
||||||
uu = subUUID
|
uu = subUUID
|
||||||
}
|
}
|
||||||
if err := app.Queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
|
if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
|
||||||
app.Logger.Printf("error fetching subscriber export data: %v", err)
|
app.log.Printf("error fetching subscriber export data: %v", err)
|
||||||
return data, nil, 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.
|
// Marshal the data into an indented payload.
|
||||||
b, err := json.MarshalIndent(data, "", " ")
|
b, err := json.MarshalIndent(data, "", " ")
|
||||||
if err != nil {
|
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, nil, err
|
||||||
}
|
}
|
||||||
return data, b, nil
|
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.
|
// Fetch double opt-in lists from the given list IDs.
|
||||||
// Get the list of subscription lists where the subscriber hasn't confirmed.
|
// 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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,13 +610,13 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
|
||||||
for _, l := range out.Lists {
|
for _, l := range out.Lists {
|
||||||
qListIDs.Add("l", l.UUID)
|
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.
|
// Send the e-mail.
|
||||||
if err := sendNotification([]string{sub.Email},
|
if err := sendNotification([]string{sub.Email},
|
||||||
"Confirm subscription",
|
"Confirm subscription",
|
||||||
notifSubscriberOptin, out, app); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
templates.go
16
templates.go
|
@ -48,7 +48,7 @@ func handleGetTemplates(c echo.Context) error {
|
||||||
single = true
|
single = true
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Queries.GetTemplates.Select(&out, id, noBody)
|
err := app.queries.GetTemplates.Select(&out, id, noBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
|
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.")
|
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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
|
||||||
|
@ -109,12 +109,12 @@ func handlePreviewTemplate(c echo.Context) error {
|
||||||
Body: dummyTpl,
|
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))
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the message body.
|
// Render the message body.
|
||||||
m := app.Manager.NewMessage(&camp, &dummySubscriber)
|
m := app.manager.NewMessage(&camp, &dummySubscriber)
|
||||||
if err := m.Render(); err != nil {
|
if err := m.Render(); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest,
|
return echo.NewHTTPError(http.StatusBadRequest,
|
||||||
fmt.Sprintf("Error rendering message: %v", err))
|
fmt.Sprintf("Error rendering message: %v", err))
|
||||||
|
@ -140,7 +140,7 @@ func handleCreateTemplate(c echo.Context) error {
|
||||||
|
|
||||||
// Insert and read ID.
|
// Insert and read ID.
|
||||||
var newID int
|
var newID int
|
||||||
if err := app.Queries.CreateTemplate.Get(&newID,
|
if err := app.queries.CreateTemplate.Get(&newID,
|
||||||
o.Name,
|
o.Name,
|
||||||
o.Body); err != nil {
|
o.Body); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
@ -174,7 +174,7 @@ func handleUpdateTemplate(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: PASSWORD HASHING.
|
// 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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
|
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.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := app.Queries.SetDefaultTemplate.Exec(id)
|
_, err := app.queries.SetDefaultTemplate.Exec(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
|
||||||
|
@ -221,7 +221,7 @@ func handleDeleteTemplate(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var delID int
|
var delID int
|
||||||
err := app.Queries.DeleteTemplate.Get(&delID, id)
|
err := app.queries.DeleteTemplate.Get(&delID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return c.JSON(http.StatusOK, okResp{true})
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
|
|
64
utils.go
64
utils.go
|
@ -7,14 +7,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/knadh/goyesql"
|
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
@ -26,67 +23,6 @@ var (
|
||||||
tagRegexpSpaces = regexp.MustCompile(`[\s]+`)
|
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
|
// validateMIME is a helper function to validate uploaded file's MIME type
|
||||||
// against the slice of MIME types is given.
|
// against the slice of MIME types is given.
|
||||||
func validateMIME(typ string, mimes []string) (ok bool) {
|
func validateMIME(typ string, mimes []string) (ok bool) {
|
||||||
|
|
Loading…
Reference in New Issue