mirror of
				https://github.com/superseriousbusiness/gotosocial
				synced 2025-06-05 21:59:39 +02:00 
			
		
		
		
	[feature] Process Reject of interaction via fedi API, put rejected statuses in the "sin bin" 😈 (#3271)
				
					
				
			* [feature] Process `Reject` of interaction via fedi API, put rejected statuses in the "sin bin" * update test * move nil check back to `rejectStatusIRI`
This commit is contained in:
		
							
								
								
									
										2
									
								
								internal/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								internal/cache/cache.go
									
									
									
									
										vendored
									
									
								
							@@ -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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								internal/cache/db.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								internal/cache/db.go
									
									
									
									
										vendored
									
									
								
							@@ -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(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								internal/cache/size.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								internal/cache/size.go
									
									
									
									
										vendored
									
									
								
							@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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"`
 | 
			
		||||
 
 | 
			
		||||
@@ -193,6 +193,7 @@ var Defaults = Configuration{
 | 
			
		||||
		PollVoteMemRatio:                  2,
 | 
			
		||||
		PollVoteIDsMemRatio:               2,
 | 
			
		||||
		ReportMemRatio:                    1,
 | 
			
		||||
		SinBinStatusMemRatio:              0.5,
 | 
			
		||||
		StatusMemRatio:                    5,
 | 
			
		||||
		StatusBookmarkMemRatio:            0.5,
 | 
			
		||||
		StatusBookmarkIDsMemRatio:         2,
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								internal/db/bundb/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								internal/db/bundb/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@@ -48,6 +48,7 @@ type DB interface {
 | 
			
		||||
	Rule
 | 
			
		||||
	Search
 | 
			
		||||
	Session
 | 
			
		||||
	SinBinStatus
 | 
			
		||||
	Status
 | 
			
		||||
	StatusBookmark
 | 
			
		||||
	StatusFave
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								internal/db/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								internal/db/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								internal/gtsmodel/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								internal/gtsmodel/sinbinstatus.go
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
}
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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{
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user