feat: status bar added
This commit is contained in:
@@ -3,6 +3,7 @@ package extsvc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -70,6 +71,80 @@ func UploadQRZ(ctx context.Context, client *http.Client, apiKey, adifRecord stri
|
||||
return parseQRZResponse(string(body))
|
||||
}
|
||||
|
||||
// QRZFetchResult is the parsed outcome of a QRZ FETCH.
|
||||
type QRZFetchResult struct {
|
||||
ADIF string // raw ADIF document
|
||||
Result string // RESULT field (OK / FAIL / AUTH)
|
||||
Count string // COUNT field reported by QRZ
|
||||
}
|
||||
|
||||
// FetchQRZ pulls logbook records as ADIF via the QRZ FETCH action. option is
|
||||
// the QRZ OPTION string (e.g. "ALL"). The ADIF document is returned in the
|
||||
// response's ADIF field.
|
||||
func FetchQRZ(ctx context.Context, client *http.Client, apiKey, option string) (QRZFetchResult, error) {
|
||||
var out QRZFetchResult
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
if apiKey == "" {
|
||||
return out, fmt.Errorf("qrz: api key not set")
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("KEY", apiKey)
|
||||
form.Set("ACTION", "FETCH")
|
||||
if option != "" {
|
||||
form.Set("OPTION", option)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, qrzAPIURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("qrz: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 120 * time.Second}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("qrz: request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024*1024))
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("qrz: read response: %w", err)
|
||||
}
|
||||
// The response is "RESULT=OK&COUNT=N&ADIF=<adif>". The ADIF blob can
|
||||
// contain '&' and ';', so we can't url.ParseQuery the whole body (Go
|
||||
// caps the number of params). Split off the ADIF value manually and
|
||||
// only query-parse the small status header.
|
||||
full := string(body)
|
||||
head, adifPart := full, ""
|
||||
if i := strings.Index(full, "ADIF="); i >= 0 {
|
||||
head = full[:i]
|
||||
adifPart = full[i+len("ADIF="):]
|
||||
}
|
||||
vals, _ := url.ParseQuery(strings.TrimRight(head, "&"))
|
||||
out.Result = strings.ToUpper(strings.TrimSpace(vals.Get("RESULT")))
|
||||
out.Count = strings.TrimSpace(vals.Get("COUNT"))
|
||||
if out.Result == "AUTH" || out.Result == "FAIL" {
|
||||
reason := strings.TrimSpace(vals.Get("REASON"))
|
||||
if reason == "" {
|
||||
reason = "fetch rejected"
|
||||
}
|
||||
return out, fmt.Errorf("qrz: %s", reason)
|
||||
}
|
||||
// The ADIF value may be url-encoded (%3C) and/or HTML-entity-encoded
|
||||
// (QRZ returns < > &). Decode both so the ADIF parser sees
|
||||
// real '<' / '>' tags.
|
||||
if strings.Contains(adifPart, "%3C") || strings.Contains(adifPart, "%3c") {
|
||||
if dec, derr := url.QueryUnescape(adifPart); derr == nil {
|
||||
adifPart = dec
|
||||
}
|
||||
}
|
||||
if strings.Contains(adifPart, "<") || strings.Contains(adifPart, ">") || strings.Contains(adifPart, "&") {
|
||||
adifPart = html.UnescapeString(adifPart)
|
||||
}
|
||||
out.ADIF = adifPart
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TestQRZ checks a logbook API key with ACTION=STATUS and returns a short
|
||||
// human-readable summary (callsign + QSO count) for the settings UI. An
|
||||
// invalid key comes back as STATUS=AUTH → returned as an error.
|
||||
|
||||
Reference in New Issue
Block a user