From f623e03978ea3592fa9911fefdd6ee71ae504558 Mon Sep 17 00:00:00 2001 From: Changkun Ou Date: Sat, 6 Jan 2024 09:04:34 +0100 Subject: [PATCH] all: remove office feature Fixes #8 --- api/daemon/office.go | 131 ------------------- api/daemon/serve.go | 10 +- api/rest/office.go | 73 ----------- api/rest/routers.go | 18 ++- api/rest/serve.go | 11 +- api/rest/subscribe.go | 6 - go.mod | 2 +- go.sum | 2 + internal/office/meeting_darwin.go | 24 ---- internal/office/meeting_darwin.m | 177 -------------------------- internal/office/meeting_linux.go | 59 --------- internal/office/meeting_test.go | 33 ----- internal/office/meeting_windows.go | 15 --- internal/office/status.go | 198 ----------------------------- internal/types/office.go | 23 ---- internal/types/ws.go | 20 ++- 16 files changed, 23 insertions(+), 779 deletions(-) delete mode 100644 api/daemon/office.go delete mode 100644 api/rest/office.go delete mode 100644 internal/office/meeting_darwin.go delete mode 100644 internal/office/meeting_darwin.m delete mode 100644 internal/office/meeting_linux.go delete mode 100644 internal/office/meeting_test.go delete mode 100644 internal/office/meeting_windows.go delete mode 100644 internal/office/status.go delete mode 100644 internal/types/office.go diff --git a/api/daemon/office.go b/api/daemon/office.go deleted file mode 100644 index ce8b172..0000000 --- a/api/daemon/office.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package daemon - -import ( - "context" - "encoding/json" - "log" - "time" - - "changkun.de/x/midgard/internal/office" - "changkun.de/x/midgard/internal/types" - "changkun.de/x/midgard/internal/utils" -) - -func (m *Daemon) watchOfficeStatus(ctx context.Context) { - tk := time.NewTicker(10 * time.Second) - for { - select { - case <-ctx.Done(): - return - case <-m.forceUpdate: - log.Println("received force status updates") - - readerId, err := utils.NewUUIDShort() - if err != nil { - log.Printf("failed to create readerID: %v", err) - continue - } - readerCh := make(chan *types.WebsocketMessage) - m.readChs.Store(readerId, readerCh) - - b, _ := json.Marshal(&types.OfficeStatusRequest{ - Type: types.OfficeStatusStandard, - Working: m.status.Working, - Meeting: m.status.Meeting, - }) - m.writeCh <- &types.WebsocketMessage{ - Action: types.ActionUpdateOfficeStatusRequest, - UserID: m.ID, - Message: "office status has changed", - Data: b, - } - - resp := <-readerCh - switch resp.Action { - case types.ActionUpdateOfficeStatusResponse: - var data types.OfficeStatusResponse - err := json.Unmarshal(resp.Data, &data) - if err != nil { - log.Printf("failed to parse office status response, there must be a server side error: %v", err) - } - log.Println(data.Message) - m.readChs.Delete(readerId) - default: - // not interested, ingore. - } - - case <-tk.C: - log.Println("monitoring office status") - - // Figure out the current office status - locked, err := office.IsScreenLocked() - if err != nil { - locked = true - } - working := !locked - meeting, err := office.IsInMeeting() - if err != nil { - meeting = false - } - - // Check with local status and see if there are any updates - updated := false - m.status.Lock() - if m.status.Meeting != meeting { - updated = true - m.status.Meeting = meeting - } - if m.status.Working != working { - updated = true - m.status.Working = working - } - m.status.Unlock() - - log.Printf("current status: working: %v, meeting %v", working, meeting) - - // do not communicate with server if there are no updates. - if !updated { - log.Println("office status has no updates.") - continue - } - - readerId, err := utils.NewUUIDShort() - if err != nil { - log.Printf("failed to create readerID: %v", err) - continue - } - readerCh := make(chan *types.WebsocketMessage) - m.readChs.Store(readerId, readerCh) - - b, _ := json.Marshal(&types.OfficeStatusRequest{ - Type: types.OfficeStatusStandard, - Working: m.status.Working, - Meeting: m.status.Meeting, - }) - m.writeCh <- &types.WebsocketMessage{ - Action: types.ActionUpdateOfficeStatusRequest, - UserID: m.ID, - Message: "office status has changed", - Data: b, - } - - resp := <-readerCh - switch resp.Action { - case types.ActionUpdateOfficeStatusResponse: - var data types.OfficeStatusResponse - err := json.Unmarshal(resp.Data, &data) - if err != nil { - log.Printf("failed to parse office status response, there must be a server side error: %v", err) - } - log.Println(data.Message) - m.readChs.Delete(readerId) - default: - // not interested, ingore. - } - } - } -} diff --git a/api/daemon/serve.go b/api/daemon/serve.go index ea8a26b..4722d9c 100644 --- a/api/daemon/serve.go +++ b/api/daemon/serve.go @@ -14,7 +14,6 @@ import ( "time" "changkun.de/x/midgard/internal/config" - "changkun.de/x/midgard/internal/office" "changkun.de/x/midgard/internal/types" "changkun.de/x/midgard/internal/types/proto" "changkun.de/x/midgard/internal/utils" @@ -27,7 +26,6 @@ type Daemon struct { sync.Mutex ID string - status office.Status forceUpdate chan struct{} s *grpc.Server ws *websocket.Conn @@ -64,7 +62,7 @@ func (m *Daemon) Run(ctx context.Context) (onStart, onStop func() error) { defer wg.Done() m.Serve(ctx) } - + onStart = func() error { wg.Add(1) go run() @@ -104,12 +102,6 @@ func (m *Daemon) Serve(ctx context.Context) { m.watchLocalClipboard(ctx) }() wg.Add(1) - go func() { - defer wg.Done() - defer log.Println("office watcher is terminated.") - m.watchOfficeStatus(ctx) - }() - wg.Add(1) go func() { defer wg.Done() defer log.Println("rpc server is terminated.") diff --git a/api/rest/office.go b/api/rest/office.go deleted file mode 100644 index b3a4353..0000000 --- a/api/rest/office.go +++ /dev/null @@ -1,73 +0,0 @@ -package rest - -import ( - "context" - "encoding/json" - "log" - "net/http" - "time" - - "changkun.de/x/midgard/internal/office" - "changkun.de/x/midgard/internal/types" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" -) - -// Office returns the reported office status -func (m *Midgard) Office(c *gin.Context) { - c.Header("Access-Control-Allow-Headers", "*") - c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Methods", "GET") - if c.Query("format") == "plain" { - c.String(http.StatusOK, m.status.String()) - } else { - c.Header("Content-Type", "text/html") - c.String(http.StatusOK, m.status.HTML()) - } -} - -func (m *Midgard) refreshStatus(ctx context.Context) { - tk := time.NewTicker(10 * time.Second) - for { - select { - case <-tk.C: - m.mu.Lock() - // 1. No devices is connected to midgard, meaning offline - if m.users.Len() == 0 { - m.status.Update(office.Working(false)) - } - // 2. If there are devices, waiting for them to report - // current status, midgard server don't do anything. - m.mu.Unlock() - case <-ctx.Done(): - log.Println("status updater is terminated.") - return - } - } -} - -// handleOfficeStatusUpdate handles the update request from daemon. -func (m *Midgard) handleOfficeStatusUpdate(conn *websocket.Conn, u *user, data []byte) (err error) { - m.mu.Lock() - defer m.mu.Unlock() - defer func() { - if err != nil { - err = terminate(conn, err) - } - }() - - var s types.OfficeStatusRequest - err = json.Unmarshal(data, &s) - if err != nil { - return - } - - m.status.Update(office.Working(s.Working), office.Meeting(s.Meeting)) - b, _ := json.Marshal(&types.OfficeStatusResponse{ - Message: "Office status is updated.", - }) - return conn.WriteMessage(websocket.BinaryMessage, (&types.WebsocketMessage{ - Action: types.ActionUpdateOfficeStatusResponse, - Data: b, - }).Encode()) -} diff --git a/api/rest/routers.go b/api/rest/routers.go index 393d198..a354958 100644 --- a/api/rest/routers.go +++ b/api/rest/routers.go @@ -26,7 +26,6 @@ func (m *Midgard) routers() (r *gin.Engine) { mg := r.Group("/midgard") mg.GET("/ping", m.PingPong) mg.GET("/code", m.Code) - mg.GET("/office", m.Office) v1auth := mg.Group("/api/v1", BasicAuthWithAttemptsControl(Credentials{ config.S().Auth.User: config.S().Auth.Pass, @@ -75,15 +74,14 @@ func FixPath(p string) string { // // Basic Usage: // -// - use the pprof tool to look at the heap profile: -// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/heap -// - look at a 30-second CPU profile: -// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/profile -// - look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate: -// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/block -// - collect a 5-second execution trace: -// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/trace?seconds=5 -// +// - use the pprof tool to look at the heap profile: +// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/heap +// - look at a 30-second CPU profile: +// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/profile +// - look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate: +// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/block +// - collect a 5-second execution trace: +// go tool pprof localhost:8080/midgard/api/v1/debug/pprof/trace?seconds=5 func profile(r *gin.RouterGroup) { pprofHandler := func(h http.HandlerFunc) gin.HandlerFunc { handler := http.HandlerFunc(h) diff --git a/api/rest/serve.go b/api/rest/serve.go index 1ededd2..19dca6a 100644 --- a/api/rest/serve.go +++ b/api/rest/serve.go @@ -21,14 +21,12 @@ import ( "time" "changkun.de/x/midgard/internal/config" - "changkun.de/x/midgard/internal/office" "changkun.de/x/midgard/internal/utils" ) // Midgard is the midgard server that serves all API endpoints. type Midgard struct { - s *http.Server - status *office.Status + s *http.Server mu sync.Mutex users *list.List @@ -36,7 +34,7 @@ type Midgard struct { // NewMidgard creates a new midgard server func NewMidgard() *Midgard { - return &Midgard{status: office.NewStatus(), users: list.New()} + return &Midgard{users: list.New()} } // Serve serves Midgard RESTful APIs. @@ -61,11 +59,6 @@ func (m *Midgard) Serve() { } }() wg.Add(1) - go func() { - defer wg.Done() - m.refreshStatus(ctx) - }() - wg.Add(1) go func() { defer wg.Done() m.serveHTTP() diff --git a/api/rest/subscribe.go b/api/rest/subscribe.go index 8bea185..32be2e3 100644 --- a/api/rest/subscribe.go +++ b/api/rest/subscribe.go @@ -162,12 +162,6 @@ func (m *Midgard) Subscribe(c *gin.Context) { if err != nil { log.Println("failed to list daemons:", err) } - case types.ActionUpdateOfficeStatusRequest: - log.Println("update office status request is received.") - err := m.handleOfficeStatusUpdate(conn, u, wsm.Data) - if err != nil { - log.Printf("failed to update office status: %v", err) - } default: log.Printf("unsupported message: action(%v), msg(%v)", wsm.Action, utils.BytesToString(msg)) } diff --git a/go.mod b/go.mod index 845f3c4..ac61f96 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module changkun.de/x/midgard -go 1.20 +go 1.21 require ( github.com/gin-gonic/gin v1.9.1 diff --git a/go.sum b/go.sum index a1a8f22..09afa58 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -80,6 +81,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= diff --git a/internal/office/meeting_darwin.go b/internal/office/meeting_darwin.go deleted file mode 100644 index 0fb90c3..0000000 --- a/internal/office/meeting_darwin.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package office - -/* -#cgo CFLAGS: -DGLES_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c - -#include - -bool isScreenLocked(); -bool isCameraOn(); -*/ -import "C" - -func IsInMeeting() (bool, error) { - return bool(C.isCameraOn()), nil -} - -// IsScreenLocked returns true if the screen is locked, and false otherwise. -func IsScreenLocked() (bool, error) { - return bool(C.isScreenLocked()), nil -} diff --git a/internal/office/meeting_darwin.m b/internal/office/meeting_darwin.m deleted file mode 100644 index e7f8806..0000000 --- a/internal/office/meeting_darwin.m +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -@import Quartz; -@import Foundation; -@import AVFoundation; -@import CoreMediaIO; - -bool isScreenLocked() { - @autoreleasepool { - bool locked = false; - CFDictionaryRef d = CGSessionCopyCurrentDictionary(); - id o = ((__bridge NSDictionary *) d)[@"CGSSessionScreenIsLocked"]; - if (o) { - locked = [o boolValue]; - } - CFRelease(d); - return locked; - } -} - -OSStatus getVideoDeviceUID(CMIOObjectID device, NSString **uid) { - @autoreleasepool { - OSStatus err; - UInt32 dataSize = 0; - UInt32 dataUsed = 0; - - CMIOObjectPropertyAddress opa = { - kCMIODevicePropertyDeviceUID, - kCMIOObjectPropertyScopeWildcard, - kCMIOObjectPropertyElementWildcard - }; - - err = CMIOObjectGetPropertyDataSize(device, - &opa, 0, nil, &dataSize); - if (err != kCMIOHardwareNoError) { - return err; - } - - CFStringRef uidStringRef = NULL; - err = CMIOObjectGetPropertyData(device, - &opa, 0, nil, dataSize, &dataUsed, &uidStringRef); - if (err != kCMIOHardwareNoError) { - return err; - } - - *uid = (__bridge NSString *)uidStringRef; - return err; - } -} - -bool isIgnoredDeviceUID(NSString *uid) { - @autoreleasepool { - // OBS virtual device always returns "is used" even when OBS is - // not running - if ([uid isEqual:@"obs-virtual-cam-device"]) { - return true; - } - return false; - } -} - -OSStatus getVideoDeviceIsUsed(CMIOObjectID device, int *isUsed) { - @autoreleasepool { - OSStatus err; - UInt32 dataSize = 0; - UInt32 dataUsed = 0; - - CMIOObjectPropertyAddress prop = { - kCMIODevicePropertyDeviceIsRunningSomewhere, - kCMIOObjectPropertyScopeWildcard, - kCMIOObjectPropertyElementWildcard - }; - - err = CMIOObjectGetPropertyDataSize(device, - &prop, 0, nil, &dataSize); - if (err != kCMIOHardwareNoError) { - return err; - } - - err = CMIOObjectGetPropertyData(device, - &prop, 0, nil, dataSize, &dataUsed, isUsed); - if (err != kCMIOHardwareNoError) { - return err; - } - return err; - } -} - -// isCameraOn is a best effort camera on detection. It may report false positive. -bool isCameraOn() { - @autoreleasepool { - bool on = false; - CMIOObjectPropertyAddress propertyAddress = { - kCMIOHardwarePropertyDevices, - kCMIOObjectPropertyScopeGlobal, - kCMIOObjectPropertyElementMaster // This should be kCMIOObjectPropertyElementMain after macOS 12.0 - }; - UInt32 dataSize = 0; - OSStatus status = CMIOObjectGetPropertyDataSize( - kCMIOObjectSystemObject, &propertyAddress, 0, NULL, &dataSize); - if(status != kCMIOHardwareNoError) { - return false; - } - UInt32 deviceCount = (UInt32)(dataSize/sizeof(CMIOObjectID)); - CMIOObjectID *videoDevices = (CMIOObjectID *)(calloc(dataSize,1)); - if(NULL == videoDevices) { - return false; - } - - UInt32 used = 0; - status = CMIOObjectGetPropertyData(kCMIOObjectSystemObject, - &propertyAddress, 0, NULL, dataSize, &used, videoDevices); - if(status != kCMIOHardwareNoError) { - free(videoDevices); - videoDevices = NULL; - return false; - } - - OSStatus err; - int usedDevices = 0; - int failedDeviceCount = 0; - int ignoredDeviceCount = 0; - for(UInt32 i = 0; i < deviceCount; ++i) { - CMIOObjectID device = videoDevices[i]; - - NSString *uid; - err = getVideoDeviceUID(device, &uid); - if (err) { - failedDeviceCount++; - continue; - } - - if (isIgnoredDeviceUID(uid)) { - ignoredDeviceCount++; - continue; - } - - CFStringRef deviceName = NULL; - dataSize = sizeof(deviceName); - propertyAddress.mSelector = kCMIOObjectPropertyName; - status = CMIOObjectGetPropertyData(videoDevices[i], - &propertyAddress, 0, NULL, dataSize, &used, &deviceName); - if(status != kCMIOHardwareNoError) { - continue; - } - - - int isDeviceUsed = 0; - err = getVideoDeviceIsUsed(device, &isDeviceUsed); - if (err) { - failedDeviceCount++; - continue; - } - - if (isDeviceUsed != 0) { - usedDevices++; - } - } - if (failedDeviceCount == deviceCount) { - free(videoDevices); - videoDevices = NULL; - return false; - } - if (usedDevices < 1) { - free(videoDevices); - videoDevices = NULL; - return false; - } - - free(videoDevices); - videoDevices = NULL; - return true; - } -} - diff --git a/internal/office/meeting_linux.go b/internal/office/meeting_linux.go deleted file mode 100644 index ff32926..0000000 --- a/internal/office/meeting_linux.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020-2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package office - -import ( - "bytes" - "fmt" - "os/exec" - "regexp" - "strings" -) - -func IsInMeeting() (bool, error) { - // Do this command: - // $ lsmod | grep uvcvideo - // uvcvideo 98304 0 # if camera is off - // uvcvideo 98304 1 # if camera is on - - cmd := exec.Command("lsmod") - b, err := cmd.CombinedOutput() - if err != nil { - return false, err - } - re := regexp.MustCompile(`uvcvideo(.*)`) - match := re.FindStringSubmatch(string(b)) - - if len(match) < 1 { - return false, nil - } - matches := strings.Fields(match[1]) - if len(matches) < 1 { - return false, nil - } - - return matches[1] == "1", nil -} - -func IsScreenLocked() (bool, error) { - var ( - out bytes.Buffer - outErr bytes.Buffer - ) - - // Do this command: - // gnome-screensaver-command -q | grep "is active" - cmd := exec.Command("gnome-screensaver-command", "-q") - cmd.Stdout = &out - cmd.Stderr = &outErr - err := cmd.Run() - if err != nil { - return false, fmt.Errorf("%w: %v", err, outErr.String()) - } - if !strings.Contains(out.String(), "is active") { - return false, nil - } - return true, nil -} diff --git a/internal/office/meeting_test.go b/internal/office/meeting_test.go deleted file mode 100644 index c1688fa..0000000 --- a/internal/office/meeting_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package office_test - -import ( - "testing" - - "changkun.de/x/midgard/internal/office" -) - -func TestIsScreenLocked(t *testing.T) { - locked, err := office.IsScreenLocked() - if err != nil { - t.Fatalf("check screen status failed: %v", err) - } - - if locked { - t.Fatalf("check screen status failed: screen is locked") - } -} - -func TestIsInMeeting(t *testing.T) { - meeting, err := office.IsInMeeting() - if err != nil { - t.Fatalf("check meeting status failed: %v", err) - } - - if meeting { - t.Fatalf("check meeting status failed: currently in a meeting") - } -} diff --git a/internal/office/meeting_windows.go b/internal/office/meeting_windows.go deleted file mode 100644 index 17d19a0..0000000 --- a/internal/office/meeting_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2020-2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package office - -func IsInMeeting() (bool, error) { - // TODO: - return false, nil -} - -func IsScreenLocked() (bool, error) { - // TODO: - return false, nil -} diff --git a/internal/office/status.go b/internal/office/status.go deleted file mode 100644 index 7487cfa..0000000 --- a/internal/office/status.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package office - -import ( - "fmt" - "sync" - "time" - - "changkun.de/x/midgard/internal/config" -) - -type StatusType = int32 - -const ( - StatusUnknown StatusType = iota - StatusOn - StatusOff - StatusVacation -) - -type Status struct { - sync.RWMutex - - System string `json:"os"` - Working bool `json:"working"` - Meeting bool `json:"meeting"` - - Status StatusType `json:"-"` - LastAvailable time.Time `json:"-"` // when did he leave? - - // TODO: predict return time. - EstimateReturn time.Time `json:"-"` // when will he be back? -} - -func NewStatus() *Status { - s := &Status{ - Status: StatusUnknown, - LastAvailable: time.Now(), - EstimateReturn: time.Now().Add(time.Hour), // default back in an hour. - } - return s -} - -var ( - unknownMessage = "Sorry, I can't tell you at the moment." - htmlTmpl = `
Is %s in the office? -

