// 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) }