diff options
Diffstat (limited to 'internal/processing/status/create.go')
-rw-r--r-- | internal/processing/status/create.go | 118 |
1 files changed, 114 insertions, 4 deletions
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index b77d0af9c..46052d0aa 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -19,10 +19,14 @@ package status import ( "context" + "errors" "time" "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" @@ -92,11 +96,54 @@ func (p *Processor) Create( // Get current time. now := time.Now() + // Default to current time as creation time. + createdAt := now + + // Handle backfilled/scheduled statuses. + backfill := false + if form.ScheduledAt != nil { + scheduledAt := *form.ScheduledAt + + // Statuses may only be scheduled a minimum time into the future. + if now.Before(scheduledAt) { + const errText = "scheduled statuses are not yet supported" + err := gtserror.New(errText) + return nil, gtserror.NewErrorNotImplemented(err, errText) + } + + // If not scheduled into the future, this status is being backfilled. + if !config.GetInstanceAllowBackdatingStatuses() { + const errText = "backdating statuses has been disabled on this instance" + err := gtserror.New(errText) + return nil, gtserror.NewErrorForbidden(err) + } + + // Statuses can't be backdated to or before the UNIX epoch + // since this would prevent generating a ULID. + // If backdated even further to the Go epoch, + // this would also cause issues with time.Time.IsZero() checks + // that normally signify an absent optional time, + // but this check covers both cases. + if scheduledAt.Compare(time.UnixMilli(0)) <= 0 { + const errText = "statuses can't be backdated to or before the UNIX epoch" + err := gtserror.New(errText) + return nil, gtserror.NewErrorNotAcceptable(err, errText) + } + + // Allow the backfill and generate an appropriate ID for the creation time. + backfill = true + createdAt = scheduledAt + var err error + if statusID, err = p.backfilledStatusID(ctx, createdAt); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } + status := >smodel.Status{ ID: statusID, URI: accountURIs.StatusesURI + "/" + statusID, URL: accountURIs.StatusesURL + "/" + statusID, - CreatedAt: now, + CreatedAt: createdAt, Local: util.Ptr(true), Account: requester, AccountID: requester.ID, @@ -134,11 +181,24 @@ func (p *Processor) Create( PendingApproval: util.Ptr(false), } + if backfill { + log.Infof(ctx, "%d mentions", len(status.Mentions)) + for _, mention := range status.Mentions { + log.Infof(ctx, "mention: target account ID = %s, requester ID = %s", mention.TargetAccountID, requester.ID) + if mention.TargetAccountID != requester.ID { + const errText = "statuses mentioning others can't be backfilled" + err := gtserror.New(errText) + return nil, gtserror.NewErrorForbidden(err, errText) + } + } + } + // Check + attach in-reply-to status. if errWithCode := p.processInReplyTo(ctx, requester, status, form.InReplyToID, + backfill, ); errWithCode != nil { return nil, errWithCode } @@ -165,11 +225,17 @@ func (p *Processor) Create( } if form.Poll != nil { + if backfill { + const errText = "statuses with polls can't be backfilled" + err := gtserror.New(errText) + return nil, gtserror.NewErrorForbidden(err, errText) + } + // Process poll, inserting into database. poll, errWithCode := p.processPoll(ctx, statusID, form.Poll, - now, + createdAt, ) if errWithCode != nil { return nil, errWithCode @@ -200,10 +266,14 @@ func (p *Processor) Create( } // Send it to the client API worker for async side-effects. + var model any = status + if backfill { + model = >smodel.BackfillStatus{Status: status} + } p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{ APObjectType: ap.ObjectNote, APActivityType: ap.ActivityCreate, - GTSModel: status, + GTSModel: model, Origin: requester, }) @@ -227,7 +297,40 @@ func (p *Processor) Create( return p.c.GetAPIStatus(ctx, requester, status) } -func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode { +// backfilledStatusID tries to find an unused ULID for a backfilled status. +func (p *Processor) backfilledStatusID(ctx context.Context, createdAt time.Time) (string, error) { + // backfilledStatusIDRetries should be more than enough attempts. + const backfilledStatusIDRetries = 100 + + for try := 0; try < backfilledStatusIDRetries; try++ { + var err error + + // Generate a ULID based on the backfilled status's original creation time. + statusID := id.NewULIDFromTime(createdAt) + + // Check for an existing status with that ID. + _, err = p.state.DB.GetStatusByID(gtscontext.SetBarebones(ctx), statusID) + if errors.Is(err, db.ErrNoEntries) { + // We found an unused one. + return statusID, nil + } else if err != nil { + err := gtserror.Newf("DB error checking if a status ID was in use: %w", err) + return "", err + } + // That status ID is in use. Try again. + } + + err := gtserror.Newf("failed to find an unused ID after %d tries", backfilledStatusIDRetries) + return "", err +} + +func (p *Processor) processInReplyTo( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, + inReplyToID string, + backfill bool, +) gtserror.WithCode { if inReplyToID == "" { // Not a reply. // Nothing to do. @@ -269,6 +372,13 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac return gtserror.NewErrorForbidden(err, errText) } + // When backfilling, only self-replies are allowed. + if backfill && requester.ID != inReplyTo.AccountID { + const errText = "replies to others can't be backfilled" + err := gtserror.New(errText) + return gtserror.NewErrorForbidden(err, errText) + } + // Derive pendingApproval status. var pendingApproval bool switch { |