Files
pedestrian-simulator/server/user.go
Steven Polley 24ecddd034
All checks were successful
pedestrian-simulator / build (push) Successful in 53s
initial commit
2026-01-11 17:16:59 -07:00

157 lines
3.6 KiB
Go

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
}