%s

-
` -) - -func (s *Status) HTML() string { - s.RLock() - defer s.RUnlock() - - var message string - - title := config.Get().Title - - // now update display message - switch s.Status { - case StatusUnknown: - message = fmt.Sprintf(htmlTmpl, title, "gray", unknownMessage) - case StatusOn: - if !s.Meeting { - message = fmt.Sprintf(htmlTmpl, title, "green", "Yes!") - } else { - message = fmt.Sprintf(htmlTmpl, title, "brown", "Yes! But current in a meeting.") - } - case StatusOff: - message = fmt.Sprintf(htmlTmpl, title, "tomato", - fmt.Sprintf("No, he left %s ago.", time.Since(s.LastAvailable).Round(time.Second))) - case StatusVacation: - if time.Since(s.EstimateReturn) > 0 { - // date invalid - message = fmt.Sprintf(htmlTmpl, title, "tomato", - "No, he is on vacation and will return soon.") - } else { - var date string - if s.EstimateReturn.Year() == time.Now().Year() { // same year - date = s.EstimateReturn.Format("02 Jan") - } else { - date = s.EstimateReturn.Format("Jan 2, 2006") - } - message = fmt.Sprintf(htmlTmpl, title, "tomato", - fmt.Sprintf("No, he is on vacation and will return at %v", date)) - } - } - - return message -} - -func (s *Status) String() string { - s.RLock() - defer s.RUnlock() - - // TODO: think about what type of status information I'd like to let - // others to know - - // 1. current working machine (which system are you working on?) - // 2. either on a meeting or not (are you in a meeting?) - // 3. be able to update to a custom message (can I update my status using an iOS shortcut?) - // 4. last avaliable time (when did you stopped working?) - // 5. vacation (are you in a long term vacation?) - // 6. anymore? - - var message string - - // now update display message - switch s.Status { - case StatusUnknown: - message = unknownMessage - case StatusOn: - if !s.Meeting { - message = "Yes!" - } else { - message = "Yes! But current in a meeting." - } - case StatusOff: - message = fmt.Sprintf("No, he left %s ago.", time.Since(s.LastAvailable).Round(time.Second)) - case StatusVacation: - if time.Since(s.EstimateReturn) > 0 { - // date invalid - message = "No, he is on vacation and will return soon." - } else { - var date string - if s.EstimateReturn.Year() == time.Now().Year() { // same year - date = s.EstimateReturn.Format("02 Jan") - } else { - date = s.EstimateReturn.Format("Jan 2, 2006") - } - message = fmt.Sprintf("No, he is on vacation and will return at %v", date) - } - } - - return message -} - -type UpdateOption func(s *Status) - -func Working(working bool) func(s *Status) { - return func(s *Status) { - var newStatus StatusType - if working { - newStatus = StatusOn - } else { - newStatus = StatusOff - s.Meeting = false - } - - oldStatus := s.Status - s.Status = newStatus - switch { - case oldStatus == StatusUnknown && newStatus == StatusOn: - case oldStatus == StatusUnknown && newStatus == StatusOff: - s.LastAvailable = time.Now() - case oldStatus == StatusUnknown && newStatus == StatusVacation: - s.LastAvailable = time.Now() - case oldStatus == StatusOn && newStatus == StatusOff: - s.LastAvailable = time.Now() - case oldStatus == StatusOn && newStatus == StatusVacation: - s.LastAvailable = time.Now() - case oldStatus == StatusOn && newStatus == StatusOn: - // nothing - case oldStatus == StatusOff && newStatus == StatusOn: - // nothing - case oldStatus == StatusOff && newStatus == StatusVacation: - // nothing - case oldStatus == StatusOff && newStatus == StatusOff: - // nothing - case oldStatus == StatusVacation && newStatus == StatusOn: - // nothing - case oldStatus == StatusVacation && newStatus == StatusOff: - // nothing - case oldStatus == StatusVacation && newStatus == StatusVacation: - // nothing - default: - // nothing - } - } -} - -func Meeting(meeting bool) func(s *Status) { - return func(s *Status) { - s.Meeting = meeting - } -} - -func (s *Status) Update(opts ...UpdateOption) { - s.Lock() - defer s.Unlock() - - for _, opt := range opts { - opt(s) - } -} diff --git a/internal/types/office.go b/internal/types/office.go deleted file mode 100644 index 575de0d..0000000 --- a/internal/types/office.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 Changkun Ou. All rights reserved. -// Use of this source code is governed by a GPL-3.0 -// license that can be found in the LICENSE file. - -package types - -type OfficeStatusType int - -const ( - OfficeStatusStandard OfficeStatusType = iota - officeStatusCustom -) - -type OfficeStatusRequest struct { - Type OfficeStatusType `json:"type"` - Working bool `json:"working"` - Meeting bool `json:"meeting"` - Message string `json:"message"` -} - -type OfficeStatusResponse struct { - Message string `json:"message"` -} diff --git a/internal/types/ws.go b/internal/types/ws.go index f92927f..2bf8b82 100644 --- a/internal/types/ws.go +++ b/internal/types/ws.go @@ -16,17 +16,15 @@ var ( // All actions from daemons const ( - ActionNone WebsocketAction = "none" - ActionHandshakeRegister = "register" - ActionHandshakeReady = "ready" - ActionClipboardChanged = "cbchanged" - ActionClipboardGet = "cbget" - ActionClipboardPut = "cbput" - ActionListDaemonsRequest = "lsdaemonreq" - ActionListDaemonsResponse = "lsdaemonsres" - ActionUpdateOfficeStatusRequest = "updateofficereq" - ActionUpdateOfficeStatusResponse = "updateofficeres" - ActionTerminate = "terminate" + ActionNone WebsocketAction = "none" + ActionHandshakeRegister = "register" + ActionHandshakeReady = "ready" + ActionClipboardChanged = "cbchanged" + ActionClipboardGet = "cbget" + ActionClipboardPut = "cbput" + ActionListDaemonsRequest = "lsdaemonreq" + ActionListDaemonsResponse = "lsdaemonsres" + ActionTerminate = "terminate" ) // WebsocketAction is an action between midgard daemon and midgard server