// 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 to come. ) // 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" ) // 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 // // 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 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)) if c.UploadMode != ModeDelayed { c.UploadMode = ModeImmediate } return c } // ExternalServices bundles every service's config for the settings UI. // LoTW fields will be added as that service lands. type ExternalServices struct { QRZ ServiceConfig `json:"qrz"` Clublog ServiceConfig `json:"clublog"` } // 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) }