package main import ( "encoding/json" "fmt" "net/http" "os" "sync" "time" ) type User struct { FitbitUserID string `json:"fitbit_user_id"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` } type UserRegistry struct { Users map[string]*User `json:"users"` // Map of FitbitUserID -> User mu sync.RWMutex } var userRegistry *UserRegistry // InitUserRegistry loads or creates the user registry func InitUserRegistry() { userRegistry = &UserRegistry{ Users: make(map[string]*User), } userRegistry.Load() } // Load reads the user registry from disk func (ur *UserRegistry) Load() error { ur.mu.Lock() defer ur.mu.Unlock() data, err := os.ReadFile("data/users.json") if err != nil { if os.IsNotExist(err) { return nil // First run, no users yet } return err } return json.Unmarshal(data, ur) } // Save writes the user registry to disk func (ur *UserRegistry) Save() error { ur.mu.RLock() defer ur.mu.RUnlock() return ur.saveUnlocked() } // saveUnlocked writes the user registry to disk without locking (caller must hold lock) func (ur *UserRegistry) saveUnlocked() error { if err := os.MkdirAll("data", 0755); err != nil { return err } data, err := json.MarshalIndent(ur, "", " ") if err != nil { return err } return os.WriteFile("data/users.json", data, 0644) } // GetUser retrieves a user by Fitbit user ID func (ur *UserRegistry) GetUser(fitbitUserID string) (*User, bool) { ur.mu.RLock() defer ur.mu.RUnlock() user, exists := ur.Users[fitbitUserID] return user, exists } // CreateOrUpdateUser adds or updates a user in the registry func (ur *UserRegistry) CreateOrUpdateUser(fitbitUserID, displayName, avatarURL string) (*User, error) { ur.mu.Lock() defer ur.mu.Unlock() user, exists := ur.Users[fitbitUserID] if exists { // Update existing user user.DisplayName = displayName user.AvatarURL = avatarURL } else { // Create new user user = &User{ FitbitUserID: fitbitUserID, DisplayName: displayName, AvatarURL: avatarURL, CreatedAt: time.Now(), } ur.Users[fitbitUserID] = user } // Save without locking (we already have the lock) if err := ur.saveUnlocked(); err != nil { return nil, err } // Create user directory userDir := fmt.Sprintf("data/users/%s", fitbitUserID) if err := os.MkdirAll(userDir, 0755); err != nil { return nil, err } kmlDir := fmt.Sprintf("data/users/%s/kml", fitbitUserID) if err := os.MkdirAll(kmlDir, 0755); err != nil { return nil, err } return user, nil } // FetchFitbitUserProfile fetches the user's profile from Fitbit API func FetchFitbitUserProfile(accessToken string) (userID, displayName, avatarURL string, err error) { apiURL := "https://api.fitbit.com/1/user/-/profile.json" req, _ := http.NewRequest("GET", apiURL, nil) req.Header.Set("Authorization", "Bearer "+accessToken) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return "", "", "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", "", "", fmt.Errorf("fitbit profile api error: %s", resp.Status) } var result struct { User struct { EncodedID string `json:"encodedId"` DisplayName string `json:"displayName"` Avatar string `json:"avatar"` Avatar150 string `json:"avatar150"` } `json:"user"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", "", "", err } avatar := result.User.Avatar150 if avatar == "" { avatar = result.User.Avatar } return result.User.EncodedID, result.User.DisplayName, avatar, nil }