2018-10-25 21:51:47 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2023-05-18 19:25:59 +08:00
|
|
|
"net/http"
|
|
|
|
|
2024-01-27 23:25:50 +08:00
|
|
|
"github.com/gofrs/uuid/v5"
|
2023-05-18 19:25:59 +08:00
|
|
|
"github.com/knadh/listmonk/internal/core"
|
|
|
|
"github.com/knadh/listmonk/internal/manager"
|
|
|
|
"github.com/knadh/listmonk/internal/media"
|
2018-10-25 21:51:47 +08:00
|
|
|
"github.com/knadh/listmonk/models"
|
|
|
|
"github.com/lib/pq"
|
|
|
|
)
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
// store implements DataSource over the primary
|
2018-10-25 21:51:47 +08:00
|
|
|
// database.
|
2023-05-18 19:25:59 +08:00
|
|
|
type store struct {
|
2022-04-03 23:24:40 +08:00
|
|
|
queries *models.Queries
|
2023-05-18 19:25:59 +08:00
|
|
|
core *core.Core
|
|
|
|
media media.Store
|
|
|
|
h *http.Client
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
func newManagerStore(q *models.Queries, c *core.Core, m media.Store) *store {
|
|
|
|
return &store{
|
2018-10-25 21:51:47 +08:00
|
|
|
queries: q,
|
2023-05-18 19:25:59 +08:00
|
|
|
core: c,
|
|
|
|
media: m,
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-22 22:05:53 +08:00
|
|
|
// NextCampaigns retrieves active campaigns ready to be processed excluding
|
|
|
|
// campaigns that are also being processed. Additionally, it takes a map of campaignID:sentCount
|
|
|
|
// of campaigns that are being processed and updates them in the DB.
|
|
|
|
func (s *store) NextCampaigns(currentIDs []int64, sentCounts []int64) ([]*models.Campaign, error) {
|
2018-10-25 21:51:47 +08:00
|
|
|
var out []*models.Campaign
|
2023-12-22 22:05:53 +08:00
|
|
|
err := s.queries.NextCampaigns.Select(&out, pq.Int64Array(currentIDs), pq.Int64Array(sentCounts))
|
2018-10-25 21:51:47 +08:00
|
|
|
return out, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// NextSubscribers retrieves a subset of subscribers of a given campaign.
|
|
|
|
// Since batches are processed sequentially, the retrieval is ordered by ID,
|
|
|
|
// and every batch takes the last ID of the last batch and fetches the next
|
|
|
|
// batch above that.
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) NextSubscribers(campID, limit int) ([]models.Subscriber, error) {
|
2020-03-08 13:37:24 +08:00
|
|
|
var out []models.Subscriber
|
2023-05-18 19:25:59 +08:00
|
|
|
err := s.queries.NextCampaignSubscribers.Select(&out, campID, limit)
|
2018-10-25 21:51:47 +08:00
|
|
|
return out, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCampaign fetches a campaign from the database.
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) GetCampaign(campID int) (*models.Campaign, error) {
|
2018-10-25 21:51:47 +08:00
|
|
|
var out = &models.Campaign{}
|
2024-01-10 02:04:08 +08:00
|
|
|
err := s.queries.GetCampaign.Get(out, campID, nil, nil, "default")
|
2018-10-25 21:51:47 +08:00
|
|
|
return out, err
|
|
|
|
}
|
|
|
|
|
2018-11-26 19:10:51 +08:00
|
|
|
// UpdateCampaignStatus updates a campaign's status.
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) UpdateCampaignStatus(campID int, status string) error {
|
|
|
|
_, err := s.queries.UpdateCampaignStatus.Exec(campID, status)
|
2018-10-25 21:51:47 +08:00
|
|
|
return err
|
|
|
|
}
|
2018-10-31 20:54:21 +08:00
|
|
|
|
2023-12-22 22:05:53 +08:00
|
|
|
// UpdateCampaignStatus updates a campaign's status.
|
|
|
|
func (s *store) UpdateCampaignCounts(campID int, toSend int, sent int, lastSubID int) error {
|
|
|
|
_, err := s.queries.UpdateCampaignCounts.Exec(campID, toSend, sent, lastSubID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
// GetAttachment fetches a media attachment blob.
|
|
|
|
func (s *store) GetAttachment(mediaID int) (models.Attachment, error) {
|
|
|
|
m, err := s.core.GetMedia(mediaID, "", s.media)
|
|
|
|
if err != nil {
|
|
|
|
return models.Attachment{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := s.media.GetBlob(m.URL)
|
|
|
|
if err != nil {
|
|
|
|
return models.Attachment{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return models.Attachment{
|
|
|
|
Name: m.Filename,
|
|
|
|
Content: b,
|
|
|
|
Header: manager.MakeAttachmentHeader(m.Filename, "base64", m.ContentType),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-10-31 20:54:21 +08:00
|
|
|
// CreateLink registers a URL with a UUID for tracking clicks and returns the UUID.
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) CreateLink(url string) (string, error) {
|
2018-10-31 20:54:21 +08:00
|
|
|
// Create a new UUID for the URL. If the URL already exists in the DB
|
|
|
|
// the UUID in the database is returned.
|
2020-03-07 23:07:48 +08:00
|
|
|
uu, err := uuid.NewV4()
|
|
|
|
if err != nil {
|
2018-10-31 20:54:21 +08:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2020-03-07 23:07:48 +08:00
|
|
|
var out string
|
2023-05-18 19:25:59 +08:00
|
|
|
if err := s.queries.CreateLink.Get(&out, uu, url); err != nil {
|
2020-03-07 23:07:48 +08:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
2018-10-31 20:54:21 +08:00
|
|
|
}
|
2021-05-25 01:11:48 +08:00
|
|
|
|
|
|
|
// RecordBounce records a bounce event and returns the bounce count.
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) RecordBounce(b models.Bounce) (int64, int, error) {
|
2021-05-25 01:11:48 +08:00
|
|
|
var res = struct {
|
|
|
|
SubscriberID int64 `db:"subscriber_id"`
|
|
|
|
Num int `db:"num"`
|
|
|
|
}{}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
err := s.queries.UpdateCampaignStatus.Select(&res,
|
2021-05-25 01:11:48 +08:00
|
|
|
b.SubscriberUUID,
|
|
|
|
b.Email,
|
|
|
|
b.CampaignUUID,
|
|
|
|
b.Type,
|
|
|
|
b.Source,
|
|
|
|
b.Meta)
|
|
|
|
|
|
|
|
return res.SubscriberID, res.Num, err
|
|
|
|
}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) BlocklistSubscriber(id int64) error {
|
|
|
|
_, err := s.queries.BlocklistSubscribers.Exec(pq.Int64Array{id})
|
2021-05-25 01:11:48 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
func (s *store) DeleteSubscriber(id int64) error {
|
|
|
|
_, err := s.queries.DeleteSubscribers.Exec(pq.Int64Array{id})
|
2021-05-25 01:11:48 +08:00
|
|
|
return err
|
|
|
|
}
|