chore: release v0.19.3

This commit is contained in:
2026-07-09 17:32:13 +02:00
parent 19c2de5f61
commit 1f74e4d234
11 changed files with 482 additions and 9 deletions
+70 -6
View File
@@ -61,9 +61,12 @@ const (
eqslBaseURL = "https://www.eQSL.cc"
)
// eqslAdiHrefRe pulls the generated .adi path out of the DownloadInBox reply,
// e.g. `<A HREF="/qslcard/downloadedfiles/xxxx.adi">`.
var eqslAdiHrefRe = regexp.MustCompile(`(?i)href="(/[^"]+\.adi)"`)
// eqslAdiHrefRe pulls the generated .adi link out of the DownloadInBox reply.
// eQSL serves old HTML 4.01 with UPPERCASE tags and often UNQUOTED attributes,
// e.g. `<A HREF=/downloadedFiles/xxxx.adi>` or `<a href="…/xxxx.adi">`, and the
// path may be root-relative or a bare filename. Match case-insensitively with
// optional quoting; resolve to an absolute URL afterwards.
var eqslAdiHrefRe = regexp.MustCompile(`(?i)href\s*=\s*["']?([^"'>\s]+\.adi)`)
// DownloadEQSLConfirmations fetches the account's Inbox (received eQSLs =
// confirmations) as ADIF text. since is "YYYY-MM-DD" (only QSLs received since
@@ -97,18 +100,79 @@ func DownloadEQSLConfirmations(ctx context.Context, client *http.Client, cfg Ser
}
m := eqslAdiHrefRe.FindStringSubmatch(page)
if m == nil {
// eQSL reports problems inline ("Error: …", or "no QSLs" wording).
return "", fmt.Errorf("eqsl: no download link in response: %s", eqslReason(page))
// eQSL reports problems inline ("Error: …", "You have no … confirmations").
return "", fmt.Errorf("eqsl: no download link in response: %s", eqslNoLinkReason(page))
}
// Resolve the link to an absolute URL. eQSL is a Windows/ColdFusion server, so
// the path may use BACKSLASHES and a leading `..` (e.g. `..\downloadedFiles\
// xxx.adi`); normalise those. It may also be absolute or a bare filename.
link := strings.TrimSpace(m[1])
link = strings.ReplaceAll(link, `\`, "/")
link = strings.TrimPrefix(link, "..") // "../downloadedFiles/…" → "/downloadedFiles/…"
var fileURL string
switch {
case strings.HasPrefix(strings.ToLower(link), "http"):
fileURL = link
case strings.HasPrefix(link, "/"):
fileURL = eqslBaseURL + link
default:
fileURL = eqslBaseURL + "/" + link
}
// Step 2: fetch the generated .adi file.
adif, err := eqslGet(ctx, client, eqslBaseURL+m[1])
adif, err := eqslGet(ctx, client, fileURL)
if err != nil {
return "", fmt.Errorf("eqsl: fetch adif: %w", err)
}
return adif, nil
}
// eqslNoLinkReason builds a helpful message when no .adi link was found: eQSL's
// own error/notice text if present, else a short tag-stripped snippet of the page
// so the real wording (e.g. "You have no Inbox items") is visible in the log.
func eqslNoLinkReason(page string) string {
low := strings.ToLower(page)
for _, marker := range []string{"you have no", "no inbox", "error", "invalid", "not found"} {
if i := strings.Index(low, marker); i >= 0 {
seg := page[i:]
if len(seg) > 160 {
seg = seg[:160]
}
return strings.Join(strings.Fields(stripTags(seg)), " ")
}
}
txt := strings.Join(strings.Fields(stripTags(page)), " ")
if len(txt) > 200 {
txt = txt[:200]
}
if txt == "" {
txt = "empty response"
}
return txt
}
// stripTags removes HTML tags for a readable one-line diagnostic.
func stripTags(s string) string {
var b strings.Builder
depth := 0
for _, r := range s {
switch r {
case '<':
depth++
case '>':
if depth > 0 {
depth--
}
default:
if depth == 0 {
b.WriteRune(r)
}
}
}
return b.String()
}
// eqslGet does a plain GET and returns the body as text (32 MB cap), erroring on
// a non-200 status.
func eqslGet(ctx context.Context, client *http.Client, u string) (string, error) {