diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index f1c382d11..5554445b2 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -93,6 +93,7 @@ func (c *Caches) Init() {
c.initPollVote()
c.initPollVoteIDs()
c.initReport()
+ c.initSinBinStatus()
c.initStatus()
c.initStatusBookmark()
c.initStatusBookmarkIDs()
@@ -170,6 +171,7 @@ func (c *Caches) Sweep(threshold float64) {
c.DB.PollVote.Trim(threshold)
c.DB.PollVoteIDs.Trim(threshold)
c.DB.Report.Trim(threshold)
+ c.DB.SinBinStatus.Trim(threshold)
c.DB.Status.Trim(threshold)
c.DB.StatusBookmark.Trim(threshold)
c.DB.StatusBookmarkIDs.Trim(threshold)
diff --git a/internal/cache/db.go b/internal/cache/db.go
index 5e86c92a2..7f54ee8c5 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -145,6 +145,9 @@ type DBCaches struct {
// Report provides access to the gtsmodel Report database cache.
Report StructCache[*gtsmodel.Report]
+ // SinBinStatus provides access to the gtsmodel SinBinStatus database cache.
+ SinBinStatus StructCache[*gtsmodel.SinBinStatus]
+
// Status provides access to the gtsmodel Status database cache.
Status StructCache[*gtsmodel.Status]
@@ -1170,6 +1173,32 @@ func (c *Caches) initReport() {
})
}
+func (c *Caches) initSinBinStatus() {
+ // Calculate maximum cache size.
+ cap := calculateResultCacheMax(
+ sizeofSinBinStatus(), // model in-mem size.
+ config.GetCacheSinBinStatusMemRatio(),
+ )
+
+ log.Infof(nil, "cache size = %d", cap)
+
+ copyF := func(s1 *gtsmodel.SinBinStatus) *gtsmodel.SinBinStatus {
+ s2 := new(gtsmodel.SinBinStatus)
+ *s2 = *s1
+ return s2
+ }
+
+ c.DB.SinBinStatus.Init(structr.CacheConfig[*gtsmodel.SinBinStatus]{
+ Indices: []structr.IndexConfig{
+ {Fields: "ID"},
+ {Fields: "URI"},
+ },
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ Copy: copyF,
+ })
+}
+
func (c *Caches) initStatus() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
diff --git a/internal/cache/size.go b/internal/cache/size.go
index 29ab77fbf..49c2f4318 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -593,6 +593,29 @@ func sizeofReport() uintptr {
}))
}
+func sizeofSinBinStatus() uintptr {
+ return uintptr(size.Of(>smodel.SinBinStatus{
+ ID: exampleID,
+ CreatedAt: exampleTime,
+ UpdatedAt: exampleTime,
+ URI: exampleURI,
+ URL: exampleURI,
+ Domain: exampleURI,
+ AccountURI: exampleURI,
+ InReplyToURI: exampleURI,
+ Content: exampleText,
+ AttachmentLinks: []string{exampleURI, exampleURI},
+ MentionTargetURIs: []string{exampleURI},
+ EmojiLinks: []string{exampleURI},
+ PollOptions: []string{exampleTextSmall, exampleTextSmall, exampleTextSmall, exampleTextSmall},
+ ContentWarning: exampleTextSmall,
+ Visibility: gtsmodel.VisibilityPublic,
+ Sensitive: util.Ptr(false),
+ Language: "en",
+ ActivityStreamsType: ap.ObjectNote,
+ }))
+}
+
func sizeofStatus() uintptr {
return uintptr(size.Of(>smodel.Status{
ID: exampleID,
diff --git a/internal/config/config.go b/internal/config/config.go
index d6b8f0a54..e24cb639b 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -230,6 +230,7 @@ type CacheConfiguration struct {
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
ReportMemRatio float64 `name:"report-mem-ratio"`
+ SinBinStatusMemRatio float64 `name:"sin-bin-status-mem-ratio"`
StatusMemRatio float64 `name:"status-mem-ratio"`
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index e71711cb3..58e11a292 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -193,6 +193,7 @@ var Defaults = Configuration{
PollVoteMemRatio: 2,
PollVoteIDsMemRatio: 2,
ReportMemRatio: 1,
+ SinBinStatusMemRatio: 0.5,
StatusMemRatio: 5,
StatusBookmarkMemRatio: 0.5,
StatusBookmarkIDsMemRatio: 2,
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index d19e4e241..75231d37b 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -3712,6 +3712,31 @@ func GetCacheReportMemRatio() float64 { return global.GetCacheReportMemRatio() }
// SetCacheReportMemRatio safely sets the value for global configuration 'Cache.ReportMemRatio' field
func SetCacheReportMemRatio(v float64) { global.SetCacheReportMemRatio(v) }
+// GetCacheSinBinStatusMemRatio safely fetches the Configuration value for state's 'Cache.SinBinStatusMemRatio' field
+func (st *ConfigState) GetCacheSinBinStatusMemRatio() (v float64) {
+ st.mutex.RLock()
+ v = st.config.Cache.SinBinStatusMemRatio
+ st.mutex.RUnlock()
+ return
+}
+
+// SetCacheSinBinStatusMemRatio safely sets the Configuration value for state's 'Cache.SinBinStatusMemRatio' field
+func (st *ConfigState) SetCacheSinBinStatusMemRatio(v float64) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.SinBinStatusMemRatio = v
+ st.reloadToViper()
+}
+
+// CacheSinBinStatusMemRatioFlag returns the flag name for the 'Cache.SinBinStatusMemRatio' field
+func CacheSinBinStatusMemRatioFlag() string { return "cache-sin-bin-status-mem-ratio" }
+
+// GetCacheSinBinStatusMemRatio safely fetches the value for global configuration 'Cache.SinBinStatusMemRatio' field
+func GetCacheSinBinStatusMemRatio() float64 { return global.GetCacheSinBinStatusMemRatio() }
+
+// SetCacheSinBinStatusMemRatio safely sets the value for global configuration 'Cache.SinBinStatusMemRatio' field
+func SetCacheSinBinStatusMemRatio(v float64) { global.SetCacheSinBinStatusMemRatio(v) }
+
// GetCacheStatusMemRatio safely fetches the Configuration value for state's 'Cache.StatusMemRatio' field
func (st *ConfigState) GetCacheStatusMemRatio() (v float64) {
st.mutex.RLock()
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index 6ecd43cbc..45607ea15 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -76,6 +76,7 @@ type DBService struct {
db.Rule
db.Search
db.Session
+ db.SinBinStatus
db.Status
db.StatusBookmark
db.StatusFave
@@ -271,6 +272,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
Session: &sessionDB{
db: db,
},
+ SinBinStatus: &sinBinStatusDB{
+ db: db,
+ state: state,
+ },
Status: &statusDB{
db: db,
state: state,
diff --git a/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go
new file mode 100644
index 000000000..d97d35372
--- /dev/null
+++ b/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction.go
@@ -0,0 +1,67 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ if _, err := tx.
+ NewCreateTable().
+ Model(>smodel.SinBinStatus{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ for idx, col := range map[string]string{
+ "sin_bin_statuses_account_uri_idx": "account_uri",
+ "sin_bin_statuses_domain_idx": "domain",
+ "sin_bin_statuses_in_reply_to_uri_idx": "in_reply_to_uri",
+ } {
+ if _, err := tx.
+ NewCreateIndex().
+ Table("sin_bin_statuses").
+ Index(idx).
+ Column(col).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/sinbinstatus.go b/internal/db/bundb/sinbinstatus.go
new file mode 100644
index 000000000..5fc368022
--- /dev/null
+++ b/internal/db/bundb/sinbinstatus.go
@@ -0,0 +1,122 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package bundb
+
+import (
+ "context"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/uptrace/bun"
+)
+
+type sinBinStatusDB struct {
+ db *bun.DB
+ state *state.State
+}
+
+func (s *sinBinStatusDB) GetSinBinStatusByID(ctx context.Context, id string) (*gtsmodel.SinBinStatus, error) {
+ return s.getSinBinStatus(
+ "ID",
+ func(sbStatus *gtsmodel.SinBinStatus) error {
+ return s.db.
+ NewSelect().
+ Model(sbStatus).
+ Where("? = ?", bun.Ident("sin_bin_status.id"), id).
+ Scan(ctx)
+ },
+ id,
+ )
+}
+
+func (s *sinBinStatusDB) GetSinBinStatusByURI(ctx context.Context, uri string) (*gtsmodel.SinBinStatus, error) {
+ return s.getSinBinStatus(
+ "URI",
+ func(sbStatus *gtsmodel.SinBinStatus) error {
+ return s.db.
+ NewSelect().
+ Model(sbStatus).
+ Where("? = ?", bun.Ident("sin_bin_status.uri"), uri).
+ Scan(ctx)
+ },
+ uri,
+ )
+}
+
+func (s *sinBinStatusDB) getSinBinStatus(
+ lookup string,
+ dbQuery func(*gtsmodel.SinBinStatus) error,
+ keyParts ...any,
+) (*gtsmodel.SinBinStatus, error) {
+ // Fetch from database cache with loader callback.
+ return s.state.Caches.DB.SinBinStatus.LoadOne(lookup, func() (*gtsmodel.SinBinStatus, error) {
+ // Not cached! Perform database query.
+ sbStatus := new(gtsmodel.SinBinStatus)
+ if err := dbQuery(sbStatus); err != nil {
+ return nil, err
+ }
+
+ return sbStatus, nil
+ }, keyParts...)
+}
+
+func (s *sinBinStatusDB) PutSinBinStatus(ctx context.Context, sbStatus *gtsmodel.SinBinStatus) error {
+ return s.state.Caches.DB.SinBinStatus.Store(sbStatus, func() error {
+ _, err := s.db.
+ NewInsert().
+ Model(sbStatus).
+ Exec(ctx)
+ return err
+ })
+}
+
+func (s *sinBinStatusDB) UpdateSinBinStatus(
+ ctx context.Context,
+ sbStatus *gtsmodel.SinBinStatus,
+ columns ...string,
+) error {
+ sbStatus.UpdatedAt = time.Now()
+ if len(columns) > 0 {
+ // If we're updating by column,
+ // ensure "updated_at" is included.
+ columns = append(columns, "updated_at")
+ }
+
+ return s.state.Caches.DB.SinBinStatus.Store(sbStatus, func() error {
+ _, err := s.db.
+ NewUpdate().
+ Model(sbStatus).
+ Column(columns...).
+ Where("? = ?", bun.Ident("sin_bin_status.id"), sbStatus.ID).
+ Exec(ctx)
+ return err
+ })
+}
+
+func (s *sinBinStatusDB) DeleteSinBinStatusByID(ctx context.Context, id string) error {
+ // On return ensure status invalidated from cache.
+ defer s.state.Caches.DB.SinBinStatus.Invalidate("ID", id)
+
+ _, err := s.db.
+ NewDelete().
+ TableExpr("? AS ?", bun.Ident("sin_bin_statuses"), bun.Ident("sin_bin_status")).
+ Where("? = ?", bun.Ident("sin_bin_status.id"), id).
+ Exec(ctx)
+ return err
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index cd621871a..c42985912 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -48,6 +48,7 @@ type DB interface {
Rule
Search
Session
+ SinBinStatus
Status
StatusBookmark
StatusFave
diff --git a/internal/db/sinbinstatus.go b/internal/db/sinbinstatus.go
new file mode 100644
index 000000000..16abcf8bd
--- /dev/null
+++ b/internal/db/sinbinstatus.go
@@ -0,0 +1,41 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package db
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+type SinBinStatus interface {
+ // GetSinBinStatusByID fetches the sin bin status from the database with matching id column.
+ GetSinBinStatusByID(ctx context.Context, id string) (*gtsmodel.SinBinStatus, error)
+
+ // GetSinBinStatusByURI fetches the sin bin status from the database with matching uri column.
+ GetSinBinStatusByURI(ctx context.Context, uri string) (*gtsmodel.SinBinStatus, error)
+
+ // PutSinBinStatus stores one sin bin status in the database.
+ PutSinBinStatus(ctx context.Context, sbStatus *gtsmodel.SinBinStatus) error
+
+ // UpdateSinBinStatus updates one sin bin status in the database.
+ UpdateSinBinStatus(ctx context.Context, sbStatus *gtsmodel.SinBinStatus, columns ...string) error
+
+ // DeleteSinBinStatusByID deletes one sin bin status from the database.
+ DeleteSinBinStatusByID(ctx context.Context, id string) error
+}
diff --git a/internal/federation/federatingdb/reject.go b/internal/federation/federatingdb/reject.go
index 929559031..404e19c4c 100644
--- a/internal/federation/federatingdb/reject.go
+++ b/internal/federation/federatingdb/reject.go
@@ -20,12 +20,17 @@ package federatingdb
import (
"context"
"errors"
- "fmt"
+ "time"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
@@ -48,63 +53,450 @@ func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsR
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
- for _, obj := range ap.ExtractObjects(reject) {
+ activityID := ap.GetJSONLDId(reject)
+ if activityID == nil {
+ // We need an ID.
+ const text = "Reject had no id property"
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
- if obj.IsIRI() {
- // we have just the URI of whatever is being rejected, so we need to find out what it is
- rejectedObjectIRI := obj.GetIRI()
- if uris.IsFollowPath(rejectedObjectIRI) {
- // REJECT FOLLOW
- followReq, err := f.state.DB.GetFollowRequestByURI(ctx, rejectedObjectIRI.String())
- if err != nil {
- return fmt.Errorf("Reject: couldn't get follow request with id %s from the database: %s", rejectedObjectIRI.String(), err)
- }
+ for _, object := range ap.ExtractObjects(reject) {
+ if asType := object.GetType(); asType != nil {
+ // Check and handle any
+ // vocab.Type objects.
+ // nolint:gocritic
+ switch asType.GetTypeName() {
- // Make sure the creator of the original follow
- // is the same as whatever inbox this landed in.
- if followReq.AccountID != receivingAcct.ID {
- return errors.New("Reject: follow account and inbox account were not the same")
- }
-
- // Make sure the target of the original follow
- // is the same as the account making the request.
- if followReq.TargetAccountID != requestingAcct.ID {
- return errors.New("Reject: follow target account and requesting account were not the same")
- }
-
- return f.state.DB.RejectFollowRequest(ctx, followReq.AccountID, followReq.TargetAccountID)
- }
- }
-
- if t := obj.GetType(); t != nil {
- // we have the whole object so we can figure out what we're rejecting
// REJECT FOLLOW
- asFollow, ok := t.(vocab.ActivityStreamsFollow)
- if !ok {
- return errors.New("Reject: couldn't parse follow into vocab.ActivityStreamsFollow")
+ case ap.ActivityFollow:
+ if err := f.rejectFollowType(
+ ctx,
+ asType,
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
}
- // convert the follow to something we can understand
- gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
- if err != nil {
- return fmt.Errorf("Reject: error converting asfollow to gtsfollow: %s", err)
- }
+ } else if object.IsIRI() {
+ // Check and handle any
+ // IRI type objects.
+ switch objIRI := object.GetIRI(); {
- // Make sure the creator of the original follow
- // is the same as whatever inbox this landed in.
- if gtsFollow.AccountID != receivingAcct.ID {
- return errors.New("Reject: follow account and inbox account were not the same")
- }
+ // REJECT FOLLOW
+ case uris.IsFollowPath(objIRI):
+ if err := f.rejectFollowIRI(
+ ctx,
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
- // Make sure the target of the original follow
- // is the same as the account making the request.
- if gtsFollow.TargetAccountID != requestingAcct.ID {
- return errors.New("Reject: follow target account and requesting account were not the same")
- }
+ // REJECT STATUS (reply/boost)
+ case uris.IsStatusesPath(objIRI):
+ if err := f.rejectStatusIRI(
+ ctx,
+ activityID.String(),
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
- return f.state.DB.RejectFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID)
+ // REJECT LIKE
+ case uris.IsLikePath(objIRI):
+ if err := f.rejectLikeIRI(
+ ctx,
+ activityID.String(),
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
+ }
}
}
return nil
}
+
+func (f *federatingDB) rejectFollowType(
+ ctx context.Context,
+ asType vocab.Type,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Cast the vocab.Type object to known AS type.
+ asFollow := asType.(vocab.ActivityStreamsFollow)
+
+ // Reconstruct the follow.
+ follow, err := f.converter.ASFollowToFollow(ctx, asFollow)
+ if err != nil {
+ err := gtserror.Newf("error converting Follow to *gtsmodel.Follow: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Lock on the Follow URI
+ // as we may be updating it.
+ unlock := f.state.FedLocks.Lock(follow.URI)
+ defer unlock()
+
+ // Make sure the creator of the original follow
+ // is the same as whatever inbox this landed in.
+ if follow.AccountID != receivingAcct.ID {
+ const text = "Follow account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the original follow
+ // is the same as the account making the request.
+ if follow.TargetAccountID != requestingAcct.ID {
+ const text = "Follow target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Reject the follow.
+ err = f.state.DB.RejectFollowRequest(
+ ctx,
+ follow.AccountID,
+ follow.TargetAccountID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error rejecting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+func (f *federatingDB) rejectFollowIRI(
+ ctx context.Context,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential Follow
+ // URI as we may be updating it.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the follow req from the db.
+ followReq, err := f.state.DB.GetFollowRequestByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if followReq == nil {
+ // We didn't have a follow request
+ // with this URI, so nothing to do.
+ // Just return.
+ //
+ // TODO: Handle Reject Follow to remove
+ // an already-accepted follow relationship.
+ return nil
+ }
+
+ // Make sure the creator of the original follow
+ // is the same as whatever inbox this landed in.
+ if followReq.AccountID != receivingAcct.ID {
+ const text = "Follow account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the original follow
+ // is the same as the account making the request.
+ if followReq.TargetAccountID != requestingAcct.ID {
+ const text = "Follow target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Reject the follow.
+ err = f.state.DB.RejectFollowRequest(
+ ctx,
+ followReq.AccountID,
+ followReq.TargetAccountID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error rejecting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+func (f *federatingDB) rejectStatusIRI(
+ ctx context.Context,
+ activityID string,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential status URI.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the status from the db.
+ status, err := f.state.DB.GetStatusByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if status == nil {
+ // We didn't have a status with
+ // this URI, so nothing to do.
+ // Just return.
+ return nil
+ }
+
+ if !status.IsLocal() {
+ // We don't process Rejects of statuses
+ // that weren't created on our instance.
+ // Just return.
+ //
+ // TODO: Handle Reject to remove *remote*
+ // posts replying-to or boosting the
+ // Rejecting account.
+ return nil
+ }
+
+ // Make sure the creator of the original status
+ // is the same as the inbox processing the Reject;
+ // this also ensures the status is local.
+ if status.AccountID != receivingAcct.ID {
+ const text = "status author account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Check if we're dealing with a reply
+ // or an announce, and make sure the
+ // requester is permitted to Reject.
+ var apObjectType string
+ if status.InReplyToID != "" {
+ // Rejecting a Reply.
+ apObjectType = ap.ObjectNote
+ if status.InReplyToAccountID != requestingAcct.ID {
+ const text = "status reply to account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // You can't mention an account and then Reject replies from that
+ // same account (harassment vector); don't process these Rejects.
+ if status.InReplyTo != nil && status.InReplyTo.MentionsAccount(status.AccountID) {
+ const text = "refusing to process Reject of a reply from a mentioned account"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ } else {
+ // Rejecting an Announce.
+ apObjectType = ap.ActivityAnnounce
+ if status.BoostOfAccountID != requestingAcct.ID {
+ const text = "status boost of account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+ }
+
+ // Check if there's an interaction request in the db for this status.
+ req, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, status.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ switch {
+ case req == nil:
+ // No interaction request existed yet for this
+ // status, create a pre-rejected request now.
+ req = >smodel.InteractionRequest{
+ ID: id.NewULID(),
+ TargetAccountID: requestingAcct.ID,
+ TargetAccount: requestingAcct,
+ InteractingAccountID: receivingAcct.ID,
+ InteractingAccount: receivingAcct,
+ InteractionURI: status.URI,
+ URI: activityID,
+ RejectedAt: time.Now(),
+ }
+
+ if apObjectType == ap.ObjectNote {
+ // Reply.
+ req.InteractionType = gtsmodel.InteractionReply
+ req.StatusID = status.InReplyToID
+ req.Status = status.InReplyTo
+ req.Reply = status
+ } else {
+ // Announce.
+ req.InteractionType = gtsmodel.InteractionAnnounce
+ req.StatusID = status.BoostOfID
+ req.Status = status.BoostOf
+ req.Announce = status
+ }
+
+ if err := f.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ err := gtserror.Newf("db error inserting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ case req.IsRejected():
+ // Interaction has already been rejected. Just
+ // update to this Reject URI and then return early.
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+
+ default:
+ // Mark existing interaction request as
+ // Rejected, even if previously Accepted.
+ req.AcceptedAt = time.Time{}
+ req.RejectedAt = time.Now()
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req,
+ "accepted_at",
+ "rejected_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // Send the rejected request through to
+ // the fedi worker to process side effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: apObjectType,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
+ return nil
+}
+
+func (f *federatingDB) rejectLikeIRI(
+ ctx context.Context,
+ activityID string,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential Like
+ // URI as we may be updating it.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the fave from the db.
+ fave, err := f.state.DB.GetStatusFaveByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting fave: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if fave == nil {
+ // We didn't have a fave with
+ // this URI, so nothing to do.
+ // Just return.
+ return nil
+ }
+
+ if !fave.Account.IsLocal() {
+ // We don't process Rejects of Likes
+ // that weren't created on our instance.
+ // Just return.
+ //
+ // TODO: Handle Reject to remove *remote*
+ // likes targeting the Rejecting account.
+ return nil
+ }
+
+ // Make sure the creator of the original Like
+ // is the same as the inbox processing the Reject;
+ // this also ensures the Like is local.
+ if fave.AccountID != receivingAcct.ID {
+ const text = "fave creator account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the Like is the
+ // same as the account doing the Reject.
+ if fave.TargetAccountID != requestingAcct.ID {
+ const text = "status fave target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Check if there's an interaction request in the db for this like.
+ req, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, fave.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ switch {
+ case req == nil:
+ // No interaction request existed yet for this
+ // fave, create a pre-rejected request now.
+ req = >smodel.InteractionRequest{
+ ID: id.NewULID(),
+ TargetAccountID: requestingAcct.ID,
+ TargetAccount: requestingAcct,
+ InteractingAccountID: receivingAcct.ID,
+ InteractingAccount: receivingAcct,
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Like: fave,
+ URI: activityID,
+ RejectedAt: time.Now(),
+ }
+
+ if err := f.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ err := gtserror.Newf("db error inserting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ case req.IsRejected():
+ // Interaction has already been rejected. Just
+ // update to this Reject URI and then return early.
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+
+ default:
+ // Mark existing interaction request as
+ // Rejected, even if previously Accepted.
+ req.AcceptedAt = time.Time{}
+ req.RejectedAt = time.Now()
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req,
+ "accepted_at",
+ "rejected_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // Send the rejected request through to
+ // the fedi worker to process side effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/reject_test.go b/internal/federation/federatingdb/reject_test.go
index f51ffaf56..8efa71ca0 100644
--- a/internal/federation/federatingdb/reject_test.go
+++ b/internal/federation/federatingdb/reject_test.go
@@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@@ -61,10 +62,11 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
// create a Reject
reject := streams.NewActivityStreamsReject()
+ // set an ID on it
+ ap.SetJSONLDId(reject, testrig.URLMustParse("https://example.org/some/reject/id"))
+
// set the rejecting actor on it
- acceptActorProp := streams.NewActivityStreamsActorProperty()
- acceptActorProp.AppendIRI(rejectingAccountURI)
- reject.SetActivityStreamsActor(acceptActorProp)
+ ap.AppendActorIRIs(reject, rejectingAccountURI)
// Set the recreated follow as the 'object' property.
acceptObject := streams.NewActivityStreamsObjectProperty()
@@ -72,9 +74,7 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
reject.SetActivityStreamsObject(acceptObject)
// Set the To of the reject as the originator of the follow
- acceptTo := streams.NewActivityStreamsToProperty()
- acceptTo.AppendIRI(requestingAccountURI)
- reject.SetActivityStreamsTo(acceptTo)
+ ap.AppendTo(reject, requestingAccountURI)
// process the reject in the federating database
err = suite.federatingDB.Reject(ctx, reject)
diff --git a/internal/gtsmodel/sinbinstatus.go b/internal/gtsmodel/sinbinstatus.go
new file mode 100644
index 000000000..d1dfcddd1
--- /dev/null
+++ b/internal/gtsmodel/sinbinstatus.go
@@ -0,0 +1,45 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package gtsmodel
+
+import "time"
+
+// SinBinStatus represents a status that's been rejected and/or reported + quarantined.
+//
+// Automatically rejected statuses are not put in the sin bin, only statuses that were
+// stored on the instance and which someone (local or remote) has subsequently rejected.
+type SinBinStatus struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item.
+ URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status.
+ URL string `bun:",nullzero"` // Web url for viewing this status.
+ Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`.
+ AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status.
+ InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to.
+ Content string `bun:",nullzero"` // Content of this status.
+ AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status.
+ MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts.
+ EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status.
+ PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status.
+ ContentWarning string `bun:",nullzero"` // CW / subject string for this status.
+ Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status.
+ Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive.
+ Language string `bun:",nullzero"` // Language code for this status.
+ ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status.
+}
diff --git a/internal/processing/interactionrequests/reject_test.go b/internal/processing/interactionrequests/reject_test.go
index f1f6aed72..6e4aac691 100644
--- a/internal/processing/interactionrequests/reject_test.go
+++ b/internal/processing/interactionrequests/reject_test.go
@@ -71,6 +71,16 @@ func (suite *RejectTestSuite) TestReject() {
)
return status == nil && errors.Is(err, db.ErrNoEntries)
})
+
+ // Wait for a copy of the status
+ // to be hurled into the sin bin.
+ testrig.WaitFor(func() bool {
+ sbStatus, err := state.DB.GetSinBinStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ dbReq.InteractionURI,
+ )
+ return err == nil && sbStatus != nil
+ })
}
func TestRejectTestSuite(t *testing.T) {
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index c723a6001..c8bc8352f 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -911,11 +911,6 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientA
}
func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
- // Don't delete attachments, just unattach them:
- // this request comes from the client API and the
- // poster may want to use attachments again later.
- const deleteAttachments = false
-
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
@@ -942,8 +937,22 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
// (stops processing of remote origin data targeting this status).
p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI)
- // First perform the actual status deletion.
- if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ // Don't delete attachments, just unattach them:
+ // this request comes from the client API and the
+ // poster may want to use attachments again later.
+ const deleteAttachments = false
+
+ // This is just a deletion, not a Reject,
+ // we don't need to take a copy of this status.
+ const copyToSinBin = false
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ status,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
@@ -1275,9 +1284,23 @@ func (p *clientAPI) RejectReply(ctx context.Context, cMsg *messages.FromClientAP
return gtserror.Newf("db error getting rejected reply: %w", err)
}
- // Totally wipe the status.
- if err := p.utils.wipeStatus(ctx, status, true); err != nil {
- return gtserror.Newf("error wiping status: %w", err)
+ // Delete attachments from this status.
+ // It's rejected so there's no possibility
+ // for the poster to delete + redraft it.
+ const deleteAttachments = true
+
+ // Keep a copy of the status in
+ // the sin bin for future review.
+ const copyToSinBin = true
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ status,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
+ log.Errorf(ctx, "error wiping reply: %v", err)
}
return nil
@@ -1306,9 +1329,22 @@ func (p *clientAPI) RejectAnnounce(ctx context.Context, cMsg *messages.FromClien
return gtserror.Newf("db error getting rejected announce: %w", err)
}
- // Totally wipe the status.
- if err := p.utils.wipeStatus(ctx, boost, true); err != nil {
- return gtserror.Newf("error wiping status: %w", err)
+ // Boosts don't have attachments anyway
+ // so it doesn't matter what we set here.
+ const deleteAttachments = true
+
+ // This is just a boost, don't
+ // keep a copy in the sin bin.
+ const copyToSinBin = true
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ boost,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
+ log.Errorf(ctx, "error wiping announce: %v", err)
}
return nil
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 42e5e9db2..d8abaa865 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@@ -146,6 +147,23 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
}
+ // REJECT SOMETHING
+ case ap.ActivityReject:
+ switch fMsg.APObjectType {
+
+ // REJECT LIKE
+ case ap.ActivityLike:
+ return p.fediAPI.RejectLike(ctx, fMsg)
+
+ // REJECT NOTE/STATUS (ie., reject a reply)
+ case ap.ObjectNote:
+ return p.fediAPI.RejectReply(ctx, fMsg)
+
+ // REJECT BOOST
+ case ap.ActivityAnnounce:
+ return p.fediAPI.RejectAnnounce(ctx, fMsg)
+ }
+
// DELETE SOMETHING
case ap.ActivityDelete:
switch fMsg.APObjectType {
@@ -878,11 +896,6 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
}
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
- // Delete attachments from this status, since this request
- // comes from the federating API, and there's no way the
- // poster can do a delete + redraft for it on our instance.
- const deleteAttachments = true
-
status, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
@@ -909,8 +922,22 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// (stops processing of remote origin data targeting this status).
p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI)
- // First perform the actual status deletion.
- if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ // Delete attachments from this status, since this request
+ // comes from the federating API, and there's no way the
+ // poster can do a delete + redraft for it on our instance.
+ const deleteAttachments = true
+
+ // This is just a deletion, not a Reject,
+ // we don't need to take a copy of this status.
+ const copyToSinBin = false
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ status,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
@@ -956,3 +983,113 @@ func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg *messages.FromFediAPI)
return nil
}
+
+func (p *fediAPI) RejectLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Send out the Reject.
+ if err := p.federate.RejectInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating rejection of like: %v", err)
+ }
+
+ // Get the rejected fave.
+ fave, err := p.state.DB.GetStatusFaveByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected fave: %w", err)
+ }
+
+ // Delete the fave.
+ if err := p.state.DB.DeleteStatusFaveByID(ctx, fave.ID); err != nil {
+ return gtserror.Newf("db error deleting fave: %w", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Get the rejected status.
+ status, err := p.state.DB.GetStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected reply: %w", err)
+ }
+
+ // Delete attachments from this status.
+ // It's rejected so there's no possibility
+ // for the poster to delete + redraft it.
+ const deleteAttachments = true
+
+ // Keep a copy of the status in
+ // the sin bin for future review.
+ const copyToSinBin = true
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ status,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
+ log.Errorf(ctx, "error wiping reply: %v", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) RejectAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Get the rejected boost.
+ boost, err := p.state.DB.GetStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected announce: %w", err)
+ }
+
+ // Boosts don't have attachments anyway
+ // so it doesn't matter what we set here.
+ const deleteAttachments = true
+
+ // This is just a boost, don't
+ // keep a copy in the sin bin.
+ const copyToSinBin = true
+
+ // Perform the actual status deletion.
+ if err := p.utils.wipeStatus(
+ ctx,
+ boost,
+ deleteAttachments,
+ copyToSinBin,
+ ); err != nil {
+ log.Errorf(ctx, "error wiping announce: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go
index bb7faffbf..042f4827c 100644
--- a/internal/processing/workers/util.go
+++ b/internal/processing/workers/util.go
@@ -37,69 +37,90 @@ import (
// util provides util functions used by both
// the fromClientAPI and fromFediAPI functions.
type utils struct {
- state *state.State
- media *media.Processor
- account *account.Processor
- surface *Surface
+ state *state.State
+ media *media.Processor
+ account *account.Processor
+ surface *Surface
+ converter *typeutils.Converter
}
-// wipeStatus encapsulates common logic
-// used to totally delete a status + all
-// its attachments, notifications, boosts,
-// and timeline entries.
+// wipeStatus encapsulates common logic used to
+// totally delete a status + all its attachments,
+// notifications, boosts, and timeline entries.
+//
+// If deleteAttachments is true, then any status
+// attachments will also be deleted, else they
+// will just be detached.
+//
+// If copyToSinBin is true, then a version of the
+// status will be put in the `sin_bin_statuses`
+// table prior to deletion.
func (u *utils) wipeStatus(
ctx context.Context,
- statusToDelete *gtsmodel.Status,
+ status *gtsmodel.Status,
deleteAttachments bool,
+ copyToSinBin bool,
) error {
var errs gtserror.MultiError
+ if copyToSinBin {
+ // Copy this status to the sin bin before we delete it.
+ sbStatus, err := u.converter.StatusToSinBinStatus(ctx, status)
+ if err != nil {
+ errs.Appendf("error converting status to sinBinStatus: %w", err)
+ } else {
+ if err := u.state.DB.PutSinBinStatus(ctx, sbStatus); err != nil {
+ errs.Appendf("db error storing sinBinStatus: %w", err)
+ }
+ }
+ }
+
// Either delete all attachments for this status,
- // or simply unattach + clean them separately later.
+ // or simply detach + clean them separately later.
//
- // Reason to unattach rather than delete is that
- // the poster might want to reattach them to another
- // status immediately (in case of delete + redraft)
+ // Reason to detach rather than delete is that
+ // the author might want to reattach them to another
+ // status immediately (in case of delete + redraft).
if deleteAttachments {
// todo:u.state.DB.DeleteAttachmentsForStatus
- for _, id := range statusToDelete.AttachmentIDs {
+ for _, id := range status.AttachmentIDs {
if err := u.media.Delete(ctx, id); err != nil {
errs.Appendf("error deleting media: %w", err)
}
}
} else {
// todo:u.state.DB.UnattachAttachmentsForStatus
- for _, id := range statusToDelete.AttachmentIDs {
- if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil {
+ for _, id := range status.AttachmentIDs {
+ if _, err := u.media.Unattach(ctx, status.Account, id); err != nil {
errs.Appendf("error unattaching media: %w", err)
}
}
}
- // delete all mention entries generated by this status
+ // Delete all mentions generated by this status.
// todo:u.state.DB.DeleteMentionsForStatus
- for _, id := range statusToDelete.MentionIDs {
+ for _, id := range status.MentionIDs {
if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil {
errs.Appendf("error deleting status mention: %w", err)
}
}
- // delete all notification entries generated by this status
- if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
+ // Delete all notifications generated by this status.
+ if err := u.state.DB.DeleteNotificationsForStatus(ctx, status.ID); err != nil {
errs.Appendf("error deleting status notifications: %w", err)
}
- // delete all bookmarks that point to this status
- if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
+ // Delete all bookmarks of this status.
+ if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, status.ID); err != nil {
errs.Appendf("error deleting status bookmarks: %w", err)
}
- // delete all faves of this status
- if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
+ // Delete all faves of this status.
+ if err := u.state.DB.DeleteStatusFavesForStatus(ctx, status.ID); err != nil {
errs.Appendf("error deleting status faves: %w", err)
}
- if pollID := statusToDelete.PollID; pollID != "" {
+ if pollID := status.PollID; pollID != "" {
// Delete this poll by ID from the database.
if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil {
errs.Appendf("error deleting status poll: %w", err)
@@ -114,38 +135,42 @@ func (u *utils) wipeStatus(
_ = u.state.Workers.Scheduler.Cancel(pollID)
}
- // delete all boosts for this status + remove them from timelines
+ // Get all boost of this status so that we can
+ // delete those boosts + remove them from timelines.
boosts, err := u.state.DB.GetStatusBoosts(
- // we MUST set a barebones context here,
+ // We MUST set a barebones context here,
// as depending on where it came from the
// original BoostOf may already be gone.
gtscontext.SetBarebones(ctx),
- statusToDelete.ID)
+ status.ID)
if err != nil {
errs.Appendf("error fetching status boosts: %w", err)
}
for _, boost := range boosts {
- if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
- errs.Appendf("error deleting boost from timelines: %w", err)
- }
+ // Delete the boost itself.
if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
errs.Appendf("error deleting boost: %w", err)
}
+
+ // Remove the boost from any and all timelines.
+ if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
+ errs.Appendf("error deleting boost from timelines: %w", err)
+ }
}
- // delete this status from any and all timelines
- if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
+ // Delete the status itself from any and all timelines.
+ if err := u.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil {
errs.Appendf("error deleting status from timelines: %w", err)
}
- // delete this status from any conversations that it's part of
- if err := u.state.DB.DeleteStatusFromConversations(ctx, statusToDelete.ID); err != nil {
+ // Delete this status from any conversations it's part of.
+ if err := u.state.DB.DeleteStatusFromConversations(ctx, status.ID); err != nil {
errs.Appendf("error deleting status from conversations: %w", err)
}
- // finally, delete the status itself
- if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
+ // Finally delete the status itself.
+ if err := u.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
errs.Appendf("error deleting status: %w", err)
}
diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go
index d4b525783..ad673481b 100644
--- a/internal/processing/workers/workers.go
+++ b/internal/processing/workers/workers.go
@@ -70,10 +70,11 @@ func New(
// Init shared util funcs.
utils := &utils{
- state: state,
- media: media,
- account: account,
- surface: surface,
+ state: state,
+ media: media,
+ account: account,
+ surface: surface,
+ converter: converter,
}
return Processor{
diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go
index ed4ed4dd9..ccde6a38f 100644
--- a/internal/typeutils/internal.go
+++ b/internal/typeutils/internal.go
@@ -19,10 +19,15 @@ package typeutils
import (
"context"
+ "errors"
+ "net/url"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -175,3 +180,91 @@ func StatusFaveToInteractionRequest(
Like: fave,
}, nil
}
+
+func (c *Converter) StatusToSinBinStatus(
+ ctx context.Context,
+ status *gtsmodel.Status,
+) (*gtsmodel.SinBinStatus, error) {
+ // Populate status first so we have
+ // polls, mentions etc to copy over.
+ //
+ // ErrNoEntries is fine, we'll do our best.
+ err := c.state.DB.PopulateStatus(ctx, status)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.Newf("db error populating status: %w", err)
+ }
+
+ // Get domain of this status,
+ // empty for our own domain.
+ var domain string
+ if status.Account != nil {
+ domain = status.Account.Domain
+ } else {
+ uri, err := url.Parse(status.URI)
+ if err != nil {
+ return nil, gtserror.Newf("error parsing status URI: %w", err)
+ }
+
+ host := uri.Host
+ if host != config.GetAccountDomain() &&
+ host != config.GetHost() {
+ domain = host
+ }
+ }
+
+ // Extract just the image URLs from attachments.
+ attachLinks := make([]string, len(status.Attachments))
+ for i, attach := range status.Attachments {
+ if attach.IsLocal() {
+ attachLinks[i] = attach.URL
+ } else {
+ attachLinks[i] = attach.RemoteURL
+ }
+ }
+
+ // Extract just the target account URIs from mentions.
+ mentionTargetURIs := make([]string, 0, len(status.Mentions))
+ for _, mention := range status.Mentions {
+ if err := c.state.DB.PopulateMention(ctx, mention); err != nil {
+ log.Errorf(ctx, "error populating mention: %v", err)
+ continue
+ }
+
+ mentionTargetURIs = append(mentionTargetURIs, mention.TargetAccount.URI)
+ }
+
+ // Extract just the image URLs from emojis.
+ emojiLinks := make([]string, len(status.Emojis))
+ for i, emoji := range status.Emojis {
+ if emoji.IsLocal() {
+ emojiLinks[i] = emoji.ImageURL
+ } else {
+ emojiLinks[i] = emoji.ImageRemoteURL
+ }
+ }
+
+ // Extract just the poll option strings.
+ var pollOptions []string
+ if status.Poll != nil {
+ pollOptions = status.Poll.Options
+ }
+
+ return >smodel.SinBinStatus{
+ ID: status.ID, // Reuse the status ID.
+ URI: status.URI,
+ URL: status.URL,
+ Domain: domain,
+ AccountURI: status.AccountURI,
+ InReplyToURI: status.InReplyToURI,
+ Content: status.Content,
+ AttachmentLinks: attachLinks,
+ MentionTargetURIs: mentionTargetURIs,
+ EmojiLinks: emojiLinks,
+ PollOptions: pollOptions,
+ ContentWarning: status.ContentWarning,
+ Visibility: status.Visibility,
+ Sensitive: status.Sensitive,
+ Language: status.Language,
+ ActivityStreamsType: status.ActivityStreamsType,
+ }, nil
+}
diff --git a/test/envparsing.sh b/test/envparsing.sh
index 32842bee8..ab01578d6 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -59,6 +59,7 @@ EXPECT=$(cat << "EOF"
"poll-vote-ids-mem-ratio": 2,
"poll-vote-mem-ratio": 2,
"report-mem-ratio": 1,
+ "sin-bin-status-mem-ratio": 0.5,
"status-bookmark-ids-mem-ratio": 2,
"status-bookmark-mem-ratio": 0.5,
"status-fave-ids-mem-ratio": 3,