2021-02-03 23:28:02 +03:00
package api
2019-05-05 23:36:49 +03:00
import (
2023-03-16 23:02:28 +03:00
"github.com/duke-git/lancet/v2/condition"
2023-03-03 22:40:50 +03:00
"github.com/go-chi/chi/v5"
2022-12-01 12:57:07 +03:00
"github.com/muety/wakapi/helpers"
2022-01-02 03:22:58 +03:00
"net/http"
2020-11-01 22:14:10 +03:00
conf "github.com/muety/wakapi/config"
2021-02-06 22:09:08 +03:00
"github.com/muety/wakapi/middlewares"
2021-02-03 23:28:02 +03:00
customMiddleware "github.com/muety/wakapi/middlewares/custom"
2021-05-19 11:18:18 +03:00
routeutils "github.com/muety/wakapi/routes/utils"
2020-03-31 13:22:17 +03:00
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
2019-05-06 01:40:41 +03:00
2020-03-31 13:22:17 +03:00
"github.com/muety/wakapi/models"
2019-05-05 23:36:49 +03:00
)
2021-02-03 23:28:02 +03:00
type HeartbeatApiHandler struct {
2020-11-01 22:14:10 +03:00
config * conf . Config
2021-02-06 22:09:08 +03:00
userSrvc services . IUserService
2020-11-08 12:12:49 +03:00
heartbeatSrvc services . IHeartbeatService
languageMappingSrvc services . ILanguageMappingService
2019-05-06 01:40:41 +03:00
}
2021-02-06 22:09:08 +03:00
func NewHeartbeatApiHandler ( userService services . IUserService , heartbeatService services . IHeartbeatService , languageMappingService services . ILanguageMappingService ) * HeartbeatApiHandler {
2021-02-03 23:28:02 +03:00
return & HeartbeatApiHandler {
2020-11-01 22:14:10 +03:00
config : conf . Get ( ) ,
2021-02-06 22:09:08 +03:00
userSrvc : userService ,
2020-11-01 22:14:10 +03:00
heartbeatSrvc : heartbeatService ,
languageMappingSrvc : languageMappingService ,
2019-05-05 23:36:49 +03:00
}
2020-05-24 14:41:19 +03:00
}
2019-05-09 01:07:38 +03:00
2020-08-29 22:13:56 +03:00
type heartbeatResponseVm struct {
Responses [ ] [ ] interface { } ` json:"responses" `
}
2023-03-03 22:40:50 +03:00
func ( h * HeartbeatApiHandler ) RegisterRoutes ( router chi . Router ) {
router . Group ( func ( r chi . Router ) {
r . Use (
middlewares . NewAuthenticateMiddleware ( h . userSrvc ) . Handler ,
customMiddleware . NewWakatimeRelayMiddleware ( ) . Handler ,
)
// see https://github.com/muety/wakapi/issues/203
r . Post ( "/heartbeat" , h . Post )
r . Post ( "/heartbeats" , h . Post )
r . Post ( "/users/{user}/heartbeats" , h . Post )
r . Post ( "/users/{user}/heartbeats.bulk" , h . Post )
r . Post ( "/v1/users/{user}/heartbeats" , h . Post )
r . Post ( "/v1/users/{user}/heartbeats.bulk" , h . Post )
r . Post ( "/compat/wakatime/v1/users/{user}/heartbeats" , h . Post )
r . Post ( "/compat/wakatime/v1/users/{user}/heartbeats.bulk" , h . Post )
} )
2021-01-30 12:34:52 +03:00
}
2021-02-07 13:54:07 +03:00
// @Summary Push a new heartbeat
// @ID post-heartbeat
// @Tags heartbeat
// @Accept json
2021-06-21 22:53:47 +03:00
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
2021-02-07 13:54:07 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /heartbeat [post]
2021-02-03 23:28:02 +03:00
func ( h * HeartbeatApiHandler ) Post ( w http . ResponseWriter , r * http . Request ) {
2021-05-19 11:18:18 +03:00
user , err := routeutils . CheckEffectiveUser ( w , r , h . userSrvc , "current" )
if err != nil {
return // response was already sent by util function
}
2019-05-17 09:40:03 +03:00
var heartbeats [ ] * models . Heartbeat
2022-01-21 14:35:05 +03:00
heartbeats , err = routeutils . ParseHeartbeats ( r )
2021-06-21 22:53:47 +03:00
if err != nil {
2022-01-21 14:35:05 +03:00
conf . Log ( ) . Request ( r ) . Error ( err . Error ( ) )
w . WriteHeader ( http . StatusBadRequest )
w . Write ( [ ] byte ( err . Error ( ) ) )
return
2021-06-21 22:53:47 +03:00
}
2021-08-29 11:54:00 +03:00
userAgent := r . Header . Get ( "User-Agent" )
opSys , editor , _ := utils . ParseUserAgent ( userAgent )
2020-08-29 22:20:23 +03:00
machineName := r . Header . Get ( "X-Machine-Name" )
2019-05-09 01:07:38 +03:00
2019-05-21 18:16:46 +03:00
for _ , hb := range heartbeats {
2022-08-12 10:25:43 +03:00
if hb == nil {
w . WriteHeader ( http . StatusBadRequest )
w . Write ( [ ] byte ( "invalid heartbeat object" ) )
return
}
2023-03-16 23:02:28 +03:00
// TODO: unit test this
if hb . UserAgent != "" {
userAgent = hb . UserAgent
localOpSys , localEditor , _ := utils . ParseUserAgent ( userAgent )
opSys = condition . TernaryOperator [ bool , string ] ( localOpSys != "" , localOpSys , opSys )
editor = condition . TernaryOperator [ bool , string ] ( localEditor != "" , localEditor , editor )
}
if hb . Machine != "" {
machineName = hb . Machine
}
2019-05-21 18:16:46 +03:00
hb . User = user
hb . UserID = user . ID
2023-03-16 23:02:28 +03:00
hb . Machine = machineName
hb . OperatingSystem = opSys
hb . Editor = editor
2021-08-29 11:54:00 +03:00
hb . UserAgent = userAgent
2019-05-21 18:16:46 +03:00
2022-03-17 13:35:20 +03:00
if ! hb . Valid ( ) || ! hb . Timely ( h . config . App . HeartbeatsMaxAge ( ) ) {
2019-05-19 20:49:27 +03:00
w . WriteHeader ( http . StatusBadRequest )
2021-02-13 14:59:59 +03:00
w . Write ( [ ] byte ( "invalid heartbeat object" ) )
2019-05-11 18:49:56 +03:00
return
}
2021-01-31 19:46:50 +03:00
hb . Hashed ( )
2019-05-09 01:07:38 +03:00
}
2019-05-05 23:36:49 +03:00
2020-05-24 14:41:19 +03:00
if err := h . heartbeatSrvc . InsertBatch ( heartbeats ) ; err != nil {
2019-05-19 20:49:27 +03:00
w . WriteHeader ( http . StatusInternalServerError )
2021-02-13 14:59:59 +03:00
w . Write ( [ ] byte ( conf . ErrInternalServerError ) )
2022-02-17 14:20:22 +03:00
conf . Log ( ) . Request ( r ) . Error ( "failed to batch-insert heartbeats - %v" , err )
2019-05-05 23:36:49 +03:00
return
}
2021-02-13 14:59:59 +03:00
if ! user . HasData {
user . HasData = true
if _ , err := h . userSrvc . Update ( user ) ; err != nil {
w . WriteHeader ( http . StatusInternalServerError )
w . Write ( [ ] byte ( conf . ErrInternalServerError ) )
2022-02-17 14:20:22 +03:00
conf . Log ( ) . Request ( r ) . Error ( "failed to update user - %v" , err )
2021-02-13 14:59:59 +03:00
return
}
}
2021-06-21 22:53:47 +03:00
defer func ( ) { } ( )
2022-12-01 12:57:07 +03:00
helpers . RespondJSON ( w , r , http . StatusCreated , constructSuccessResponse ( len ( heartbeats ) ) )
2020-08-29 22:13:56 +03:00
}
// construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288)
// to make the cli consider all heartbeats to having been successfully saved
2021-06-22 01:27:46 +03:00
// response looks like: { "responses": [ [ null, 201 ], ... ] }
// this was probably a temporary bug at wakatime, responses actually looks like so: https://pastr.de/p/nyf6kj2e6843fbw4xkj4h4pj
2021-06-21 22:53:47 +03:00
// TODO: adapt response format some time
2021-06-22 01:27:46 +03:00
// however, wakatime-cli is still able to parse the response (see https://github.com/wakatime/wakatime-cli/blob/c2076c0e1abc1449baf5b7ac7db391b06041c719/pkg/api/heartbeat.go#L127), so no urgent need for action
2020-08-29 22:13:56 +03:00
func constructSuccessResponse ( n int ) * heartbeatResponseVm {
responses := make ( [ ] [ ] interface { } , n )
for i := 0 ; i < n ; i ++ {
r := make ( [ ] interface { } , 2 )
r [ 0 ] = nil
r [ 1 ] = http . StatusCreated
responses [ i ] = r
}
return & heartbeatResponseVm {
Responses : responses ,
}
2019-05-05 23:36:49 +03:00
}
2021-06-21 22:53:47 +03:00
// Only for Swagger
// @Summary Push a new heartbeat
// @ID post-heartbeat-2
// @Tags heartbeat
// @Accept json
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-21 22:53:47 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /v1/users/{user}/heartbeats [post]
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) postAlias1 ( ) { }
// @Summary Push a new heartbeat
// @ID post-heartbeat-3
// @Tags heartbeat
// @Accept json
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-21 22:53:47 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /compat/wakatime/v1/users/{user}/heartbeats [post]
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) postAlias2 ( ) { }
2021-06-22 01:27:46 +03:00
// @Summary Push a new heartbeat
2021-06-21 22:53:47 +03:00
// @ID post-heartbeat-4
// @Tags heartbeat
// @Accept json
2021-06-22 01:27:46 +03:00
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-21 22:53:47 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /users/{user}/heartbeats [post]
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) postAlias3 ( ) { }
// @Summary Push new heartbeats
// @ID post-heartbeat-5
// @Tags heartbeat
// @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /heartbeats [post]
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) postAlias4 ( ) { }
// @Summary Push new heartbeats
// @ID post-heartbeat-6
// @Tags heartbeat
// @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-21 22:53:47 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /v1/users/{user}/heartbeats.bulk [post]
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) postAlias5 ( ) { }
2021-06-22 01:27:46 +03:00
// @Summary Push new heartbeats
// @ID post-heartbeat-7
// @Tags heartbeat
// @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-22 01:27:46 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /compat/wakatime/v1/users/{user}/heartbeats.bulk [post]
2021-06-22 01:27:46 +03:00
func ( h * HeartbeatApiHandler ) postAlias6 ( ) { }
// @Summary Push new heartbeats
// @ID post-heartbeat-8
// @Tags heartbeat
// @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
2022-01-28 14:28:47 +03:00
// @Param user path string true "Username (or current)"
2021-06-22 01:27:46 +03:00
// @Security ApiKeyAuth
// @Success 201
2022-01-02 03:22:58 +03:00
// @Router /users/{user}/heartbeats.bulk [post]
2021-06-22 01:27:46 +03:00
func ( h * HeartbeatApiHandler ) postAlias7 ( ) { }