scan-alert-hardening: different-finder re-alert within 250m window + fingerprint 24h block (12/12 scenarios pass)
This commit is contained in:
@@ -23,8 +23,10 @@ const (
|
||||
|
||||
// ScanRequest is the JSON body posted by the geolocation script.
|
||||
type ScanRequest struct {
|
||||
Lat *float64 `json:"lat"`
|
||||
Lng *float64 `json:"lng"`
|
||||
Lat *float64 `json:"lat"`
|
||||
Lng *float64 `json:"lng"`
|
||||
Phone *string `json:"phone"`
|
||||
Fingerprint *string `json:"fingerprint"`
|
||||
}
|
||||
|
||||
// Scan records a scan (with optional location) and alerts the owner per the
|
||||
@@ -56,13 +58,15 @@ func (a *App) Scan(w http.ResponseWriter, r *http.Request) {
|
||||
Lat: lat,
|
||||
Lng: lng,
|
||||
LocationShared: hasLoc,
|
||||
ScannerPhone: textOrNil(ptrStr(req.Phone)),
|
||||
Fingerprint: textOrNil(ptrStr(req.Fingerprint)),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
alertSent := a.shouldAlert(r.Context(), tag, scan, hasLoc, req.Lat, req.Lng)
|
||||
alertSent := a.shouldAlert(r.Context(), tag, scan, hasLoc, req.Lat, req.Lng, ptrStr(req.Phone))
|
||||
if alertSent {
|
||||
if a.alertOwner(r.Context(), tag, scan, req.Lat, req.Lng) {
|
||||
_ = a.Queries.SetScanAlertSent(r.Context(), db.SetScanAlertSentParams{ID: scan.ID, AlertSent: true})
|
||||
@@ -73,7 +77,9 @@ func (a *App) Scan(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// shouldAlert applies the throttle rules and sms_enabled flag.
|
||||
func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc bool, lat, lng *float64) bool {
|
||||
// Order: sms_enabled → owner phone → 24 h fingerprint block → 10-min window
|
||||
// (re-alert on movement >250 m OR a different finder phone).
|
||||
func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc bool, lat, lng *float64, phone string) bool {
|
||||
if !tag.SmsEnabled {
|
||||
return false
|
||||
}
|
||||
@@ -81,6 +87,13 @@ func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc
|
||||
return false
|
||||
}
|
||||
|
||||
// 24 h per-device block: same fingerprint seen recently (excluding this scan) => record only.
|
||||
if fp := scan.Fingerprint.String; fp != "" {
|
||||
if _, err := a.Queries.GetRecentScanByFingerprint(ctx, db.GetRecentScanByFingerprintParams{Fingerprint: pgtype.Text{String: fp, Valid: true}, ID: scan.ID}); err == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
last, err := a.Queries.GetLastAlertByTag(ctx, tag.ID)
|
||||
if err != nil {
|
||||
return errors.Is(err, pgx.ErrNoRows) // no prior alert -> alert
|
||||
@@ -88,11 +101,11 @@ func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc
|
||||
|
||||
// Within the window?
|
||||
if time.Since(last.ScannedAt.Time) < alertWindow {
|
||||
// Re-alert only if we have a location and it moved >250 m.
|
||||
if !hasLoc || !last.Lat.Valid || !last.Lng.Valid {
|
||||
return false
|
||||
}
|
||||
return sms.HaversineMeters(last.Lat.Float64, last.Lng.Float64, *lat, *lng) > alertMinDistance
|
||||
// Re-alert on movement >250 m, or a different finder phone.
|
||||
moved := hasLoc && last.Lat.Valid && last.Lng.Valid &&
|
||||
sms.HaversineMeters(last.Lat.Float64, last.Lng.Float64, *lat, *lng) > alertMinDistance
|
||||
differentPhone := phone != "" && phone != last.ScannerPhone.String
|
||||
return moved || differentPhone
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -136,6 +149,8 @@ func alertMessage(tag db.Tag, t time.Time, lat, lng *float64) string {
|
||||
}
|
||||
|
||||
// FinderContact stores a finder's number on the latest scan and alerts the owner.
|
||||
// Dedup: same phone within the alert window => store only; fingerprint seen in
|
||||
// the last 24 h => store only.
|
||||
func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
tag, err := a.Queries.GetTagByCode(r.Context(), r.PathValue("tag_code"))
|
||||
if err != nil {
|
||||
@@ -153,6 +168,7 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `<p class="mt-4 rounded border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">Please enter a valid phone number.</p>`)
|
||||
return
|
||||
}
|
||||
fingerprint := strings.TrimSpace(r.FormValue("fingerprint"))
|
||||
|
||||
latest, err := a.Queries.GetLatestScanByTag(r.Context(), tag.ID)
|
||||
if err != nil {
|
||||
@@ -160,19 +176,40 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// No scan yet: record one carrying the finder's number.
|
||||
_, err = a.Queries.InsertScan(r.Context(), db.InsertScanParams{
|
||||
// No scan yet: record one carrying the finder's number + fingerprint.
|
||||
ins, insErr := a.Queries.InsertScan(r.Context(), db.InsertScanParams{
|
||||
TagID: tag.ID, ScannerPhone: pgtype.Text{String: phone, Valid: true},
|
||||
Fingerprint: textOrNil(fingerprint),
|
||||
})
|
||||
if insErr != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
latest = ins
|
||||
} else {
|
||||
err = a.Queries.SetScanPhone(r.Context(), db.SetScanPhoneParams{ID: latest.ID, ScannerPhone: pgtype.Text{String: phone, Valid: true}})
|
||||
err = a.Queries.SetScanPhone(r.Context(), db.SetScanPhoneParams{ID: latest.ID, ScannerPhone: pgtype.Text{String: phone, Valid: true}, Fingerprint: textOrNil(fingerprint)})
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if tag.SmsEnabled && tag.Phone.Valid && a.Sender != nil {
|
||||
// Should we notify? Skip when the same phone was alerted within the window,
|
||||
// or when this device (fingerprint) has already alerted in the last 24 h.
|
||||
notify := tag.SmsEnabled && tag.Phone.Valid && a.Sender != nil
|
||||
if notify && fingerprint != "" {
|
||||
if _, err := a.Queries.GetRecentScanByFingerprint(r.Context(), db.GetRecentScanByFingerprintParams{Fingerprint: pgtype.Text{String: fingerprint, Valid: true}, ID: latest.ID}); err == nil {
|
||||
notify = false
|
||||
}
|
||||
}
|
||||
if notify {
|
||||
if last, err := a.Queries.GetLastAlertByTag(r.Context(), tag.ID); err == nil &&
|
||||
time.Since(last.ScannedAt.Time) < alertWindow && last.ScannerPhone.String == phone {
|
||||
notify = false
|
||||
}
|
||||
}
|
||||
|
||||
if notify {
|
||||
itemType := "item"
|
||||
if tag.ItemType.Valid {
|
||||
itemType = tag.ItemType.String
|
||||
@@ -181,6 +218,9 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
itemType, phone, tag.TagCode)
|
||||
if err := a.Sender.Send(sms.NormalizeAU(tag.Phone.String), msg); err != nil {
|
||||
fmt.Println("contact sms failed:", err)
|
||||
} else {
|
||||
// Mark alerted so dedup (same phone / same device) can see it.
|
||||
_ = a.Queries.SetScanAlertSent(r.Context(), db.SetScanAlertSentParams{ID: latest.ID, AlertSent: true})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +228,13 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `<p class="mt-4 rounded border border-green-200 bg-green-50 px-4 py-2 text-sm text-green-800">Thanks! The owner has been notified with your number.</p>`)
|
||||
}
|
||||
|
||||
func ptrStr(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
|
||||
Reference in New Issue
Block a user