package extsvc import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time" ) // HamQTH real-time QSO upload. // // One POST per QSO to qso_realtime.php with the account username/password — // the same credentials the HamQTH callbook lookup uses. The answer is the // HTTP status code, not a body format: 200 saved, 400 rejected (bad band, // duplicate…, reason in the body), 403 wrong credentials, 500 server error. const hamqthUploadURL = "https://www.hamqth.com/qso_realtime.php" // hamqthLoginURL is the callbook session login — the one authenticated HamQTH // endpoint that cannot change anything in the log, which is what the settings // Test button must call. const hamqthLoginURL = "https://www.hamqth.com/xml.php" // UploadHamQTH pushes one ADIF record to the HamQTH online logbook. func UploadHamQTH(ctx context.Context, client *http.Client, cfg ServiceConfig, adifRecord string) (UploadResult, error) { return uploadHamQTHTo(ctx, client, hamqthUploadURL, cfg, adifRecord) } func uploadHamQTHTo(ctx context.Context, client *http.Client, endpoint string, cfg ServiceConfig, adifRecord string) (UploadResult, error) { user := strings.TrimSpace(cfg.Username) switch { case user == "": return UploadResult{}, fmt.Errorf("hamqth: username not set") case cfg.Password == "": return UploadResult{}, fmt.Errorf("hamqth: password not set") } rec := strings.TrimSpace(adifRecord) if rec == "" { return UploadResult{}, fmt.Errorf("hamqth: empty ADIF record") } form := url.Values{} form.Set("u", user) form.Set("p", cfg.Password) // c: the logbook callsign when the account holds several; empty means the // account's own call, which is the common case. if c := strings.ToUpper(strings.TrimSpace(cfg.Callsign)); c != "" { form.Set("c", c) } form.Set("adif", rec) form.Set("prg", "OpsLog") form.Set("cmd", "insert") req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode())) if err != nil { return UploadResult{}, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if client == nil { client = &http.Client{Timeout: 30 * time.Second} } resp, err := client.Do(req) if err != nil { return UploadResult{}, err } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10)) msg := strings.TrimSpace(string(body)) switch resp.StatusCode { case http.StatusOK: return UploadResult{OK: true}, nil case http.StatusBadRequest: // "Rejected" covers duplicates too. A duplicate is a SUCCESS for our // purposes — the QSO is in the logbook, retrying it forever isn't — // same treatment HRDLog's 0 gets. if strings.Contains(strings.ToLower(msg), "dupl") { return UploadResult{OK: true, Ignored: true, Message: "already in logbook"}, nil } if msg == "" { msg = "QSO rejected" } return UploadResult{OK: false, Message: msg}, nil case http.StatusForbidden: return UploadResult{}, fmt.Errorf("hamqth: wrong username or password") default: if msg != "" && len(msg) < 200 { return UploadResult{}, fmt.Errorf("hamqth: HTTP %d: %s", resp.StatusCode, msg) } return UploadResult{}, fmt.Errorf("hamqth: HTTP %d", resp.StatusCode) } } // TestHamQTH verifies the credentials against the callbook session login — // authenticated, and unable to touch the log. func TestHamQTH(ctx context.Context, client *http.Client, cfg ServiceConfig) (string, error) { user := strings.TrimSpace(cfg.Username) if user == "" || cfg.Password == "" { return "", fmt.Errorf("hamqth: set the username and password first") } q := url.Values{} q.Set("u", user) q.Set("p", cfg.Password) req, err := http.NewRequestWithContext(ctx, http.MethodGet, hamqthLoginURL+"?"+q.Encode(), nil) if err != nil { return "", err } if client == nil { client = &http.Client{Timeout: 30 * time.Second} } resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10)) s := string(body) if strings.Contains(s, "") { return fmt.Sprintf("Connected to HamQTH as %s.", user), nil } if i := strings.Index(s, ""); i >= 0 { e := s[i+len(""):] if j := strings.Index(e, ""); j >= 0 { return "", fmt.Errorf("hamqth: %s", strings.TrimSpace(e[:j])) } } return "", fmt.Errorf("hamqth: unexpected answer — check the username and password") }