diff options
author | 2025-02-12 09:49:33 -0800 | |
---|---|---|
committer | 2025-02-12 09:49:33 -0800 | |
commit | fccb0bc102f2a54a21eed343cda64f9a5221b677 (patch) | |
tree | b7c1858f4a92841dfaf59e7102189a635d7136f8 /internal/api | |
parent | [performance] improved enum migrations (#3782) (diff) | |
download | gotosocial-fccb0bc102f2a54a21eed343cda64f9a5221b677.tar.xz |
[feature] Implement backfilling statuses thru scheduled_at (#3685)
* Implement backfilling statuses thru scheduled_at
* Forbid mentioning others in backfills
* Update error messages & codes
* Add new tests for backfilled statuses
* Test that backfilling doesn't timeline or notify
* Fix check for absence of notification
* Test that backfills do not cause federation
* Fix type of apimodel.StatusCreateRequest.ScheduledAt in tests
* Add config file switch and min date check
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client/statuses/statuscreate.go | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statuscreate_test.go | 132 | ||||
-rw-r--r-- | internal/api/model/status.go | 15 |
3 files changed, 147 insertions, 14 deletions
diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index d187e823f..bfb1c486d 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -179,11 +179,15 @@ import ( // x-go-name: ScheduledAt // description: |- // ISO 8601 Datetime at which to schedule a status. -// Providing this parameter will cause ScheduledStatus to be returned instead of Status. +// +// Providing this parameter with a *future* time will cause ScheduledStatus to be returned instead of Status. // Must be at least 5 minutes in the future. +// This feature isn't implemented yet. // -// This feature isn't implemented yet; attemping to set it will return 501 Not Implemented. +// Providing this parameter with a *past* time will cause the status to be backdated, +// and will not push it to the user's followers. This is intended for importing old statuses. // type: string +// format: date-time // in: formData // - // name: language @@ -384,12 +388,6 @@ func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, gtser return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text) } - // Check not scheduled status. - if form.ScheduledAt != "" { - const text = "scheduled_at is not yet implemented" - return nil, gtserror.NewErrorNotImplemented(errors.New(text), text) - } - // Check if the deprecated "federated" field was // set in lieu of "local_only", and use it if so. if form.LocalOnly == nil && form.Federated != nil { // nolint:staticcheck diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index 227e7d83e..53e517a6e 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -20,14 +20,18 @@ package statuses_test import ( "bytes" "context" + "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -41,10 +45,11 @@ const ( statusMarkdown = "# Title\n\n## Smaller title\n\nThis is a post written in [markdown](https://www.markdownguide.org/)\n\n<img src=\"https://d33wubrfki0l68.cloudfront.net/f1f475a6fda1c2c4be4cac04033db5c3293032b4/513a4/assets/images/markdown-mark-white.svg\"/>" ) -func (suite *StatusCreateTestSuite) postStatus( +// Post a status. +func (suite *StatusCreateTestSuite) postStatusCore( formData map[string][]string, jsonData string, -) (string, *httptest.ResponseRecorder) { +) *httptest.ResponseRecorder { recorder := httptest.NewRecorder() ctx, _ := testrig.CreateGinTestContext(recorder, nil) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) @@ -77,9 +82,42 @@ func (suite *StatusCreateTestSuite) postStatus( // Trigger handler. suite.statusModule.StatusCreatePOSTHandler(ctx) + + return recorder +} + +// Post a status and return the result as deterministic JSON. +func (suite *StatusCreateTestSuite) postStatus( + formData map[string][]string, + jsonData string, +) (string, *httptest.ResponseRecorder) { + recorder := suite.postStatusCore(formData, jsonData) return suite.parseStatusResponse(recorder) } +// Post a status and return the result as a non-deterministic API structure. +func (suite *StatusCreateTestSuite) postStatusStruct( + formData map[string][]string, + jsonData string, +) (*apimodel.Status, *httptest.ResponseRecorder) { + recorder := suite.postStatusCore(formData, jsonData) + + result := recorder.Result() + defer result.Body.Close() + + data, err := io.ReadAll(result.Body) + if err != nil { + suite.FailNow(err.Error()) + } + + apiStatus := apimodel.Status{} + if err := json.Unmarshal(data, &apiStatus); err != nil { + suite.FailNow(err.Error()) + } + + return &apiStatus, recorder +} + // Post a new status with some custom visibility settings func (suite *StatusCreateTestSuite) TestPostNewStatus() { out, recorder := suite.postStatus(map[string][]string{ @@ -383,10 +421,98 @@ func (suite *StatusCreateTestSuite) TestPostNewScheduledStatus() { // We should have a helpful error message. suite.Equal(`{ - "error": "Not Implemented: scheduled_at is not yet implemented" + "error": "Not Implemented: scheduled statuses are not yet supported" }`, out) } +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatus() { + // A time in the past. + scheduledAtStr := "2020-10-04T15:32:02.018Z" + scheduledAt, err := time.Parse(time.RFC3339Nano, scheduledAtStr) + if err != nil { + suite.FailNow(err.Error()) + } + + status, recorder := suite.postStatusStruct(map[string][]string{ + "status": {"this is a recycled status from the past!"}, + "scheduled_at": {scheduledAtStr}, + }, "") + + // Creating a status in the past should succeed. + suite.Equal(http.StatusOK, recorder.Code) + + // The status should be backdated. + createdAt, err := time.Parse(time.RFC3339Nano, status.CreatedAt) + if err != nil { + suite.FailNow(err.Error()) + return + } + suite.Equal(scheduledAt, createdAt.UTC()) + + // The status's ULID should be backdated. + timeFromULID, err := id.TimeFromULID(status.ID) + if err != nil { + suite.FailNow(err.Error()) + return + } + suite.Equal(scheduledAt, timeFromULID.UTC()) +} + +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatusWithSelfMention() { + _, recorder := suite.postStatus(map[string][]string{ + "status": {"@the_mighty_zork this is a recycled mention from the past!"}, + "scheduled_at": {"2020-10-04T15:32:02.018Z"}, + }, "") + + // Mentioning yourself is allowed in backfilled statuses. + suite.Equal(http.StatusOK, recorder.Code) +} + +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatusWithMention() { + _, recorder := suite.postStatus(map[string][]string{ + "status": {"@admin this is a recycled mention from the past!"}, + "scheduled_at": {"2020-10-04T15:32:02.018Z"}, + }, "") + + // Mentioning others is forbidden in backfilled statuses. + suite.Equal(http.StatusForbidden, recorder.Code) +} + +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatusWithSelfReply() { + _, recorder := suite.postStatus(map[string][]string{ + "status": {"this is a recycled reply from the past!"}, + "scheduled_at": {"2020-10-04T15:32:02.018Z"}, + "in_reply_to_id": {suite.testStatuses["local_account_1_status_1"].ID}, + }, "") + + // Replying to yourself is allowed in backfilled statuses. + suite.Equal(http.StatusOK, recorder.Code) +} + +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatusWithReply() { + _, recorder := suite.postStatus(map[string][]string{ + "status": {"this is a recycled reply from the past!"}, + "scheduled_at": {"2020-10-04T15:32:02.018Z"}, + "in_reply_to_id": {suite.testStatuses["admin_account_status_1"].ID}, + }, "") + + // Replying to others is forbidden in backfilled statuses. + suite.Equal(http.StatusForbidden, recorder.Code) +} + +func (suite *StatusCreateTestSuite) TestPostNewBackfilledStatusWithPoll() { + _, recorder := suite.postStatus(map[string][]string{ + "status": {"this is a recycled poll from the past!"}, + "scheduled_at": {"2020-10-04T15:32:02.018Z"}, + "poll[options][]": {"first option", "second option"}, + "poll[expires_in]": {"3600"}, + "poll[multiple]": {"true"}, + }, "") + + // Polls are forbidden in backfilled statuses. + suite.Equal(http.StatusForbidden, recorder.Code) +} + func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { out, recorder := suite.postStatus(map[string][]string{ "status": {statusMarkdown}, diff --git a/internal/api/model/status.go b/internal/api/model/status.go index ea9fbaa35..2ee3123e6 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -17,7 +17,11 @@ package model -import "github.com/superseriousbusiness/gotosocial/internal/language" +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/language" +) // Status models a status or post. // @@ -231,9 +235,14 @@ type StatusCreateRequest struct { Federated *bool `form:"federated" json:"federated"` // ISO 8601 Datetime at which to schedule a status. - // Providing this parameter will cause ScheduledStatus to be returned instead of Status. + // + // Providing this parameter with a *future* time will cause ScheduledStatus to be returned instead of Status. // Must be at least 5 minutes in the future. - ScheduledAt string `form:"scheduled_at" json:"scheduled_at"` + // This feature isn't implemented yet. + // + // Providing this parameter with a *past* time will cause the status to be backdated, + // and will not push it to the user's followers. This is intended for importing old statuses. + ScheduledAt *time.Time `form:"scheduled_at" json:"scheduled_at"` // ISO 639 language code for this status. Language string `form:"language" json:"language"` |