mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 04:32:25 -05: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:
		
					parent
					
						
							
								3254ef1923
							
						
					
				
			
			
				commit
				
					
						307d98e386
					
				
			
		
					 21 changed files with 1172 additions and 115 deletions
				
			
		
							
								
								
									
										2
									
								
								internal/cache/cache.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								internal/cache/cache.go
									
										
									
									
										vendored
									
									
								
							|  | @ -93,6 +93,7 @@ func (c *Caches) Init() { | ||||||
| 	c.initPollVote() | 	c.initPollVote() | ||||||
| 	c.initPollVoteIDs() | 	c.initPollVoteIDs() | ||||||
| 	c.initReport() | 	c.initReport() | ||||||
|  | 	c.initSinBinStatus() | ||||||
| 	c.initStatus() | 	c.initStatus() | ||||||
| 	c.initStatusBookmark() | 	c.initStatusBookmark() | ||||||
| 	c.initStatusBookmarkIDs() | 	c.initStatusBookmarkIDs() | ||||||
|  | @ -170,6 +171,7 @@ func (c *Caches) Sweep(threshold float64) { | ||||||
| 	c.DB.PollVote.Trim(threshold) | 	c.DB.PollVote.Trim(threshold) | ||||||
| 	c.DB.PollVoteIDs.Trim(threshold) | 	c.DB.PollVoteIDs.Trim(threshold) | ||||||
| 	c.DB.Report.Trim(threshold) | 	c.DB.Report.Trim(threshold) | ||||||
|  | 	c.DB.SinBinStatus.Trim(threshold) | ||||||
| 	c.DB.Status.Trim(threshold) | 	c.DB.Status.Trim(threshold) | ||||||
| 	c.DB.StatusBookmark.Trim(threshold) | 	c.DB.StatusBookmark.Trim(threshold) | ||||||
| 	c.DB.StatusBookmarkIDs.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 provides access to the gtsmodel Report database cache. | ||||||
| 	Report StructCache[*gtsmodel.Report] | 	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 provides access to the gtsmodel Status database cache. | ||||||
| 	Status StructCache[*gtsmodel.Status] | 	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() { | func (c *Caches) initStatus() { | ||||||
| 	// Calculate maximum cache size. | 	// Calculate maximum cache size. | ||||||
| 	cap := calculateResultCacheMax( | 	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 { | func sizeofStatus() uintptr { | ||||||
| 	return uintptr(size.Of(>smodel.Status{ | 	return uintptr(size.Of(>smodel.Status{ | ||||||
| 		ID:                       exampleID, | 		ID:                       exampleID, | ||||||
|  |  | ||||||
|  | @ -230,6 +230,7 @@ type CacheConfiguration struct { | ||||||
| 	PollVoteMemRatio                  float64       `name:"poll-vote-mem-ratio"` | 	PollVoteMemRatio                  float64       `name:"poll-vote-mem-ratio"` | ||||||
| 	PollVoteIDsMemRatio               float64       `name:"poll-vote-ids-mem-ratio"` | 	PollVoteIDsMemRatio               float64       `name:"poll-vote-ids-mem-ratio"` | ||||||
| 	ReportMemRatio                    float64       `name:"report-mem-ratio"` | 	ReportMemRatio                    float64       `name:"report-mem-ratio"` | ||||||
|  | 	SinBinStatusMemRatio              float64       `name:"sin-bin-status-mem-ratio"` | ||||||
| 	StatusMemRatio                    float64       `name:"status-mem-ratio"` | 	StatusMemRatio                    float64       `name:"status-mem-ratio"` | ||||||
| 	StatusBookmarkMemRatio            float64       `name:"status-bookmark-mem-ratio"` | 	StatusBookmarkMemRatio            float64       `name:"status-bookmark-mem-ratio"` | ||||||
| 	StatusBookmarkIDsMemRatio         float64       `name:"status-bookmark-ids-mem-ratio"` | 	StatusBookmarkIDsMemRatio         float64       `name:"status-bookmark-ids-mem-ratio"` | ||||||
|  |  | ||||||
|  | @ -193,6 +193,7 @@ var Defaults = Configuration{ | ||||||
| 		PollVoteMemRatio:                  2, | 		PollVoteMemRatio:                  2, | ||||||
| 		PollVoteIDsMemRatio:               2, | 		PollVoteIDsMemRatio:               2, | ||||||
| 		ReportMemRatio:                    1, | 		ReportMemRatio:                    1, | ||||||
|  | 		SinBinStatusMemRatio:              0.5, | ||||||
| 		StatusMemRatio:                    5, | 		StatusMemRatio:                    5, | ||||||
| 		StatusBookmarkMemRatio:            0.5, | 		StatusBookmarkMemRatio:            0.5, | ||||||
| 		StatusBookmarkIDsMemRatio:         2, | 		StatusBookmarkIDsMemRatio:         2, | ||||||
|  |  | ||||||
|  | @ -3712,6 +3712,31 @@ func GetCacheReportMemRatio() float64 { return global.GetCacheReportMemRatio() } | ||||||
| // SetCacheReportMemRatio safely sets the value for global configuration 'Cache.ReportMemRatio' field | // SetCacheReportMemRatio safely sets the value for global configuration 'Cache.ReportMemRatio' field | ||||||
| func SetCacheReportMemRatio(v float64) { global.SetCacheReportMemRatio(v) } | 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 | // GetCacheStatusMemRatio safely fetches the Configuration value for state's 'Cache.StatusMemRatio' field | ||||||
| func (st *ConfigState) GetCacheStatusMemRatio() (v float64) { | func (st *ConfigState) GetCacheStatusMemRatio() (v float64) { | ||||||
| 	st.mutex.RLock() | 	st.mutex.RLock() | ||||||
|  |  | ||||||
|  | @ -76,6 +76,7 @@ type DBService struct { | ||||||
| 	db.Rule | 	db.Rule | ||||||
| 	db.Search | 	db.Search | ||||||
| 	db.Session | 	db.Session | ||||||
|  | 	db.SinBinStatus | ||||||
| 	db.Status | 	db.Status | ||||||
| 	db.StatusBookmark | 	db.StatusBookmark | ||||||
| 	db.StatusFave | 	db.StatusFave | ||||||
|  | @ -271,6 +272,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { | ||||||
| 		Session: &sessionDB{ | 		Session: &sessionDB{ | ||||||
| 			db: db, | 			db: db, | ||||||
| 		}, | 		}, | ||||||
|  | 		SinBinStatus: &sinBinStatusDB{ | ||||||
|  | 			db:    db, | ||||||
|  | 			state: state, | ||||||
|  | 		}, | ||||||
| 		Status: &statusDB{ | 		Status: &statusDB{ | ||||||
| 			db:    db, | 			db:    db, | ||||||
| 			state: state, | 			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 | 	Rule | ||||||
| 	Search | 	Search | ||||||
| 	Session | 	Session | ||||||
|  | 	SinBinStatus | ||||||
| 	Status | 	Status | ||||||
| 	StatusBookmark | 	StatusBookmark | ||||||
| 	StatusFave | 	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 ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"codeberg.org/gruf/go-logger/v2/level" | 	"codeberg.org/gruf/go-logger/v2/level" | ||||||
| 	"github.com/superseriousbusiness/activity/streams/vocab" | 	"github.com/superseriousbusiness/activity/streams/vocab" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"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/log" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -48,63 +53,450 @@ func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsR | ||||||
| 	requestingAcct := activityContext.requestingAcct | 	requestingAcct := activityContext.requestingAcct | ||||||
| 	receivingAcct := activityContext.receivingAcct | 	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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, object := range ap.ExtractObjects(reject) { | ||||||
|  | 		if asType := object.GetType(); asType != nil { | ||||||
|  | 			// Check and handle any | ||||||
|  | 			// vocab.Type objects. | ||||||
|  | 			// nolint:gocritic | ||||||
|  | 			switch asType.GetTypeName() { | ||||||
| 
 | 
 | ||||||
| 		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 | 			// REJECT FOLLOW | ||||||
| 				followReq, err := f.state.DB.GetFollowRequestByURI(ctx, rejectedObjectIRI.String()) | 			case ap.ActivityFollow: | ||||||
| 				if err != nil { | 				if err := f.rejectFollowType( | ||||||
| 					return fmt.Errorf("Reject: couldn't get follow request with id %s from the database: %s", rejectedObjectIRI.String(), err) | 					ctx, | ||||||
| 				} | 					asType, | ||||||
| 
 | 					receivingAcct, | ||||||
| 				// Make sure the creator of the original follow | 					requestingAcct, | ||||||
| 				// is the same as whatever inbox this landed in. | 				); err != nil { | ||||||
| 				if followReq.AccountID != receivingAcct.ID { | 					return err | ||||||
| 					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 { | 		} else if object.IsIRI() { | ||||||
| 			// we have the whole object so we can figure out what we're rejecting | 			// Check and handle any | ||||||
|  | 			// IRI type objects. | ||||||
|  | 			switch objIRI := object.GetIRI(); { | ||||||
|  | 
 | ||||||
| 			// REJECT FOLLOW | 			// REJECT FOLLOW | ||||||
| 			asFollow, ok := t.(vocab.ActivityStreamsFollow) | 			case uris.IsFollowPath(objIRI): | ||||||
| 			if !ok { | 				if err := f.rejectFollowIRI( | ||||||
| 				return errors.New("Reject: couldn't parse follow into vocab.ActivityStreamsFollow") | 					ctx, | ||||||
|  | 					objIRI.String(), | ||||||
|  | 					receivingAcct, | ||||||
|  | 					requestingAcct, | ||||||
|  | 				); err != nil { | ||||||
|  | 					return err | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 			// convert the follow to something we can understand | 			// REJECT STATUS (reply/boost) | ||||||
| 			gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow) | 			case uris.IsStatusesPath(objIRI): | ||||||
| 			if err != nil { | 				if err := f.rejectStatusIRI( | ||||||
| 				return fmt.Errorf("Reject: error converting asfollow to gtsfollow: %s", err) | 					ctx, | ||||||
|  | 					activityID.String(), | ||||||
|  | 					objIRI.String(), | ||||||
|  | 					receivingAcct, | ||||||
|  | 					requestingAcct, | ||||||
|  | 				); err != nil { | ||||||
|  | 					return err | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 			// Make sure the creator of the original follow | 			// REJECT LIKE | ||||||
| 			// is the same as whatever inbox this landed in. | 			case uris.IsLikePath(objIRI): | ||||||
| 			if gtsFollow.AccountID != receivingAcct.ID { | 				if err := f.rejectLikeIRI( | ||||||
| 				return errors.New("Reject: follow account and inbox account were not the same") | 					ctx, | ||||||
|  | 					activityID.String(), | ||||||
|  | 					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") |  | ||||||
| 			} | 			} | ||||||
| 
 |  | ||||||
| 			return f.state.DB.RejectFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	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/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/activity/streams" | 	"github.com/superseriousbusiness/activity/streams" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
|  | @ -61,10 +62,11 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() { | ||||||
| 	// create a Reject | 	// create a Reject | ||||||
| 	reject := streams.NewActivityStreamsReject() | 	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 | 	// set the rejecting actor on it | ||||||
| 	acceptActorProp := streams.NewActivityStreamsActorProperty() | 	ap.AppendActorIRIs(reject, rejectingAccountURI) | ||||||
| 	acceptActorProp.AppendIRI(rejectingAccountURI) |  | ||||||
| 	reject.SetActivityStreamsActor(acceptActorProp) |  | ||||||
| 
 | 
 | ||||||
| 	// Set the recreated follow as the 'object' property. | 	// Set the recreated follow as the 'object' property. | ||||||
| 	acceptObject := streams.NewActivityStreamsObjectProperty() | 	acceptObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | @ -72,9 +74,7 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() { | ||||||
| 	reject.SetActivityStreamsObject(acceptObject) | 	reject.SetActivityStreamsObject(acceptObject) | ||||||
| 
 | 
 | ||||||
| 	// Set the To of the reject as the originator of the follow | 	// Set the To of the reject as the originator of the follow | ||||||
| 	acceptTo := streams.NewActivityStreamsToProperty() | 	ap.AppendTo(reject, requestingAccountURI) | ||||||
| 	acceptTo.AppendIRI(requestingAccountURI) |  | ||||||
| 	reject.SetActivityStreamsTo(acceptTo) |  | ||||||
| 
 | 
 | ||||||
| 	// process the reject in the federating database | 	// process the reject in the federating database | ||||||
| 	err = suite.federatingDB.Reject(ctx, reject) | 	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) | 		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) { | 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 { | 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) | 	status, ok := cMsg.GTSModel.(*gtsmodel.Status) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel) | 		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). | 	// (stops processing of remote origin data targeting this status). | ||||||
| 	p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI) | 	p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI) | ||||||
| 
 | 
 | ||||||
| 	// First perform the actual status deletion. | 	// Don't delete attachments, just unattach them: | ||||||
| 	if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil { | 	// 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) | 		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) | 		return gtserror.Newf("db error getting rejected reply: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Totally wipe the status. | 	// Delete attachments from this status. | ||||||
| 	if err := p.utils.wipeStatus(ctx, status, true); err != nil { | 	// It's rejected so there's no possibility | ||||||
| 		return gtserror.Newf("error wiping status: %w", err) | 	// 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 | 	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) | 		return gtserror.Newf("db error getting rejected announce: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Totally wipe the status. | 	// Boosts don't have attachments anyway | ||||||
| 	if err := p.utils.wipeStatus(ctx, boost, true); err != nil { | 	// so it doesn't matter what we set here. | ||||||
| 		return gtserror.Newf("error wiping status: %w", err) | 	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 | 	return nil | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" | 	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"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) | 			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 | 	// DELETE SOMETHING | ||||||
| 	case ap.ActivityDelete: | 	case ap.ActivityDelete: | ||||||
| 		switch fMsg.APObjectType { | 		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 { | 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) | 	status, ok := fMsg.GTSModel.(*gtsmodel.Status) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) | 		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). | 	// (stops processing of remote origin data targeting this status). | ||||||
| 	p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI) | 	p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI) | ||||||
| 
 | 
 | ||||||
| 	// First perform the actual status deletion. | 	// Delete attachments from this status, since this request | ||||||
| 	if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil { | 	// 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) | 		log.Errorf(ctx, "error wiping status: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -956,3 +983,113 @@ func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg *messages.FromFediAPI) | ||||||
| 
 | 
 | ||||||
| 	return nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -41,65 +41,86 @@ type utils struct { | ||||||
| 	media     *media.Processor | 	media     *media.Processor | ||||||
| 	account   *account.Processor | 	account   *account.Processor | ||||||
| 	surface   *Surface | 	surface   *Surface | ||||||
|  | 	converter *typeutils.Converter | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // wipeStatus encapsulates common logic | // wipeStatus encapsulates common logic used to | ||||||
| // used to totally delete a status + all | // totally delete a status + all its attachments, | ||||||
| // its attachments, notifications, boosts, | // notifications, boosts, and timeline entries. | ||||||
| // 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( | func (u *utils) wipeStatus( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	statusToDelete *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	deleteAttachments bool, | 	deleteAttachments bool, | ||||||
|  | 	copyToSinBin bool, | ||||||
| ) error { | ) error { | ||||||
| 	var errs gtserror.MultiError | 	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, | 	// 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 | 	// Reason to detach rather than delete is that | ||||||
| 	// the poster might want to reattach them to another | 	// the author might want to reattach them to another | ||||||
| 	// status immediately (in case of delete + redraft) | 	// status immediately (in case of delete + redraft). | ||||||
| 	if deleteAttachments { | 	if deleteAttachments { | ||||||
| 		// todo:u.state.DB.DeleteAttachmentsForStatus | 		// todo:u.state.DB.DeleteAttachmentsForStatus | ||||||
| 		for _, id := range statusToDelete.AttachmentIDs { | 		for _, id := range status.AttachmentIDs { | ||||||
| 			if err := u.media.Delete(ctx, id); err != nil { | 			if err := u.media.Delete(ctx, id); err != nil { | ||||||
| 				errs.Appendf("error deleting media: %w", err) | 				errs.Appendf("error deleting media: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// todo:u.state.DB.UnattachAttachmentsForStatus | 		// todo:u.state.DB.UnattachAttachmentsForStatus | ||||||
| 		for _, id := range statusToDelete.AttachmentIDs { | 		for _, id := range status.AttachmentIDs { | ||||||
| 			if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil { | 			if _, err := u.media.Unattach(ctx, status.Account, id); err != nil { | ||||||
| 				errs.Appendf("error unattaching media: %w", err) | 				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 | 	// 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 { | 		if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil { | ||||||
| 			errs.Appendf("error deleting status mention: %w", err) | 			errs.Appendf("error deleting status mention: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all notification entries generated by this status | 	// Delete all notifications generated by this status. | ||||||
| 	if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { | 	if err := u.state.DB.DeleteNotificationsForStatus(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status notifications: %w", err) | 		errs.Appendf("error deleting status notifications: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all bookmarks that point to this status | 	// Delete all bookmarks of this status. | ||||||
| 	if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { | 	if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status bookmarks: %w", err) | 		errs.Appendf("error deleting status bookmarks: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete all faves of this status | 	// Delete all faves of this status. | ||||||
| 	if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { | 	if err := u.state.DB.DeleteStatusFavesForStatus(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status faves: %w", err) | 		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. | 		// Delete this poll by ID from the database. | ||||||
| 		if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil { | 		if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil { | ||||||
| 			errs.Appendf("error deleting status poll: %w", err) | 			errs.Appendf("error deleting status poll: %w", err) | ||||||
|  | @ -114,38 +135,42 @@ func (u *utils) wipeStatus( | ||||||
| 		_ = u.state.Workers.Scheduler.Cancel(pollID) | 		_ = 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( | 	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 | 		// as depending on where it came from the | ||||||
| 		// original BoostOf may already be gone. | 		// original BoostOf may already be gone. | ||||||
| 		gtscontext.SetBarebones(ctx), | 		gtscontext.SetBarebones(ctx), | ||||||
| 		statusToDelete.ID) | 		status.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		errs.Appendf("error fetching status boosts: %w", err) | 		errs.Appendf("error fetching status boosts: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, boost := range boosts { | 	for _, boost := range boosts { | ||||||
| 		if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil { | 		// Delete the boost itself. | ||||||
| 			errs.Appendf("error deleting boost from timelines: %w", err) |  | ||||||
| 		} |  | ||||||
| 		if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { | 		if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { | ||||||
| 			errs.Appendf("error deleting boost: %w", err) | 			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 | 	// Delete the status itself from any and all timelines. | ||||||
| 	if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { | 	if err := u.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status from timelines: %w", err) | 		errs.Appendf("error deleting status from timelines: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// delete this status from any conversations that it's part of | 	// Delete this status from any conversations it's part of. | ||||||
| 	if err := u.state.DB.DeleteStatusFromConversations(ctx, statusToDelete.ID); err != nil { | 	if err := u.state.DB.DeleteStatusFromConversations(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status from conversations: %w", err) | 		errs.Appendf("error deleting status from conversations: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// finally, delete the status itself | 	// Finally delete the status itself. | ||||||
| 	if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { | 	if err := u.state.DB.DeleteStatusByID(ctx, status.ID); err != nil { | ||||||
| 		errs.Appendf("error deleting status: %w", err) | 		errs.Appendf("error deleting status: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -74,6 +74,7 @@ func New( | ||||||
| 		media:     media, | 		media:     media, | ||||||
| 		account:   account, | 		account:   account, | ||||||
| 		surface:   surface, | 		surface:   surface, | ||||||
|  | 		converter: converter, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return Processor{ | 	return Processor{ | ||||||
|  |  | ||||||
|  | @ -19,10 +19,15 @@ package typeutils | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"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/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
|  | @ -175,3 +180,91 @@ func StatusFaveToInteractionRequest( | ||||||
| 		Like:                 fave, | 		Like:                 fave, | ||||||
| 	}, nil | 	}, 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-ids-mem-ratio": 2, | ||||||
|         "poll-vote-mem-ratio": 2, |         "poll-vote-mem-ratio": 2, | ||||||
|         "report-mem-ratio": 1, |         "report-mem-ratio": 1, | ||||||
|  |         "sin-bin-status-mem-ratio": 0.5, | ||||||
|         "status-bookmark-ids-mem-ratio": 2, |         "status-bookmark-ids-mem-ratio": 2, | ||||||
|         "status-bookmark-mem-ratio": 0.5, |         "status-bookmark-mem-ratio": 0.5, | ||||||
|         "status-fave-ids-mem-ratio": 3, |         "status-fave-ids-mem-ratio": 3, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue