package database import ( "context" "encoding/json" "fmt" "io" "net/http" "sync" "time" ) type ExternalAPIClient struct { httpClient *http.Client mutex sync.RWMutex // Configuration timeout time.Duration maxRetries int userAgent string // Rate limiting lastRequest time.Time minInterval time.Duration } type APIClientConfig struct { Timeout time.Duration MaxRetries int UserAgent string MinInterval time.Duration // Minimum interval between requests } type OpenSkyFlightInfo struct { ICAO string `json:"icao"` Callsign string `json:"callsign"` Origin string `json:"origin"` Destination string `json:"destination"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` AircraftType string `json:"aircraft_type"` Registration string `json:"registration"` FlightNumber string `json:"flight_number"` Airline string `json:"airline"` } type APIError struct { Operation string StatusCode int Message string Retryable bool RetryAfter time.Duration } func (e *APIError) Error() string { return fmt.Sprintf("API error in %s: %s (status: %d, retryable: %v)", e.Operation, e.Message, e.StatusCode, e.Retryable) } func NewExternalAPIClient(config APIClientConfig) *ExternalAPIClient { if config.Timeout == 0 { config.Timeout = 10 * time.Second } if config.MaxRetries == 0 { config.MaxRetries = 3 } if config.UserAgent == "" { config.UserAgent = "SkyView-ADSB/1.0 (https://github.com/user/skyview)" } if config.MinInterval == 0 { config.MinInterval = 1 * time.Second // Default rate limit } return &ExternalAPIClient{ httpClient: &http.Client{ Timeout: config.Timeout, }, timeout: config.Timeout, maxRetries: config.MaxRetries, userAgent: config.UserAgent, minInterval: config.MinInterval, } } func (c *ExternalAPIClient) enforceRateLimit() { c.mutex.Lock() defer c.mutex.Unlock() elapsed := time.Since(c.lastRequest) if elapsed < c.minInterval { time.Sleep(c.minInterval - elapsed) } c.lastRequest = time.Now() } func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http.Response, error) { c.enforceRateLimit() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", c.userAgent) req.Header.Set("Accept", "application/json") var resp *http.Response var lastErr error for attempt := 0; attempt <= c.maxRetries; attempt++ { if attempt > 0 { // Exponential backoff backoff := time.Duration(1<= 500 || resp.StatusCode == 429 { resp.Body.Close() // Handle rate limiting if resp.StatusCode == 429 { retryAfter := parseRetryAfter(resp.Header.Get("Retry-After")) if retryAfter > 0 { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(retryAfter): } } } continue } // Success or non-retryable error break } if lastErr != nil { return nil, lastErr } return resp, nil } func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao string) (*OpenSkyFlightInfo, error) { if icao == "" { return nil, fmt.Errorf("empty ICAO code") } // OpenSky Network API endpoint for flight information apiURL := fmt.Sprintf("https://opensky-network.org/api/flights/aircraft?icao24=%s&begin=%d&end=%d", icao, time.Now().Add(-24*time.Hour).Unix(), time.Now().Unix(), ) resp, err := c.makeRequest(ctx, apiURL) if err != nil { return nil, &APIError{ Operation: "opensky_flight_info", Message: err.Error(), Retryable: true, } } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, &APIError{ Operation: "opensky_flight_info", StatusCode: resp.StatusCode, Message: string(body), Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, } } var flights [][]interface{} decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&flights); err != nil { return nil, &APIError{ Operation: "opensky_parse_response", Message: err.Error(), Retryable: false, } } if len(flights) == 0 { return nil, nil // No flight information available } // Parse the most recent flight flight := flights[0] if len(flight) < 10 { return nil, &APIError{ Operation: "opensky_invalid_response", Message: "invalid flight data format", Retryable: false, } } info := &OpenSkyFlightInfo{ ICAO: icao, } // Parse fields based on OpenSky API documentation if callsign, ok := flight[1].(string); ok { info.Callsign = callsign } if firstSeen, ok := flight[2].(float64); ok { info.FirstSeen = time.Unix(int64(firstSeen), 0) } if lastSeen, ok := flight[3].(float64); ok { info.LastSeen = time.Unix(int64(lastSeen), 0) } if origin, ok := flight[4].(string); ok { info.Origin = origin } if destination, ok := flight[5].(string); ok { info.Destination = destination } return info, nil } func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao string) (map[string]interface{}, error) { if icao == "" { return nil, fmt.Errorf("empty ICAO code") } // OpenSky Network metadata API apiURL := fmt.Sprintf("https://opensky-network.org/api/metadata/aircraft/icao/%s", icao) resp, err := c.makeRequest(ctx, apiURL) if err != nil { return nil, &APIError{ Operation: "opensky_aircraft_info", Message: err.Error(), Retryable: true, } } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, nil // Aircraft not found } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, &APIError{ Operation: "opensky_aircraft_info", StatusCode: resp.StatusCode, Message: string(body), Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, } } var aircraft map[string]interface{} decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&aircraft); err != nil { return nil, &APIError{ Operation: "opensky_parse_aircraft", Message: err.Error(), Retryable: false, } } return aircraft, nil } func (c *ExternalAPIClient) EnhanceCallsignWithExternalData(ctx context.Context, callsign, icao string) (map[string]interface{}, error) { enhancement := make(map[string]interface{}) enhancement["callsign"] = callsign enhancement["icao"] = icao enhancement["enhanced"] = false // Try to get flight information from OpenSky if flightInfo, err := c.GetFlightInfoFromOpenSky(ctx, icao); err == nil && flightInfo != nil { enhancement["flight_info"] = map[string]interface{}{ "origin": flightInfo.Origin, "destination": flightInfo.Destination, "first_seen": flightInfo.FirstSeen, "last_seen": flightInfo.LastSeen, "flight_number": flightInfo.FlightNumber, "airline": flightInfo.Airline, } enhancement["enhanced"] = true } // Try to get aircraft metadata if aircraftInfo, err := c.GetAircraftInfoFromOpenSky(ctx, icao); err == nil && aircraftInfo != nil { enhancement["aircraft_info"] = aircraftInfo enhancement["enhanced"] = true } return enhancement, nil } func (c *ExternalAPIClient) BatchEnhanceCallsigns(ctx context.Context, callsigns map[string]string) (map[string]map[string]interface{}, error) { results := make(map[string]map[string]interface{}) for callsign, icao := range callsigns { select { case <-ctx.Done(): return results, ctx.Err() default: } enhanced, err := c.EnhanceCallsignWithExternalData(ctx, callsign, icao) if err != nil { // Log error but continue with other callsigns fmt.Printf("Warning: failed to enhance callsign %s (ICAO: %s): %v\n", callsign, icao, err) continue } results[callsign] = enhanced } return results, nil } func (c *ExternalAPIClient) TestConnection(ctx context.Context) error { // Test with a simple API call testURL := "https://opensky-network.org/api/states?time=0&lamin=0&lomin=0&lamax=1&lomax=1" resp, err := c.makeRequest(ctx, testURL) if err != nil { return fmt.Errorf("connection test failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("connection test returned status %d", resp.StatusCode) } return nil } func parseRetryAfter(header string) time.Duration { if header == "" { return 0 } // Try parsing as seconds if seconds, err := time.ParseDuration(header + "s"); err == nil { return seconds } // Try parsing as HTTP date if t, err := http.ParseTime(header); err == nil { return time.Until(t) } return 0 } // HealthCheck provides information about the client's health func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interface{} { health := make(map[string]interface{}) // Test connection if err := c.TestConnection(ctx); err != nil { health["status"] = "unhealthy" health["error"] = err.Error() } else { health["status"] = "healthy" } // Add configuration info health["timeout"] = c.timeout.String() health["max_retries"] = c.maxRetries health["min_interval"] = c.minInterval.String() health["user_agent"] = c.userAgent c.mutex.RLock() health["last_request"] = c.lastRequest c.mutex.RUnlock() return health }