feat(hamqth): QSO upload to the HamQTH logbook — the 8th external service
qso_realtime.php with the account's callbook credentials (which the external-services config falls back to when its own are blank), one ADIF record per QSO, prg=OpsLog. HTTP status IS the answer: 200 saved, 400 rejected (a duplicate counts as delivered, like HRDLog's insert 0), 403 credentials. Sent-state lives in APP_OPSLOG_HAMQTH_SENT extras like HAMLOG.online — ADIF names no HamQTH field. Auto-upload on log, on-close batch, right-click Send to, QSL Manager backlog, and a Test button that authenticates against the callbook login, which cannot touch the log. Fixes a real mis-route on the way: manual 'Send to HAMLOG.online' had no branch in runManualUpload and fell through to QRZ.com — the selection was uploaded to the wrong service with the QRZ key. Both extras-stamped services now have their own branch.
This commit is contained in:
@@ -401,6 +401,12 @@ const (
|
||||
keyExtCloudlogAutoUpload = "extsvc.cloudlog.auto_upload"
|
||||
keyExtCloudlogUploadMode = "extsvc.cloudlog.upload_mode"
|
||||
|
||||
keyExtHamqthUsername = "extsvc.hamqth.username"
|
||||
keyExtHamqthPassword = "extsvc.hamqth.password"
|
||||
keyExtHamqthCallsign = "extsvc.hamqth.callsign"
|
||||
keyExtHamqthAutoUpload = "extsvc.hamqth.auto_upload"
|
||||
keyExtHamqthUploadMode = "extsvc.hamqth.upload_mode"
|
||||
|
||||
keyExtHamlogAPIKey = "extsvc.hamlog.api_key"
|
||||
keyExtHamlogAutoUpload = "extsvc.hamlog.auto_upload"
|
||||
keyExtHamlogUploadMode = "extsvc.hamlog.upload_mode"
|
||||
@@ -11177,7 +11183,9 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
|
||||
keyExtEQSLUsername, keyExtEQSLPassword, keyExtEQSLQTHNick, keyExtEQSLAutoUpload, keyExtEQSLUploadMode,
|
||||
keyExtCloudlogURL, keyExtCloudlogAPIKey, keyExtCloudlogStationID,
|
||||
keyExtCloudlogAutoUpload, keyExtCloudlogUploadMode, keyExtDeleteRemote,
|
||||
keyExtHamlogAPIKey, keyExtHamlogAutoUpload, keyExtHamlogUploadMode)
|
||||
keyExtHamlogAPIKey, keyExtHamlogAutoUpload, keyExtHamlogUploadMode,
|
||||
keyExtHamqthUsername, keyExtHamqthPassword, keyExtHamqthCallsign,
|
||||
keyExtHamqthAutoUpload, keyExtHamqthUploadMode)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
@@ -11261,6 +11269,21 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
|
||||
AutoUpload: m[keyExtHamlogAutoUpload] == "1",
|
||||
UploadMode: extsvc.UploadMode(m[keyExtHamlogUploadMode]),
|
||||
}
|
||||
out.HamQTH = extsvc.ServiceConfig{
|
||||
Username: m[keyExtHamqthUsername],
|
||||
Password: m[keyExtHamqthPassword],
|
||||
Callsign: m[keyExtHamqthCallsign],
|
||||
AutoUpload: m[keyExtHamqthAutoUpload] == "1",
|
||||
UploadMode: extsvc.UploadMode(m[keyExtHamqthUploadMode]),
|
||||
}
|
||||
// The callbook lookup already knows these credentials; blanks fall back to
|
||||
// them so an operator who set up the lookup years ago is one checkbox away.
|
||||
if out.HamQTH.Username == "" && out.HamQTH.Password == "" {
|
||||
u, _ := a.settings.Get(a.ctx, keyHQUser)
|
||||
p, _ := a.settings.Get(a.ctx, keyHQPassword)
|
||||
out.HamQTH.Username = strings.TrimSpace(u)
|
||||
out.HamQTH.Password = p
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -11335,6 +11358,7 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
|
||||
if hamMode == string(extsvc.ModeOnClose) {
|
||||
hamMode = string(extsvc.ModeImmediate)
|
||||
}
|
||||
hqMode := modeOf(cfg.HamQTH.UploadMode)
|
||||
hamAuto := "0"
|
||||
if cfg.Hamlog.AutoUpload {
|
||||
hamAuto = "1"
|
||||
@@ -11390,6 +11414,11 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
|
||||
keyExtHamlogAPIKey: strings.TrimSpace(cfg.Hamlog.APIKey),
|
||||
keyExtHamlogAutoUpload: hamAuto,
|
||||
keyExtHamlogUploadMode: hamMode,
|
||||
keyExtHamqthUsername: strings.TrimSpace(cfg.HamQTH.Username),
|
||||
keyExtHamqthPassword: cfg.HamQTH.Password,
|
||||
keyExtHamqthCallsign: strings.ToUpper(strings.TrimSpace(cfg.HamQTH.Callsign)),
|
||||
keyExtHamqthAutoUpload: boolStr(cfg.HamQTH.AutoUpload),
|
||||
keyExtHamqthUploadMode: hqMode,
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, scope+k, v); err != nil {
|
||||
return err
|
||||
@@ -11418,6 +11447,13 @@ func (a *App) TestClublogUpload() (string, error) {
|
||||
return extsvc.TestClublog(a.ctx, a.loadExternalServices().Clublog)
|
||||
}
|
||||
|
||||
// TestHamQTHUpload checks the HamQTH credentials against the callbook login —
|
||||
// authenticated, and unable to touch the log.
|
||||
func (a *App) TestHamQTHUpload() (string, error) {
|
||||
cfg := a.loadExternalServices().HamQTH
|
||||
return extsvc.TestHamQTH(a.ctx, nil, cfg)
|
||||
}
|
||||
|
||||
// TestHRDLogUpload validates that the HRDLog credentials are complete.
|
||||
func (a *App) TestHRDLogUpload() (string, error) {
|
||||
return extsvc.TestHRDLog(a.ctx, nil, a.loadExternalServices().HRDLog)
|
||||
@@ -11477,6 +11513,9 @@ func (a *App) FindQSOsForUpload(service, sentStatus string) ([]qso.QSO, error) {
|
||||
if extsvc.Service(service) == extsvc.ServiceHamlog {
|
||||
return a.qso.ListMissingExtra(a.ctx, hamlogSentKey)
|
||||
}
|
||||
if extsvc.Service(service) == extsvc.ServiceHamQTH {
|
||||
return a.qso.ListMissingExtra(a.ctx, hamqthSentKey)
|
||||
}
|
||||
col := uploadColumnFor(service)
|
||||
if col == "" {
|
||||
return nil, fmt.Errorf("unknown service %q", service)
|
||||
@@ -11492,7 +11531,7 @@ func (a *App) UploadQSOsManual(service string, ids []int64) error {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
svc := extsvc.Service(service)
|
||||
if uploadColumnFor(service) == "" && svc != extsvc.ServiceHamlog {
|
||||
if uploadColumnFor(service) == "" && svc != extsvc.ServiceHamlog && svc != extsvc.ServiceHamQTH {
|
||||
return fmt.Errorf("unknown service %q", service)
|
||||
}
|
||||
cfg := a.loadExternalServices()
|
||||
@@ -11716,6 +11755,43 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
}
|
||||
flush()
|
||||
}
|
||||
} else if svc == extsvc.ServiceHamlog || svc == extsvc.ServiceHamQTH {
|
||||
// One record per request, extras-stamped services. Hamlog used to fall
|
||||
// through to the QRZ branch below and got uploaded to the WRONG service
|
||||
// with the QRZ key — the context menu offered what the loop never handled.
|
||||
for i, id := range ids {
|
||||
if i > 0 {
|
||||
time.Sleep(manualUploadPace)
|
||||
}
|
||||
q, gerr := a.qso.GetByID(ctx, id)
|
||||
call := ""
|
||||
if gerr == nil {
|
||||
call = q.Callsign
|
||||
}
|
||||
rec, ok := a.buildUploadADIF(id, "")
|
||||
if !ok {
|
||||
emit(call + " — skipped (no record)")
|
||||
continue
|
||||
}
|
||||
var res extsvc.UploadResult
|
||||
var err error
|
||||
if svc == extsvc.ServiceHamlog {
|
||||
res, err = extsvc.UploadHamlog(ctx, nil, cfg.Hamlog, rec)
|
||||
} else {
|
||||
res, err = extsvc.UploadHamQTH(ctx, nil, cfg.HamQTH, rec)
|
||||
}
|
||||
if err == nil && res.OK {
|
||||
a.markExtUploaded(svc, id, res.LogID)
|
||||
uploaded++
|
||||
emit(call + " — OK")
|
||||
} else {
|
||||
msg := res.Message
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
emit(call + " — FAILED: " + msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// QRZ.com: one record per request (its logbook API has no batch upload),
|
||||
// paced for the same reason as HRDLog above.
|
||||
@@ -11757,6 +11833,7 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
label := map[extsvc.Service]string{
|
||||
extsvc.ServiceQRZ: "QRZ.com", extsvc.ServiceClublog: "Club Log", extsvc.ServiceHRDLog: "HRDLog",
|
||||
extsvc.ServiceLoTW: "LoTW", extsvc.ServiceEQSL: "eQSL",
|
||||
extsvc.ServiceHamlog: "HAMLOG.online", extsvc.ServiceHamQTH: "HamQTH",
|
||||
}[svc]
|
||||
if label == "" {
|
||||
label = string(svc)
|
||||
@@ -13500,6 +13577,13 @@ func (a *App) extShouldUpload(svc extsvc.Service, id int64) bool {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case extsvc.ServiceHamQTH:
|
||||
// Same extras stamp as HAMLOG.online — ADIF names no HamQTH field.
|
||||
if q.Extras != nil && strings.TrimSpace(q.Extras[hamqthSentKey]) != "" {
|
||||
applog.Printf("extsvc: QSO %d not eligible for hamqth — already sent on %s", id, q.Extras[hamqthSentKey])
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case extsvc.ServiceLoTW:
|
||||
for _, f := range a.loadExternalServices().LoTW.UploadFlags {
|
||||
if strings.EqualFold(q.LOTWSent, f) {
|
||||
@@ -13520,6 +13604,8 @@ func (a *App) extShouldUpload(svc extsvc.Service, id int64) bool {
|
||||
const (
|
||||
hamlogSentKey = "APP_OPSLOG_HAMLOG_SENT"
|
||||
hamlogSentDateKey = "APP_OPSLOG_HAMLOG_SENT_DATE"
|
||||
hamqthSentKey = "APP_OPSLOG_HAMQTH_SENT"
|
||||
hamqthSentDateKey = "APP_OPSLOG_HAMQTH_SENT_DATE"
|
||||
)
|
||||
|
||||
func (a *App) markExtUploaded(svc extsvc.Service, id int64, logID string) {
|
||||
@@ -13567,6 +13653,10 @@ func (a *App) markExtUploaded(svc extsvc.Service, id int64, logID string) {
|
||||
if err = a.qso.SetExtra(ctx, id, hamlogSentKey, "Y"); err == nil {
|
||||
err = a.qso.SetExtra(ctx, id, hamlogSentDateKey, date)
|
||||
}
|
||||
case extsvc.ServiceHamQTH:
|
||||
if err = a.qso.SetExtra(ctx, id, hamqthSentKey, "Y"); err == nil {
|
||||
err = a.qso.SetExtra(ctx, id, hamqthSentDateKey, date)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
applog.Printf("extsvc: mark %s uploaded %d failed: %v", svc, id, err)
|
||||
|
||||
Reference in New Issue
Block a user