208 lines
5.0 KiB
Go
208 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func runDiscussion(ctx context.Context, cache *Cache, loc Location, searchQuery, userAgent string) (string, error) {
|
|
client := NewNWSClient(userAgent)
|
|
if loc.OfficeID == "" {
|
|
office, err := client.FetchOfficeID(ctx, loc)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
loc.OfficeID = office
|
|
if err := cache.UpdateLocation(loc); err != nil {
|
|
if searchQuery == "" {
|
|
return "", err
|
|
}
|
|
if err := cache.SaveLocation(searchQuery, loc); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
|
|
cached, ok, err := cache.LoadAFD()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
productID, productURL, err := client.FetchLatestAFDMeta(ctx, loc.OfficeID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if ok && cached.OfficeID == loc.OfficeID && cached.ProductID == productID {
|
|
cached.CheckedAt = time.Now().UTC()
|
|
if err := cache.SaveAFD(cached); err != nil {
|
|
return "", err
|
|
}
|
|
return cached.Text, nil
|
|
}
|
|
|
|
text, issuanceTime, err := client.FetchAFDText(ctx, productURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
payload := AFDCache{
|
|
OfficeID: loc.OfficeID,
|
|
ProductID: productID,
|
|
Text: text,
|
|
IssuanceTime: issuanceTime,
|
|
CheckedAt: time.Now().UTC(),
|
|
}
|
|
if err := cache.SaveAFD(payload); err != nil {
|
|
return "", err
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func (c *NWSClient) FetchLatestAFDMeta(ctx context.Context, officeID string) (string, string, error) {
|
|
officeID = strings.TrimSpace(strings.ToUpper(officeID))
|
|
if officeID == "" {
|
|
return "", "", fmt.Errorf("office id is empty")
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.weather.gov/products/types/AFD/locations/%s", officeID)
|
|
var resp struct {
|
|
Graph []struct {
|
|
ID string `json:"id"`
|
|
Ref string `json:"@id"`
|
|
} `json:"@graph"`
|
|
}
|
|
if err := c.getJSON(ctx, url, &resp); err != nil {
|
|
return "", "", err
|
|
}
|
|
if len(resp.Graph) == 0 {
|
|
return "", "", fmt.Errorf("no AFD products for %s", officeID)
|
|
}
|
|
productID := strings.TrimSpace(resp.Graph[0].ID)
|
|
productURL := strings.TrimSpace(resp.Graph[0].Ref)
|
|
if productID == "" || productURL == "" {
|
|
return "", "", fmt.Errorf("invalid AFD metadata")
|
|
}
|
|
return productID, productURL, nil
|
|
}
|
|
|
|
func (c *NWSClient) FetchAFDText(ctx context.Context, url string) (string, string, error) {
|
|
var resp struct {
|
|
ProductText string `json:"productText"`
|
|
IssuanceTime string `json:"issuanceTime"`
|
|
}
|
|
if err := c.getJSON(ctx, url, &resp); err != nil {
|
|
return "", "", err
|
|
}
|
|
text := sanitizeAFDText(resp.ProductText)
|
|
if text == "" {
|
|
return "", "", fmt.Errorf("AFD product text missing")
|
|
}
|
|
return text, strings.TrimSpace(resp.IssuanceTime), nil
|
|
}
|
|
|
|
func sanitizeAFDText(text string) string {
|
|
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
|
|
lines = lines[1:]
|
|
}
|
|
if len(lines) == 0 {
|
|
return ""
|
|
}
|
|
|
|
for i, line := range lines {
|
|
if strings.Contains(strings.ToLower(line), "area forecast discussion") {
|
|
lines = lines[i:]
|
|
break
|
|
}
|
|
}
|
|
|
|
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
|
|
lines = lines[1:]
|
|
}
|
|
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
func formatAFDMarkdown(text string) string {
|
|
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
out := make([]string, 0, len(lines))
|
|
titleDone := false
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "&&" {
|
|
continue
|
|
}
|
|
if !titleDone && strings.EqualFold(trimmed, "Area Forecast Discussion") {
|
|
out = append(out, "# Area Forecast Discussion")
|
|
titleDone = true
|
|
continue
|
|
}
|
|
if header, rest := splitKeyMessage(trimmed); header != "" {
|
|
out = append(out, "## "+header)
|
|
if rest != "" {
|
|
out = append(out, rest)
|
|
}
|
|
continue
|
|
}
|
|
if isAFDHeaderLine(trimmed) {
|
|
title := afdHeaderToTitle(trimmed)
|
|
out = append(out, "# "+title)
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return strings.TrimSpace(strings.Join(out, "\n"))
|
|
}
|
|
|
|
func isAFDHeaderLine(line string) bool {
|
|
if !strings.HasPrefix(line, ".") {
|
|
return false
|
|
}
|
|
return strings.HasSuffix(line, "...") || strings.HasSuffix(line, ".")
|
|
}
|
|
|
|
func afdHeaderToTitle(line string) string {
|
|
trimmed := strings.Trim(line, ". ")
|
|
parts := strings.Fields(trimmed)
|
|
for i, part := range parts {
|
|
subparts := strings.Split(part, "/")
|
|
for j, sub := range subparts {
|
|
subparts[j] = titleWord(sub)
|
|
}
|
|
parts[i] = strings.Join(subparts, "/")
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func titleWord(word string) string {
|
|
if word == "" {
|
|
return ""
|
|
}
|
|
lower := strings.ToLower(word)
|
|
r := []rune(lower)
|
|
r[0] = []rune(strings.ToUpper(string(r[0])))[0]
|
|
return string(r)
|
|
}
|
|
|
|
func splitKeyMessage(line string) (string, string) {
|
|
lower := strings.ToLower(line)
|
|
if !strings.HasPrefix(lower, "key message") {
|
|
return "", ""
|
|
}
|
|
parts := strings.SplitN(line, "...", 2)
|
|
header := strings.TrimSpace(parts[0])
|
|
rest := ""
|
|
if len(parts) > 1 {
|
|
rest = strings.TrimSpace(parts[1])
|
|
}
|
|
fields := strings.Fields(header)
|
|
if len(fields) < 2 {
|
|
return "", ""
|
|
}
|
|
fields[0] = "Key"
|
|
fields[1] = "Message"
|
|
return strings.Join(fields, " "), rest
|
|
}
|