wakapi/services/report.go

143 lines
4.2 KiB
Go

package services
import (
"github.com/duke-git/lancet/v2/datetime"
"github.com/duke-git/lancet/v2/slice"
"github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/artifex/v2"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"math/rand"
"time"
)
// delay between evey report generation task (to throttle email sending frequency)
const reportDelay = 10 * time.Second
// past time range to cover in the report
const reportRange = 7 * 24 * time.Hour
type ReportService struct {
config *config.Config
eventBus *hub.Hub
summaryService ISummaryService
userService IUserService
mailService IMailService
rand *rand.Rand
queueDefault *artifex.Dispatcher
queueWorkers *artifex.Dispatcher
}
func NewReportService(summaryService ISummaryService, userService IUserService, mailService IMailService) *ReportService {
srv := &ReportService{
config: config.Get(),
eventBus: config.EventBus(),
summaryService: summaryService,
userService: userService,
mailService: mailService,
rand: rand.New(rand.NewSource(time.Now().Unix())),
queueDefault: config.GetDefaultQueue(),
queueWorkers: config.GetQueue(config.QueueReports),
}
return srv
}
func (srv *ReportService) Schedule() {
logbuch.Info("scheduling report generation")
scheduleUserReport := func(u *models.User) {
if err := srv.queueWorkers.Dispatch(func() {
t0 := time.Now()
if err := srv.SendReport(u, reportRange); err != nil {
config.Log().Error("failed to generate report for '%s', %v", u.ID, err)
}
// make the job take at least reportDelay seconds
if diff := reportDelay - time.Now().Sub(t0); diff > 0 {
logbuch.Debug("waiting for %v before sending next report", diff)
time.Sleep(diff)
}
}); err != nil {
config.Log().Error("failed to dispatch report generation job for user '%s', %v", u.ID, err)
}
}
_, err := srv.queueDefault.DispatchCron(func() {
// fetch all users with reports enabled
users, err := srv.userService.GetAllByReports(true)
if err != nil {
config.Log().Error("failed to get users for report generation, %v", err)
return
}
// filter users who have their email set
users = slice.Filter[*models.User](users, func(i int, u *models.User) bool {
return u.Email != ""
})
// schedule jobs, throttled by one job per x seconds
logbuch.Info("scheduling report generation for %d users", len(users))
for _, u := range users {
scheduleUserReport(u)
}
}, srv.config.App.GetWeeklyReportCron())
if err != nil {
config.Log().Error("failed to dispatch report generation jobs, %v", err)
}
}
func (srv *ReportService) SendReport(user *models.User, duration time.Duration) error {
if user.Email == "" {
logbuch.Warn("not generating report for '%s' as no e-mail address is set")
return nil
}
logbuch.Info("generating report for '%s'", user.ID)
end := time.Now().In(user.TZ())
start := time.Now().Add(-1 * duration)
fullSummary, err := srv.summaryService.Aliased(start, end, user, srv.summaryService.Retrieve, nil, false)
if err != nil {
config.Log().Error("failed to generate report for '%s' - %v", user.ID, err)
return err
}
// generate per-day summaries
dayIntervals := utils.SplitRangeByDays(start, end)
dailySummaries := make([]*models.Summary, len(dayIntervals))
for i, interval := range dayIntervals {
from, to := datetime.BeginOfDay(interval[0]), interval[1]
summary, err := srv.summaryService.Aliased(from, to, user, srv.summaryService.Retrieve, nil, false)
if err != nil {
config.Log().Error("failed to generate day summary (%v to %v) for report for '%s' - %v", from, to, user.ID, err)
break
}
summary.FromTime = models.CustomTime(from)
summary.ToTime = models.CustomTime(to.Add(-1 * time.Second))
dailySummaries[i] = summary
}
report := &models.Report{
From: start,
To: end,
User: user,
Summary: fullSummary,
DailySummaries: dailySummaries,
}
if err := srv.mailService.SendReport(user, report); err != nil {
config.Log().Error("failed to send report for '%s', %v", user.ID, err)
return err
}
logbuch.Info("sent report to user '%s'", user.ID)
return nil
}