1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

fix: single heartbeat endpoint (resolve #212)

docs: swagger docs for all available heartbeat endpoints
This commit is contained in:
Ferdinand Mütsch 2021-06-21 21:53:47 +02:00
parent 466f2e1786
commit 9f1266957b
5 changed files with 580 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"bytes"
"encoding/json" "encoding/json"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
@ -9,6 +10,7 @@ import (
routeutils "github.com/muety/wakapi/routes/utils" routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"io/ioutil"
"net/http" "net/http"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
@ -40,16 +42,20 @@ func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) {
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler, middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
customMiddleware.NewWakatimeRelayMiddleware().Handler, customMiddleware.NewWakatimeRelayMiddleware().Handler,
) )
r.PathPrefix("/heartbeat").Methods(http.MethodPost).HandlerFunc(h.Post) // see https://github.com/muety/wakapi/issues/203
r.PathPrefix("/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post) r.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(h.Post)
r.PathPrefix("/compat/wakatime/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post) r.Path("/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
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)
} }
// @Summary Push a new heartbeat // @Summary Push a new heartbeat
// @ID post-heartbeat // @ID post-heartbeat
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body models.Heartbeat true "A heartbeat" // @Param heartbeat body models.Heartbeat true "A single heartbeat"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /heartbeat [post] // @Router /heartbeat [post]
@ -60,16 +66,19 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
} }
var heartbeats []*models.Heartbeat var heartbeats []*models.Heartbeat
heartbeats, err = h.tryParseBulk(r)
if err != nil {
heartbeats, err = h.tryParseSingle(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
}
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent")) opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
machineName := r.Header.Get("X-Machine-Name") machineName := r.Header.Get("X-Machine-Name")
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&heartbeats); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
for _, hb := range heartbeats { for _, hb := range heartbeats {
hb.OperatingSystem = opSys hb.OperatingSystem = opSys
hb.Editor = editor hb.Editor = editor
@ -103,12 +112,46 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
} }
} }
defer func() {}()
utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats))) utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
} }
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
}
// construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288) // 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 // to make the cli consider all heartbeats to having been successfully saved
// response looks like: { "responses": [ [ { "data": {...} }, 201 ], ... ] } // response looks like: { "responses": [ [ { "data": {...} }, 201 ], ... ] }
// update: this was probably a temporary bug at wakatime, responses actually looks like so: https://pastr.de/p/nyf6kj2e6843fbw4xkj4h4pj
// TODO: adapt response format some time
func constructSuccessResponse(n int) *heartbeatResponseVm { func constructSuccessResponse(n int) *heartbeatResponseVm {
responses := make([][]interface{}, n) responses := make([][]interface{}, n)
@ -123,3 +166,55 @@ func constructSuccessResponse(n int) *heartbeatResponseVm {
Responses: responses, Responses: responses,
} }
} }
// 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
// @Router /v1/users/{user}/heartbeats [post]
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
// @Router /compat/wakatime/v1/users/{user}/heartbeats [post]
func (h *HeartbeatApiHandler) postAlias2() {}
// @Summary Push new heartbeats
// @ID post-heartbeat-4
// @Tags heartbeat
// @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
// @Security ApiKeyAuth
// @Success 201
// @Router /heartbeats [post]
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
// @Router /v1/users/{user}/heartbeats.bulk [post]
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
// @Router /compat/wakatime/v1/users/{user}/heartbeats.bulk [post]
func (h *HeartbeatApiHandler) postAlias5() {}

View File

