mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix(lotw): decouple QSO records from certificate station; skip un-signable records
- Remove LOCATION_MISMATCH check: the recorded myGrid never enters the TQ8 (the tSTATION grid is signed from the current certificate station), so a portable/ADIF-imported record with a different grid must not block upload. Logging and uploading are now fully decoupled from the certificate location. - Fingerprint now covers the CONTACT only (call, date, band, mode, satellite, freq), excluding station fields: changing grid/zones/county/ IOTA no longer invalidates ledger entries, so already-uploaded contacts are not re-queued and rejected as duplicates. - prepare(): catch un-signable contacts per-record instead of aborting the whole batch; count them as skipped and upload the rest. - audit(): count lotwUploaded records as uploaded (ledger-loss fallback).
This commit is contained in:
1 parent
caddba5b81
commit
fe85703e56
2 files changed
+15
-12
No files matched your search
@@ -24,14 +24,6 @@ internal class LoTWSigner(private val config: LoTWConfig) {
|
||||
if (date < key.info.firstQsoDate || (key.info.lastQsoDate.isNotBlank() && date > key.info.lastQsoDate) || record.startUtcMillis > now) {
|
||||
fail(LoTWProblem.QSO_DATE, call)
|
||||
}
|
||||
val grids = buildList {
|
||||
addAll(station.getValue("GRIDSQUARE").split(',').map(String::trim))
|
||||
station["MY_VUCC_GRIDS"]?.split(',')?.map(String::trim)?.let(::addAll)
|
||||
}.filter(String::isNotBlank)
|
||||
if (record.myGrid.isNotBlank() && grids.none { grid ->
|
||||
val local = record.myGrid.trim().uppercase(Locale.US)
|
||||
grid.startsWith(local) || local.startsWith(grid)
|
||||
}) fail(LoTWProblem.LOCATION_MISMATCH, call)
|
||||
fun mhz(hz: Long?): String = hz?.let { BigDecimal.valueOf(it, 6).stripTrailingZeros().toPlainString() }.orEmpty()
|
||||
val fields = linkedMapOf(
|
||||
"BAND" to config.band(record.band, record.txFrequencyHz, true),
|
||||
@@ -46,8 +38,12 @@ internal class LoTWSigner(private val config: LoTWConfig) {
|
||||
"SAT_NAME" to if (record.isSatellite) config.satellite(record.satelliteName, date) else ""
|
||||
).filterValues { it.isNotBlank() }
|
||||
val signData = (config.stationOrder.map { station[it].orEmpty() } + config.contactOrder.map { fields[it].orEmpty() }).joinToString("")
|
||||
// Fingerprint identifies the CONTACT only (call, date/time, band, mode,
|
||||
// satellite, frequency). Station fields (grid, zones, county, IOTA) are
|
||||
// deliberately excluded: changing the station location must not change
|
||||
// the fingerprint, otherwise previously-uploaded contacts would lose
|
||||
// their ledger entry and be re-uploaded (and rejected as duplicates).
|
||||
val identity = field("CALL", key.info.callsign) + field("DXCC", key.info.dxcc.toString()) +
|
||||
station.toSortedMap().entries.joinToString("") { field(it.key, it.value) } +
|
||||
fields.entries.joinToString("") { field(it.key, it.value) }
|
||||
val hash = MessageDigest.getInstance("SHA-256").digest(identity.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
||||
return LoTWContact(record, fields, signData, hash)
|
||||
|
||||
+10
-3
@@ -167,7 +167,7 @@ class LoTWUploadRepository internal constructor(
|
||||
coroutineContext.ensureActive()
|
||||
when {
|
||||
record.status != QsoStatus.COMPLETE -> unavailable++
|
||||
record.lotwReceived -> uploaded++
|
||||
record.lotwReceived || record.lotwUploaded -> uploaded++
|
||||
else -> {
|
||||
val contact = try {
|
||||
signing.signer.contact(record, signing.key, signing.location, now())
|
||||
@@ -199,8 +199,15 @@ class LoTWUploadRepository internal constructor(
|
||||
val unique = hashSetOf<String>()
|
||||
val contacts = records.sortedBy { it.startUtcMillis }.mapNotNull { record ->
|
||||
coroutineContext.ensureActive()
|
||||
if (record.status != QsoStatus.COMPLETE || (record.lotwReceived && !resubmit)) { skipped++; return@mapNotNull null }
|
||||
val contact = signing.signer.contact(record, signing.key, signing.location, now())
|
||||
if (record.status != QsoStatus.COMPLETE || ((record.lotwReceived || record.lotwUploaded) && !resubmit)) { skipped++; return@mapNotNull null }
|
||||
val contact = try {
|
||||
signing.signer.contact(record, signing.key, signing.location, now())
|
||||
} catch (_: LoTWOperationException) {
|
||||
// One un-signable record must not abort the whole batch:
|
||||
// skip it, count it, and let the rest upload.
|
||||
skipped++
|
||||
return@mapNotNull null
|
||||
}
|
||||
val previous = ledger[contact.fingerprint]
|
||||
when {
|
||||
(previous == "accepted" && !resubmit) || !unique.add(contact.fingerprint) -> { skipped++; null }
|
||||
|
||||
Reference in new issue
Block a user