113 lines
4.6 KiB
Go
113 lines
4.6 KiB
Go
// Package extsvc uploads logged QSOs to external logbook services
|
||
// (QRZ.com first; Clublog and LoTW to follow). Each service has its own
|
||
// credentials and an upload mode chosen per-service: "immediate" pushes as
|
||
// soon as the QSO is saved, "delayed" waits a random 1–2 minutes (like
|
||
// Log4OM) so a mistakenly-logged QSO can still be edited or removed before
|
||
// it leaves.
|
||
//
|
||
// The Manager is intentionally fire-and-forget: a failed or skipped upload
|
||
// just leaves the QSO's per-service upload-status column empty, and the
|
||
// (future) manual-upload window will let the user retry the backlog.
|
||
package extsvc
|
||
|
||
import (
|
||
"errors"
|
||
"strings"
|
||
)
|
||
|
||
// errFromResult turns a non-OK result with no transport error into one
|
||
// (defensive — uploaders normally return an error alongside !OK).
|
||
func errFromResult(r UploadResult) error {
|
||
if r.Message != "" {
|
||
return errors.New(r.Message)
|
||
}
|
||
return errors.New("upload rejected")
|
||
}
|
||
|
||
// Service identifies one external logbook.
|
||
type Service string
|
||
|
||
const (
|
||
ServiceQRZ Service = "qrz" // QRZ.com Logbook
|
||
ServiceClublog Service = "clublog" // Club Log real-time upload
|
||
ServiceLoTW Service = "lotw" // ARRL Logbook of The World (via TQSL)
|
||
)
|
||
|
||
// UploadMode selects when an auto-upload fires after a QSO is saved.
|
||
type UploadMode string
|
||
|
||
const (
|
||
// ModeImmediate uploads as soon as the QSO is logged.
|
||
ModeImmediate UploadMode = "immediate"
|
||
// ModeDelayed waits a random 1–2 minutes before uploading.
|
||
ModeDelayed UploadMode = "delayed"
|
||
// ModeOnClose queues QSOs and uploads them in one batch when the app
|
||
// closes. This is the LoTW-friendly mode (ARRL discourages per-QSO
|
||
// uploads), and it lets the user fix the whole session before sending.
|
||
ModeOnClose UploadMode = "on_close"
|
||
)
|
||
|
||
// ServiceConfig is the per-service user configuration. It's a superset of
|
||
// the credential shapes the different services need — each service reads
|
||
// only the fields it uses:
|
||
//
|
||
// QRZ.com → APIKey, ForceStationCallsign
|
||
// Club Log → Email, Password, Callsign, APIKey
|
||
// LoTW → TQSLPath, StationLocation, KeyPassword (signs+uploads via TQSL)
|
||
//
|
||
// AutoUpload + UploadMode are common to all (timing is per-service, so the
|
||
// user can run e.g. Club Log immediate and QRZ delayed).
|
||
type ServiceConfig struct {
|
||
APIKey string `json:"api_key"`
|
||
Email string `json:"email"` // Club Log account email
|
||
Password string `json:"password"` // Club Log account password
|
||
Callsign string `json:"callsign"` // Club Log logbook (owner) callsign
|
||
ForceStationCallsign string `json:"force_station_callsign"` // QRZ
|
||
TQSLPath string `json:"tqsl_path"` // LoTW: path to tqsl.exe
|
||
StationLocation string `json:"station_location"` // LoTW: TQSL Station Location name
|
||
KeyPassword string `json:"key_password"` // LoTW: certificate private-key password (optional)
|
||
UploadFlag string `json:"upload_flag"` // LoTW: sent status that means "ready to upload" — "N" or "R"
|
||
WriteLog bool `json:"write_log"` // LoTW: pass -t to write a TQSL diagnostic log
|
||
AutoUpload bool `json:"auto_upload"`
|
||
UploadMode UploadMode `json:"upload_mode"`
|
||
}
|
||
|
||
// normalised returns the config with whitespace trimmed and a valid upload
|
||
// mode (defaults to immediate).
|
||
func (c ServiceConfig) normalised() ServiceConfig {
|
||
c.APIKey = strings.TrimSpace(c.APIKey)
|
||
c.Email = strings.TrimSpace(c.Email)
|
||
c.Callsign = strings.ToUpper(strings.TrimSpace(c.Callsign))
|
||
c.ForceStationCallsign = strings.ToUpper(strings.TrimSpace(c.ForceStationCallsign))
|
||
c.TQSLPath = strings.TrimSpace(c.TQSLPath)
|
||
c.StationLocation = strings.TrimSpace(c.StationLocation)
|
||
// Upload flag is the LoTW sent-status that marks a QSO ready to upload.
|
||
// Only "N" (no) and "R" (requested) are valid; default to "R".
|
||
if uf := strings.ToUpper(strings.TrimSpace(c.UploadFlag)); uf == "N" || uf == "R" {
|
||
c.UploadFlag = uf
|
||
} else {
|
||
c.UploadFlag = "R"
|
||
}
|
||
switch c.UploadMode {
|
||
case ModeDelayed, ModeOnClose:
|
||
// keep
|
||
default:
|
||
c.UploadMode = ModeImmediate
|
||
}
|
||
return c
|
||
}
|
||
|
||
// ExternalServices bundles every service's config for the settings UI.
|
||
type ExternalServices struct {
|
||
QRZ ServiceConfig `json:"qrz"`
|
||
Clublog ServiceConfig `json:"clublog"`
|
||
LoTW ServiceConfig `json:"lotw"`
|
||
}
|
||
|
||
// UploadResult is the outcome of a single upload attempt.
|
||
type UploadResult struct {
|
||
OK bool // the service accepted (or already had) the QSO
|
||
LogID string // service-assigned record id, when provided
|
||
Message string // human-readable detail (reason on failure)
|
||
}
|