package client import ( "context" "fmt" "io" "net/http" "regexp" "strings" "time" "github.com/scottyah/paragliding/internal/model" ) const ( faaTFRListURL = "https://tfr.faa.gov/tfr2/list.jsp" faaBaseURL = "https://tfr.faa.gov" ) // FAAClient fetches TFR data from the FAA website type FAAClient struct { httpClient *http.Client } // NewFAAClient creates a new FAA TFR client func NewFAAClient() *FAAClient { return &FAAClient{ httpClient: &http.Client{ Timeout: 15 * time.Second, }, } } var ( anchorRe = regexp.MustCompile(`(?is)]*)>(.*?)`) hrefRe = regexp.MustCompile(`(?i)href="([^"]*)"`) dateRe = regexp.MustCompile(`^\d{2}/\d{2}/\d{4}$`) htmlTagRe = regexp.MustCompile(`<[^>]*>`) ) type anchor struct { href string text string } // FetchTFRs retrieves the list of active TFRs from the FAA website func (c *FAAClient) FetchTFRs(ctx context.Context) ([]model.TFR, error) { req, err := http.NewRequestWithContext(ctx, "GET", faaTFRListURL, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("User-Agent", "ParaglidingWeatherApp/1.0") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch TFR list: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("TFR list returned status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } return parseTFRList(string(body)), nil } // extractAnchors finds all tags in HTML and returns their href and text content func extractAnchors(html string) []anchor { matches := anchorRe.FindAllStringSubmatch(html, -1) anchors := make([]anchor, 0, len(matches)) for _, m := range matches { text := htmlTagRe.ReplaceAllString(m[2], "") text = strings.TrimSpace(text) a := anchor{text: text} if hrefMatch := hrefRe.FindStringSubmatch(m[1]); hrefMatch != nil { a.href = hrefMatch[1] } anchors = append(anchors, a) } return anchors } // parseTFRList extracts TFR entries from the FAA list page HTML. // The page contains a table where each row has anchor tags for: // date, notam ID (with detail link), facility, state, type, description, zoom link func parseTFRList(html string) []model.TFR { rows := strings.Split(html, "