From dc6c10155c33033b386665e7882b91be9837f55a Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Fri, 11 Jul 2025 16:07:15 +0300 Subject: Added anonymous_device_analytics option on the client and added cache dir to store things like device ID and TOS hashes (only storing device ID currently) --- internal/client/config/cache.go | 126 +++++++++++++++++++++++++++++++++++++++ internal/client/config/config.go | 60 ++++++++++--------- 2 files changed, 157 insertions(+), 29 deletions(-) create mode 100644 internal/client/config/cache.go (limited to 'internal/client/config') diff --git a/internal/client/config/cache.go b/internal/client/config/cache.go new file mode 100644 index 0000000..a78425b --- /dev/null +++ b/internal/client/config/cache.go @@ -0,0 +1,126 @@ +package config + +import ( + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + + "github.com/kyren223/eko/pkg/assert" +) + +type Cache struct { + // Key is a server URL like "eko.kyren.codes" and value is the base 16 hash + TosHashes map[string]string `json:"tos_hashes"` + DeviceID string `json:"device_id"` +} + +func DefaultCache() Cache { + return Cache{ + TosHashes: map[string]string{}, + DeviceID: "", + } +} + +func VerifyAndFixCache(cache *Cache) error { + if cache.DeviceID == "" { + cache.DeviceID = GenerateDeviceID() + } + return nil +} + +func GenerateDeviceID() string { + // Generate a random 32-byte device ID + // Gurantees uniqueness, so it's not reverseable and it is annonymized + // For legal reasons: this may still be logged on the server, so associated with an IP address + deviceId := [32]byte{} + _, err := rand.Read(deviceId[:]) + assert.NoError(err, "random never fails") + return fmt.Sprintf("%x", deviceId) +} + +var ( + cache = Cache{} + CacheDir string + CacheFile string +) + +func LoadCache() error { + userCacheDir, err := os.UserCacheDir() + if err != nil { + return err + } + + CacheDir = filepath.Join(userCacheDir, "eko") + err = os.MkdirAll(CacheDir, 0o750) + if err != nil { + return err + } + + CacheFile = filepath.Join(CacheDir, "cache.json") + contents, err := os.ReadFile(CacheFile) // #nosec 304 + if errors.Is(err, os.ErrNotExist) { + cache = DefaultCache() + return writeCache() + } + if err != nil { + return err + } + + var rawMap map[string]json.RawMessage + if err := json.Unmarshal(contents, &rawMap); err != nil { + return err + } + + defaultVal := reflect.ValueOf(DefaultCache()) + finalCache := reflect.New(defaultVal.Type()).Elem() + finalCache.Set(defaultVal) + + for i := 0; i < defaultVal.NumField(); i++ { + field := defaultVal.Type().Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + jsonTag = field.Name + } + + rawValue, found := rawMap[jsonTag] + fieldValue := finalCache.Field(i) + if !found || !fieldValue.CanAddr() { + continue + } + + err := json.Unmarshal(rawValue, fieldValue.Addr().Interface()) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", field.Name, err) + } + } + + cache = finalCache.Interface().(Cache) + + err = VerifyAndFixCache(&cache) + if err != nil { + return err + } + + return writeCache() +} + +func writeCache() error { + b, err := json.MarshalIndent(cache, "", " ") + if err != nil { + return err + } + return os.WriteFile(CacheFile, b, 0o600) +} + +func ReadCache() Cache { + return cache +} + +func UseCache(f func(cache *Cache)) error { + f(&cache) + return writeCache() +} diff --git a/internal/client/config/config.go b/internal/client/config/config.go index 4ceea6b..2de0ea4 100644 --- a/internal/client/config/config.go +++ b/internal/client/config/config.go @@ -12,28 +12,30 @@ import ( ) type Config struct { - ServerName string `json:"server_name"` - PrivateKeyPath string `json:"private_key_path"` - InsertModeTabToSpace bool `json:"insert_mode_tab_to_space"` - InsertModeSpacesPerTab uint8 `json:"insert_mode_spaces_per_tab"` - InsecureDebugMode bool `json:"insecure_debug_mode"` - Colors []string `json:"colors"` + ServerName string `json:"server_name"` + PrivateKeyPath string `json:"private_key_path"` + InsertModeTabToSpace bool `json:"insert_mode_tab_to_space"` + InsertModeSpacesPerTab uint8 `json:"insert_mode_spaces_per_tab"` + InsecureDebugMode bool `json:"insecure_debug_mode"` + Colors []string `json:"colors"` + AnonymousDeviceAnalytics bool `json:"anonymous_device_analytics"` } -func Default() Config { +func DefaultConfig() Config { return Config{ - ServerName: "eko.kyren.codes", - PrivateKeyPath: "", - InsertModeTabToSpace: true, - InsertModeSpacesPerTab: 4, - InsecureDebugMode: false, - Colors: nil, + ServerName: "eko.kyren.codes", + PrivateKeyPath: "", + InsertModeTabToSpace: true, + InsertModeSpacesPerTab: 4, + InsecureDebugMode: false, + Colors: nil, + AnonymousDeviceAnalytics: true, } } -func VerifyAndFix(config *Config) error { +func VerifyAndFixConfig(config *Config) error { if config.ServerName == "" { - config.ServerName = Default().ServerName + config.ServerName = DefaultConfig().ServerName } if config.Colors != nil { @@ -58,27 +60,27 @@ func VerifyAndFix(config *Config) error { var ( config = Config{} - Dir string + ConfigDir string ConfigFile string ) -func Load() error { +func LoadConfig() error { userConfigDir, err := os.UserConfigDir() if err != nil { return err } - Dir = filepath.Join(userConfigDir, "eko") - err = os.MkdirAll(Dir, 0o750) + ConfigDir = filepath.Join(userConfigDir, "eko") + err = os.MkdirAll(ConfigDir, 0o750) if err != nil { return err } - ConfigFile = filepath.Join(Dir, "config.json") + ConfigFile = filepath.Join(ConfigDir, "config.json") contents, err := os.ReadFile(ConfigFile) // #nosec 304 if errors.Is(err, os.ErrNotExist) { - config = Default() - return write() + config = DefaultConfig() + return writeConfig() } if err != nil { return err @@ -89,7 +91,7 @@ func Load() error { return err } - defaultVal := reflect.ValueOf(Default()) + defaultVal := reflect.ValueOf(DefaultConfig()) finalConfig := reflect.New(defaultVal.Type()).Elem() finalConfig.Set(defaultVal) @@ -114,15 +116,15 @@ func Load() error { config = finalConfig.Interface().(Config) - err = VerifyAndFix(&config) + err = VerifyAndFixConfig(&config) if err != nil { return err } - return write() + return writeConfig() } -func write() error { +func writeConfig() error { b, err := json.MarshalIndent(config, "", " ") if err != nil { return err @@ -130,11 +132,11 @@ func write() error { return os.WriteFile(ConfigFile, b, 0o600) } -func Read() Config { +func ReadConfig() Config { return config } -func Use(f func(config *Config)) error { +func UseConfig(f func(config *Config)) error { f(&config) - return write() + return writeConfig() } -- cgit v1.3.1