diff --git a/example/config.yaml b/example/config.yaml
index 12e49e420..71d26e50c 100644
--- a/example/config.yaml
+++ b/example/config.yaml
@@ -207,6 +207,10 @@ cache:
notification-ttl: "5m"
notification-sweep-freq: "10s"
+ report-max-size: 100
+ report-ttl: "5m"
+ report-sweep-freq: "10s"
+
status-max-size: 500
status-ttl: "5m"
status-sweep-freq: "10s"
diff --git a/internal/cache/gts.go b/internal/cache/gts.go
index 8b087b6ad..f3f7a33ef 100644
--- a/internal/cache/gts.go
+++ b/internal/cache/gts.go
@@ -57,6 +57,9 @@ type GTSCaches interface {
// Notification provides access to the gtsmodel Notification database cache.
Notification() *result.Cache[*gtsmodel.Notification]
+ // Report provides access to the gtsmodel Report database cache.
+ Report() *result.Cache[*gtsmodel.Report]
+
// Status provides access to the gtsmodel Status database cache.
Status() *result.Cache[*gtsmodel.Status]
@@ -80,6 +83,7 @@ type gtsCaches struct {
emojiCategory *result.Cache[*gtsmodel.EmojiCategory]
mention *result.Cache[*gtsmodel.Mention]
notification *result.Cache[*gtsmodel.Notification]
+ report *result.Cache[*gtsmodel.Report]
status *result.Cache[*gtsmodel.Status]
tombstone *result.Cache[*gtsmodel.Tombstone]
user *result.Cache[*gtsmodel.User]
@@ -93,6 +97,7 @@ func (c *gtsCaches) Init() {
c.initEmojiCategory()
c.initMention()
c.initNotification()
+ c.initReport()
c.initStatus()
c.initTombstone()
c.initUser()
@@ -120,6 +125,9 @@ func (c *gtsCaches) Start() {
tryUntil("starting gtsmodel.Notification cache", 5, func() bool {
return c.notification.Start(config.GetCacheGTSNotificationSweepFreq())
})
+ tryUntil("starting gtsmodel.Report cache", 5, func() bool {
+ return c.report.Start(config.GetCacheGTSReportSweepFreq())
+ })
tryUntil("starting gtsmodel.Status cache", 5, func() bool {
return c.status.Start(config.GetCacheGTSStatusSweepFreq())
})
@@ -139,6 +147,7 @@ func (c *gtsCaches) Stop() {
tryUntil("stopping gtsmodel.EmojiCategory cache", 5, c.emojiCategory.Stop)
tryUntil("stopping gtsmodel.Mention cache", 5, c.mention.Stop)
tryUntil("stopping gtsmodel.Notification cache", 5, c.notification.Stop)
+ tryUntil("stopping gtsmodel.Report cache", 5, c.report.Stop)
tryUntil("stopping gtsmodel.Status cache", 5, c.status.Stop)
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
tryUntil("stopping gtsmodel.User cache", 5, c.user.Stop)
@@ -172,6 +181,10 @@ func (c *gtsCaches) Notification() *result.Cache[*gtsmodel.Notification] {
return c.notification
}
+func (c *gtsCaches) Report() *result.Cache[*gtsmodel.Report] {
+ return c.report
+}
+
func (c *gtsCaches) Status() *result.Cache[*gtsmodel.Status] {
return c.status
}
@@ -267,6 +280,17 @@ func (c *gtsCaches) initNotification() {
c.notification.SetTTL(config.GetCacheGTSNotificationTTL(), true)
}
+func (c *gtsCaches) initReport() {
+ c.report = result.New([]result.Lookup{
+ {Name: "ID"},
+ }, func(r1 *gtsmodel.Report) *gtsmodel.Report {
+ r2 := new(gtsmodel.Report)
+ *r2 = *r1
+ return r2
+ }, config.GetCacheGTSReportMaxSize())
+ c.report.SetTTL(config.GetCacheGTSReportTTL(), true)
+}
+
func (c *gtsCaches) initStatus() {
c.status = result.New([]result.Lookup{
{Name: "ID"},
diff --git a/internal/config/config.go b/internal/config/config.go
index 107a94285..ec8675f2d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -175,6 +175,10 @@ type GTSCacheConfiguration struct {
NotificationTTL time.Duration `name:"notification-ttl"`
NotificationSweepFreq time.Duration `name:"notification-sweep-freq"`
+ ReportMaxSize int `name:"report-max-size"`
+ ReportTTL time.Duration `name:"report-ttl"`
+ ReportSweepFreq time.Duration `name:"report-sweep-freq"`
+
StatusMaxSize int `name:"status-max-size"`
StatusTTL time.Duration `name:"status-ttl"`
StatusSweepFreq time.Duration `name:"status-sweep-freq"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 26fecaa73..4d61bec05 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -138,6 +138,10 @@
NotificationTTL: time.Minute * 5,
NotificationSweepFreq: time.Second * 10,
+ ReportMaxSize: 100,
+ ReportTTL: time.Minute * 5,
+ ReportSweepFreq: time.Second * 10,
+
StatusMaxSize: 500,
StatusTTL: time.Minute * 5,
StatusSweepFreq: time.Second * 10,
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index a1f920ac7..f340360b2 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -2378,6 +2378,81 @@ func GetCacheGTSNotificationSweepFreq() time.Duration {
// SetCacheGTSNotificationSweepFreq safely sets the value for global configuration 'Cache.GTS.NotificationSweepFreq' field
func SetCacheGTSNotificationSweepFreq(v time.Duration) { global.SetCacheGTSNotificationSweepFreq(v) }
+// GetCacheGTSReportMaxSize safely fetches the Configuration value for state's 'Cache.GTS.ReportMaxSize' field
+func (st *ConfigState) GetCacheGTSReportMaxSize() (v int) {
+ st.mutex.Lock()
+ v = st.config.Cache.GTS.ReportMaxSize
+ st.mutex.Unlock()
+ return
+}
+
+// SetCacheGTSReportMaxSize safely sets the Configuration value for state's 'Cache.GTS.ReportMaxSize' field
+func (st *ConfigState) SetCacheGTSReportMaxSize(v int) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.GTS.ReportMaxSize = v
+ st.reloadToViper()
+}
+
+// CacheGTSReportMaxSizeFlag returns the flag name for the 'Cache.GTS.ReportMaxSize' field
+func CacheGTSReportMaxSizeFlag() string { return "cache-gts-report-max-size" }
+
+// GetCacheGTSReportMaxSize safely fetches the value for global configuration 'Cache.GTS.ReportMaxSize' field
+func GetCacheGTSReportMaxSize() int { return global.GetCacheGTSReportMaxSize() }
+
+// SetCacheGTSReportMaxSize safely sets the value for global configuration 'Cache.GTS.ReportMaxSize' field
+func SetCacheGTSReportMaxSize(v int) { global.SetCacheGTSReportMaxSize(v) }
+
+// GetCacheGTSReportTTL safely fetches the Configuration value for state's 'Cache.GTS.ReportTTL' field
+func (st *ConfigState) GetCacheGTSReportTTL() (v time.Duration) {
+ st.mutex.Lock()
+ v = st.config.Cache.GTS.ReportTTL
+ st.mutex.Unlock()
+ return
+}
+
+// SetCacheGTSReportTTL safely sets the Configuration value for state's 'Cache.GTS.ReportTTL' field
+func (st *ConfigState) SetCacheGTSReportTTL(v time.Duration) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.GTS.ReportTTL = v
+ st.reloadToViper()
+}
+
+// CacheGTSReportTTLFlag returns the flag name for the 'Cache.GTS.ReportTTL' field
+func CacheGTSReportTTLFlag() string { return "cache-gts-report-ttl" }
+
+// GetCacheGTSReportTTL safely fetches the value for global configuration 'Cache.GTS.ReportTTL' field
+func GetCacheGTSReportTTL() time.Duration { return global.GetCacheGTSReportTTL() }
+
+// SetCacheGTSReportTTL safely sets the value for global configuration 'Cache.GTS.ReportTTL' field
+func SetCacheGTSReportTTL(v time.Duration) { global.SetCacheGTSReportTTL(v) }
+
+// GetCacheGTSReportSweepFreq safely fetches the Configuration value for state's 'Cache.GTS.ReportSweepFreq' field
+func (st *ConfigState) GetCacheGTSReportSweepFreq() (v time.Duration) {
+ st.mutex.Lock()
+ v = st.config.Cache.GTS.ReportSweepFreq
+ st.mutex.Unlock()
+ return
+}
+
+// SetCacheGTSReportSweepFreq safely sets the Configuration value for state's 'Cache.GTS.ReportSweepFreq' field
+func (st *ConfigState) SetCacheGTSReportSweepFreq(v time.Duration) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.GTS.ReportSweepFreq = v
+ st.reloadToViper()
+}
+
+// CacheGTSReportSweepFreqFlag returns the flag name for the 'Cache.GTS.ReportSweepFreq' field
+func CacheGTSReportSweepFreqFlag() string { return "cache-gts-report-sweep-freq" }
+
+// GetCacheGTSReportSweepFreq safely fetches the value for global configuration 'Cache.GTS.ReportSweepFreq' field
+func GetCacheGTSReportSweepFreq() time.Duration { return global.GetCacheGTSReportSweepFreq() }
+
+// SetCacheGTSReportSweepFreq safely sets the value for global configuration 'Cache.GTS.ReportSweepFreq' field
+func SetCacheGTSReportSweepFreq(v time.Duration) { global.SetCacheGTSReportSweepFreq(v) }
+
// GetCacheGTSStatusMaxSize safely fetches the Configuration value for state's 'Cache.GTS.StatusMaxSize' field
func (st *ConfigState) GetCacheGTSStatusMaxSize() (v int) {
st.mutex.Lock()
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index e749484a8..1225b2bb0 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -83,6 +83,7 @@ type DBService struct {
db.Mention
db.Notification
db.Relationship
+ db.Report
db.Session
db.Status
db.Timeline
@@ -197,6 +198,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
conn: conn,
state: state,
},
+ Report: &reportDB{
+ conn: conn,
+ state: state,
+ },
Session: &sessionDB{
conn: conn,
},
diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go
index 45d2e70a7..e050c2b5d 100644
--- a/internal/db/bundb/bundb_test.go
+++ b/internal/db/bundb/bundb_test.go
@@ -42,6 +42,7 @@ type BunDBStandardTestSuite struct {
testMentions map[string]*gtsmodel.Mention
testFollows map[string]*gtsmodel.Follow
testEmojis map[string]*gtsmodel.Emoji
+ testReports map[string]*gtsmodel.Report
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@@ -56,6 +57,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testMentions = testrig.NewTestMentions()
suite.testFollows = testrig.NewTestFollows()
suite.testEmojis = testrig.NewTestEmojis()
+ suite.testReports = testrig.NewTestReports()
}
func (suite *BunDBStandardTestSuite) SetupTest() {
diff --git a/internal/db/bundb/migrations/20230105171144_report_model.go b/internal/db/bundb/migrations/20230105171144_report_model.go
new file mode 100644
index 000000000..b175e2995
--- /dev/null
+++ b/internal/db/bundb/migrations/20230105171144_report_model.go
@@ -0,0 +1,66 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package migrations
+
+import (
+ "context"
+
+ gtsmodel "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.Report{}).IfNotExists().Exec(ctx); err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewCreateIndex().
+ Model(>smodel.Report{}).
+ Index("report_account_id_idx").
+ Column("account_id").
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewCreateIndex().
+ Model(>smodel.Report{}).
+ Index("report_target_account_id_idx").
+ Column("target_account_id").
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go
new file mode 100644
index 000000000..8cc1d8de9
--- /dev/null
+++ b/internal/db/bundb/report.go
@@ -0,0 +1,138 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package bundb
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/uptrace/bun"
+)
+
+type reportDB struct {
+ conn *DBConn
+ state *state.State
+}
+
+func (r *reportDB) newReportQ(report interface{}) *bun.SelectQuery {
+ return r.conn.NewSelect().Model(report)
+}
+
+func (r *reportDB) GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, db.Error) {
+ return r.getReport(
+ ctx,
+ "ID",
+ func(report *gtsmodel.Report) error {
+ return r.newReportQ(report).Where("? = ?", bun.Ident("report.id"), id).Scan(ctx)
+ },
+ id,
+ )
+}
+
+func (r *reportDB) getReport(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Report) error, keyParts ...any) (*gtsmodel.Report, db.Error) {
+ // Fetch report from database cache with loader callback
+ report, err := r.state.Caches.GTS.Report().Load(lookup, func() (*gtsmodel.Report, error) {
+ var report gtsmodel.Report
+
+ // Not cached! Perform database query
+ if err := dbQuery(&report); err != nil {
+ return nil, r.conn.ProcessError(err)
+ }
+
+ return &report, nil
+ }, keyParts...)
+ if err != nil {
+ // error already processed
+ return nil, err
+ }
+
+ // Set the report author account
+ report.Account, err = r.state.DB.GetAccountByID(ctx, report.AccountID)
+ if err != nil {
+ return nil, fmt.Errorf("error getting report account: %w", err)
+ }
+
+ // Set the report target account
+ report.TargetAccount, err = r.state.DB.GetAccountByID(ctx, report.TargetAccountID)
+ if err != nil {
+ return nil, fmt.Errorf("error getting report target account: %w", err)
+ }
+
+ if len(report.StatusIDs) > 0 {
+ // Fetch reported statuses
+ report.Statuses, err = r.state.DB.GetStatuses(ctx, report.StatusIDs)
+ if err != nil {
+ return nil, fmt.Errorf("error getting status mentions: %w", err)
+ }
+ }
+
+ if report.ActionTakenByAccountID != "" {
+ // Set the report action taken by account
+ report.ActionTakenByAccount, err = r.state.DB.GetAccountByID(ctx, report.ActionTakenByAccountID)
+ if err != nil {
+ return nil, fmt.Errorf("error getting report action taken by account: %w", err)
+ }
+ }
+
+ return report, nil
+}
+
+func (r *reportDB) PutReport(ctx context.Context, report *gtsmodel.Report) db.Error {
+ return r.state.Caches.GTS.Report().Store(report, func() error {
+ _, err := r.conn.NewInsert().Model(report).Exec(ctx)
+ return r.conn.ProcessError(err)
+ })
+}
+
+func (r *reportDB) UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, db.Error) {
+ // Update the report's last-updated
+ report.UpdatedAt = time.Now()
+ if len(columns) != 0 {
+ columns = append(columns, "updated_at")
+ }
+
+ if _, err := r.conn.
+ NewUpdate().
+ Model(report).
+ Where("? = ?", bun.Ident("report.id"), report.ID).
+ Column(columns...).
+ Exec(ctx); err != nil {
+ return nil, r.conn.ProcessError(err)
+ }
+
+ r.state.Caches.GTS.Report().Invalidate("ID", report.ID)
+ return report, nil
+}
+
+func (r *reportDB) DeleteReportByID(ctx context.Context, id string) db.Error {
+ if _, err := r.conn.
+ NewDelete().
+ TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")).
+ Where("? = ?", bun.Ident("report.id"), id).
+ Exec(ctx); err != nil {
+ return r.conn.ProcessError(err)
+ }
+
+ r.state.Caches.GTS.Report().Invalidate("ID", id)
+ return nil
+}
diff --git a/internal/db/bundb/report_test.go b/internal/db/bundb/report_test.go
new file mode 100644
index 000000000..85bc4b36f
--- /dev/null
+++ b/internal/db/bundb/report_test.go
@@ -0,0 +1,147 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package bundb_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type ReportTestSuite struct {
+ BunDBStandardTestSuite
+}
+
+func (suite *ReportTestSuite) TestGetReportByID() {
+ report, err := suite.db.GetReportByID(context.Background(), suite.testReports["local_account_2_report_remote_account_1"].ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.NotNil(report)
+ suite.NotNil(report.Account)
+ suite.NotNil(report.TargetAccount)
+ suite.Zero(report.ActionTakenAt)
+ suite.Nil(report.ActionTakenByAccount)
+ suite.Empty(report.ActionTakenByAccountID)
+ suite.NotEmpty(report.URI)
+}
+
+func (suite *ReportTestSuite) TestGetReportByURI() {
+ report, err := suite.db.GetReportByID(context.Background(), suite.testReports["remote_account_1_report_local_account_2"].ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.NotNil(report)
+ suite.NotNil(report.Account)
+ suite.NotNil(report.TargetAccount)
+ suite.NotZero(report.ActionTakenAt)
+ suite.NotNil(report.ActionTakenByAccount)
+ suite.NotEmpty(report.ActionTakenByAccountID)
+ suite.NotEmpty(report.URI)
+}
+
+func (suite *ReportTestSuite) TestPutReport() {
+ ctx := context.Background()
+
+ reportID := "01GP3ECY8QJD8DBJSS8B1CR0AX"
+ report := >smodel.Report{
+ ID: reportID,
+ CreatedAt: testrig.TimeMustParse("2022-05-14T12:20:03+02:00"),
+ UpdatedAt: testrig.TimeMustParse("2022-05-14T12:20:03+02:00"),
+ URI: "http://localhost:8080/01GP3ECY8QJD8DBJSS8B1CR0AX",
+ AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
+ TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ Comment: "another report",
+ StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
+ Forwarded: testrig.TrueBool(),
+ }
+
+ err := suite.db.PutReport(ctx, report)
+ suite.NoError(err)
+}
+
+func (suite *ReportTestSuite) TestUpdateReport() {
+ ctx := context.Background()
+
+ report := >smodel.Report{}
+ *report = *suite.testReports["local_account_2_report_remote_account_1"]
+ report.ActionTaken = "nothing"
+ report.ActionTakenByAccountID = suite.testAccounts["admin_account"].ID
+ report.ActionTakenAt = testrig.TimeMustParse("2022-05-14T12:20:03+02:00")
+
+ if _, err := suite.db.UpdateReport(ctx, report, "action_taken", "action_taken_by_account_id", "action_taken_at"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ dbReport, err := suite.db.GetReportByID(ctx, report.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.NotNil(dbReport)
+ suite.NotNil(dbReport.Account)
+ suite.NotNil(dbReport.TargetAccount)
+ suite.NotZero(dbReport.ActionTakenAt)
+ suite.NotNil(dbReport.ActionTakenByAccount)
+ suite.NotEmpty(dbReport.ActionTakenByAccountID)
+ suite.NotEmpty(dbReport.URI)
+}
+
+func (suite *ReportTestSuite) TestUpdateReportAllColumns() {
+ ctx := context.Background()
+
+ report := >smodel.Report{}
+ *report = *suite.testReports["local_account_2_report_remote_account_1"]
+ report.ActionTaken = "nothing"
+ report.ActionTakenByAccountID = suite.testAccounts["admin_account"].ID
+ report.ActionTakenAt = testrig.TimeMustParse("2022-05-14T12:20:03+02:00")
+
+ if _, err := suite.db.UpdateReport(ctx, report); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ dbReport, err := suite.db.GetReportByID(ctx, report.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.NotNil(dbReport)
+ suite.NotNil(dbReport.Account)
+ suite.NotNil(dbReport.TargetAccount)
+ suite.NotZero(dbReport.ActionTakenAt)
+ suite.NotNil(dbReport.ActionTakenByAccount)
+ suite.NotEmpty(dbReport.ActionTakenByAccountID)
+ suite.NotEmpty(dbReport.URI)
+}
+
+func (suite *ReportTestSuite) TestDeleteReport() {
+ if err := suite.db.DeleteReportByID(context.Background(), suite.testReports["remote_account_1_report_local_account_2"].ID); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ report, err := suite.db.GetReportByID(context.Background(), suite.testReports["remote_account_1_report_local_account_2"].ID)
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Nil(report)
+}
+
+func TestReportTestSuite(t *testing.T) {
+ suite.Run(t, new(ReportTestSuite))
+}
diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go
index b52c06978..709105f72 100644
--- a/internal/db/bundb/status.go
+++ b/internal/db/bundb/status.go
@@ -67,6 +67,24 @@ func(status *gtsmodel.Status) error {
)
}
+func (s *statusDB) GetStatuses(ctx context.Context, ids []string) ([]*gtsmodel.Status, db.Error) {
+ statuses := make([]*gtsmodel.Status, 0, len(ids))
+
+ for _, id := range ids {
+ // Attempt fetch from DB
+ status, err := s.GetStatusByID(ctx, id)
+ if err != nil {
+ log.Errorf("GetStatuses: error getting status %q: %v", id, err)
+ continue
+ }
+
+ // Append status
+ statuses = append(statuses, status)
+ }
+
+ return statuses, nil
+}
+
func (s *statusDB) GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.Status, db.Error) {
return s.getStatus(
ctx,
diff --git a/internal/db/bundb/status_test.go b/internal/db/bundb/status_test.go
index bef8c7912..d86e0bcf9 100644
--- a/internal/db/bundb/status_test.go
+++ b/internal/db/bundb/status_test.go
@@ -50,6 +50,48 @@ func (suite *StatusTestSuite) TestGetStatusByID() {
suite.True(*status.Likeable)
}
+func (suite *StatusTestSuite) TestGetStatusesByID() {
+ ids := []string{
+ suite.testStatuses["local_account_1_status_1"].ID,
+ suite.testStatuses["local_account_2_status_3"].ID,
+ }
+
+ statuses, err := suite.db.GetStatuses(context.Background(), ids)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if len(statuses) != 2 {
+ suite.FailNow("expected 2 statuses in slice")
+ }
+
+ status1 := statuses[0]
+ suite.NotNil(status1)
+ suite.NotNil(status1.Account)
+ suite.NotNil(status1.CreatedWithApplication)
+ suite.Nil(status1.BoostOf)
+ suite.Nil(status1.BoostOfAccount)
+ suite.Nil(status1.InReplyTo)
+ suite.Nil(status1.InReplyToAccount)
+ suite.True(*status1.Federated)
+ suite.True(*status1.Boostable)
+ suite.True(*status1.Replyable)
+ suite.True(*status1.Likeable)
+
+ status2 := statuses[1]
+ suite.NotNil(status2)
+ suite.NotNil(status2.Account)
+ suite.NotNil(status2.CreatedWithApplication)
+ suite.Nil(status2.BoostOf)
+ suite.Nil(status2.BoostOfAccount)
+ suite.Nil(status2.InReplyTo)
+ suite.Nil(status2.InReplyToAccount)
+ suite.True(*status2.Federated)
+ suite.True(*status2.Boostable)
+ suite.False(*status2.Replyable)
+ suite.False(*status2.Likeable)
+}
+
func (suite *StatusTestSuite) TestGetStatusByURI() {
status, err := suite.db.GetStatusByURI(context.Background(), suite.testStatuses["local_account_2_status_3"].URI)
if err != nil {
diff --git a/internal/db/db.go b/internal/db/db.go
index efe867e3e..aa1929da9 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -41,6 +41,7 @@ type DB interface {
Mention
Notification
Relationship
+ Report
Session
Status
Timeline
diff --git a/internal/db/report.go b/internal/db/report.go
new file mode 100644
index 000000000..216e10fdd
--- /dev/null
+++ b/internal/db/report.go
@@ -0,0 +1,41 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package db
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// Report handles getting/creation/deletion/updating of user reports/flags.
+type Report interface {
+ // GetReportByID gets one report by its db id
+ GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, Error)
+ // PutReport puts the given report in the database.
+ PutReport(ctx context.Context, report *gtsmodel.Report) Error
+ // UpdateReport updates one report by its db id.
+ // The given columns will be updated; if no columns are
+ // provided, then all columns will be updated.
+ // updated_at will also be updated, no need to pass this
+ // as a specific column.
+ UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, Error)
+ // DeleteReportByID deletes report with the given id.
+ DeleteReportByID(ctx context.Context, id string) Error
+}
diff --git a/internal/db/status.go b/internal/db/status.go
index f854664c8..15d1362f5 100644
--- a/internal/db/status.go
+++ b/internal/db/status.go
@@ -29,6 +29,9 @@ type Status interface {
// GetStatusByID returns one status from the database, with no rel fields populated, only their linking ID / URIs
GetStatusByID(ctx context.Context, id string) (*gtsmodel.Status, Error)
+ // GetStatuses gets a slice of statuses corresponding to the given status IDs.
+ GetStatuses(ctx context.Context, ids []string) ([]*gtsmodel.Status, Error)
+
// GetStatusByURI returns one status from the database, with no rel fields populated, only their linking ID / URIs
GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.Status, Error)
diff --git a/internal/gtsmodel/report.go b/internal/gtsmodel/report.go
new file mode 100644
index 000000000..782519af1
--- /dev/null
+++ b/internal/gtsmodel/report.go
@@ -0,0 +1,46 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel
+
+import "time"
+
+// Report models a user-created reported about an account, which should be reviewed
+// and acted upon by instance admins.
+//
+// This can be either a report created locally (on this instance) about a user on this
+// or another instance, OR a report that was created remotely (on another instance)
+// about a user on this instance, and received via the federated (s2s) API.
+type Report struct {
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
+ URI string `validate:"required,url" bun:",unique,nullzero,notnull"` // activitypub URI of this report
+ AccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // which account created this report
+ Account *Account `validate:"-" bun:"-"` // account corresponding to AccountID
+ TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // which account is targeted by this report
+ TargetAccount *Account `validate:"-" bun:"-"` // account corresponding to TargetAccountID
+ Comment string `validate:"-" bun:",nullzero"` // comment / explanation for this report, by the reporter
+ StatusIDs []string `validate:"dive,ulid" bun:"statuses,array"` // database IDs of any statuses referenced by this report
+ Statuses []*Status `validate:"-" bun:"-"` // statuses corresponding to StatusIDs
+ Forwarded *bool `validate:"-" bun:",nullzero,notnull,default:false"` // flag to indicate report should be forwarded to remote instance
+ ActionTaken string `validate:"-" bun:",nullzero"` // string description of what action was taken in response to this report
+ ActionTakenAt time.Time `validate:"-" bun:"type:timestamptz,nullzero"` // time at which action was taken, if any
+ ActionTakenByAccountID string `validate:",omitempty,ulid" bun:"type:CHAR(26),nullzero"` // database ID of account which took action, if any
+ ActionTakenByAccount *Account `validate:"-" bun:"-"` // account corresponding to ActionTakenByID, if any
+}
diff --git a/internal/regexes/regexes.go b/internal/regexes/regexes.go
index 8c5b0b601..4c9d48dac 100644
--- a/internal/regexes/regexes.go
+++ b/internal/regexes/regexes.go
@@ -36,12 +36,10 @@
followers = "followers"
following = "following"
liked = "liked"
- // collections = "collections"
- // featured = "featured"
publicKey = "main-key"
follow = "follow"
- // update = "updates"
- blocks = "blocks"
+ blocks = "blocks"
+ reports = "reports"
)
const (
@@ -141,6 +139,11 @@
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
BlockPath = regexp.MustCompile(blockPath)
+ reportPath = fmt.Sprintf(`^/?%s/(%s)$`, reports, ulid)
+ // ReportPath parses a path that validates and captures the ulid part
+ // from eg /reports/01GP3AWY4CRDVRNZKW0TEAMB5R
+ ReportPath = regexp.MustCompile(reportPath)
+
filePath = fmt.Sprintf(`^(%s)/([a-z]+)/([a-z]+)/(%s)\.([a-z]+)$`, ulid, ulid)
// FilePath parses a file storage path of the form [ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
diff --git a/internal/uris/uri.go b/internal/uris/uri.go
index 287e66439..f6e06ca25 100644
--- a/internal/uris/uri.go
+++ b/internal/uris/uri.go
@@ -28,7 +28,6 @@
const (
UsersPath = "users" // UsersPath is for serving users info
- ActorsPath = "actors" // ActorsPath is for serving actors info
StatusesPath = "statuses" // StatusesPath is for serving statuses
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
@@ -41,6 +40,7 @@
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
UpdatePath = "updates" // UpdatePath is used to generate the URI for an account update
BlocksPath = "blocks" // BlocksPath is used to generate the URI for a block
+ ReportsPath = "reports" // ReportsPath is used to generate the URI for a report/flag
ConfirmEmailPath = "confirm_email" // ConfirmEmailPath is used to generate the URI for an email confirmation link
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
@@ -107,6 +107,17 @@ func GenerateURIForBlock(username string, thisBlockID string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, BlocksPath, thisBlockID)
}
+// GenerateURIForReport returns the API URI for a new Flag activity -- something like:
+// https://example.org/reports/01GP3AWY4CRDVRNZKW0TEAMB5R
+//
+// This path specifically doesn't contain any info about the user who did the reporting,
+// to protect their privacy.
+func GenerateURIForReport(thisReportID string) string {
+ protocol := config.GetProtocol()
+ host := config.GetHost()
+ return fmt.Sprintf("%s://%s/%s/%s", protocol, host, ReportsPath, thisReportID)
+}
+
// GenerateURIForEmailConfirm returns a link for email confirmation -- something like:
// https://example.org/confirm_email?token=490e337c-0162-454f-ac48-4b22bb92a205
func GenerateURIForEmailConfirm(token string) string {
@@ -228,6 +239,11 @@ func IsBlockPath(id *url.URL) bool {
return regexes.BlockPath.MatchString(id.Path)
}
+// IsReportPath returns true if the given URL path corresponds to eg /reports/SOME_ULID_OF_A_REPORT
+func IsReportPath(id *url.URL) bool {
+ return regexes.ReportPath.MatchString(id.Path)
+}
+
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
matches := regexes.StatusesPath.FindStringSubmatch(id.Path)
@@ -318,3 +334,14 @@ func ParseBlockPath(id *url.URL) (username string, ulid string, err error) {
ulid = matches[2]
return
}
+
+// ParseReportPath returns the ulid from a path such as /reports/SOME_ULID_OF_A_REPORT
+func ParseReportPath(id *url.URL) (ulid string, err error) {
+ matches := regexes.ReportPath.FindStringSubmatch(id.Path)
+ if len(matches) != 2 {
+ err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
+ return
+ }
+ ulid = matches[1]
+ return
+}
diff --git a/test/envparsing.sh b/test/envparsing.sh
index 0de6d0bcf..b7d3083be 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -2,7 +2,7 @@
set -eu
-EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"advanced-throttling-multiplier":-1,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":false,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}'
+EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"advanced-throttling-multiplier":-1,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"report-max-size":100,"report-sweep-freq":10000000000,"report-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":false,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}'
# Set all the environment variables to
# ensure that these are parsed without panic
diff --git a/testrig/db.go b/testrig/db.go
index ed9c1b916..4304050cf 100644
--- a/testrig/db.go
+++ b/testrig/db.go
@@ -58,6 +58,7 @@
>smodel.Client{},
>smodel.EmojiCategory{},
>smodel.Tombstone{},
+ >smodel.Report{},
}
// NewTestDB returns a new initialized, empty database for testing.
@@ -157,6 +158,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
}
}
+ for _, v := range NewTestReports() {
+ if err := db.Put(ctx, v); err != nil {
+ log.Panic(err)
+ }
+ }
+
for _, v := range NewTestDomainBlocks() {
if err := db.Put(ctx, v); err != nil {
log.Panic(err)
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 6845abdb9..88c5df77a 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -1971,6 +1971,36 @@ func NewTestBlocks() map[string]*gtsmodel.Block {
}
}
+func NewTestReports() map[string]*gtsmodel.Report {
+ return map[string]*gtsmodel.Report{
+ "local_account_2_report_remote_account_1": {
+ ID: "01GP3AWY4CRDVRNZKW0TEAMB5R",
+ CreatedAt: TimeMustParse("2022-05-14T12:20:03+02:00"),
+ UpdatedAt: TimeMustParse("2022-05-14T12:20:03+02:00"),
+ URI: "http://localhost:8080/01GP3AWY4CRDVRNZKW0TEAMB5R",
+ AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
+ TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ Comment: "dark souls sucks, please yeet this nerd",
+ StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
+ Forwarded: TrueBool(),
+ },
+ "remote_account_1_report_local_account_2": {
+ ID: "01GP3DFY9XQ1TJMZT5BGAZPXX7",
+ CreatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ UpdatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ URI: "http://fossbros-anonymous.io/87fb1478-ac46-406a-8463-96ce05645219",
+ AccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
+ Comment: "this is a turtle, not a person, therefore should not be a poster",
+ StatusIDs: []string{},
+ Forwarded: TrueBool(),
+ ActionTaken: "user was warned not to be a turtle anymore",
+ ActionTakenAt: TimeMustParse("2022-05-15T17:01:56+02:00"),
+ ActionTakenByAccountID: "01AY6P665V14JJR0AFVRT7311Y",
+ },
+ }
+}
+
// ActivityWithSignature wraps a pub.Activity along with its signature headers, for testing.
type ActivityWithSignature struct {
Activity pub.Activity