From 64ec186fa085d60e28b81126e3a4c4cb328d94a9 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 16 Jan 2023 23:36:52 -0800 Subject: [PATCH] new API for client to log activity --- cmd/main-server.go | 35 +++++++++++++++++++ db/migrations/000002_activity.down.sql | 1 + db/migrations/000002_activity.up.sql | 12 +++++++ pkg/scbase/scbase.go | 4 +++ pkg/sstore/dbops.go | 48 ++++++++++++++++++++++++++ pkg/sstore/sstore.go | 22 ++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 db/migrations/000002_activity.down.sql create mode 100644 db/migrations/000002_activity.up.sql diff --git a/cmd/main-server.go b/cmd/main-server.go index c1ab5f00..5f9878a4 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -46,6 +46,12 @@ var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string +type ClientActiveState struct { + Fg bool `json:"fg"` + Active bool `json:"active"` + Open bool `json:"open"` +} + func setWSState(state *scws.WSState) { GlobalLock.Lock() defer GlobalLock.Unlock() @@ -150,6 +156,34 @@ func HandleSetWinSize(w http.ResponseWriter, r *http.Request) { return } +// params: fg, active, open +func HandleLogActiveState(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var activeState ClientActiveState + err := decoder.Decode(&activeState) + if err != nil { + WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) + return + } + activity := sstore.ActivityUpdate{} + if activeState.Fg { + activity.FgMinutes = 1 + } + if activeState.Active { + activity.ActiveMinutes = 1 + } + if activeState.Open { + activity.OpenMinutes = 1 + } + err = sstore.UpdateCurrentActivity(r.Context(), activity) + if err != nil { + WriteJsonError(w, fmt.Errorf("error updating activity: %w", err)) + return + } + WriteJsonSuccess(w, true) + return +} + // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() @@ -461,6 +495,7 @@ func main() { gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) + gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState)) serverAddr := MainServerAddr if scbase.IsDevMode() { serverAddr = MainServerDevAddr diff --git a/db/migrations/000002_activity.down.sql b/db/migrations/000002_activity.down.sql new file mode 100644 index 00000000..6a508d33 --- /dev/null +++ b/db/migrations/000002_activity.down.sql @@ -0,0 +1 @@ +DROP TABLE activity; diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql new file mode 100644 index 00000000..c2ccd32e --- /dev/null +++ b/db/migrations/000002_activity.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE activity ( + day varchar(20) PRIMARY KEY, + uploaded boolean NOT NULL, + numlines int NOT NULL, + activeminutes int NOT NULL, + fgminutes int NOT NULL, + openminutes int NOT NULL, + tzname varchar(50) NOT NULL, + tzoffset int NOT NULL, + clientversion varchar(20) NOT NULL, + clientarch varchar(20) NOT NULL +); diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index cc278916..5a25af57 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -318,3 +318,7 @@ func NumFormatB2(num int64) string { return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "G" } } + +func ClientArch() string { + return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a665741a..d02a5301 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1837,3 +1837,51 @@ func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([] } return rtn, nil } + +func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { + now := time.Now() + dayStr := now.Format("2006-01-02") + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT day FROM activity WHERE day = ?` + if !tx.Exists(query, dayStr) { + query = `INSERT INTO activity (day, uploaded, numlines, fgminutes, activeminutes, openminutes, tzname, tzoffset, clientversion, clientarch) + VALUES (?, 0, 0, 0, 0, 0, ?, ?, ?, ?)` + tzName, tzOffset := now.Zone() + if len(tzName) > MaxTzNameLen { + tzName = tzName[0:MaxTzNameLen] + } + tx.ExecWrap(query, dayStr, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) + } + query = `UPDATE activity SET numlines = numlines + ?, fgminutes = fgminutes + ?, activeminutes = activeminutes + ?, openminutes = openminutes + ? WHERE day = ?` + tx.ExecWrap(query, update.NumLines, update.FgMinutes, update.ActiveMinutes, update.OpenMinutes, dayStr) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) { + var rtn []*ActivityType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM activity WHERE uploaded = 0 ORDER BY day DESC LIMIT 30` + tx.SelectWrap(&rtn, query) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + +func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE activity SET uploaded = 1 WHERE day = ?` + for _, activity := range activityArr { + tx.ExecWrap(query, activity.Day) + } + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c6493a6c..ecea27bb 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -70,6 +70,8 @@ const ( SWFocusCmdFg = "cmd-fg" ) +const MaxTzNameLen = 50 + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error @@ -110,6 +112,26 @@ type ClientWinSizeType struct { FullScreen bool `json:"fullscreen,omitempty"` } +type ActivityUpdate struct { + FgMinutes int + ActiveMinutes int + OpenMinutes int + NumLines int +} + +type ActivityType struct { + Day string `json:"day"` + Uploaded bool `json:"-"` + NumLines int `json:"numlines"` + ActiveMinutes int `json:"activeminutes"` + FgMinutes int `json:"fgminutes"` + OpenMinutes int `json:"openminutes"` + TzName string `json:"tzname"` + TzOffset int `json:"tzoffset"` + ClientVersion string `json:"clientversion"` + ClientArch string `json:"clientarch"` +} + type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"`