From 1d7d82b793b0e3c54953416bed9f7892cc6a1e0b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 27 May 2025 14:57:56 +0100 Subject: [PATCH] search: fix server-side date behaviour OffsetMinutesFromUTC was being passed incorrectly by the web app (getTimezoneOffset if UTC - Timezone, we wanted Timezone - UTC), now fixed. This value is now used if given in comparisons. Times are truncated to minute-deep precision, and Any date comparison ignores empty date fields (i.e. a unix timestamp being 0 or a time.Time.IsZero() == true). --- activitysort.go | 2 +- ts/modules/common.ts | 7 ++++++- ts/modules/search.ts | 2 +- ts/typings/d.ts | 1 + usercache.go | 39 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/activitysort.go b/activitysort.go index d7aab59..63d27c5 100644 --- a/activitysort.go +++ b/activitysort.go @@ -249,7 +249,7 @@ func matchTimeAsQuery(query *badgerhold.Query, q QueryDTO) *badgerhold.Query { } criterion := andField(query, "Time") query = criterion.MatchFunc(func(ra *badgerhold.RecordAccess) (bool, error) { - return q.Value.(DateAttempt).Compare(ra.Field().(time.Time)) == int(operator), nil + return q.Value.(DateAttempt).CompareWithOperator(ra.Field().(time.Time), operator), nil }) return query } diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 768591f..6a3319f 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -30,7 +30,12 @@ export const parseDateString = (value: string): ParsedDate => { // note Date.fromString is also provided by dateParser. date: (Date as any).fromString(value) as Date }; - out.attempt.offsetMinutesFromUTC = out.date.getTimezoneOffset(); + if (("invalid" in (out.date as any))) { + out.invalid = true; + } else { + // getTimezoneOffset returns UTC - Timezone, so invert it to get distance from UTC -to- timezone. + out.attempt.offsetMinutesFromUTC = -1 * out.date.getTimezoneOffset(); + } // Month in Date objects is 0-based, so make our parsed date that way too if ("month" in out.attempt) out.attempt.month -= 1; return out; diff --git a/ts/modules/search.ts b/ts/modules/search.ts index 8d4d139..ac92692 100644 --- a/ts/modules/search.ts +++ b/ts/modules/search.ts @@ -219,7 +219,7 @@ export class DateQuery extends Query { } let out = parseDateString(valueString); let isValid = true; - if ("invalid" in (out.date as any)) { isValid = false; }; + if (out.invalid) isValid = false; return [out, op, isValid]; } diff --git a/ts/typings/d.ts b/ts/typings/d.ts index e3c5a3c..e0038ca 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -174,6 +174,7 @@ interface ParsedDate { attempt: DateAttempt; date: Date; text: string; + invalid?: boolean; }; declare var config: Object; diff --git a/usercache.go b/usercache.go index b26be92..b815e00 100644 --- a/usercache.go +++ b/usercache.go @@ -243,10 +243,34 @@ type DateAttempt struct { OffsetMinutesFromUTC *int `json:"offsetMinutesFromUTC,omitempty"` } +// CompareWithOperator roughly compares a time.Time to a DateAttempt according to the given operator. +// **Considers zero-dates as invalid!** (i.e. any comparison to a subject.IsZero() will be false). +func (d DateAttempt) CompareWithOperator(subject time.Time, operator CompareResult) bool { + if subject.IsZero() { + return false + } + return d.Compare(subject) == int(operator) +} + +// CompareUnixWithOperator roughly compares a unix timestamp to a DateAttempt according to the given operator. +// **Considers zero-dates as invalid!** (i.e. any comparison to a time.Unix(subject, 0).IsZero() or (subject == 0) will be false). +func (d DateAttempt) CompareUnixWithOperator(subject int64, operator CompareResult) bool { + if subject == 0 { + return false + } + subjectTime := time.Unix(subject, 0) + if subjectTime.IsZero() { + return false + } + return d.Compare(subjectTime) == int(operator) +} + // Compare roughly compares a time.Time to a DateAttempt. // We want to compare only the fields given in DateAttempt, // so we copy subjectDate and apply on those fields from this._value. func (d DateAttempt) Compare(subject time.Time) int { + // Remove anything more precise than a second + subject = subject.Truncate(time.Minute) yy, mo, dd := subject.Date() hh, mm, _ := subject.Clock() if d.Year != nil { @@ -265,8 +289,17 @@ func (d DateAttempt) Compare(subject time.Time) int { if d.Minute != nil { mm = *d.Minute } + + location := time.UTC + if d.OffsetMinutesFromUTC != nil { + location = time.FixedZone("", 60*(*d.OffsetMinutesFromUTC)) + } + // FIXME: Transmit timezone in request maybe? - return subject.Compare(time.Date(yy, mo, dd, hh, mm, 0, 0, time.UTC)) + daAsTime := time.Date(yy, mo, dd, hh, mm, 0, 0, location) + comp := subject.Compare(daAsTime) + + return comp } // CompareUnix roughly compares a unix timestamp to a DateAttempt. @@ -310,7 +343,7 @@ func (q QueryDTO) AsFilter() Filter { switch q.Class { case DateQuery: return func(a *respUser) bool { - return q.Value.(DateAttempt).CompareUnix(a.LastActive) == int(operator) + return q.Value.(DateAttempt).CompareUnixWithOperator(a.LastActive, operator) } case BoolQuery: return func(a *respUser) bool { @@ -329,7 +362,7 @@ func (q QueryDTO) AsFilter() Filter { switch q.Class { case DateQuery: return func(a *respUser) bool { - return q.Value.(DateAttempt).CompareUnix(a.Expiry) == int(operator) + return q.Value.(DateAttempt).CompareUnixWithOperator(a.Expiry, operator) } case BoolQuery: return func(a *respUser) bool {