Skip to content

Commit

Permalink
feat(wip): implement activity chart generation (see #12)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Sep 28, 2023
1 parent dff9587 commit 6135ca0
Show file tree
Hide file tree
Showing 14 changed files with 656 additions and 331 deletions.
697 changes: 372 additions & 325 deletions coverage/coverage.out

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
codeberg.org/Codeberg/avatars v1.0.0
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
github.com/alitto/pond v1.8.3
github.com/duke-git/lancet/v2 v2.2.5
Expand Down Expand Up @@ -33,7 +34,6 @@ require (
github.com/swaggo/swag v1.16.2
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.12.0
golang.org/x/sync v0.3.0
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.3
Expand Down
21 changes: 19 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 h1:qZaEtLxnqY5mJ0fVKbk31NVhlgi0yrKm51Pq/I5wcz4=
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTeFRcTdnpzOlRjMoFYC/80HwVUreupyAiqPkCZQOXc=
github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs=
Expand Down Expand Up @@ -81,6 +85,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -145,10 +150,13 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
Expand All @@ -157,10 +165,13 @@ golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand All @@ -170,12 +181,14 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -198,11 +211,14 @@ golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand All @@ -224,6 +240,7 @@ gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ var (
mailService services.IMailService
keyValueService services.IKeyValueService
reportService services.IReportService
activityService services.IActivityService
diagnosticsService services.IDiagnosticsService
housekeepingService services.IHousekeepingService
miscService services.IMiscService
Expand Down Expand Up @@ -189,6 +190,7 @@ func main() {
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(keyValueRepository)
reportService = services.NewReportService(summaryService, userService, mailService)
activityService = services.NewActivityService(summaryService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService)
miscService = services.NewMiscService(userService, heartbeatService, summaryService, keyValueService, mailService)
Expand All @@ -210,6 +212,7 @@ func main() {
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
avatarHandler := api.NewAvatarHandler()
activityHandler := api.NewActivityApiHandler(userService, activityService)
badgeHandler := api.NewBadgeHandler(userService, summaryService)

// Compat Handlers
Expand Down Expand Up @@ -282,6 +285,7 @@ func main() {
metricsHandler.RegisterRoutes(apiRouter)
diagnosticsHandler.RegisterRoutes(apiRouter)
avatarHandler.RegisterRoutes(apiRouter)
activityHandler.RegisterRoutes(apiRouter)
badgeHandler.RegisterRoutes(apiRouter)
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
Expand Down
21 changes: 21 additions & 0 deletions models/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package models

import (
"errors"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"sort"
"time"
)
Expand Down Expand Up @@ -77,6 +79,19 @@ func PersistedSummaryTypes() []uint8 {
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine}
}

func NewEmptySummary() *Summary {
return &Summary{
Projects: SummaryItems{},
Languages: SummaryItems{},
Editors: SummaryItems{},
OperatingSystems: SummaryItems{},
Machines: SummaryItems{},
Labels: SummaryItems{},
Branches: SummaryItems{},
Entities: SummaryItems{},
}
}

func (s *Summary) Sorted() *Summary {
sort.Sort(sort.Reverse(s.Projects))
sort.Sort(sort.Reverse(s.Machines))
Expand Down Expand Up @@ -373,6 +388,12 @@ func (s *SummaryItem) TotalFixed() time.Duration {
return s.Total * time.Second
}

func (s Summaries) MaxTotalTime() time.Duration {
return mathutil.Max(slice.Map[*Summary, time.Duration](s, func(i int, item *Summary) time.Duration {
return item.TotalTime()
})...)
}

func (s Summaries) Len() int {
return len(s)
}
Expand Down
2 changes: 1 addition & 1 deletion models/view/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (s *ProjectsViewModel) getMaxCount() int64 {
}

func fadeColorToTransparent(colorHex string, transparency float64) string {
left := utils.ParseHexColor(colorHex)
left := utils.HexToRGBA(colorHex)
right := &color.RGBA{R: left.R, G: left.G, B: left.B, A: uint8(transparency * 255)}
return fmt.Sprintf("background: transparent; background: linear-gradient(90deg, rgba(%d, %d, %d, 0) 0%%, rgba(%d, %d, %d, 0) 50%%, rgba(%d, %d, %d, %.2f) 100%%);",
left.R, left.G, left.B,
Expand Down
67 changes: 67 additions & 0 deletions routes/api/activity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package api

import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"time"
)

type ActivityApiHandler struct {
config *conf.Config
userService services.IUserService
activityService services.IActivityService
}

func NewActivityApiHandler(userService services.IUserService, activityService services.IActivityService) *ActivityApiHandler {
return &ActivityApiHandler{
activityService: activityService,
userService: userService,
config: conf.Get(),
}
}

func (h *ActivityApiHandler) RegisterRoutes(router chi.Router) {
r := chi.NewRouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userService).WithOptionalFor([]string{"/api/activity/chart/"}).Handler,
middleware.Compress(9, "image/svg+xml"),
)
r.Get("/chart/{user}.svg", h.GetActivityChart)

router.Mount("/activity", r)
}

func (h *ActivityApiHandler) GetActivityChart(w http.ResponseWriter, r *http.Request) {
authorizedUser := middlewares.GetPrincipal(r)
requestedUser, err := h.userService.GetUserById(chi.URLParam(r, "user"))
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}

if authorizedUser == nil || authorizedUser.ID != requestedUser.ID {
if _, userRange := helpers.ResolveMaximumRange(requestedUser.ShareDataMaxDays); userRange != models.IntervalPast12Months && userRange != models.IntervalAny { // TODO: build "hierarchy" of intervals to easily check if one is contained in another
w.WriteHeader(http.StatusForbidden)
return
}
}

chart, err := h.activityService.GetChart(requestedUser, models.IntervalPast12Months, utils.IsNoCache(r, 6*time.Hour))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to get activity chart for user %s - %v", err)
return
}

w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "max-age=43200") // 12 hours
w.WriteHeader(http.StatusOK)
w.Write([]byte(chart))
}
6 changes: 5 additions & 1 deletion routes/api/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"codeberg.org/Codeberg/avatars"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
lru "github.com/hashicorp/golang-lru"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/utils"
Expand All @@ -28,7 +29,10 @@ func NewAvatarHandler() *AvatarHandler {
}

func (h *AvatarHandler) RegisterRoutes(router chi.Router) {
router.Get("/avatar/{hash}.svg", h.Get)
r := chi.NewRouter()
r.Use(middleware.Compress(9, "image/svg+xml"))
r.Get("/avatar/{hash}.svg", h.Get)
router.Mount("/", r)
}

func (h *AvatarHandler) Get(w http.ResponseWriter, r *http.Request) {
Expand Down
121 changes: 121 additions & 0 deletions services/activity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package services

import (
"bytes"
"errors"
"fmt"
svg "github.com/ajstarks/svgo/float"
"github.com/alitto/pond"
"github.com/duke-git/lancet/v2/datetime"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
"math"
"sync"
"time"
)

const (
gridRows = 7
cellWidth = 20
cellHeight = 20
colorMin = "#dce3e1"
colorMax = "#047857"
)

type ActivityService struct {
config *config.Config
cache *cache.Cache
summaryService ISummaryService
}

func NewActivityService(summaryService ISummaryService) *ActivityService {
return &ActivityService{
config: config.Get(),
cache: cache.New(6*time.Hour, 6*time.Hour),
summaryService: summaryService,
}
}

// GetChart generates an activity chart for a given user and the given time interval, similar to GitHub's contribution timeline. See https://github.com/muety/wakapi/issues/12.
// Please note: currently, only yearly charts ("last_12_months") are supported. However, we could fairly easily restructure this to support dynamic intervals.
func (s *ActivityService) GetChart(user *models.User, interval *models.IntervalKey, skipCache bool) (string, error) {
cacheKey := fmt.Sprintf("chart_%s_%s", user.ID, (*interval)[0])
if result, found := s.cache.Get(cacheKey); found && !skipCache {
return result.(string), nil
}

switch interval {
case models.IntervalPast12Months:
chart, err := s.getChartPastYear(user)
if err == nil {
s.cache.SetDefault(cacheKey, chart) // TODO: cache compressed?
}
return chart, err
default:
return "", errors.New("unsupported interval")
}
}

func (s *ActivityService) getChartPastYear(user *models.User) (string, error) {
err, from, to := helpers.ResolveIntervalTZ(models.IntervalPast12Months, user.TZ())
from = datetime.BeginOfWeek(from, time.Monday)
if err != nil {
return "", err
}

intervals := utils.SplitRangeByDays(from, to)
summaries := make([]*models.Summary, len(intervals))

wp := pond.New(utils.HalfCPUs(), 0)
mut := sync.RWMutex{}

// fetch summaries
for i, interval := range intervals {
i := i // https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
interval := interval

wp.Submit(func() {
summary, err := s.summaryService.Retrieve(interval[0], interval[1], user, nil)
fmt.Println(summary == nil)
if err != nil {
config.Log().Warn("failed to retrieve summary for '%s' between %v and %v for activity chart", user.ID, from, to)
summary = models.NewEmptySummary()
summary.FromTime = models.CustomTime(from)
summary.ToTime = models.CustomTime(to)
summary.UserID = user.ID
summary.User = user
}
mut.Lock()
summaries[i] = summary
mut.Unlock()
})
}

wp.StopAndWait()

maxTotal := models.Summaries(summaries).MaxTotalTime()

var (
colorRGBAMin = utils.HexToRGBA(colorMin)
colorRGBAMax = utils.HexToRGBA(colorMax)
)

// generate svg
buf := &bytes.Buffer{}
canvas := svg.New(buf)
canvas.Start(math.Ceil(float64(len(summaries))/float64(gridRows))*cellWidth, gridRows*cellHeight)
for i, s := range summaries {
total := s.TotalTime()
fillColor := utils.RGBAToHex(utils.FadeColors(colorRGBAMin, colorRGBAMax, float64(total)/float64(maxTotal)))
canvas.Group()
canvas.Title(fmt.Sprintf("%s on %s", helpers.FmtWakatimeDuration(total), helpers.FormatDateHuman(s.FromTime.T())))
canvas.Rect(float64(i/gridRows)*cellWidth, float64((i%gridRows)*cellHeight), cellWidth, cellHeight, fmt.Sprintf("fill: %s; fill-opacity: 1; stroke: #fff; stroke-width: 1; stroke-linecap: square; stroke-opacity: 1", fillColor))
canvas.Gend()
}
canvas.End()

return buf.String(), nil
}
Loading

0 comments on commit 6135ca0

Please sign in to comment.