[feature] Implement Report database model and utility functions (#1310)
* implement report database model * implement report cache + config changes * implement report database functions * report uri / regex functions * update envparsing test * remove unnecessary uri index * remove unused function + cache lookup * process error when storing report
This commit is contained in:
parent
36aa6854bd
commit
d6487933c7
|
@ -207,6 +207,10 @@ cache:
|
||||||
notification-ttl: "5m"
|
notification-ttl: "5m"
|
||||||
notification-sweep-freq: "10s"
|
notification-sweep-freq: "10s"
|
||||||
|
|
||||||
|
report-max-size: 100
|
||||||
|
report-ttl: "5m"
|
||||||
|
report-sweep-freq: "10s"
|
||||||
|
|
||||||
status-max-size: 500
|
status-max-size: 500
|
||||||
status-ttl: "5m"
|
status-ttl: "5m"
|
||||||
status-sweep-freq: "10s"
|
status-sweep-freq: "10s"
|
||||||
|
|
|
@ -57,6 +57,9 @@ type GTSCaches interface {
|
||||||
// Notification provides access to the gtsmodel Notification database cache.
|
// Notification provides access to the gtsmodel Notification database cache.
|
||||||
Notification() *result.Cache[*gtsmodel.Notification]
|
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 provides access to the gtsmodel Status database cache.
|
||||||
Status() *result.Cache[*gtsmodel.Status]
|
Status() *result.Cache[*gtsmodel.Status]
|
||||||
|
|
||||||
|
@ -80,6 +83,7 @@ type gtsCaches struct {
|
||||||
emojiCategory *result.Cache[*gtsmodel.EmojiCategory]
|
emojiCategory *result.Cache[*gtsmodel.EmojiCategory]
|
||||||
mention *result.Cache[*gtsmodel.Mention]
|
mention *result.Cache[*gtsmodel.Mention]
|
||||||
notification *result.Cache[*gtsmodel.Notification]
|
notification *result.Cache[*gtsmodel.Notification]
|
||||||
|
report *result.Cache[*gtsmodel.Report]
|
||||||
status *result.Cache[*gtsmodel.Status]
|
status *result.Cache[*gtsmodel.Status]
|
||||||
tombstone *result.Cache[*gtsmodel.Tombstone]
|
tombstone *result.Cache[*gtsmodel.Tombstone]
|
||||||
user *result.Cache[*gtsmodel.User]
|
user *result.Cache[*gtsmodel.User]
|
||||||
|
@ -93,6 +97,7 @@ func (c *gtsCaches) Init() {
|
||||||
c.initEmojiCategory()
|
c.initEmojiCategory()
|
||||||
c.initMention()
|
c.initMention()
|
||||||
c.initNotification()
|
c.initNotification()
|
||||||
|
c.initReport()
|
||||||
c.initStatus()
|
c.initStatus()
|
||||||
c.initTombstone()
|
c.initTombstone()
|
||||||
c.initUser()
|
c.initUser()
|
||||||
|
@ -120,6 +125,9 @@ func (c *gtsCaches) Start() {
|
||||||
tryUntil("starting gtsmodel.Notification cache", 5, func() bool {
|
tryUntil("starting gtsmodel.Notification cache", 5, func() bool {
|
||||||
return c.notification.Start(config.GetCacheGTSNotificationSweepFreq())
|
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 {
|
tryUntil("starting gtsmodel.Status cache", 5, func() bool {
|
||||||
return c.status.Start(config.GetCacheGTSStatusSweepFreq())
|
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.EmojiCategory cache", 5, c.emojiCategory.Stop)
|
||||||
tryUntil("stopping gtsmodel.Mention cache", 5, c.mention.Stop)
|
tryUntil("stopping gtsmodel.Mention cache", 5, c.mention.Stop)
|
||||||
tryUntil("stopping gtsmodel.Notification cache", 5, c.notification.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.Status cache", 5, c.status.Stop)
|
||||||
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
|
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
|
||||||
tryUntil("stopping gtsmodel.User cache", 5, c.user.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
|
return c.notification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *gtsCaches) Report() *result.Cache[*gtsmodel.Report] {
|
||||||
|
return c.report
|
||||||
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) Status() *result.Cache[*gtsmodel.Status] {
|
func (c *gtsCaches) Status() *result.Cache[*gtsmodel.Status] {
|
||||||
return c.status
|
return c.status
|
||||||
}
|
}
|
||||||
|
@ -267,6 +280,17 @@ func (c *gtsCaches) initNotification() {
|
||||||
c.notification.SetTTL(config.GetCacheGTSNotificationTTL(), true)
|
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() {
|
func (c *gtsCaches) initStatus() {
|
||||||
c.status = result.New([]result.Lookup{
|
c.status = result.New([]result.Lookup{
|
||||||
{Name: "ID"},
|
{Name: "ID"},
|
||||||
|
|
|
@ -175,6 +175,10 @@ type GTSCacheConfiguration struct {
|
||||||
NotificationTTL time.Duration `name:"notification-ttl"`
|
NotificationTTL time.Duration `name:"notification-ttl"`
|
||||||
NotificationSweepFreq time.Duration `name:"notification-sweep-freq"`
|
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"`
|
StatusMaxSize int `name:"status-max-size"`
|
||||||
StatusTTL time.Duration `name:"status-ttl"`
|
StatusTTL time.Duration `name:"status-ttl"`
|
||||||
StatusSweepFreq time.Duration `name:"status-sweep-freq"`
|
StatusSweepFreq time.Duration `name:"status-sweep-freq"`
|
||||||
|
|
|
@ -138,6 +138,10 @@ var Defaults = Configuration{
|
||||||
NotificationTTL: time.Minute * 5,
|
NotificationTTL: time.Minute * 5,
|
||||||
NotificationSweepFreq: time.Second * 10,
|
NotificationSweepFreq: time.Second * 10,
|
||||||
|
|
||||||
|
ReportMaxSize: 100,
|
||||||
|
ReportTTL: time.Minute * 5,
|
||||||
|
ReportSweepFreq: time.Second * 10,
|
||||||
|
|
||||||
StatusMaxSize: 500,
|
StatusMaxSize: 500,
|
||||||
StatusTTL: time.Minute * 5,
|
StatusTTL: time.Minute * 5,
|
||||||
StatusSweepFreq: time.Second * 10,
|
StatusSweepFreq: time.Second * 10,
|
||||||
|
|
|
@ -2378,6 +2378,81 @@ func GetCacheGTSNotificationSweepFreq() time.Duration {
|
||||||
// SetCacheGTSNotificationSweepFreq safely sets the value for global configuration 'Cache.GTS.NotificationSweepFreq' field
|
// SetCacheGTSNotificationSweepFreq safely sets the value for global configuration 'Cache.GTS.NotificationSweepFreq' field
|
||||||
func SetCacheGTSNotificationSweepFreq(v time.Duration) { global.SetCacheGTSNotificationSweepFreq(v) }
|
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
|
// GetCacheGTSStatusMaxSize safely fetches the Configuration value for state's 'Cache.GTS.StatusMaxSize' field
|
||||||
func (st *ConfigState) GetCacheGTSStatusMaxSize() (v int) {
|
func (st *ConfigState) GetCacheGTSStatusMaxSize() (v int) {
|
||||||
st.mutex.Lock()
|
st.mutex.Lock()
|
||||||
|
|
|
@ -83,6 +83,7 @@ type DBService struct {
|
||||||
db.Mention
|
db.Mention
|
||||||
db.Notification
|
db.Notification
|
||||||
db.Relationship
|
db.Relationship
|
||||||
|
db.Report
|
||||||
db.Session
|
db.Session
|
||||||
db.Status
|
db.Status
|
||||||
db.Timeline
|
db.Timeline
|
||||||
|
@ -197,6 +198,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||||
conn: conn,
|
conn: conn,
|
||||||
state: state,
|
state: state,
|
||||||
},
|
},
|
||||||
|
Report: &reportDB{
|
||||||
|
conn: conn,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
Session: &sessionDB{
|
Session: &sessionDB{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,6 +42,7 @@ type BunDBStandardTestSuite struct {
|
||||||
testMentions map[string]*gtsmodel.Mention
|
testMentions map[string]*gtsmodel.Mention
|
||||||
testFollows map[string]*gtsmodel.Follow
|
testFollows map[string]*gtsmodel.Follow
|
||||||
testEmojis map[string]*gtsmodel.Emoji
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
|
testReports map[string]*gtsmodel.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
|
@ -56,6 +57,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
suite.testMentions = testrig.NewTestMentions()
|
suite.testMentions = testrig.NewTestMentions()
|
||||||
suite.testFollows = testrig.NewTestFollows()
|
suite.testFollows = testrig.NewTestFollows()
|
||||||
suite.testEmojis = testrig.NewTestEmojis()
|
suite.testEmojis = testrig.NewTestEmojis()
|
||||||
|
suite.testReports = testrig.NewTestReports()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -67,6 +67,24 @@ func (s *statusDB) GetStatusByID(ctx context.Context, id string) (*gtsmodel.Stat
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *statusDB) GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.Status, db.Error) {
|
||||||
return s.getStatus(
|
return s.getStatus(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
|
@ -50,6 +50,48 @@ func (suite *StatusTestSuite) TestGetStatusByID() {
|
||||||
suite.True(*status.Likeable)
|
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() {
|
func (suite *StatusTestSuite) TestGetStatusByURI() {
|
||||||
status, err := suite.db.GetStatusByURI(context.Background(), suite.testStatuses["local_account_2_status_3"].URI)
|
status, err := suite.db.GetStatusByURI(context.Background(), suite.testStatuses["local_account_2_status_3"].URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -41,6 +41,7 @@ type DB interface {
|
||||||
Mention
|
Mention
|
||||||
Notification
|
Notification
|
||||||
Relationship
|
Relationship
|
||||||
|
Report
|
||||||
Session
|
Session
|
||||||
Status
|
Status
|
||||||
Timeline
|
Timeline
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 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)
|
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 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)
|
GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.Status, Error)
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -36,12 +36,10 @@ const (
|
||||||
followers = "followers"
|
followers = "followers"
|
||||||
following = "following"
|
following = "following"
|
||||||
liked = "liked"
|
liked = "liked"
|
||||||
// collections = "collections"
|
|
||||||
// featured = "featured"
|
|
||||||
publicKey = "main-key"
|
publicKey = "main-key"
|
||||||
follow = "follow"
|
follow = "follow"
|
||||||
// update = "updates"
|
blocks = "blocks"
|
||||||
blocks = "blocks"
|
reports = "reports"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -141,6 +139,11 @@ var (
|
||||||
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
BlockPath = regexp.MustCompile(blockPath)
|
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 = 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]
|
// FilePath parses a file storage path of the form [ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]
|
||||||
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
|
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
|
||||||
|
|
|
@ -28,7 +28,6 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UsersPath = "users" // UsersPath is for serving users info
|
UsersPath = "users" // UsersPath is for serving users info
|
||||||
ActorsPath = "actors" // ActorsPath is for serving actors info
|
|
||||||
StatusesPath = "statuses" // StatusesPath is for serving statuses
|
StatusesPath = "statuses" // StatusesPath is for serving statuses
|
||||||
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
|
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
|
||||||
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
|
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
|
||||||
|
@ -41,6 +40,7 @@ const (
|
||||||
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
|
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
|
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
|
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
|
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
|
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
||||||
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
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)
|
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:
|
// GenerateURIForEmailConfirm returns a link for email confirmation -- something like:
|
||||||
// https://example.org/confirm_email?token=490e337c-0162-454f-ac48-4b22bb92a205
|
// https://example.org/confirm_email?token=490e337c-0162-454f-ac48-4b22bb92a205
|
||||||
func GenerateURIForEmailConfirm(token string) string {
|
func GenerateURIForEmailConfirm(token string) string {
|
||||||
|
@ -228,6 +239,11 @@ func IsBlockPath(id *url.URL) bool {
|
||||||
return regexes.BlockPath.MatchString(id.Path)
|
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
|
// 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) {
|
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
matches := regexes.StatusesPath.FindStringSubmatch(id.Path)
|
matches := regexes.StatusesPath.FindStringSubmatch(id.Path)
|
||||||
|
@ -318,3 +334,14 @@ func ParseBlockPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
ulid = matches[2]
|
ulid = matches[2]
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
set -eu
|
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
|
# Set all the environment variables to
|
||||||
# ensure that these are parsed without panic
|
# ensure that these are parsed without panic
|
||||||
|
|
|
@ -58,6 +58,7 @@ var testModels = []interface{}{
|
||||||
>smodel.Client{},
|
>smodel.Client{},
|
||||||
>smodel.EmojiCategory{},
|
>smodel.EmojiCategory{},
|
||||||
>smodel.Tombstone{},
|
>smodel.Tombstone{},
|
||||||
|
>smodel.Report{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestDB returns a new initialized, empty database for testing.
|
// 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() {
|
for _, v := range NewTestDomainBlocks() {
|
||||||
if err := db.Put(ctx, v); err != nil {
|
if err := db.Put(ctx, v); err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
|
|
|
@ -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.
|
// ActivityWithSignature wraps a pub.Activity along with its signature headers, for testing.
|
||||||
type ActivityWithSignature struct {
|
type ActivityWithSignature struct {
|
||||||
Activity pub.Activity
|
Activity pub.Activity
|
||||||
|
|
Loading…
Reference in New Issue