diff options
| author | Kyren223 <Kyren223@proton.me> | 2025-07-11 16:07:15 +0300 |
|---|---|---|
| committer | Kyren223 <Kyren223@proton.me> | 2025-07-19 18:32:12 +0300 |
| commit | dc6c10155c33033b386665e7882b91be9837f55a (patch) | |
| tree | 64ca15fdb2aec5a17c93bae99508f70e61b951a6 /internal | |
| parent | 86c3f3f730dd8867f4d5df6fef428df36ff2d193 (diff) | |
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)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/client/client.go | 7 | ||||
| -rw-r--r-- | internal/client/config/cache.go | 126 | ||||
| -rw-r--r-- | internal/client/config/config.go | 60 | ||||
| -rw-r--r-- | internal/client/gateway/gateway.go | 8 | ||||
| -rw-r--r-- | internal/client/ui/auth/auth.go | 4 | ||||
| -rw-r--r-- | internal/client/ui/core/core.go | 40 | ||||
| -rw-r--r-- | internal/client/ui/viminput/viminput.go | 2 |
7 files changed, 195 insertions, 52 deletions
diff --git a/internal/client/client.go b/internal/client/client.go index 0ba3530..57e1bb1 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -37,11 +37,16 @@ func Run() { log.Println("client started") - err := config.Load() + err := config.LoadConfig() if err != nil { fmt.Printf("Config file at '%v' was unable to load successfully\n%v\n", config.ConfigFile, err) os.Exit(1) } + err = config.LoadCache() + if err != nil { + fmt.Printf("Cache file at '%v' was unable to load successfully\n%v\n", config.CacheFile, err) + os.Exit(1) + } program := tea.NewProgram(initialModel(dump), tea.WithAltScreen()) assert.AddFlush(BubbleTeaCloser{program}) 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() } diff --git a/internal/client/gateway/gateway.go b/internal/client/gateway/gateway.go index 20ca777..ef4f31a 100644 --- a/internal/client/gateway/gateway.go +++ b/internal/client/gateway/gateway.go @@ -63,16 +63,16 @@ func connect(ctx context.Context) error { tlsConfig := &tls.Config{ RootCAs: certPool, - ServerName: config.Read().ServerName, + ServerName: config.ReadConfig().ServerName, MinVersion: tls.VersionTLS12, // This is fine, it's always false by default // The user may change the config, the name should be clear enough // that this is insecure (valid use cases are for testing purposes) - InsecureSkipVerify: config.Read().InsecureDebugMode, // #nosec 402 + InsecureSkipVerify: config.ReadConfig().InsecureDebugMode, // #nosec 402 } - address := config.Read().ServerName - if config.Read().InsecureDebugMode { + address := config.ReadConfig().ServerName + if config.ReadConfig().InsecureDebugMode { address = "localhost" } diff --git a/internal/client/ui/auth/auth.go b/internal/client/ui/auth/auth.go index 8e6dffb..47a3f89 100644 --- a/internal/client/ui/auth/auth.go +++ b/internal/client/ui/auth/auth.go @@ -378,7 +378,7 @@ func (m *Model) SetSignup(signup bool) tea.Cmd { m.focusIndex = -1 m.CycleForward() - privateKey := config.Read().PrivateKeyPath + privateKey := config.ReadConfig().PrivateKeyPath if !m.signup && privateKey != "" { m.fields[privateKeyField].Input.SetValue(privateKey) } @@ -544,7 +544,7 @@ func (m *Model) signin() tea.Cmd { } if m.remember { - _ = config.Use(func(config *config.Config) { + _ = config.UseConfig(func(config *config.Config) { config.PrivateKeyPath = privateKeyFilepath }) } diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index 5a0f8db..11772fa 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -5,6 +5,8 @@ import ( "fmt" "log" "math" + "os" + "runtime" "slices" "time" @@ -13,6 +15,7 @@ import ( "github.com/charmbracelet/bubbles/timer" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/kyren223/eko/internal/client/config" "github.com/kyren223/eko/internal/client/gateway" "github.com/kyren223/eko/internal/client/ui" "github.com/kyren223/eko/internal/client/ui/colors" @@ -294,25 +297,11 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd { combined := msg.Tos + "\n" + msg.PrivacyPolicy tos := tosscreen.New(combined) m.tos = &tos - // var setName tea.Cmd - // if m.name != "" { - // setName = gateway.Send(&packet.SetUserData{ - // Data: nil, - // User: &data.User{ - // Name: m.name, - // Description: "", - // IsPublicDM: true, - // }, - // }) - // m.name = "" - // } - // - // return tea.Batch(m.timer.Stop(), setName) case *packet.Error: if m.state == ConnectedAcceptedTos { if msg.Error == "success" { - return gateway.Send(&packet.GetNonce{}) + return tea.Batch(gateway.Send(&packet.GetNonce{}), SendAnalytics()) } else { log.Println("received error:", msg.Error) gateway.Disconnect() @@ -408,6 +397,7 @@ func (m *Model) updateAuthenticated(message tea.Msg) tea.Cmd { case *packet.Error: err := "new connection from another location, closing this one" if err == msg.Error { + gateway.Disconnect() return ui.Transition(ui.NewAuth()) } @@ -945,3 +935,23 @@ func getSignalNotification(signal snowflake.ID) int { func (m Model) ViewTos() string { return "TODO TOS" } + +func SendAnalytics() tea.Cmd { + if !config.ReadConfig().AnonymousDeviceAnalytics { + return nil // User disabled analytics + } + assert.Assert(config.ReadConfig().AnonymousDeviceAnalytics, "who is paranoid") + + osName := runtime.GOOS + arch := runtime.GOARCH + term := os.Getenv("TERM") + colorterm := os.Getenv("COLORTERM") + + return gateway.Send(&packet.DeviceAnalytics{ + DeviceID: config.ReadCache().DeviceID, + OS: osName, + Arch: arch, + Term: term, + Colorterm: colorterm, + }) +} diff --git a/internal/client/ui/viminput/viminput.go b/internal/client/ui/viminput/viminput.go index 6d1f01b..5c4b6b2 100644 --- a/internal/client/ui/viminput/viminput.go +++ b/internal/client/ui/viminput/viminput.go @@ -649,7 +649,7 @@ func (m *Model) handleInsertModeKeys(key tea.KeyMsg) { } if key.Type == tea.KeyTab || key.Type == tea.KeyShiftTab { - conf := config.Read() + conf := config.ReadConfig() var runes []rune if conf.InsertModeTabToSpace { runes = []rune(strings.Repeat(" ", int(conf.InsertModeSpacesPerTab))) |