@ -160,6 +160,75 @@ var doc = `{
} }
} }
}, },
"/compat/wakatime/v1/users/{user}/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push a new heartbeat",
"operationId": "post-heartbeat-3",
"parameters": [
{
"description": "A single heartbeat",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Heartbeat"
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/compat/wakatime/v1/users/{user}/heartbeats.bulk": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-6",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/compat/wakatime/v1/users/{user}/projects": { "/compat/wakatime/v1/users/{user}/projects": {
"get": { "get": {
"security": [ "security": [
@ -361,7 +430,7 @@ var doc = `{
"operationId": "post-heartbeat", "operationId": "post-heartbeat",
"parameters": [ "parameters": [
{ {
"description": "A heartbeat", "description": "A single heartbeat",
"name": "heartbeat", "name": "heartbeat",
"in": "body", "in": "body",
"required": true, "required": true,
@ -377,6 +446,42 @@ var doc = `{
} }
} }
}, },
"/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-4",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/summary": { "/summary": {
"get": { "get": {
"security": [ "security": [
@ -441,6 +546,75 @@ var doc = `{
} }
} }
} }
},
"/v1/users/{user}/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push a new heartbeat",
"operationId": "post-heartbeat-2",
"parameters": [
{
"description": "A single heartbeat",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Heartbeat"
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/v1/users/{user}/heartbeats.bulk": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-5",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -506,6 +680,12 @@ var doc = `{
"format": "date", "format": "date",
"example": "2006-01-02 15:04:05.000" "example": "2006-01-02 15:04:05.000"
}, },
"labels": {
"type": "array",
"items": {
"$ref": "#/definitions/models.SummaryItem"
}
},
"languages": { "languages": {
"type": "array", "type": "array",
"items": { "items": {

View File

@ -144,6 +144,75 @@
} }
} }
}, },
"/compat/wakatime/v1/users/{user}/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push a new heartbeat",
"operationId": "post-heartbeat-3",
"parameters": [
{
"description": "A single heartbeat",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Heartbeat"
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/compat/wakatime/v1/users/{user}/heartbeats.bulk": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-6",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/compat/wakatime/v1/users/{user}/projects": { "/compat/wakatime/v1/users/{user}/projects": {
"get": { "get": {
"security": [ "security": [
@ -345,7 +414,7 @@
"operationId": "post-heartbeat", "operationId": "post-heartbeat",
"parameters": [ "parameters": [
{ {
"description": "A heartbeat", "description": "A single heartbeat",
"name": "heartbeat", "name": "heartbeat",
"in": "body", "in": "body",
"required": true, "required": true,
@ -361,6 +430,42 @@
} }
} }
}, },
"/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-4",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/summary": { "/summary": {
"get": { "get": {
"security": [ "security": [
@ -425,6 +530,75 @@
} }
} }
} }
},
"/v1/users/{user}/heartbeats": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push a new heartbeat",
"operationId": "post-heartbeat-2",
"parameters": [
{
"description": "A single heartbeat",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Heartbeat"
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
},
"/v1/users/{user}/heartbeats.bulk": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"consumes": [
"application/json"
],
"tags": [
"heartbeat"
],
"summary": "Push new heartbeats",
"operationId": "post-heartbeat-5",
"parameters": [
{
"description": "Multiple heartbeats",
"name": "heartbeat",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Heartbeat"
}
}
}
],
"responses": {
"201": {
"description": ""
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -490,6 +664,12 @@
"format": "date", "format": "date",
"example": "2006-01-02 15:04:05.000" "example": "2006-01-02 15:04:05.000"
}, },
"labels": {
"type": "array",
"items": {
"$ref": "#/definitions/models.SummaryItem"
}
},
"languages": { "languages": {
"type": "array", "type": "array",
"items": { "items": {

View File

@ -43,6 +43,10 @@ definitions:
example: "2006-01-02 15:04:05.000" example: "2006-01-02 15:04:05.000"
format: date format: date
type: string type: string
labels:
items:
$ref: '#/definitions/models.SummaryItem'
type: array
languages: languages:
items: items:
$ref: '#/definitions/models.SummaryItem' $ref: '#/definitions/models.SummaryItem'
@ -408,6 +412,48 @@ paths:
summary: Retrieve summary for all time summary: Retrieve summary for all time
tags: tags:
- wakatime - wakatime
/compat/wakatime/v1/users/{user}/heartbeats:
post:
consumes:
- application/json
operationId: post-heartbeat-3
parameters:
- description: A single heartbeat
in: body
name: heartbeat
required: true
schema:
$ref: '#/definitions/models.Heartbeat'
responses:
"201":
description: ""
security:
- ApiKeyAuth: []
summary: Push a new heartbeat
tags:
- heartbeat
/compat/wakatime/v1/users/{user}/heartbeats.bulk:
post:
consumes:
- application/json
operationId: post-heartbeat-6
parameters:
- description: Multiple heartbeats
in: body
name: heartbeat
required: true
schema:
items:
$ref: '#/definitions/models.Heartbeat'
type: array
responses:
"201":
description: ""
security:
- ApiKeyAuth: []
summary: Push new heartbeats
tags:
- heartbeat
/compat/wakatime/v1/users/{user}/projects: /compat/wakatime/v1/users/{user}/projects:
get: get:
description: Mimics https://wakatime.com/developers#projects description: Mimics https://wakatime.com/developers#projects
@ -540,7 +586,7 @@ paths:
- application/json - application/json
operationId: post-heartbeat operationId: post-heartbeat
parameters: parameters:
- description: A heartbeat - description: A single heartbeat
in: body in: body
name: heartbeat name: heartbeat
required: true required: true
@ -554,6 +600,28 @@ paths:
summary: Push a new heartbeat summary: Push a new heartbeat
tags: tags:
- heartbeat - heartbeat
/heartbeats:
post:
consumes:
- application/json
operationId: post-heartbeat-4
parameters:
- description: Multiple heartbeats
in: body
name: heartbeat
required: true
schema:
items:
$ref: '#/definitions/models.Heartbeat'
type: array
responses:
"201":
description: ""
security:
- ApiKeyAuth: []
summary: Push new heartbeats
tags:
- heartbeat
/summary: /summary:
get: get:
operationId: get-summary operationId: get-summary
@ -599,6 +667,48 @@ paths:
summary: Retrieve a summary summary: Retrieve a summary
tags: tags:
- summary - summary
/v1/users/{user}/heartbeats:
post:
consumes:
- application/json
operationId: post-heartbeat-2
parameters:
- description: A single heartbeat
in: body
name: heartbeat
required: true
schema:
$ref: '#/definitions/models.Heartbeat'
responses:
"201":
description: ""
security:
- ApiKeyAuth: []
summary: Push a new heartbeat
tags:
- heartbeat
/v1/users/{user}/heartbeats.bulk:
post:
consumes:
- application/json
operationId: post-heartbeat-5
parameters:
- description: Multiple heartbeats
in: body
name: heartbeat
required: true
schema:
items:
$ref: '#/definitions/models.Heartbeat'
type: array
responses:
"201":
description: ""
security:
- ApiKeyAuth: []
summary: Push new heartbeats
tags:
- heartbeat
securityDefinitions: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
in: header in: header

View File

@ -1 +1 @@
1.28.2 1.28.3