summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2025-02-12 09:49:33 -0800
committerLibravatar GitHub <noreply@github.com>2025-02-12 09:49:33 -0800
commitfccb0bc102f2a54a21eed343cda64f9a5221b677 (patch)
treeb7c1858f4a92841dfaf59e7102189a635d7136f8 /internal/api
parent[performance] improved enum migrations (#3782) (diff)
downloadgotosocial-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.go14
-rw-r--r--internal/api/client/statuses/statuscreate_test.go132
-rw-r--r--internal/api/model/status.go15
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"`