feat: Implemented the download of eQSL
This commit is contained in:
@@ -53,6 +53,91 @@ func eqslPost(ctx context.Context, client *http.Client, user, pswd, adif string)
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// eQSL Inbox download endpoint + base host. DownloadInBox.cfm builds an ADIF of
|
||||
// the account's *received* eQSLs (confirmations) and returns an HTML page that
|
||||
// links to the generated .adi file; we then fetch that file (two-step API).
|
||||
const (
|
||||
eqslDownloadInboxURL = "https://www.eQSL.cc/qslcard/DownloadInBox.cfm"
|
||||
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)"`)
|
||||
|
||||
// DownloadEQSLConfirmations fetches the account's Inbox (received eQSLs =
|
||||
// confirmations) as ADIF text. since is "YYYY-MM-DD" (only QSLs received since
|
||||
// then, incremental "last download"); qthNick comes from the config when the
|
||||
// account has several QTH profiles.
|
||||
func DownloadEQSLConfirmations(ctx context.Context, client *http.Client, cfg ServiceConfig, since string) (string, error) {
|
||||
user := strings.ToUpper(strings.TrimSpace(cfg.Username))
|
||||
if user == "" || strings.TrimSpace(cfg.Password) == "" {
|
||||
return "", fmt.Errorf("eqsl: username/password not set")
|
||||
}
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 120 * time.Second}
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("UserName", user)
|
||||
q.Set("Password", cfg.Password)
|
||||
if nick := strings.TrimSpace(cfg.QTHNickname); nick != "" {
|
||||
q.Set("QTHNickname", nick)
|
||||
}
|
||||
if s := eqslRcvdSince(since); s != "" {
|
||||
q.Set("RcvdSince", s) // eQSL wants YYYYMMDDHHMM
|
||||
}
|
||||
|
||||
// Step 1: ask eQSL to build the inbox ADIF; the reply is HTML linking to it.
|
||||
page, err := eqslGet(ctx, client, eqslDownloadInboxURL+"?"+q.Encode())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reason := authErrEQSL(page); reason != "" {
|
||||
return "", fmt.Errorf("eqsl: %s", reason)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// Step 2: fetch the generated .adi file.
|
||||
adif, err := eqslGet(ctx, client, eqslBaseURL+m[1])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("eqsl: fetch adif: %w", err)
|
||||
}
|
||||
return adif, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("eqsl: build request: %w", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("eqsl: request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 32*1024*1024))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("eqsl: http %d", resp.StatusCode)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// eqslRcvdSince converts the app's "YYYY-MM-DD" into eQSL's RcvdSince format
|
||||
// "YYYYMMDDHHMM" (midnight). Empty in → empty out (full pull).
|
||||
func eqslRcvdSince(since string) string {
|
||||
s := strings.ReplaceAll(strings.TrimSpace(since), "-", "")
|
||||
if len(s) == 8 { // YYYYMMDD → append 0000 (00:00)
|
||||
return s + "0000"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// authErrEQSL returns a reason when the response signals bad credentials, else
|
||||
// "". eQSL replies "Error: No match on eQSL_User/eQSL_Pswd".
|
||||
func authErrEQSL(body string) string {
|
||||
|
||||
Reference in New Issue
Block a user