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) {
|
if (date < key.info.firstQsoDate || (key.info.lastQsoDate.isNotBlank() && date > key.info.lastQsoDate) || record.startUtcMillis > now) {
|
||||||
fail(LoTWProblem.QSO_DATE, call)
|
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()
|
fun mhz(hz: Long?): String = hz?.let { BigDecimal.valueOf(it, 6).stripTrailingZeros().toPlainString() }.orEmpty()
|
||||||
val fields = linkedMapOf(
|
val fields = linkedMapOf(
|
||||||
"BAND" to config.band(record.band, record.txFrequencyHz, true),
|
"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 ""
|
"SAT_NAME" to if (record.isSatellite) config.satellite(record.satelliteName, date) else ""
|
||||||
).filterValues { it.isNotBlank() }
|
).filterValues { it.isNotBlank() }
|
||||||
val signData = (config.stationOrder.map { station[it].orEmpty() } + config.contactOrder.map { fields[it].orEmpty() }).joinToString("")
|
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()) +
|
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) }
|
fields.entries.joinToString("") { field(it.key, it.value) }
|
||||||
val hash = MessageDigest.getInstance("SHA-256").digest(identity.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
val hash = MessageDigest.getInstance("SHA-256").digest(identity.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
||||||
return LoTWContact(record, fields, signData, hash)
|
return LoTWContact(record, fields, signData, hash)
|
||||||
|
|||||||
+10
-3
@@ -167,7 +167,7 @@ class LoTWUploadRepository internal constructor(
|
|||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
when {
|
when {
|
||||||
record.status != QsoStatus.COMPLETE -> unavailable++
|
record.status != QsoStatus.COMPLETE -> unavailable++
|
||||||
record.lotwReceived -> uploaded++
|
record.lotwReceived || record.lotwUploaded -> uploaded++
|
||||||
else -> {
|
else -> {
|
||||||
val contact = try {
|
val contact = try {
|
||||||
signing.signer.contact(record, signing.key, signing.location, now())
|
signing.signer.contact(record, signing.key, signing.location, now())
|
||||||
@@ -199,8 +199,15 @@ class LoTWUploadRepository internal constructor(
|
|||||||
val unique = hashSetOf<String>()
|
val unique = hashSetOf<String>()
|
||||||
val contacts = records.sortedBy { it.startUtcMillis }.mapNotNull { record ->
|
val contacts = records.sortedBy { it.startUtcMillis }.mapNotNull { record ->
|
||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
if (record.status != QsoStatus.COMPLETE || (record.lotwReceived && !resubmit)) { skipped++; return@mapNotNull null }
|
if (record.status != QsoStatus.COMPLETE || ((record.lotwReceived || record.lotwUploaded) && !resubmit)) { skipped++; return@mapNotNull null }
|
||||||
val contact = signing.signer.contact(record, signing.key, signing.location, now())
|
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]
|
val previous = ledger[contact.fingerprint]
|
||||||
when {
|
when {
|
||||||
(previous == "accepted" && !resubmit) || !unique.add(contact.fingerprint) -> { skipped++; null }
|
(previous == "accepted" && !resubmit) || !unique.add(contact.fingerprint) -> { skipped++; null }
|
||||||
|
|||||||
Reference in new issue
Block a user