diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go index 83170b374b54c..6757c11cb6c56 100644 --- a/modules/timeutil/datetime.go +++ b/modules/timeutil/datetime.go @@ -7,6 +7,7 @@ import ( "fmt" "html" "html/template" + "strconv" "time" ) @@ -58,3 +59,22 @@ func DateTime(format string, datetime any) template.HTML { } panic(fmt.Sprintf("Unsupported format %s", format)) } + +func ParseDateTimeGraceful(datetime any) (time.Time, error) { + switch val := datetime.(type) { + case string: + if timestamp, err := strconv.ParseInt(val, 10, 64); err == nil { + return time.Unix(val, 0), nil + } + + t, err := time.Parse(time.RFC3339, val) + if err != nil { + return time.Time{}, err + } + return t, nil + case int64: + return time.Unix(val, 0), nil + default: + return time.Time{}, fmt.Errorf("unsupported data type: %T", datetime) + } +} diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go index f44b7aaae3c1b..898391eb5691e 100644 --- a/modules/timeutil/datetime_test.go +++ b/modules/timeutil/datetime_test.go @@ -43,3 +43,71 @@ func TestDateTime(t *testing.T) { actual = DateTime("full", refTimeStamp) assert.EqualValues(t, `2017-12-31 19:00:00 -05:00`, actual) } + +func TestParseDateTimeGraceful(t *testing.T) { + testData := []struct { + value any + expected any + isFail bool + }{ + { + value: "2006-01-02T15:04:05Z", + expected: int64(1136214245), + }, + { + value: "2006-01-02T15:04:05.999Z", + expected: int64(1136214245), + }, + { + value: "2006-01-02T15:04:05-07:00", + expected: int64(1136239445), + }, + { + value: "2006-01-02T15:04:05.999-07:00", + expected: int64(1136239445), + }, + { + value: int64(1136214245), + expected: int64(1136214245), + }, + { + value: "1136214245", + expected: int64(1136214245), + }, + { + value: int64(1622040867000), + expected: int64(1622040867000), + }, + { + value: "1622040867000", + expected: int64(1622040867000), + }, + { + value: 0, + expected: int64(-62135596800), + isFail: true, + }, + { + value: nil, + expected: int64(-62135596800), + isFail: true, + }, + { + value: "", + expected: int64(-62135596800), + isFail: true, + }, + } + + for _, testCase := range testData { + actual, err := ParseDateTimeGraceful(testCase.value) + + if testCase.isFail == true { + assert.NotNil(t, err) + assert.True(t, actual.IsZero()) + } else { + assert.Nil(t, err) + assert.EqualValues(t, testCase.expected, actual.Unix()) + } + } +} diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index bd3b86a6f1525..744fe9bc70274 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -6,12 +6,12 @@ package notify import ( "net/http" "strings" - "time" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/convert" ) @@ -181,7 +181,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { lastRead := int64(0) qLastRead := ctx.FormTrim("last_read_at") if len(qLastRead) > 0 { - tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) + tmpLastRead, err := timeutil.ParseDateTimeGraceful(qLastRead) if err != nil { ctx.InternalServerError(err) return diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 2261610c09238..02c1de512c037 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -5,11 +5,11 @@ package notify import ( "net/http" - "time" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/convert" ) @@ -130,11 +130,12 @@ func ReadNotifications(ctx *context.APIContext) { lastRead := int64(0) qLastRead := ctx.FormTrim("last_read_at") if len(qLastRead) > 0 { - tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) + tmpLastRead, err := timeutil.ParseDateTimeGraceful(qLastRead) if err != nil { ctx.InternalServerError(err) return } + if !tmpLastRead.IsZero() { lastRead = tmpLastRead.Unix() } diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go index 0ff13704cf69d..79bbc042ffb69 100644 --- a/tests/integration/api_notification_test.go +++ b/tests/integration/api_notification_test.go @@ -115,8 +115,12 @@ func TestAPINotification(t *testing.T) { DecodeJSON(t, resp, &apiNL) assert.Len(t, apiNL, 2) - lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ... - req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) + lastReadAt1 := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ... + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt1, token)) + MakeRequest(t, req, http.StatusResetContent) + + lastReadAt2 := "946687801" + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt2, token)) MakeRequest(t, req, http.StatusResetContent) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))