mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-08 00:02:12 +00:00
264 lines
8.6 KiB
Go
264 lines
8.6 KiB
Go
package placement
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/config"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/observability/errs"
|
|
"github.com/warmbly/warmbly/internal/pkg/mailhost"
|
|
"github.com/warmbly/warmbly/internal/pkg/warmlint"
|
|
"github.com/warmbly/warmbly/internal/repository"
|
|
)
|
|
|
|
// TestView is a test with where its copies landed, overall and per host
|
|
// family. The copy itself is left out of lists.
|
|
type TestView struct {
|
|
models.PlacementTest
|
|
Summary models.PlacementCounts `json:"summary"`
|
|
Families []models.PlacementFamilyCounts `json:"families"`
|
|
}
|
|
|
|
// ResultView is one probe as the dashboard shows it.
|
|
type ResultView struct {
|
|
// Seed is the seed's address, masked on the shared panels so the panel
|
|
// cannot be listed and whitelisted.
|
|
Seed string `json:"seed"`
|
|
Family string `json:"family"`
|
|
FamilyLabel string `json:"family_label"`
|
|
Folder string `json:"folder"`
|
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
|
SentAt *time.Time `json:"sent_at"`
|
|
DetectedAt *time.Time `json:"detected_at"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// ContentCheck is the rules pass over the test's copy.
|
|
type ContentCheck struct {
|
|
Score int `json:"score"`
|
|
Issues []warmlint.Issue `json:"issues"`
|
|
}
|
|
|
|
// TestDetail is one test in full.
|
|
type TestDetail struct {
|
|
TestView
|
|
Results []ResultView `json:"results"`
|
|
Content ContentCheck `json:"content"`
|
|
Compare *TestView `json:"compare,omitempty"`
|
|
}
|
|
|
|
// Overview is what a workspace can test on and how much of its allowance is
|
|
// left.
|
|
type Overview struct {
|
|
Panels []models.PlacementPanelInfo `json:"panels"`
|
|
Usage models.PlacementUsage `json:"usage"`
|
|
WorkspaceSeeds int `json:"workspace_seeds"`
|
|
SeedsPerTest int `json:"seeds_per_test"`
|
|
SpacingSeconds int `json:"spacing_seconds"`
|
|
}
|
|
|
|
// AdminSeed is a mailbox on (or offered for) the instance panel.
|
|
type AdminSeed struct {
|
|
ID uuid.UUID `json:"id"`
|
|
OrganizationID *uuid.UUID `json:"organization_id"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
Family string `json:"family"`
|
|
FamilyLabel string `json:"family_label"`
|
|
Status string `json:"status"`
|
|
WorkerID *uuid.UUID `json:"worker_id"`
|
|
SeedScope string `json:"seed_scope"`
|
|
}
|
|
|
|
func familyLabel(f string) string {
|
|
if l := mailhost.Host(f).Label(); l != "" {
|
|
return l
|
|
}
|
|
return "Other provider"
|
|
}
|
|
|
|
// view counts a test's probes overall and per family. withBody keeps the copy.
|
|
func (s *service) view(t models.PlacementTest, results []models.PlacementResult, withBody bool) TestView {
|
|
if !withBody {
|
|
t.BodyHTML, t.BodyPlain = "", ""
|
|
}
|
|
v := TestView{PlacementTest: t, Families: []models.PlacementFamilyCounts{}}
|
|
byFamily := map[string]*models.PlacementCounts{}
|
|
for _, r := range results {
|
|
v.Summary.Add(r.Folder)
|
|
c, ok := byFamily[r.Family]
|
|
if !ok {
|
|
c = &models.PlacementCounts{}
|
|
byFamily[r.Family] = c
|
|
}
|
|
c.Add(r.Folder)
|
|
}
|
|
v.Summary.Finish()
|
|
for fam, c := range byFamily {
|
|
c.Finish()
|
|
v.Families = append(v.Families, models.PlacementFamilyCounts{Family: fam, Label: familyLabel(fam), Counts: *c})
|
|
}
|
|
sort.Slice(v.Families, func(i, j int) bool { return v.Families[i].Label < v.Families[j].Label })
|
|
return v
|
|
}
|
|
|
|
func resultViews(panel string, results []models.PlacementResult) []ResultView {
|
|
out := make([]ResultView, 0, len(results))
|
|
for _, r := range results {
|
|
seed := r.SeedAddress
|
|
if panel != models.PlacementPanelWorkspace {
|
|
seed = maskAddress(seed)
|
|
}
|
|
out = append(out, ResultView{
|
|
Seed: seed,
|
|
Family: r.Family,
|
|
FamilyLabel: familyLabel(r.Family),
|
|
Folder: r.Folder,
|
|
ScheduledAt: r.ScheduledAt,
|
|
SentAt: r.SentAt,
|
|
DetectedAt: r.DetectedAt,
|
|
Error: r.Error,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// maskAddress keeps the first letter of the local part and the domain.
|
|
func maskAddress(address string) string {
|
|
at := strings.LastIndex(address, "@")
|
|
if at <= 0 {
|
|
return "***"
|
|
}
|
|
return address[:1] + "***" + address[at:]
|
|
}
|
|
|
|
func contentCheck(t models.PlacementTest) ContentCheck {
|
|
res := warmlint.Score(t.Subject, t.BodyHTML, t.BodyPlain)
|
|
return ContentCheck{Score: res.Score, Issues: res.Issues}
|
|
}
|
|
|
|
// usage is a workspace's count against the monthly allowance on the metered
|
|
// panel. A self-hosted instance does not meter its own seeds.
|
|
func (s *service) usage(ctx context.Context, orgID uuid.UUID) (models.PlacementUsage, *errx.Error) {
|
|
now := s.now().UTC()
|
|
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
u := models.PlacementUsage{PeriodStart: start, PeriodEnd: start.AddDate(0, 1, 0)}
|
|
used, err := s.Repo.CountMeteredTests(ctx, orgID, start)
|
|
if err != nil {
|
|
errs.CaptureException(err)
|
|
return u, errx.InternalError()
|
|
}
|
|
u.Used = used
|
|
if config.SelfHosted() {
|
|
return u, nil
|
|
}
|
|
pol := s.policy(ctx)
|
|
limit, paid := pol.TestsPerMonthTrial, false
|
|
if s.Gate != nil {
|
|
if paid, _ = s.Gate.IsPaidOrganization(ctx, orgID); paid {
|
|
limit = pol.TestsPerMonthPaid
|
|
}
|
|
}
|
|
u.Limit = &limit
|
|
// Credits come with a paid plan, so a trial past its free tests waits.
|
|
if paid && s.Credits != nil && !s.Credits.Unmetered() {
|
|
// An unreadable balance leaves it unknown; the charge itself still
|
|
// refuses what the workspace cannot pay.
|
|
if u.CreditsPerTest = pol.CreditPrice(); u.CreditsPerTest > 0 {
|
|
if bal, xerr := s.Credits.GetBalance(ctx, orgID); xerr == nil {
|
|
u.CreditBalance = &bal
|
|
} else {
|
|
errs.CaptureException(xerr)
|
|
}
|
|
}
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func panelInfo(panel string, seeds []models.PlacementSeed, metered bool) models.PlacementPanelInfo {
|
|
info := models.PlacementPanelInfo{Panel: panel, Seeds: len(seeds), Metered: metered, Families: []models.PlacementPanelFamily{}}
|
|
counts := map[string]int{}
|
|
for _, s := range seeds {
|
|
counts[s.Family]++
|
|
}
|
|
for fam, n := range counts {
|
|
info.Families = append(info.Families, models.PlacementPanelFamily{Family: fam, Label: familyLabel(fam), Seeds: n})
|
|
}
|
|
sort.Slice(info.Families, func(i, j int) bool { return info.Families[i].Label < info.Families[j].Label })
|
|
info.Available = len(seeds) > 0
|
|
return info
|
|
}
|
|
|
|
// Overview lists the three panels with their seed mix, and the allowance.
|
|
func (s *service) Overview(ctx context.Context, orgID uuid.UUID) (*Overview, *errx.Error) {
|
|
pol := s.policy(ctx)
|
|
out := &Overview{SeedsPerTest: pol.SeedsPerTest, SpacingSeconds: pol.SpacingSeconds}
|
|
usage, xerr := s.usage(ctx, orgID)
|
|
if xerr != nil {
|
|
return nil, xerr
|
|
}
|
|
out.Usage = usage
|
|
|
|
instance, err := s.Repo.ListSeeds(ctx, models.SeedScopeInstance, nil, true)
|
|
if err != nil {
|
|
errs.CaptureException(err)
|
|
return nil, errx.InternalError()
|
|
}
|
|
inst := panelInfo(models.PlacementPanelInstance, familySeeds(instance), !config.SelfHosted())
|
|
if !inst.Available {
|
|
inst.Reason = "No seed inboxes are on this instance's panel yet."
|
|
if config.SelfHosted() {
|
|
inst.Reason = "No seed inboxes are on this instance's panel yet. An administrator adds them in the admin panel."
|
|
}
|
|
}
|
|
out.Panels = append(out.Panels, inst)
|
|
|
|
own, err := s.Repo.ListSeeds(ctx, models.SeedScopeWorkspace, &orgID, true)
|
|
if err != nil {
|
|
errs.CaptureException(err)
|
|
return nil, errx.InternalError()
|
|
}
|
|
ws := panelInfo(models.PlacementPanelWorkspace, familySeeds(own), false)
|
|
if !ws.Available {
|
|
ws.Reason = "Mark one of this workspace's mailboxes as a seed inbox to test on it."
|
|
}
|
|
out.Panels = append(out.Panels, ws)
|
|
out.WorkspaceSeeds = len(own)
|
|
|
|
cloud := models.PlacementPanelInfo{Panel: models.PlacementPanelCloud, Metered: true, Families: []models.PlacementPanelFamily{}}
|
|
switch {
|
|
case !config.SelfHosted():
|
|
// The hosted product's own panel is the instance panel.
|
|
cloud.Reason = "Warmbly Cloud's panel is the instance panel here."
|
|
case s.Cloud == nil:
|
|
cloud.Reason = "Link this instance to Warmbly Cloud to test on its seed panel."
|
|
default:
|
|
if panel, xerr := s.Cloud.PlacementPanel(ctx); xerr != nil {
|
|
cloud.Reason = xerr.Message
|
|
} else if panel != nil {
|
|
cloud = panel.Panel
|
|
cloud.Panel = models.PlacementPanelCloud
|
|
cloud.Metered = true
|
|
}
|
|
}
|
|
if config.SelfHosted() || cloud.Available {
|
|
out.Panels = append(out.Panels, cloud)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func familySeeds(rows []repository.SeedAccount) []models.PlacementSeed {
|
|
out := make([]models.PlacementSeed, 0, len(rows))
|
|
for _, r := range rows {
|
|
out = append(out, models.PlacementSeed{Address: r.Email, Family: string(mailhost.ForMailbox(r.MailHost, r.Provider, r.Email))})
|
|
}
|
|
return out
|
|
}
|