mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-12 01:14:50 +08:00
277 lines
7.5 KiB
Go
277 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/gorilla/feeds"
|
|
"github.com/knadh/listmonk/internal/manager"
|
|
"github.com/knadh/listmonk/models"
|
|
"github.com/labstack/echo/v4"
|
|
null "gopkg.in/volatiletech/null.v6"
|
|
)
|
|
|
|
type campArchive struct {
|
|
UUID string `json:"uuid"`
|
|
Subject string `json:"subject"`
|
|
Content string `json:"content"`
|
|
CreatedAt null.Time `json:"created_at"`
|
|
SendAt null.Time `json:"send_at"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// GetCampaignArchives renders the public campaign archives page.
|
|
func (a *App) GetCampaignArchives(c echo.Context) error {
|
|
// Get archives from the DB.
|
|
pg := a.pg.NewFromURL(c.Request().URL.Query())
|
|
camps, total, err := a.getCampaignArchives(pg.Offset, pg.Limit, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(camps) == 0 {
|
|
return c.JSON(http.StatusOK, okResp{models.PageResults{
|
|
Results: []campArchive{},
|
|
}})
|
|
}
|
|
|
|
// Meta.
|
|
out := models.PageResults{
|
|
Results: camps,
|
|
Total: total,
|
|
Page: pg.Page,
|
|
PerPage: pg.PerPage,
|
|
}
|
|
|
|
return c.JSON(200, okResp{out})
|
|
}
|
|
|
|
// GetCampaignArchivesFeed renders the public campaign archives RSS feed.
|
|
func (a *App) GetCampaignArchivesFeed(c echo.Context) error {
|
|
var (
|
|
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
|
showFullContent = a.cfg.EnablePublicArchiveRSSContent
|
|
)
|
|
|
|
// Get archives from the DB.
|
|
camps, _, err := a.getCampaignArchives(pg.Offset, pg.Limit, showFullContent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Format output for the feed.
|
|
out := make([]*feeds.Item, 0, len(camps))
|
|
for _, c := range camps {
|
|
pubDate := c.CreatedAt.Time
|
|
|
|
if c.SendAt.Valid {
|
|
pubDate = c.SendAt.Time
|
|
}
|
|
|
|
out = append(out, &feeds.Item{
|
|
Title: c.Subject,
|
|
Link: &feeds.Link{Href: c.URL},
|
|
Content: c.Content,
|
|
Created: pubDate,
|
|
})
|
|
}
|
|
|
|
// Generate the feed.
|
|
feed := &feeds.Feed{
|
|
Title: a.cfg.SiteName,
|
|
Link: &feeds.Link{Href: a.urlCfg.RootURL},
|
|
Description: a.i18n.T("public.archiveTitle"),
|
|
Items: out,
|
|
}
|
|
|
|
if err := feed.WriteRss(c.Response().Writer); err != nil {
|
|
a.log.Printf("error generating archive RSS feed: %v", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("public.errorProcessingRequest"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CampaignArchivesPage renders the public campaign archives page.
|
|
func (a *App) CampaignArchivesPage(c echo.Context) error {
|
|
// Get archives from the DB.
|
|
pg := a.pg.NewFromURL(c.Request().URL.Query())
|
|
out, total, err := a.getCampaignArchives(pg.Offset, pg.Limit, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pg.SetTotal(total)
|
|
|
|
title := a.i18n.T("public.archiveTitle")
|
|
return c.Render(http.StatusOK, "archive", struct {
|
|
Title string
|
|
Description string
|
|
Campaigns []campArchive
|
|
TotalPages int
|
|
Pagination template.HTML
|
|
}{title, title, out, pg.TotalPages, template.HTML(pg.HTML("?page=%d"))})
|
|
}
|
|
|
|
// CampaignArchivePage renders the public campaign archives page.
|
|
func (a *App) CampaignArchivePage(c echo.Context) error {
|
|
// ID can be the UUID or slug.
|
|
var (
|
|
idStr = c.Param("id")
|
|
uuid, slug string
|
|
)
|
|
if reUUID.MatchString(idStr) {
|
|
uuid = idStr
|
|
} else {
|
|
slug = idStr
|
|
}
|
|
|
|
// Get the campaign from the DB.
|
|
pubCamp, err := a.core.GetArchivedCampaign(0, uuid, slug)
|
|
if err != nil || pubCamp.Type != models.CampaignTypeRegular {
|
|
notFound := false
|
|
|
|
// Camppaig doesn't exist.
|
|
if er, ok := err.(*echo.HTTPError); ok {
|
|
if er.Code == http.StatusBadRequest {
|
|
notFound = true
|
|
}
|
|
} else if pubCamp.Type != models.CampaignTypeRegular {
|
|
// Campaign isn't of regular type.
|
|
notFound = true
|
|
}
|
|
|
|
// 404.
|
|
if notFound {
|
|
return c.Render(http.StatusNotFound, tplMessage,
|
|
makeMsgTpl(a.i18n.T("public.notFoundTitle"), "", a.i18n.T("public.campaignNotFound")))
|
|
}
|
|
|
|
// Some other internal error.
|
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
|
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.errorFetchingCampaign")))
|
|
}
|
|
|
|
// "Compile" the campaign template with appropriate data.
|
|
out, err := a.compileArchiveCampaigns([]models.Campaign{pubCamp})
|
|
if err != nil {
|
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
|
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.errorFetchingCampaign")))
|
|
}
|
|
|
|
// Render the campaign body.
|
|
camp := out[0].Campaign
|
|
msg, err := a.manager.NewCampaignMessage(camp, out[0].Subscriber)
|
|
if err != nil {
|
|
a.log.Printf("error rendering campaign: %v", err)
|
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
|
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.errorFetchingCampaign")))
|
|
}
|
|
|
|
return c.HTML(http.StatusOK, string(msg.Body()))
|
|
}
|
|
|
|
// CampaignArchivePageLatest renders the latest public campaign.
|
|
func (a *App) CampaignArchivePageLatest(c echo.Context) error {
|
|
// Get the latest campaign from the DB.
|
|
camps, _, err := a.getCampaignArchives(0, 1, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(camps) == 0 {
|
|
return c.Render(http.StatusNotFound, tplMessage,
|
|
makeMsgTpl(a.i18n.T("public.notFoundTitle"), "", a.i18n.T("public.campaignNotFound")))
|
|
}
|
|
camp := camps[0]
|
|
|
|
return c.HTML(http.StatusOK, camp.Content)
|
|
}
|
|
|
|
// getCampaignArchives fetches the public campaign archives from the DB.
|
|
func (a *App) getCampaignArchives(offset, limit int, renderBody bool) ([]campArchive, int, error) {
|
|
pubCamps, total, err := a.core.GetArchivedCampaigns(offset, limit)
|
|
if err != nil {
|
|
return []campArchive{}, total, echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("public.errorFetchingCampaign"))
|
|
}
|
|
|
|
msgs, err := a.compileArchiveCampaigns(pubCamps)
|
|
if err != nil {
|
|
return []campArchive{}, total, err
|
|
}
|
|
|
|
out := make([]campArchive, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
camp := m.Campaign
|
|
|
|
archive := campArchive{
|
|
UUID: camp.UUID,
|
|
Subject: camp.Subject,
|
|
CreatedAt: camp.CreatedAt,
|
|
SendAt: camp.SendAt,
|
|
}
|
|
|
|
// The campaign may have a custom slug.
|
|
if camp.ArchiveSlug.Valid {
|
|
archive.URL, _ = url.JoinPath(a.urlCfg.ArchiveURL, camp.ArchiveSlug.String)
|
|
} else {
|
|
archive.URL, _ = url.JoinPath(a.urlCfg.ArchiveURL, camp.UUID)
|
|
}
|
|
|
|
// Render the full template body if requested.
|
|
if renderBody {
|
|
msg, err := a.manager.NewCampaignMessage(camp, m.Subscriber)
|
|
if err != nil {
|
|
return []campArchive{}, total, err
|
|
}
|
|
archive.Content = string(msg.Body())
|
|
}
|
|
|
|
out = append(out, archive)
|
|
}
|
|
|
|
return out, total, nil
|
|
}
|
|
|
|
// compileArchiveCampaigns compiles the campaign template with the subscriber data.
|
|
func (a *App) compileArchiveCampaigns(camps []models.Campaign) ([]manager.CampaignMessage, error) {
|
|
|
|
var (
|
|
b = bytes.Buffer{}
|
|
out = make([]manager.CampaignMessage, 0, len(camps))
|
|
)
|
|
for _, c := range camps {
|
|
camp := c
|
|
if err := camp.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil {
|
|
a.log.Printf("error compiling template: %v", err)
|
|
return nil, echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("public.errorFetchingCampaign"))
|
|
}
|
|
|
|
// Load the dummy subscriber meta.
|
|
var sub models.Subscriber
|
|
if err := json.Unmarshal([]byte(camp.ArchiveMeta), &sub); err != nil {
|
|
a.log.Printf("error unmarshalling campaign archive meta: %v", err)
|
|
return nil, echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("public.errorFetchingCampaign"))
|
|
}
|
|
|
|
m := manager.CampaignMessage{
|
|
Campaign: &camp,
|
|
Subscriber: sub,
|
|
}
|
|
|
|
// Render the subject if it's a template.
|
|
if camp.SubjectTpl != nil {
|
|
if err := camp.SubjectTpl.ExecuteTemplate(&b, models.ContentTpl, m); err != nil {
|
|
return nil, err
|
|
}
|
|
camp.Subject = b.String()
|
|
b.Reset()
|
|
}
|
|
|
|
out = append(out, m)
|
|
}
|
|
|
|
return out, nil
|
|
}
|