2019-05-05 23:36:49 +03:00
|
|
|
|
package models
|
|
|
|
|
|
|
|
|
|
import (
|
2021-01-31 19:46:50 +03:00
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/emvi/logbuch"
|
|
|
|
|
"github.com/mitchellh/hashstructure/v2"
|
2021-03-24 21:25:36 +03:00
|
|
|
|
"strings"
|
2019-05-05 23:36:49 +03:00
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Heartbeat struct {
|
2021-12-15 12:50:16 +03:00
|
|
|
|
ID uint64 `gorm:"primary_key" hash:"ignore"`
|
2021-02-21 15:02:11 +03:00
|
|
|
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
|
|
|
|
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
|
|
|
|
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Category string `json:"category"`
|
|
|
|
|
Project string `json:"project"`
|
|
|
|
|
Branch string `json:"branch"`
|
|
|
|
|
Language string `json:"language" gorm:"index:idx_language"`
|
|
|
|
|
IsWrite bool `json:"is_write"`
|
|
|
|
|
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
|
|
|
|
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
|
|
|
|
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
2021-08-29 11:54:00 +03:00
|
|
|
|
UserAgent string `json:"user_agent" hash:"ignore"`
|
2021-02-21 15:02:11 +03:00
|
|
|
|
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
|
|
|
|
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
|
|
|
|
Origin string `json:"-" hash:"ignore"`
|
|
|
|
|
OriginId string `json:"-" hash:"ignore"`
|
2021-03-05 23:39:21 +03:00
|
|
|
|
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
2019-05-11 18:49:56 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *Heartbeat) Valid() bool {
|
2020-11-08 14:46:12 +03:00
|
|
|
|
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
|
2019-05-05 23:36:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 22:14:10 +03:00
|
|
|
|
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
2021-04-14 00:39:31 +03:00
|
|
|
|
maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence
|
2021-03-24 21:25:36 +03:00
|
|
|
|
for ending, value := range languageMappings {
|
2021-04-14 00:39:31 +03:00
|
|
|
|
if ok, prec := strings.HasSuffix(h.Entity, "."+ending), strings.Count(ending, "."); ok && prec > maxPrec {
|
2021-03-24 21:25:36 +03:00
|
|
|
|
h.Language = value
|
2021-04-14 00:39:31 +03:00
|
|
|
|
maxPrec = prec
|
2021-03-24 21:25:36 +03:00
|
|
|
|
}
|
2019-05-21 18:16:46 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-07 14:01:35 +03:00
|
|
|
|
|
|
|
|
|
func (h *Heartbeat) GetKey(t uint8) (key string) {
|
|
|
|
|
switch t {
|
|
|
|
|
case SummaryProject:
|
|
|
|
|
key = h.Project
|
|
|
|
|
case SummaryEditor:
|
|
|
|
|
key = h.Editor
|
|
|
|
|
case SummaryLanguage:
|
|
|
|
|
key = h.Language
|
|
|
|
|
case SummaryOS:
|
|
|
|
|
key = h.OperatingSystem
|
|
|
|
|
case SummaryMachine:
|
|
|
|
|
key = h.Machine
|
2022-01-02 15:39:20 +03:00
|
|
|
|
case SummaryBranch:
|
|
|
|
|
key = h.Branch
|
2020-11-07 14:01:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
key = UnknownSummaryKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return key
|
|
|
|
|
}
|
2021-01-31 19:46:50 +03:00
|
|
|
|
|
2021-01-31 20:06:20 +03:00
|
|
|
|
func (h *Heartbeat) String() string {
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
"Heartbeat {user=%s, entity=%s, type=%s, category=%s, project=%s, branch=%s, language=%s, iswrite=%v, editor=%s, os=%s, machine=%s, time=%d}",
|
|
|
|
|
h.UserID,
|
|
|
|
|
h.Entity,
|
|
|
|
|
h.Type,
|
|
|
|
|
h.Category,
|
|
|
|
|
h.Project,
|
|
|
|
|
h.Branch,
|
|
|
|
|
h.Language,
|
|
|
|
|
h.IsWrite,
|
|
|
|
|
h.Editor,
|
|
|
|
|
h.OperatingSystem,
|
|
|
|
|
h.Machine,
|
|
|
|
|
(time.Time(h.Time)).UnixNano(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-31 19:46:50 +03:00
|
|
|
|
// Hash is used to prevent duplicate heartbeats
|
|
|
|
|
// Using a UNIQUE INDEX over all relevant columns would be more straightforward,
|
|
|
|
|
// whereas manually computing this kind of hash is quite cumbersome. However,
|
|
|
|
|
// such a unique index would, according to https://stackoverflow.com/q/65980064/3112139,
|
|
|
|
|
// essentially double the space required for heartbeats, so we decided to go this way.
|
|
|
|
|
|
|
|
|
|
func (h *Heartbeat) Hashed() *Heartbeat {
|
2021-01-31 20:29:50 +03:00
|
|
|
|
hash, err := hashstructure.Hash(h, hashstructure.FormatV2, nil)
|
2021-01-31 19:46:50 +03:00
|
|
|
|
if err != nil {
|
|
|
|
|
logbuch.Error("CRITICAL ERROR: failed to hash struct – %v", err)
|
|
|
|
|
}
|
|
|
|
|
h.Hash = fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
|
|
|
|
|
return h
|
|
|
|
|
}
|