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" `
2022-04-03 19:02:21 +03:00
UserID string ` json:"-" gorm:"not null; index:idx_time_user; index:idx_user_project" ` // idx_user_project is for quickly fetching a user's project list (settings page)
2022-03-13 10:56:35 +03:00
Entity string ` json:"entity" gorm:"not null" `
2022-09-30 16:28:11 +03:00
Type string ` json:"type" gorm:"size:255" `
Category string ` json:"category" gorm:"size:255" `
2022-04-03 19:02:21 +03:00
Project string ` json:"project" gorm:"index:idx_project; index:idx_user_project" `
2022-03-13 10:17:50 +03:00
Branch string ` json:"branch" gorm:"index:idx_branch" `
2021-02-21 15:02:11 +03:00
Language string ` json:"language" gorm:"index:idx_language" `
IsWrite bool ` json:"is_write" `
2022-03-13 10:17:50 +03:00
Editor string ` json:"editor" gorm:"index:idx_editor" hash:"ignore" ` // ignored because editor might be parsed differently by wakatime
OperatingSystem string ` json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore" ` // ignored because os might be parsed differently by wakatime
Machine string ` json:"machine" gorm:"index:idx_machine" hash:"ignore" ` // ignored because wakatime api doesn't return machines currently
UserAgent string ` json:"user_agent" hash:"ignore" gorm:"type:varchar(255)" `
2022-10-28 10:54:11 +03:00
Time CustomTime ` json:"time" gorm:"type:timestamp(3); index:idx_time; index:idx_time_user" swaggertype:"primitive,number" `
2021-02-21 15:02:11 +03:00
Hash string ` json:"-" gorm:"type:varchar(17); uniqueIndex" `
2022-03-13 10:17:50 +03:00
Origin string ` json:"-" hash:"ignore" gorm:"type:varchar(255)" `
OriginId string ` json:"-" hash:"ignore" gorm:"type:varchar(255)" `
2022-03-18 15:41:32 +03:00
CreatedAt CustomTime ` json:"created_at" gorm:"type:timestamp(3)" 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
}
2022-03-17 13:35:20 +03:00
func ( h * Heartbeat ) Timely ( maxAge time . Duration ) bool {
now := time . Now ( )
2022-03-19 11:02:15 +03:00
return now . Sub ( h . Time . T ( ) ) <= maxAge && h . Time . T ( ) . Sub ( now ) < 1 * time . Hour
2022-03-17 13:35:20 +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 {
2022-02-17 14:20:22 +03:00
logbuch . Error ( "CRITICAL ERROR: failed to hash struct - %v" , err )
2021-01-31 19:46:50 +03:00
}
h . Hash = fmt . Sprintf ( "%x" , hash ) // "uint64 values with high bit set are not supported"
return h
}
2022-03-13 10:17:50 +03:00
func GetEntityColumn ( t uint8 ) string {
return [ ] string {
"project" ,
"language" ,
"editor" ,
"operating_system" ,
"machine" ,
"label" ,
"branch" ,
} [ t ]
}