2021-02-03 23:28:02 +03:00
package api
2019-05-05 23:36:49 +03:00
import (
2021-06-21 22:53:47 +03:00
"bytes"
2019-05-05 23:36:49 +03:00
"encoding/json"
2021-01-30 12:34:52 +03:00
"github.com/gorilla/mux"
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"
2021-06-21 22:53:47 +03:00
"io/ioutil"
2021-01-31 20:06:20 +03:00
"net/http"
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" `
}
2021-02-03 23:28:02 +03:00
func ( h * HeartbeatApiHandler ) RegisterRoutes ( router * mux . Router ) {
2021-05-19 11:18:18 +03:00
r := router . PathPrefix ( "" ) . Subrouter ( )
2021-02-03 23:28:02 +03:00
r . Use (
2021-02-06 22:09:08 +03:00
middlewares . NewAuthenticateMiddleware ( h . userSrvc ) . Handler ,
2021-02-03 23:28:02 +03:00
customMiddleware . NewWakatimeRelayMiddleware ( ) . Handler ,
)
2021-06-21 22:53:47 +03:00
// see https://github.com/muety/wakapi/issues/203
r . Path ( "/heartbeat" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
r . Path ( "/heartbeats" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
2021-06-22 01:27:46 +03:00
r . Path ( "/users/{user}/heartbeats" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
r . Path ( "/users/{user}/heartbeats.bulk" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
2021-06-21 22:53:47 +03:00
r . Path ( "/v1/users/{user}/heartbeats" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
r . Path ( "/v1/users/{user}/heartbeats.bulk" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
r . Path ( "/compat/wakatime/v1/users/{user}/heartbeats" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . Post )
r . Path ( "/compat/wakatime/v1/users/{user}/heartbeats.bulk" ) . Methods ( http . MethodPost ) . HandlerFunc ( 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
2021-10-13 18:47:18 +03:00
// @Router /api/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
2021-06-21 22:53:47 +03:00
heartbeats , err = h . tryParseBulk ( r )
if err != nil {
heartbeats , err = h . tryParseSingle ( r )
if err != nil {
2021-08-07 11:16:50 +03:00
conf . Log ( ) . Request ( r ) . Error ( err . Error ( ) )
2021-06-21 22:53:47 +03:00
w . WriteHeader ( http . StatusBadRequest )
w . Write ( [ ] byte ( err . Error ( ) ) )
return
}
}
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 {
hb . OperatingSystem = opSys
hb . Editor = editor
2020-08-29 22:20:23 +03:00
hb . Machine = machineName
2019-05-21 18:16:46 +03:00
hb . User = user
hb . UserID = user . ID
2021-08-29 11:54:00 +03:00
hb . UserAgent = userAgent
2019-05-21 18:16:46 +03:00
if ! hb . Valid ( ) {
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 ) )
2021-04-16 16:59:39 +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 ) )
2021-04-16 16:59:39 +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 ( ) { } ( )
2021-04-26 22:26:47 +03:00
utils . RespondJSON ( w , r , http . StatusCreated , constructSuccessResponse ( len ( heartbeats ) ) )
2020-08-29 22:13:56 +03:00
}
2021-06-21 22:53:47 +03:00
func ( h * HeartbeatApiHandler ) tryParseBulk ( r * http . Request ) ( [ ] * models . Heartbeat , error ) {
var heartbeats [ ] * models . Heartbeat
body , _ := ioutil . ReadAll ( r . Body )
r . Body . Close ( )
r . Body = ioutil . NopCloser ( bytes . NewBuffer ( body ) )
dec := json . NewDecoder ( ioutil . NopCloser ( bytes . NewBuffer ( body ) ) )
if err := dec . Decode ( & heartbeats ) ; err != nil {
return nil , err
}
return heartbeats , nil
}
func ( h * HeartbeatApiHandler ) tryParseSingle ( r * http . Request ) ( [ ] * models . Heartbeat , error ) {
var heartbeat models . Heartbeat
body , _ := ioutil . ReadAll ( r . Body )
r . Body . Close ( )
r . Body = ioutil . NopCloser ( bytes . NewBuffer ( body ) )
dec := json . NewDecoder ( ioutil . NopCloser ( bytes . NewBuffer ( body ) ) )
if err := dec . Decode ( & heartbeat ) ; err != nil {
return nil , err
}
return [ ] * models . Heartbeat { & heartbeat } , nil
}
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"
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/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"
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/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"
2021-06-21 22:53:47 +03:00
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/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
2021-10-13 18:47:18 +03:00
// @Router /api/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"
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/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"
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/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"
// @Security ApiKeyAuth
// @Success 201
2021-10-13 18:47:18 +03:00
// @Router /api/users/{user}/heartbeats.bulk [post]
2021-06-22 01:27:46 +03:00
func ( h * HeartbeatApiHandler ) postAlias7 ( ) { }