mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-09 14:08:06 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
commit
0e137c0f2d
1759 changed files with 864109 additions and 314186 deletions
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -86,7 +87,7 @@ func (a *accountDB) GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsm
|
|||
// Reorder the statuses by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(a *gtsmodel.Account) string { return a.ID }
|
||||
util.OrderBy(accounts, ids, getID)
|
||||
xslices.OrderBy(accounts, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -136,8 +137,9 @@ func (a *accountDB) GetAccountByURL(ctx context.Context, url string) (*gtsmodel.
|
|||
|
||||
func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, error) {
|
||||
if domain != "" {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ type AccountTestSuite struct {
|
|||
func (suite *AccountTestSuite) TestGetAccountStatuses() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 8)
|
||||
suite.Len(statuses, 9)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
||||
|
|
@ -69,7 +69,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.Len(statuses, 2)
|
||||
suite.Len(statuses, 3)
|
||||
|
||||
// try to get the last page (should be empty)
|
||||
statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
|
||||
|
|
@ -80,13 +80,13 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
|||
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 7)
|
||||
suite.Len(statuses, 8)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 3)
|
||||
suite.Len(statuses, 4)
|
||||
}
|
||||
|
||||
// populateTestStatus adds mandatory fields to a partially populated status.
|
||||
|
|
@ -106,7 +106,7 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status
|
|||
status.URI = fmt.Sprintf("http://localhost:8080/users/%s/statuses/%s", testAccount.Username, status.ID)
|
||||
status.Local = util.Ptr(true)
|
||||
|
||||
if status.Visibility == "" {
|
||||
if status.Visibility == 0 {
|
||||
status.Visibility = gtsmodel.VisibilityDefault
|
||||
}
|
||||
if status.ActivityStreamsType == "" {
|
||||
|
|
@ -173,7 +173,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesExcludesSelfR
|
|||
testAccount := suite.testAccounts["local_account_1"]
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), testAccount.ID, 20, true, true, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 8)
|
||||
suite.Len(statuses, 9)
|
||||
for _, status := range statuses {
|
||||
if status.InReplyToID != "" && status.InReplyToAccountID != testAccount.ID {
|
||||
suite.FailNowf("", "Status with ID %s is a non-self reply and should have been excluded", status.ID)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ func (a *applicationDB) GetAllTokens(ctx context.Context) ([]*gtsmodel.Token, er
|
|||
// Reoroder the tokens by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(t *gtsmodel.Token) string { return t.ID }
|
||||
util.OrderBy(tokens, tokenIDs, getID)
|
||||
xslices.OrderBy(tokens, tokenIDs, getID)
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
|
|||
s := []*gtsmodel.Status{}
|
||||
err := suite.db.GetAll(context.Background(), &s)
|
||||
suite.NoError(err)
|
||||
suite.Len(s, 25)
|
||||
suite.Len(s, 28)
|
||||
}
|
||||
|
||||
func (suite *BasicTestSuite) TestGetAllNotNull() {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
|
|
@ -48,6 +49,7 @@ import (
|
|||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
// DBService satisfies the DB interface
|
||||
|
|
@ -79,6 +81,7 @@ type DBService struct {
|
|||
db.SinBinStatus
|
||||
db.Status
|
||||
db.StatusBookmark
|
||||
db.StatusEdit
|
||||
db.StatusFave
|
||||
db.Tag
|
||||
db.Thread
|
||||
|
|
@ -130,18 +133,18 @@ func doMigration(ctx context.Context, db *bun.DB) error {
|
|||
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
|
||||
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
|
||||
func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||
var db *bun.DB
|
||||
var sqldb *sql.DB
|
||||
var dialect func() schema.Dialect
|
||||
var err error
|
||||
t := strings.ToLower(config.GetDbType())
|
||||
|
||||
switch t {
|
||||
switch t := strings.ToLower(config.GetDbType()); t {
|
||||
case "postgres":
|
||||
db, err = pgConn(ctx)
|
||||
sqldb, dialect, err = pgConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "sqlite":
|
||||
db, err = sqliteConn(ctx)
|
||||
sqldb, dialect, err = sqliteConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -149,34 +152,20 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return nil, fmt.Errorf("database type %s not supported for bundb", t)
|
||||
}
|
||||
|
||||
// Add database query hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
// perform any pending database migrations: this includes
|
||||
// the very first 'migration' on startup which just creates
|
||||
// necessary tables
|
||||
if err := doMigration(ctx, db); err != nil {
|
||||
// perform any pending database migrations: this includes the first
|
||||
// 'migration' on startup which just creates necessary db tables.
|
||||
//
|
||||
// Note this uses its own instance of bun.DB as bun will automatically
|
||||
// store in-memory reflect type schema of any Go models passed to it,
|
||||
// and we still maintain lots of old model versions in the migrations.
|
||||
if err := doMigration(ctx, bunDB(sqldb, dialect)); err != nil {
|
||||
return nil, fmt.Errorf("db migration error: %s", err)
|
||||
}
|
||||
|
||||
// Wrap sql.DB as bun.DB type,
|
||||
// adding any connection hooks.
|
||||
db := bunDB(sqldb, dialect)
|
||||
|
||||
ps := &DBService{
|
||||
Account: &accountDB{
|
||||
db: db,
|
||||
|
|
@ -284,6 +273,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
db: db,
|
||||
state: state,
|
||||
},
|
||||
StatusEdit: &statusEditDB{
|
||||
db: db,
|
||||
state: state,
|
||||
},
|
||||
StatusFave: &statusFaveDB{
|
||||
db: db,
|
||||
state: state,
|
||||
|
|
@ -318,17 +311,47 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return ps, nil
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*bun.DB, error) {
|
||||
// bunDB returns a new bun.DB for given sql.DB connection pool and dialect
|
||||
// function. This can be used to apply any necessary opts / hooks as we
|
||||
// initialize a bun.DB object both before and after performing migrations.
|
||||
func bunDB(sqldb *sql.DB, dialect func() schema.Dialect) *bun.DB {
|
||||
db := bun.NewDB(sqldb, dialect())
|
||||
|
||||
// Add our SQL connection hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
opts, err := deriveBunDBPGOptions() //nolint:contextcheck
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
}
|
||||
|
||||
cfg := stdlib.RegisterConnConfig(opts)
|
||||
|
||||
sqldb, err := sql.Open("pgx-gts", cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
}
|
||||
|
||||
// Tune db connections for postgres, see:
|
||||
|
|
@ -338,22 +361,20 @@ func pgConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetMaxIdleConns(2) // assume default 2; if max idle is less than max open, it will be automatically adjusted
|
||||
sqldb.SetConnMaxLifetime(5 * time.Minute) // fine to kill old connections
|
||||
|
||||
db := bun.NewDB(sqldb, pgdialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("postgres ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("postgres ping: %w", err)
|
||||
}
|
||||
|
||||
log.Info(ctx, "connected to POSTGRES database")
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return pgdialect.New() }, nil
|
||||
}
|
||||
|
||||
func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
||||
func sqliteConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
// validate db address has actually been set
|
||||
address := config.GetDbAddress()
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
}
|
||||
|
||||
// Build SQLite connection address with prefs.
|
||||
|
|
@ -362,7 +383,7 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
// Open new DB instance
|
||||
sqldb, err := sql.Open("sqlite-gts", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
return nil, nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
}
|
||||
|
||||
// Tune db connections for sqlite, see:
|
||||
|
|
@ -378,16 +399,14 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetConnMaxLifetime(5 * time.Minute)
|
||||
}
|
||||
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "connected to SQLITE database with address %s", address)
|
||||
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return sqlitedialect.New() }, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -401,19 +420,30 @@ func maxOpenConns() int {
|
|||
if multiplier < 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Specifically for SQLite databases with
|
||||
// a journal mode of anything EXCEPT "wal",
|
||||
// only 1 concurrent connection is supported.
|
||||
if strings.ToLower(config.GetDbType()) == "sqlite" {
|
||||
journalMode := config.GetDbSqliteJournalMode()
|
||||
journalMode = strings.ToLower(journalMode)
|
||||
if journalMode != "wal" {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier * runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
// deriveBunDBPGOptions takes an application config and returns either a ready-to-use set of options
|
||||
// with sensible defaults, or an error if it's not satisfied by the provided config.
|
||||
func deriveBunDBPGOptions() (*pgx.ConnConfig, error) {
|
||||
url := config.GetDbPostgresConnectionString()
|
||||
|
||||
// if database URL is defined, ignore other DB related configuration fields
|
||||
if url != "" {
|
||||
cfg, err := pgx.ParseConfig(url)
|
||||
return cfg, err
|
||||
// If database URL is defined, ignore
|
||||
// other DB-related configuration fields.
|
||||
if url := config.GetDbPostgresConnectionString(); url != "" {
|
||||
return pgx.ParseConfig(url)
|
||||
}
|
||||
|
||||
// these are all optional, the db adapter figures out defaults
|
||||
address := config.GetDbAddress()
|
||||
|
||||
|
|
@ -477,7 +507,10 @@ func deriveBunDBPGOptions() (*pgx.ConnConfig, error) {
|
|||
cfg.Host = address
|
||||
}
|
||||
if port := config.GetDbPort(); port > 0 {
|
||||
cfg.Port = uint16(port)
|
||||
if port > math.MaxUint16 {
|
||||
return nil, errors.New("invalid port, must be in range 1-65535")
|
||||
}
|
||||
cfg.Port = uint16(port) // #nosec G115 -- Just validated above.
|
||||
}
|
||||
if u := config.GetDbUser(); u != "" {
|
||||
cfg.User = u
|
||||
|
|
@ -502,15 +535,12 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
//
|
||||
// - SQLite by itself supports setting a subset of its configuration options
|
||||
// via URI query arguments in the connection. Namely `mode` and `cache`.
|
||||
// This is the same situation for the directly transpiled C->Go code in
|
||||
// modernc.org/sqlite, i.e. modernc.org/sqlite/lib, NOT the Go SQL driver.
|
||||
// This is the same situation for our supported SQLite implementations.
|
||||
//
|
||||
// - `modernc.org/sqlite` has a "shim" around it to allow the directly
|
||||
// transpiled C code to be usable with a more native Go API. This is in
|
||||
// the form of a `database/sql/driver.Driver{}` implementation that calls
|
||||
// through to the transpiled C code.
|
||||
// - Both implementations have a "shim" around them in the form of a
|
||||
// `database/sql/driver.Driver{}` implementation.
|
||||
//
|
||||
// - The SQLite shim we interface with adds support for setting ANY of the
|
||||
// - The SQLite shims we interface with add support for setting ANY of the
|
||||
// configuration options via query arguments, through using a special `_pragma`
|
||||
// query key that specifies SQLite PRAGMAs to set upon opening each connection.
|
||||
// As such you will see below that most config is set with the `_pragma` key.
|
||||
|
|
@ -536,12 +566,6 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
// reached. And for whatever reason (:shrug:) SQLite is very particular about
|
||||
// setting this BEFORE the `journal_mode` is set, otherwise you can end up
|
||||
// running into more of these `SQLITE_BUSY` return codes than you might expect.
|
||||
//
|
||||
// - One final thing (I promise!): `SQLITE_BUSY` is only handled by the internal
|
||||
// `busy_timeout` handler in the case that a data race occurs contending for
|
||||
// table locks. THERE ARE STILL OTHER SITUATIONS IN WHICH THIS MAY BE RETURNED!
|
||||
// As such, we use our wrapping DB{} and Tx{} types (in "db.go") which make use
|
||||
// of our own retry-busy handler.
|
||||
|
||||
// Drop anything fancy from DB address
|
||||
addr = strings.Split(addr, "?")[0] // drop any provided query strings
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ type BunDBStandardTestSuite struct {
|
|||
testPolls map[string]*gtsmodel.Poll
|
||||
testPollVotes map[string]*gtsmodel.PollVote
|
||||
testInteractionRequests map[string]*gtsmodel.InteractionRequest
|
||||
testStatusEdits map[string]*gtsmodel.StatusEdit
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||
|
|
@ -83,6 +84,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
|||
suite.testPolls = testrig.NewTestPolls()
|
||||
suite.testPollVotes = testrig.NewTestPollVotes()
|
||||
suite.testInteractionRequests = testrig.NewTestInteractionRequests()
|
||||
suite.testStatusEdits = testrig.NewTestStatusEdits()
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -209,7 +209,7 @@ func (c *conversationDB) getConversationsByLastStatusIDs(
|
|||
|
||||
// Reorder the conversations by their last status IDs to ensure correct order.
|
||||
getID := func(b *gtsmodel.Conversation) string { return b.ID }
|
||||
util.OrderBy(conversations, conversationLastStatusIDs, getID)
|
||||
xslices.OrderBy(conversations, conversationLastStatusIDs, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -558,7 +558,7 @@ func (c *conversationDB) DeleteStatusFromConversations(ctx context.Context, stat
|
|||
|
||||
// Invalidate cache entries.
|
||||
updatedConversationIDs = append(updatedConversationIDs, deletedConversationIDs...)
|
||||
updatedConversationIDs = util.Deduplicate(updatedConversationIDs)
|
||||
updatedConversationIDs = xslices.Deduplicate(updatedConversationIDs)
|
||||
c.state.Caches.DB.Conversation.InvalidateIDs("ID", updatedConversationIDs)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -35,12 +36,12 @@ type domainDB struct {
|
|||
state *state.State
|
||||
}
|
||||
|
||||
func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
allow.Domain, err = util.Punify(allow.Domain)
|
||||
func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) (err error) {
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
allow.Domain, err = util.PunifySafely(allow.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
return gtserror.Newf("error punifying domain %s: %w", allow.Domain, err)
|
||||
}
|
||||
|
||||
// Attempt to store domain allow in DB
|
||||
|
|
@ -57,10 +58,10 @@ func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.Domain
|
|||
}
|
||||
|
||||
func (d *domainDB) GetDomainAllow(ctx context.Context, domain string) (*gtsmodel.DomainAllow, error) {
|
||||
// Normalize the domain as punycode
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Check for easy case, domain referencing *us*
|
||||
|
|
@ -110,11 +111,41 @@ func (d *domainDB) GetDomainAllowByID(ctx context.Context, id string) (*gtsmodel
|
|||
return &allow, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) UpdateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow, columns ...string) (err error) {
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
allow.Domain, err = util.PunifySafely(allow.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", allow.Domain, err)
|
||||
}
|
||||
|
||||
// Ensure updated_at is set.
|
||||
allow.UpdatedAt = time.Now()
|
||||
if len(columns) != 0 {
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
// Attempt to update domain allow.
|
||||
if _, err := d.db.
|
||||
NewUpdate().
|
||||
Model(allow).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("domain_allow.id"), allow.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain allow cache (for later reload)
|
||||
d.state.Caches.DB.DomainAllow.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
|
||||
// Normalize the domain as punycode
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
return gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Attempt to delete domain allow
|
||||
|
|
@ -132,11 +163,13 @@ func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
|
|||
}
|
||||
|
||||
func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
block.Domain, err = util.Punify(block.Domain)
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
block.Domain, err = util.PunifySafely(block.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
return gtserror.Newf("error punifying domain %s: %w", block.Domain, err)
|
||||
}
|
||||
|
||||
// Attempt to store domain block in DB
|
||||
|
|
@ -153,10 +186,10 @@ func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.Domain
|
|||
}
|
||||
|
||||
func (d *domainDB) GetDomainBlock(ctx context.Context, domain string) (*gtsmodel.DomainBlock, error) {
|
||||
// Normalize the domain as punycode
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Check for easy case, domain referencing *us*
|
||||
|
|
@ -206,11 +239,43 @@ func (d *domainDB) GetDomainBlockByID(ctx context.Context, id string) (*gtsmodel
|
|||
return &block, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) UpdateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock, columns ...string) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
block.Domain, err = util.PunifySafely(block.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", block.Domain, err)
|
||||
}
|
||||
|
||||
// Ensure updated_at is set.
|
||||
block.UpdatedAt = time.Now()
|
||||
if len(columns) != 0 {
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
// Attempt to update domain block.
|
||||
if _, err := d.db.
|
||||
NewUpdate().
|
||||
Model(block).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("domain_block.id"), block.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain block cache (for later reload)
|
||||
d.state.Caches.DB.DomainBlock.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) error {
|
||||
// Normalize the domain as punycode
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
return gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Attempt to delete domain block
|
||||
|
|
@ -228,10 +293,10 @@ func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) error {
|
|||
}
|
||||
|
||||
func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, error) {
|
||||
// Normalize the domain as punycode
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Domain referencing *us* cannot be blocked.
|
||||
|
|
|
|||
286
internal/db/bundb/domainpermissiondraft.go
Normal file
286
internal/db/bundb/domainpermissiondraft.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func (d *domainDB) getDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
dbQuery func(*gtsmodel.DomainPermissionDraft) error,
|
||||
keyParts ...any,
|
||||
) (*gtsmodel.DomainPermissionDraft, error) {
|
||||
// Fetch perm draft from database cache with loader callback.
|
||||
permDraft, err := d.state.Caches.DB.DomainPermissionDraft.LoadOne(
|
||||
lookup,
|
||||
// Only called if not cached.
|
||||
func() (*gtsmodel.DomainPermissionDraft, error) {
|
||||
var permDraft gtsmodel.DomainPermissionDraft
|
||||
if err := dbQuery(&permDraft); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &permDraft, nil
|
||||
},
|
||||
keyParts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// No need to fully populate.
|
||||
return permDraft, nil
|
||||
}
|
||||
|
||||
if permDraft.CreatedByAccount == nil {
|
||||
// Not set, fetch from database.
|
||||
permDraft.CreatedByAccount, err = d.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
permDraft.CreatedByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error populating created by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return permDraft, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionDraftByID(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*gtsmodel.DomainPermissionDraft, error) {
|
||||
return d.getDomainPermissionDraft(
|
||||
ctx,
|
||||
"ID",
|
||||
func(permDraft *gtsmodel.DomainPermissionDraft) error {
|
||||
return d.db.
|
||||
NewSelect().
|
||||
Model(permDraft).
|
||||
Where("? = ?", bun.Ident("domain_permission_draft.id"), id).
|
||||
Scan(ctx)
|
||||
},
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionDrafts(
|
||||
ctx context.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
permSubID string,
|
||||
domain string,
|
||||
page *paging.Page,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionDraft,
|
||||
error,
|
||||
) {
|
||||
var (
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
permDraftIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_drafts"),
|
||||
bun.Ident("domain_permission_draft"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_draft.id")
|
||||
|
||||
// Return only items with id
|
||||
// lower than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where(
|
||||
"? < ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
maxID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with id
|
||||
// greater than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where(
|
||||
"? > ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
minID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with
|
||||
// given permission type.
|
||||
if permType != gtsmodel.DomainPermissionUnknown {
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.permission_type"),
|
||||
permType,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with
|
||||
// given subscription ID.
|
||||
if permSubID != "" {
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.subscription_id"),
|
||||
permSubID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items
|
||||
// with given domain.
|
||||
if domain != "" {
|
||||
var err error
|
||||
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.domain"),
|
||||
domain,
|
||||
)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
// Limit amount of
|
||||
// items returned.
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.OrderExpr(
|
||||
"? ASC",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
)
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &permDraftIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(permDraftIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// If we're paging up, we still want items
|
||||
// to be sorted by ID desc, so reverse slice.
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(permDraftIDs)
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permDraftIDs)
|
||||
permDrafts := make([]*gtsmodel.DomainPermissionDraft, 0, len(permDraftIDs))
|
||||
for _, id := range permDraftIDs {
|
||||
permDraft, err := d.GetDomainPermissionDraftByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission draft %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
permDrafts = append(permDrafts, permDraft)
|
||||
}
|
||||
|
||||
return permDrafts, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) PutDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
draft *gtsmodel.DomainPermissionDraft,
|
||||
) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
draft.Domain, err = util.PunifySafely(draft.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", draft.Domain, err)
|
||||
}
|
||||
|
||||
return d.state.Caches.DB.DomainPermissionDraft.Store(
|
||||
draft,
|
||||
func() error {
|
||||
_, err := d.db.
|
||||
NewInsert().
|
||||
Model(draft).
|
||||
Exec(ctx)
|
||||
return err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) error {
|
||||
// Delete the permDraft from DB.
|
||||
q := d.db.NewDelete().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_drafts"),
|
||||
bun.Ident("domain_permission_draft"),
|
||||
).
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
id,
|
||||
)
|
||||
|
||||
_, err := q.Exec(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate any cached model by ID.
|
||||
d.state.Caches.DB.DomainPermissionDraft.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
120
internal/db/bundb/domainpermissiondraft_test.go
Normal file
120
internal/db/bundb/domainpermissiondraft_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
type DomainPermissionDraftTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionDraftTestSuite) TestPermDraftCreateGetDelete() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
draft = >smodel.DomainPermissionDraft{
|
||||
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
Domain: "exämple.org",
|
||||
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
|
||||
PrivateComment: "this domain is poo",
|
||||
PublicComment: "this domain is poo, but phrased in a more outward-facing way",
|
||||
Obfuscate: util.Ptr(false),
|
||||
SubscriptionID: "01JCZN8PG55KKEVTDAY52D0T3P",
|
||||
}
|
||||
)
|
||||
|
||||
// Whack the draft in.
|
||||
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Get the draft again.
|
||||
dbDraft, err := suite.state.DB.GetDomainPermissionDraftByID(ctx, draft.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Domain should have been stored punycoded.
|
||||
suite.Equal("xn--exmple-cua.org", dbDraft.Domain)
|
||||
|
||||
// Search for domain using both
|
||||
// punycode and unicode variants.
|
||||
search1, err := suite.state.DB.GetDomainPermissionDrafts(
|
||||
ctx,
|
||||
gtsmodel.DomainPermissionUnknown,
|
||||
"",
|
||||
"exämple.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search1) != 1 {
|
||||
suite.FailNow("couldn't get domain perm draft exämple.org")
|
||||
}
|
||||
|
||||
search2, err := suite.state.DB.GetDomainPermissionDrafts(
|
||||
ctx,
|
||||
gtsmodel.DomainPermissionUnknown,
|
||||
"",
|
||||
"xn--exmple-cua.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search2) != 1 {
|
||||
suite.FailNow("couldn't get domain perm draft example.org")
|
||||
}
|
||||
|
||||
// Change ID + try to put the same draft again.
|
||||
draft.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
|
||||
err = suite.state.DB.PutDomainPermissionDraft(ctx, draft)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("was able to insert same domain perm draft twice")
|
||||
}
|
||||
|
||||
// Put same draft but change permission type, should work.
|
||||
draft.PermissionType = gtsmodel.DomainPermissionAllow
|
||||
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Delete both drafts.
|
||||
for _, id := range []string{
|
||||
"01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
"01JCZNVYSDT3JE385FABMJ7ADQ",
|
||||
} {
|
||||
if err := suite.state.DB.DeleteDomainPermissionDraft(ctx, id); err != nil {
|
||||
suite.FailNow("error deleting domain permission draft")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainPermissionDraftTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DomainPermissionDraftTestSuite))
|
||||
}
|
||||
272
internal/db/bundb/domainpermissionexclude.go
Normal file
272
internal/db/bundb/domainpermissionexclude.go
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func (d *domainDB) PutDomainPermissionExclude(
|
||||
ctx context.Context,
|
||||
exclude *gtsmodel.DomainPermissionExclude,
|
||||
) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
exclude.Domain, err = util.PunifySafely(exclude.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", exclude.Domain, err)
|
||||
}
|
||||
|
||||
// Attempt to store domain perm exclude in DB
|
||||
if _, err := d.db.NewInsert().
|
||||
Model(exclude).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain perm exclude cache (for later reload)
|
||||
d.state.Caches.DB.DomainPermissionExclude.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string) (bool, error) {
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
// Func to scan list of all
|
||||
// excluded domain perms from DB.
|
||||
loadF := func() ([]string, error) {
|
||||
var domains []string
|
||||
|
||||
if err := d.db.
|
||||
NewSelect().
|
||||
Table("domain_permission_excludes").
|
||||
Column("domain").
|
||||
Scan(ctx, &domains); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Exclude our own domain as creating blocks
|
||||
// or allows for self will likely break things.
|
||||
domains = append(domains, config.GetHost())
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
// Check the cache for a domain perm exclude,
|
||||
// hydrating the cache with loadF if necessary.
|
||||
return d.state.Caches.DB.DomainPermissionExclude.Matches(domain, loadF)
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionExcludeByID(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*gtsmodel.DomainPermissionExclude, error) {
|
||||
exclude := new(gtsmodel.DomainPermissionExclude)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
Model(exclude).
|
||||
Where("? = ?", bun.Ident("domain_permission_exclude.id"), id)
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// No need to fully populate.
|
||||
return exclude, nil
|
||||
}
|
||||
|
||||
if exclude.CreatedByAccount == nil {
|
||||
// Not set, fetch from database.
|
||||
var err error
|
||||
exclude.CreatedByAccount, err = d.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
exclude.CreatedByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error populating created by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return exclude, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionExcludes(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
page *paging.Page,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionExclude,
|
||||
error,
|
||||
) {
|
||||
var (
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
excludeIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_excludes"),
|
||||
bun.Ident("domain_permission_exclude"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_exclude.id")
|
||||
|
||||
// Return only items with id
|
||||
// lower than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where(
|
||||
"? < ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
maxID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with id
|
||||
// greater than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where(
|
||||
"? > ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
minID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items
|
||||
// with given domain.
|
||||
if domain != "" {
|
||||
var err error
|
||||
|
||||
// Normalize domain as punycode for lookup.
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_exclude.domain"),
|
||||
domain,
|
||||
)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
// Limit amount of
|
||||
// items returned.
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.OrderExpr(
|
||||
"? ASC",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
)
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &excludeIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(excludeIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// If we're paging up, we still want items
|
||||
// to be sorted by ID desc, so reverse slice.
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(excludeIDs)
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permSubIDs).
|
||||
excludes := make([]*gtsmodel.DomainPermissionExclude, 0, len(excludeIDs))
|
||||
for _, id := range excludeIDs {
|
||||
exclude, err := d.GetDomainPermissionExcludeByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission exclude %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
excludes = append(excludes, exclude)
|
||||
}
|
||||
|
||||
return excludes, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainPermissionExclude(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) error {
|
||||
// Delete the permSub from DB.
|
||||
q := d.db.NewDelete().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_excludes"),
|
||||
bun.Ident("domain_permission_exclude"),
|
||||
).
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
id,
|
||||
)
|
||||
|
||||
_, err := q.Exec(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain perm exclude cache (for later reload)
|
||||
d.state.Caches.DB.DomainPermissionExclude.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
185
internal/db/bundb/domainpermissionexclude_test.go
Normal file
185
internal/db/bundb/domainpermissionexclude_test.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type DomainPermissionExcludeTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionExcludeTestSuite) TestPermExcludeCreateGetDelete() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
exclude = >smodel.DomainPermissionExclude{
|
||||
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
Domain: "exämple.org",
|
||||
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
|
||||
PrivateComment: "this domain is poo",
|
||||
}
|
||||
)
|
||||
|
||||
// Whack the exclude in.
|
||||
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Get the exclude again.
|
||||
dbExclude, err := suite.state.DB.GetDomainPermissionExcludeByID(ctx, exclude.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Domain should have been stored punycoded.
|
||||
suite.Equal("xn--exmple-cua.org", dbExclude.Domain)
|
||||
|
||||
// Search for domain using both
|
||||
// punycode and unicode variants.
|
||||
search1, err := suite.state.DB.GetDomainPermissionExcludes(
|
||||
ctx,
|
||||
"exämple.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search1) != 1 {
|
||||
suite.FailNow("couldn't get domain perm exclude exämple.org")
|
||||
}
|
||||
|
||||
search2, err := suite.state.DB.GetDomainPermissionExcludes(
|
||||
ctx,
|
||||
"xn--exmple-cua.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search2) != 1 {
|
||||
suite.FailNow("couldn't get domain perm exclude example.org")
|
||||
}
|
||||
|
||||
// Change ID + try to put the same exclude again.
|
||||
exclude.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
|
||||
err = suite.state.DB.PutDomainPermissionExclude(ctx, exclude)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("was able to insert same domain perm exclude twice")
|
||||
}
|
||||
|
||||
// Delete both excludes.
|
||||
for _, id := range []string{
|
||||
"01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
"01JCZNVYSDT3JE385FABMJ7ADQ",
|
||||
} {
|
||||
if err := suite.state.DB.DeleteDomainPermissionExclude(ctx, id); err != nil {
|
||||
suite.FailNow("error deleting domain permission exclude")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionExcludeTestSuite) TestExcluded() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
createdByAccountID = suite.testAccounts["admin_account"].ID
|
||||
)
|
||||
|
||||
// Insert some excludes into the db.
|
||||
for _, exclude := range []*gtsmodel.DomainPermissionExclude{
|
||||
{
|
||||
ID: "01JD7AFFBBZSPY8R2M0JCGQGPW",
|
||||
Domain: "example.org",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AMK98E2QX78KXEZJ1RF5Z",
|
||||
Domain: "boobs.com",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AMXW3R3W98E91R62ACDA0",
|
||||
Domain: "rad.boobs.com",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AYYN5TXQVASB30PT08CE1",
|
||||
Domain: "honkers.org",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
} {
|
||||
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
domain string
|
||||
excluded bool
|
||||
}
|
||||
|
||||
for i, testCase := range []testCase{
|
||||
{
|
||||
domain: config.GetHost(),
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "test.example.org",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "example.org",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "boobs.com",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "rad.boobs.com",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "sir.not.appearing.in.this.list",
|
||||
excluded: false,
|
||||
},
|
||||
} {
|
||||
excluded, err := suite.state.DB.IsDomainPermissionExcluded(ctx, testCase.domain)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
if excluded != testCase.excluded {
|
||||
suite.Failf("",
|
||||
"test %d: %s excluded should be %t",
|
||||
i, testCase.domain, testCase.excluded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainPermissionExcludeTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DomainPermissionExcludeTestSuite))
|
||||
}
|
||||
354
internal/db/bundb/domainpermissionsubscription.go
Normal file
354
internal/db/bundb/domainpermissionsubscription.go
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func (d *domainDB) getDomainPermissionSubscription(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
dbQuery func(*gtsmodel.DomainPermissionSubscription) error,
|
||||
keyParts ...any,
|
||||
) (*gtsmodel.DomainPermissionSubscription, error) {
|
||||
// Fetch perm subscription from database cache with loader callback.
|
||||
permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne(
|
||||
lookup,
|
||||
// Only called if not cached.
|
||||
func() (*gtsmodel.DomainPermissionSubscription, error) {
|
||||
var permSub gtsmodel.DomainPermissionSubscription
|
||||
if err := dbQuery(&permSub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &permSub, nil
|
||||
},
|
||||
keyParts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// No need to fully populate.
|
||||
return permSub, nil
|
||||
}
|
||||
|
||||
if permSub.CreatedByAccount == nil {
|
||||
// Not set, fetch from database.
|
||||
permSub.CreatedByAccount, err = d.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
permSub.CreatedByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error populating created by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return permSub, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionSubscriptionByID(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*gtsmodel.DomainPermissionSubscription, error) {
|
||||
return d.getDomainPermissionSubscription(
|
||||
ctx,
|
||||
"ID",
|
||||
func(permSub *gtsmodel.DomainPermissionSubscription) error {
|
||||
return d.db.
|
||||
NewSelect().
|
||||
Model(permSub).
|
||||
Where("? = ?", bun.Ident("domain_permission_subscription.id"), id).
|
||||
Scan(ctx)
|
||||
},
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionSubscriptions(
|
||||
ctx context.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
page *paging.Page,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionSubscription,
|
||||
error,
|
||||
) {
|
||||
var (
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
permSubIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_subscriptions"),
|
||||
bun.Ident("domain_permission_subscription"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_subscription.id")
|
||||
|
||||
// Return only items with id
|
||||
// lower than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where(
|
||||
"? < ?",
|
||||
bun.Ident("domain_permission_subscription.id"),
|
||||
maxID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with id
|
||||
// greater than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where(
|
||||
"? > ?",
|
||||
bun.Ident("domain_permission_subscription.id"),
|
||||
minID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with
|
||||
// given permission type.
|
||||
if permType != gtsmodel.DomainPermissionUnknown {
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_subscription.permission_type"),
|
||||
permType,
|
||||
)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
// Limit amount of
|
||||
// items returned.
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.OrderExpr(
|
||||
"? ASC",
|
||||
bun.Ident("domain_permission_subscription.id"),
|
||||
)
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_subscription.id"),
|
||||
)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &permSubIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(permSubIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// If we're paging up, we still want items
|
||||
// to be sorted by ID desc, so reverse slice.
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(permSubIDs)
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permSubIDs).
|
||||
permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
|
||||
for _, id := range permSubIDs {
|
||||
permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
permSubs = append(permSubs, permSub)
|
||||
}
|
||||
|
||||
return permSubs, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionSubscriptionsByPriority(
|
||||
ctx context.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionSubscription,
|
||||
error,
|
||||
) {
|
||||
permSubIDs := []string{}
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_subscriptions"),
|
||||
bun.Ident("domain_permission_subscription"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_subscription.id").
|
||||
// Select only subs of given perm type.
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_subscription.permission_type"),
|
||||
permType,
|
||||
).
|
||||
// Order by priority descending.
|
||||
OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_subscription.priority"),
|
||||
)
|
||||
|
||||
if err := q.Scan(ctx, &permSubIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(permSubIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permSubIDs).
|
||||
permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
|
||||
for _, id := range permSubIDs {
|
||||
permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
permSubs = append(permSubs, permSub)
|
||||
}
|
||||
|
||||
return permSubs, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) PutDomainPermissionSubscription(
|
||||
ctx context.Context,
|
||||
permSubscription *gtsmodel.DomainPermissionSubscription,
|
||||
) error {
|
||||
return d.state.Caches.DB.DomainPermissionSubscription.Store(
|
||||
permSubscription,
|
||||
func() error {
|
||||
_, err := d.db.
|
||||
NewInsert().
|
||||
Model(permSubscription).
|
||||
Exec(ctx)
|
||||
return err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) UpdateDomainPermissionSubscription(
|
||||
ctx context.Context,
|
||||
permSubscription *gtsmodel.DomainPermissionSubscription,
|
||||
columns ...string,
|
||||
) error {
|
||||
return d.state.Caches.DB.DomainPermissionSubscription.Store(
|
||||
permSubscription,
|
||||
func() error {
|
||||
_, err := d.db.
|
||||
NewUpdate().
|
||||
Model(permSubscription).
|
||||
Where("? = ?", bun.Ident("id"), permSubscription.ID).
|
||||
Column(columns...).
|
||||
Exec(ctx)
|
||||
return err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainPermissionSubscription(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) error {
|
||||
// Delete the permSub from DB.
|
||||
q := d.db.NewDelete().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_subscriptions"),
|
||||
bun.Ident("domain_permission_subscription"),
|
||||
).
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_subscription.id"),
|
||||
id,
|
||||
)
|
||||
|
||||
_, err := q.Exec(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate any cached model by ID.
|
||||
d.state.Caches.DB.DomainPermissionSubscription.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) CountDomainPermissionSubscriptionPerms(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (int, error) {
|
||||
permSubscription, err := d.GetDomainPermissionSubscriptionByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
q := d.db.NewSelect()
|
||||
|
||||
if permSubscription.PermissionType == gtsmodel.DomainPermissionBlock {
|
||||
q = q.TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_blocks"),
|
||||
bun.Ident("perm"),
|
||||
)
|
||||
} else {
|
||||
q = q.TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_allows"),
|
||||
bun.Ident("perm"),
|
||||
)
|
||||
}
|
||||
|
||||
return q.
|
||||
Column("perm.id").
|
||||
Where("? = ?", bun.Ident("perm.subscription_id"), id).
|
||||
Count(ctx)
|
||||
}
|
||||
99
internal/db/bundb/domainpermissionsubscription_test.go
Normal file
99
internal/db/bundb/domainpermissionsubscription_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type DomainPermissionSubscriptionTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionSubscriptionTestSuite) TestCount() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
testAccount = suite.testAccounts["admin_account"]
|
||||
permSub = >smodel.DomainPermissionSubscription{
|
||||
ID: "01JGV3VZ72K58BYW8H5GEVBZGN",
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
URI: "https://example.org/whatever.csv",
|
||||
ContentType: gtsmodel.DomainPermSubContentTypeCSV,
|
||||
}
|
||||
perms = []*gtsmodel.DomainBlock{
|
||||
{
|
||||
ID: "01JGV42G72YCKN06AC51RZPFES",
|
||||
Domain: "whatever.com",
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
SubscriptionID: permSub.ID,
|
||||
},
|
||||
{
|
||||
ID: "01JGV43ZQKYPHM2M0YBQDFDSD1",
|
||||
Domain: "aaaa.example.org",
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
SubscriptionID: permSub.ID,
|
||||
},
|
||||
{
|
||||
ID: "01JGV444KDDC4WFG6MZQVM0N2Z",
|
||||
Domain: "bbbb.example.org",
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
SubscriptionID: permSub.ID,
|
||||
},
|
||||
{
|
||||
ID: "01JGV44AFEMBWS6P6S72BQK376",
|
||||
Domain: "cccc.example.org",
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
SubscriptionID: permSub.ID,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Whack the perm sub in the DB.
|
||||
if err := suite.state.DB.PutDomainPermissionSubscription(ctx, permSub); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Whack the perms in the db.
|
||||
for _, perm := range perms {
|
||||
if err := suite.state.DB.CreateDomainBlock(ctx, perm); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Count 'em.
|
||||
count, err := suite.state.DB.CountDomainPermissionSubscriptionPerms(ctx, permSub.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(4, count)
|
||||
}
|
||||
|
||||
func TestDomainPermissionSubscriptionTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DomainPermissionSubscriptionTestSuite))
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -597,7 +597,7 @@ func (e *emojiDB) GetEmojisByIDs(ctx context.Context, ids []string) ([]*gtsmodel
|
|||
// Reorder the emojis by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(e *gtsmodel.Emoji) string { return e.ID }
|
||||
util.OrderBy(emojis, ids, getID)
|
||||
xslices.OrderBy(emojis, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -661,7 +661,7 @@ func (e *emojiDB) GetEmojiCategoriesByIDs(ctx context.Context, ids []string) ([]
|
|||
// Reorder the categories by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(c *gtsmodel.EmojiCategory) string { return c.ID }
|
||||
util.OrderBy(categories, ids, getID)
|
||||
xslices.OrderBy(categories, ids, getID)
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string)
|
|||
}
|
||||
|
||||
// Put the filter structs in the same order as the filter IDs.
|
||||
util.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
|
||||
xslices.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
return filters, nil
|
||||
|
|
|
|||
|
|
@ -247,6 +247,54 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
|
|||
}
|
||||
}
|
||||
|
||||
func (suite *FilterTestSuite) TestFilterTitleOverlap() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
account1 = "01HNEJXCPRTJVJY9MV0VVHGD47"
|
||||
account2 = "01JAG5BRJPJYA0FSA5HR2MMFJH"
|
||||
)
|
||||
|
||||
// Create an empty filter for account 1.
|
||||
account1filter1 := >smodel.Filter{
|
||||
ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
|
||||
AccountID: account1,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
if err := suite.db.PutFilter(ctx, account1filter1); err != nil {
|
||||
suite.FailNow("", "error putting account1filter1: %s", err)
|
||||
}
|
||||
|
||||
// Create a filter for account 2 with
|
||||
// the same title, should be no issue.
|
||||
account2filter1 := >smodel.Filter{
|
||||
ID: "01JAG5GPXG7H5Y4ZP78GV1F2ET",
|
||||
AccountID: account2,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
if err := suite.db.PutFilter(ctx, account2filter1); err != nil {
|
||||
suite.FailNow("", "error putting account2filter1: %s", err)
|
||||
}
|
||||
|
||||
// Try to create another filter for
|
||||
// account 1 with the same name as
|
||||
// an existing filter of theirs.
|
||||
account1filter2 := >smodel.Filter{
|
||||
ID: "01JAG5J8NYKQE2KYCD28Y4P05V",
|
||||
AccountID: account1,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
err := suite.db.PutFilter(ctx, account1filter2)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("", "wanted ErrAlreadyExists, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(FilterTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ func (f *filterDB) getFilterKeywords(ctx context.Context, idColumn string, id st
|
|||
}
|
||||
|
||||
// Put the filter keyword structs in the same order as the filter keyword IDs.
|
||||
util.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
|
||||
xslices.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
|
||||
return filterKeyword.ID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ func (f *filterDB) getFilterStatuses(ctx context.Context, idColumn string, id st
|
|||
}
|
||||
|
||||
// Put the filter status structs in the same order as the filter status IDs.
|
||||
util.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
|
||||
xslices.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
|
||||
return filterStatus.ID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
|
|||
// Ignore statuses that are currently pending approval.
|
||||
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
|
||||
|
||||
// Ignore statuses that are direct messages.
|
||||
q = q.Where("NOT ? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityDirect)
|
||||
|
||||
count, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
@ -155,8 +158,9 @@ func (i *instanceDB) CountInstanceDomains(ctx context.Context, domain string) (i
|
|||
}
|
||||
|
||||
func (i *instanceDB) GetInstance(ctx context.Context, domain string) (*gtsmodel.Instance, error) {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
|
|
@ -262,8 +266,9 @@ func (i *instanceDB) PopulateInstance(ctx context.Context, instance *gtsmodel.In
|
|||
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode
|
||||
instance.Domain, err = util.Punify(instance.Domain)
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
instance.Domain, err = util.PunifySafely(instance.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", instance.Domain, err)
|
||||
}
|
||||
|
|
@ -276,9 +281,11 @@ func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instanc
|
|||
}
|
||||
|
||||
func (i *instanceDB) UpdateInstance(ctx context.Context, instance *gtsmodel.Instance, columns ...string) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
instance.Domain, err = util.Punify(instance.Domain)
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
instance.Domain, err = util.PunifySafely(instance.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", instance.Domain, err)
|
||||
}
|
||||
|
|
@ -346,8 +353,9 @@ func (i *instanceDB) GetInstanceAccounts(ctx context.Context, domain string, max
|
|||
limit = 0
|
||||
}
|
||||
|
||||
// Normalize the domain as punycode.
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
|
|||
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
|
||||
suite.NoError(err)
|
||||
suite.Equal(20, count)
|
||||
suite.Equal(21, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), "fossbros-anonymous.io")
|
||||
suite.NoError(err)
|
||||
suite.Equal(3, count)
|
||||
suite.Equal(4, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceDomains() {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -84,6 +86,53 @@ func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri stri
|
|||
)
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionRequestsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Load all interaction request IDs via cache loader callbacks.
|
||||
requests, err := i.state.Caches.DB.InteractionRequest.LoadIDs("ID",
|
||||
ids,
|
||||
func(uncached []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Preallocate expected length of uncached interaction requests.
|
||||
requests := make([]*gtsmodel.InteractionRequest, 0, len(uncached))
|
||||
|
||||
// Perform database query scanning
|
||||
// the remaining (uncached) IDs.
|
||||
if err := i.db.NewSelect().
|
||||
Model(&requests).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(uncached)).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return requests, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reorder the requests by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(r *gtsmodel.InteractionRequest) string { return r.ID }
|
||||
xslices.OrderBy(requests, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
// Populate all loaded interaction requests, removing those we
|
||||
// fail to populate (removes needing so many nil checks everywhere).
|
||||
requests = slices.DeleteFunc(requests, func(request *gtsmodel.InteractionRequest) bool {
|
||||
if err := i.PopulateInteractionRequest(ctx, request); err != nil {
|
||||
log.Errorf(ctx, "error populating %s: %v", request.ID, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) getInteractionRequest(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
|
|
@ -205,13 +254,18 @@ func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *g
|
|||
}
|
||||
|
||||
func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
|
||||
defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
// Delete interaction request by ID.
|
||||
if _, err := i.db.NewDelete().
|
||||
Table("interaction_requests").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := i.db.NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("interaction_requests"), bun.Ident("interaction_request")).
|
||||
Where("? = ?", bun.Ident("interaction_request.id"), id).
|
||||
Exec(ctx)
|
||||
return err
|
||||
// Invalidate cached interaction request with ID.
|
||||
i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionsRequestsForAcct(
|
||||
|
|
@ -248,9 +302,9 @@ func (i *interactionDB) GetInteractionsRequestsForAcct(
|
|||
bun.Ident("interaction_request"),
|
||||
).
|
||||
// Select only interaction requests that
|
||||
// are neither accepted or rejected yet,
|
||||
// ie., without an Accept or Reject URI.
|
||||
Where("? IS NULL", bun.Ident("uri"))
|
||||
// are neither accepted or rejected yet.
|
||||
Where("? IS NULL", bun.Ident("accepted_at")).
|
||||
Where("? IS NULL", bun.Ident("rejected_at"))
|
||||
|
||||
// Select interactions targeting status.
|
||||
if statusID != "" {
|
||||
|
|
@ -317,19 +371,8 @@ func (i *interactionDB) GetInteractionsRequestsForAcct(
|
|||
slices.Reverse(reqIDs)
|
||||
}
|
||||
|
||||
// For each interaction request ID,
|
||||
// select the interaction request.
|
||||
reqs := make([]*gtsmodel.InteractionRequest, 0, len(reqIDs))
|
||||
for _, id := range reqIDs {
|
||||
req, err := i.GetInteractionRequestByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
|
||||
return reqs, nil
|
||||
// Load all interaction requests by their IDs.
|
||||
return i.GetInteractionRequestsByIDs(ctx, reqIDs)
|
||||
}
|
||||
|
||||
func (i *interactionDB) IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -59,11 +59,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this reply.
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, reply)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(reply)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -90,11 +86,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this boost.
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, boost)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(boost)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -121,11 +113,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this fave.
|
||||
req, err := typeutils.StatusFaveToInteractionRequest(ctx, fave)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusFaveToInteractionRequest(fave)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -333,7 +333,7 @@ func (l *listDB) GetListsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.L
|
|||
// Reorder the lists by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(l *gtsmodel.List) string { return l.ID }
|
||||
util.OrderBy(lists, ids, getID)
|
||||
xslices.OrderBy(lists, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -387,12 +387,12 @@ func (l *listDB) PutListEntries(ctx context.Context, entries []*gtsmodel.ListEnt
|
|||
}
|
||||
|
||||
// Collect unique list IDs from the provided list entries.
|
||||
listIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
listIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
return e.ListID
|
||||
})
|
||||
|
||||
// Collect unique follow IDs from the provided list entries.
|
||||
followIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
followIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
return e.FollowID
|
||||
})
|
||||
|
||||
|
|
@ -441,7 +441,7 @@ func (l *listDB) DeleteAllListEntriesByFollows(ctx context.Context, followIDs ..
|
|||
}
|
||||
|
||||
// Deduplicate IDs before invalidate.
|
||||
listIDs = util.Deduplicate(listIDs)
|
||||
listIDs = xslices.Deduplicate(listIDs)
|
||||
|
||||
// Invalidate all related list entry caches.
|
||||
l.invalidateEntryCaches(ctx, listIDs, followIDs)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ func (m *mediaDB) GetAttachmentsByIDs(ctx context.Context, ids []string) ([]*gts
|
|||
// Reorder the media by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(m *gtsmodel.MediaAttachment) string { return m.ID }
|
||||
util.OrderBy(media, ids, getID)
|
||||
xslices.OrderBy(media, ids, getID)
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
|
@ -104,12 +104,6 @@ func (m *mediaDB) PutAttachment(ctx context.Context, media *gtsmodel.MediaAttach
|
|||
}
|
||||
|
||||
func (m *mediaDB) UpdateAttachment(ctx context.Context, media *gtsmodel.MediaAttachment, columns ...string) error {
|
||||
media.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column, ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
return m.state.Caches.DB.Media.Store(media, func() error {
|
||||
_, err := m.db.NewUpdate().
|
||||
Model(media).
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func (m *mentionDB) GetMentions(ctx context.Context, ids []string) ([]*gtsmodel.
|
|||
// Reorder the mentions by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(m *gtsmodel.Mention) string { return m.ID }
|
||||
util.OrderBy(mentions, ids, getID)
|
||||
xslices.OrderBy(mentions, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func init() {
|
|||
UpdatedAt: oldAction.UpdatedAt,
|
||||
TargetCategory: gtsmodel.AdminActionCategoryAccount,
|
||||
TargetID: oldAction.TargetAccountID,
|
||||
Type: gtsmodel.NewAdminActionType(string(oldAction.Type)),
|
||||
Type: gtsmodel.ParseAdminActionType(string(oldAction.Type)),
|
||||
AccountID: oldAction.AccountID,
|
||||
Text: oldAction.Text,
|
||||
SendEmail: util.Ptr(oldAction.SendEmail),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import (
|
|||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20231002153327_add_status_polls"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Poll represents an attached (to) Status poll, i.e. a questionaire. Can be remote / local.
|
||||
type Poll struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string.
|
||||
Multiple *bool `bun:",nullzero,notnull,default:false"` // Is this a multiple choice poll? i.e. can you vote on multiple options.
|
||||
HideCounts *bool `bun:",nullzero,notnull,default:false"` // Hides vote counts until poll ends.
|
||||
Options []string `bun:",nullzero,notnull"` // The available options for this poll.
|
||||
Votes []int `bun:",nullzero,notnull"` // Vote counts per choice.
|
||||
Voters *int `bun:",nullzero,notnull"` // Total no. voters count.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull,unique"` // Status ID of which this Poll is attached to.
|
||||
ExpiresAt time.Time `bun:"type:timestamptz,nullzero,notnull"` // The expiry date of this Poll.
|
||||
ClosedAt time.Time `bun:"type:timestamptz,nullzero"` // The closure date of this poll, will be zerotime until set.
|
||||
Closing bool `bun:"-"` // An ephemeral field only set on Polls in the middle of closing.
|
||||
// no creation date, use attached Status.CreatedAt.
|
||||
}
|
||||
|
||||
// PollVote represents a single instance of vote(s) in a Poll by an account.
|
||||
// If the Poll is single-choice, len(.Choices) = 1, if multiple-choice then
|
||||
// len(.Choices) >= 1. Can be remote or local.
|
||||
type PollVote struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string.
|
||||
Choices []int `bun:",nullzero,notnull"` // The Poll's option indices of which these are votes for.
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Account ID from which this vote originated.
|
||||
PollID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Poll ID of which this is a vote in.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation date of this PollVote.
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged
|
||||
Replyable *bool `bun:",notnull"` // This status can be replied to
|
||||
Likeable *bool `bun:",notnull"` // This status can be liked/faved
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -20,7 +20,7 @@ package migrations
|
|||
import (
|
||||
"context"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240126064004_add_filters"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filter stores a filter created by a local account.
|
||||
type Filter struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
ExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Time filter should expire. If null, should not expire.
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter.
|
||||
Title string `bun:",nullzero,notnull,unique"` // The name of the filter.
|
||||
Action string `bun:",nullzero,notnull"` // The action to take.
|
||||
Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter.
|
||||
Statuses []*FilterStatus `bun:"-"` // Statuses for this filter.
|
||||
ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
|
||||
ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications.
|
||||
ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
|
||||
ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread.
|
||||
ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile.
|
||||
}
|
||||
|
||||
// FilterKeyword stores a single keyword to filter statuses against.
|
||||
type FilterKeyword struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
|
||||
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_keywords_filter_id_keyword_uniq"` // ID of the filter that this keyword belongs to.
|
||||
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
|
||||
Keyword string `bun:",nullzero,notnull,unique:filter_keywords_filter_id_keyword_uniq"` // The keyword or phrase to filter against.
|
||||
WholeWord *bool `bun:",nullzero,notnull,default:false"` // Should the filter consider word boundaries?
|
||||
Regexp *regexp.Regexp `bun:"-"` // pre-prepared regular expression
|
||||
}
|
||||
|
||||
// FilterStatus stores a single status to filter.
|
||||
type FilterStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
|
||||
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the filter that this keyword belongs to.
|
||||
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
|
||||
StatusID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the status to filter.
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ func init() {
|
|||
table string
|
||||
column string
|
||||
columnType string
|
||||
defaultVal string
|
||||
extra string
|
||||
}
|
||||
for _, spec := range []spec{
|
||||
// Statuses.
|
||||
|
|
@ -48,19 +48,19 @@ func init() {
|
|||
table: "statuses",
|
||||
column: "interaction_policy",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "statuses",
|
||||
column: "pending_approval",
|
||||
columnType: "BOOLEAN",
|
||||
defaultVal: "DEFAULT false",
|
||||
extra: "NOT NULL DEFAULT false",
|
||||
},
|
||||
{
|
||||
table: "statuses",
|
||||
column: "approved_by_uri",
|
||||
columnType: "varchar",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
|
||||
// Status faves.
|
||||
|
|
@ -68,13 +68,13 @@ func init() {
|
|||
table: "status_faves",
|
||||
column: "pending_approval",
|
||||
columnType: "BOOLEAN",
|
||||
defaultVal: "DEFAULT false",
|
||||
extra: "NOT NULL DEFAULT false",
|
||||
},
|
||||
{
|
||||
table: "status_faves",
|
||||
column: "approved_by_uri",
|
||||
columnType: "varchar",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
|
||||
// Columns that must be added to the
|
||||
|
|
@ -85,31 +85,31 @@ func init() {
|
|||
table: "account_settings",
|
||||
column: "interaction_policy_direct",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_mutuals_only",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_followers_only",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_unlocked",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_public",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
} {
|
||||
exists, err := doesColumnExist(ctx, tx,
|
||||
|
|
@ -130,9 +130,9 @@ func init() {
|
|||
}
|
||||
|
||||
qStr := "ALTER TABLE ? ADD COLUMN ? ?"
|
||||
if spec.defaultVal != "" {
|
||||
if spec.extra != "" {
|
||||
qStr += " ?"
|
||||
args = append(args, bun.Safe(spec.defaultVal))
|
||||
args = append(args, bun.Safe(spec.extra))
|
||||
}
|
||||
|
||||
log.Infof(ctx, "adding column '%s' to '%s'...", spec.column, spec.table)
|
||||
|
|
@ -161,11 +161,14 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to new integer values.
|
||||
visibilityMapping := visibilityEnumMapping[oldmodel.Visibility]()
|
||||
|
||||
// For each status found in this way, update
|
||||
// to new version of interaction policy.
|
||||
for _, oldStatus := range oldStatuses {
|
||||
// Start with default policy for this visibility.
|
||||
v := gtsmodel.Visibility(oldStatus.Visibility)
|
||||
v := visibilityMapping[oldStatus.Visibility]
|
||||
policy := gtsmodel.DefaultInteractionPolicyFor(v)
|
||||
|
||||
if !*oldStatus.Likeable {
|
||||
|
|
|
|||
|
|
@ -22,40 +22,61 @@ import (
|
|||
)
|
||||
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
URI string `bun:",unique,nullzero,notnull"`
|
||||
URL string `bun:",nullzero"`
|
||||
Content string `bun:""`
|
||||
AttachmentIDs []string `bun:"attachments,array"`
|
||||
TagIDs []string `bun:"tags,array"`
|
||||
MentionIDs []string `bun:"mentions,array"`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Local *bool `bun:",nullzero,notnull,default:false"`
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
AccountURI string `bun:",nullzero,notnull"`
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyToURI string `bun:",nullzero"`
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyTo *Status `bun:"-"`
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOfURI string `bun:"-"`
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOf *Status `bun:"-"`
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"`
|
||||
PollID string `bun:"type:CHAR(26),nullzero"`
|
||||
ContentWarning string `bun:",nullzero"`
|
||||
Visibility string `bun:",nullzero,notnull"`
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"`
|
||||
Language string `bun:",nullzero"`
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"`
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"`
|
||||
Text string `bun:""`
|
||||
Federated *bool `bun:",notnull"`
|
||||
Boostable *bool `bun:",notnull"`
|
||||
Replyable *bool `bun:",notnull"`
|
||||
Likeable *bool `bun:",notnull"`
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
URI string `bun:",unique,nullzero,notnull"`
|
||||
URL string `bun:",nullzero"`
|
||||
Content string `bun:""`
|
||||
AttachmentIDs []string `bun:"attachments,array"`
|
||||
TagIDs []string `bun:"tags,array"`
|
||||
MentionIDs []string `bun:"mentions,array"`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Local *bool `bun:",nullzero,notnull,default:false"`
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
AccountURI string `bun:",nullzero,notnull"`
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyToURI string `bun:",nullzero"`
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyTo *Status `bun:"-"`
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOfURI string `bun:"-"`
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOf *Status `bun:"-"`
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"`
|
||||
PollID string `bun:"type:CHAR(26),nullzero"`
|
||||
ContentWarning string `bun:",nullzero"`
|
||||
Visibility Visibility `bun:",nullzero,notnull"`
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"`
|
||||
Language string `bun:",nullzero"`
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"`
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"`
|
||||
Text string `bun:""`
|
||||
Federated *bool `bun:",notnull"`
|
||||
Boostable *bool `bun:",notnull"`
|
||||
Replyable *bool `bun:",notnull"`
|
||||
Likeable *bool `bun:",notnull"`
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,11 +93,7 @@ func init() {
|
|||
// For each currently pending status, check whether it's a reply or
|
||||
// a boost, and insert a corresponding interaction request into the db.
|
||||
for _, pendingStatus := range pendingStatuses {
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, pendingStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(pendingStatus)
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
Model(req).
|
||||
|
|
@ -125,10 +121,7 @@ func init() {
|
|||
}
|
||||
|
||||
for _, pendingFave := range pendingFaves {
|
||||
req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := typeutils.StatusFaveToInteractionRequest(pendingFave)
|
||||
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ package migrations
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// SinBinStatus represents a status that's been rejected and/or reported + quarantined.
|
||||
//
|
||||
// Automatically rejected statuses are not put in the sin bin, only statuses that were
|
||||
// stored on the instance and which someone (local or remote) has subsequently rejected.
|
||||
type SinBinStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item.
|
||||
URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status.
|
||||
URL string `bun:",nullzero"` // Web url for viewing this status.
|
||||
Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`.
|
||||
AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status.
|
||||
InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to.
|
||||
Content string `bun:",nullzero"` // Content of this status.
|
||||
AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status.
|
||||
MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts.
|
||||
EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status.
|
||||
PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status.
|
||||
ContentWarning string `bun:",nullzero"` // CW / subject string for this status.
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive.
|
||||
Language string `bun:",nullzero"` // Language code for this status.
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("instances"), bun.Ident("custom_css"))
|
||||
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
_, err := db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("instances"), bun.Ident("custom_css"))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/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 {
|
||||
for idx, col := range map[string]string{
|
||||
"interaction_requests_accepted_at_idx": "accepted_at",
|
||||
"interaction_requests_rejected_at_idx": "rejected_at",
|
||||
} {
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("interaction_requests").
|
||||
Index(idx).
|
||||
Column(col).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"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 {
|
||||
// Previous versions of 20240620074530_interaction_policy.go
|
||||
// didn't set NOT NULL on gtsmodel.Status.PendingApproval and
|
||||
// gtsmodel.StatusFave.PendingApproval, resulting in NULL being
|
||||
// set for that column for some statuses. Correct for this.
|
||||
|
||||
log.Info(ctx, "correcting pending_approval on statuses table...")
|
||||
res, err := tx.
|
||||
NewUpdate().
|
||||
Table("statuses").
|
||||
Set("? = ?", bun.Ident("pending_approval"), false).
|
||||
Where("? IS NULL", bun.Ident("pending_approval")).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := res.RowsAffected()
|
||||
if err == nil {
|
||||
log.Infof(ctx, "corrected %d entries", rows)
|
||||
}
|
||||
|
||||
log.Info(ctx, "correcting pending_approval on status_faves table...")
|
||||
res, err = tx.
|
||||
NewUpdate().
|
||||
Table("status_faves").
|
||||
Set("? = ?", bun.Ident("pending_approval"), false).
|
||||
Where("? IS NULL", bun.Ident("pending_approval")).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err = res.RowsAffected()
|
||||
if err == nil {
|
||||
log.Infof(ctx, "corrected %d entries", rows)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
131
internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
Normal file
131
internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Create the new filters table
|
||||
// with the unique constraint
|
||||
// set on AccountID + Title.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
ModelTableExpr("new_filters").
|
||||
Model((*gtsmodel.Filter)(nil)).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Explicitly specify columns to bring
|
||||
// from old table to new, to avoid any
|
||||
// potential Postgres shenanigans.
|
||||
columns := []string{
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"expires_at",
|
||||
"account_id",
|
||||
"title",
|
||||
"action",
|
||||
"context_home",
|
||||
"context_notifications",
|
||||
"context_public",
|
||||
"context_thread",
|
||||
"context_account",
|
||||
}
|
||||
|
||||
// Copy all data for existing
|
||||
// filters to the new table.
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
Table("new_filters").
|
||||
Table("filters").
|
||||
Column(columns...).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop the old table.
|
||||
if _, err := tx.
|
||||
NewDropTable().
|
||||
Table("filters").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename new table to old table.
|
||||
if _, err := tx.
|
||||
ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? RENAME TO ?",
|
||||
bun.Ident("new_filters"),
|
||||
bun.Ident("filters"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Index the new version
|
||||
// of the filters table.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("filters").
|
||||
Index("filters_account_id_idx").
|
||||
Column("account_id").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if db.Dialect().Name() == dialect.PG {
|
||||
// Rename "new_filters_pkey" from the
|
||||
// new table to just "filters_pkey".
|
||||
// This is only necessary on Postgres.
|
||||
if _, err := tx.ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? RENAME CONSTRAINT ? TO ?",
|
||||
bun.Ident("public.filters"),
|
||||
bun.Safe("new_filters_pkey"),
|
||||
bun.Safe("filters_pkey"),
|
||||
); 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,82 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude"
|
||||
"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 {
|
||||
|
||||
// Create `domain_permission_drafts`.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model((*gtsmodel.DomainPermissionDraft)(nil)).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create `domain_permission_ignores`.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model((*gtsmodel.DomainPermissionExclude)(nil)).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create indexes. Indices. Indie sexes.
|
||||
for table, indexes := range map[string]map[string][]string{
|
||||
"domain_permission_drafts": {
|
||||
"domain_permission_drafts_domain_idx": {"domain"},
|
||||
"domain_permission_drafts_subscription_id_idx": {"subscription_id"},
|
||||
},
|
||||
} {
|
||||
for index, columns := range indexes {
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table(table).
|
||||
Index(index).
|
||||
Column(columns...).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
type DomainPermissionDraft struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
PermissionType uint8 `bun:",notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
|
||||
Domain string `bun:",nullzero,notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
|
||||
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
PrivateComment string `bun:",nullzero"`
|
||||
PublicComment string `bun:",nullzero"`
|
||||
Obfuscate *bool `bun:",nullzero,notnull,default:false"`
|
||||
SubscriptionID string `bun:"type:CHAR(26),unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type DomainPermissionExclude struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
Domain string `bun:",nullzero,notnull,unique"`
|
||||
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
PrivateComment string `bun:",nullzero"`
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions"
|
||||
"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 {
|
||||
// Create `domain_permission_subscriptions`.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model((*gtsmodel.DomainPermissionSubscription)(nil)).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create indexes. Indices. Indie sexes.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("domain_permission_subscriptions").
|
||||
// Filter on permission type.
|
||||
Index("domain_permission_subscriptions_permission_type_idx").
|
||||
Column("permission_type").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("domain_permission_subscriptions").
|
||||
// Sort by priority DESC.
|
||||
Index("domain_permission_subscriptions_priority_order_idx").
|
||||
ColumnExpr("? DESC", bun.Ident("priority")).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
type DomainPermissionSubscription struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
Priority uint8 `bun:""`
|
||||
Title string `bun:",nullzero,unique"`
|
||||
PermissionType uint8 `bun:",nullzero,notnull"`
|
||||
AsDraft *bool `bun:",nullzero,notnull,default:true"`
|
||||
AdoptOrphans *bool `bun:",nullzero,notnull,default:false"`
|
||||
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
URI string `bun:",nullzero,notnull,unique"`
|
||||
ContentType uint16 `bun:",nullzero,notnull"`
|
||||
FetchUsername string `bun:",nullzero"`
|
||||
FetchPassword string `bun:",nullzero"`
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SuccessfullyFetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
ETag string `bun:"etag,nullzero"`
|
||||
Error string `bun:",nullzero"`
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"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 {
|
||||
|
||||
// Check for 'updated_at' column on mentions table, else return.
|
||||
exists, err := doesColumnExist(ctx, tx, "mentions", "updated_at")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 'updated_at' column.
|
||||
log.Info(ctx, "removing unused updated_at column from mentions to save space, please wait...")
|
||||
_, err = tx.NewDropColumn().
|
||||
Model((*gtsmodel.Mention)(nil)).
|
||||
Column("updated_at").
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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,69 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
||||
"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 {
|
||||
statusType := reflect.TypeOf((*gtsmodel.Status)(nil))
|
||||
|
||||
// Generate new Status.EditIDs column definition from bun.
|
||||
colDef, err := getBunColumnDef(tx, statusType, "EditIDs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add EditIDs column to Status table.
|
||||
log.Info(ctx, "adding edits column to statuses table...")
|
||||
_, err = tx.NewAddColumn().
|
||||
Model((*gtsmodel.Status)(nil)).
|
||||
ColumnExpr(colDef).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the main StatusEdits table.
|
||||
_, err = tx.NewCreateTable().
|
||||
IfNotExists().
|
||||
Model((*gtsmodel.StatusEdit)(nil)).
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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,97 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID
|
||||
InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes.
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||
BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
EditIDs []string `bun:"edits,array"` //
|
||||
Edits []*StatusEdit `bun:"-"` //
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
Poll *gtsmodel.Poll `bun:"-"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
InteractionPolicy *gtsmodel.InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatusEdit represents a **historical** view of a Status
|
||||
// after a received edit. The Status itself will always
|
||||
// contain the latest up-to-date information.
|
||||
//
|
||||
// Note that stored status edits may not exactly match that
|
||||
// of the origin server, they are a best-effort by receiver
|
||||
// to store version history. There is no AP history endpoint.
|
||||
type StatusEdit struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed.
|
||||
ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit.
|
||||
Text string `bun:""` // Original status text, without formatting, at time of edit.
|
||||
Language string `bun:",nullzero"` // Status language at time of edit.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit.
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit.
|
||||
AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit.
|
||||
PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll.
|
||||
PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server).
|
||||
|
||||
// We don't bother having a *gtsmodel.Status model here
|
||||
// as the StatusEdit is always just attached to a Status,
|
||||
// so it doesn't need a self-reference back to it.
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints"
|
||||
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
||||
"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 {
|
||||
|
||||
// Tables with visibility types.
|
||||
var visTables = []struct {
|
||||
Table string
|
||||
Column string
|
||||
Default *new_gtsmodel.Visibility
|
||||
}{
|
||||
{Table: "statuses", Column: "visibility"},
|
||||
{Table: "sin_bin_statuses", Column: "visibility"},
|
||||
{Table: "account_settings", Column: "privacy", Default: util.Ptr(new_gtsmodel.VisibilityDefault)},
|
||||
{Table: "account_settings", Column: "web_visibility", Default: util.Ptr(new_gtsmodel.VisibilityDefault)},
|
||||
}
|
||||
|
||||
// Visibility type indices.
|
||||
var visIndices = []struct {
|
||||
name string
|
||||
cols []string
|
||||
order string
|
||||
}{
|
||||
{
|
||||
name: "statuses_visibility_idx",
|
||||
cols: []string{"visibility"},
|
||||
order: "",
|
||||
},
|
||||
{
|
||||
name: "statuses_profile_web_view_idx",
|
||||
cols: []string{"account_id", "visibility"},
|
||||
order: "id DESC",
|
||||
},
|
||||
{
|
||||
name: "statuses_public_timeline_idx",
|
||||
cols: []string{"visibility"},
|
||||
order: "id DESC",
|
||||
},
|
||||
}
|
||||
|
||||
// Before making changes to the visibility col
|
||||
// we must drop all indices that rely on it.
|
||||
log.Info(ctx, "dropping old visibility indexes...")
|
||||
for _, index := range visIndices {
|
||||
log.Info(ctx, "dropping old index %s...", index.name)
|
||||
if _, err := tx.NewDropIndex().
|
||||
Index(index.name).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to new integer values.
|
||||
visibilityMapping := visibilityEnumMapping[old_gtsmodel.Visibility]()
|
||||
|
||||
// Convert all visibility tables.
|
||||
for _, table := range visTables {
|
||||
if err := convertEnums(ctx, tx, table.Table, table.Column,
|
||||
visibilityMapping, table.Default); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the visibility indices.
|
||||
log.Info(ctx, "creating new visibility indexes...")
|
||||
for _, index := range visIndices {
|
||||
log.Info(ctx, "creating new index %s...", index.name)
|
||||
q := tx.NewCreateIndex().
|
||||
Table("statuses").
|
||||
Index(index.name).
|
||||
Column(index.cols...)
|
||||
if index.order != "" {
|
||||
q = q.ColumnExpr(index.order)
|
||||
}
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to the new integer value types.
|
||||
notificationMapping := notificationEnumMapping[old_gtsmodel.NotificationType]()
|
||||
|
||||
// Migrate over old notifications table column over to new column type.
|
||||
if err := convertEnums(ctx, tx, "notifications", "notification_type", //nolint:revive
|
||||
notificationMapping, nil); 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)
|
||||
}
|
||||
}
|
||||
|
||||
// visibilityEnumMapping maps old Visibility enum values to their newer integer type.
|
||||
func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility {
|
||||
return map[T]new_gtsmodel.Visibility{
|
||||
T(old_gtsmodel.VisibilityNone): new_gtsmodel.VisibilityNone,
|
||||
T(old_gtsmodel.VisibilityPublic): new_gtsmodel.VisibilityPublic,
|
||||
T(old_gtsmodel.VisibilityUnlocked): new_gtsmodel.VisibilityUnlocked,
|
||||
T(old_gtsmodel.VisibilityFollowersOnly): new_gtsmodel.VisibilityFollowersOnly,
|
||||
T(old_gtsmodel.VisibilityMutualsOnly): new_gtsmodel.VisibilityMutualsOnly,
|
||||
T(old_gtsmodel.VisibilityDirect): new_gtsmodel.VisibilityDirect,
|
||||
}
|
||||
}
|
||||
|
||||
// notificationEnumMapping maps old NotificationType enum values to their newer integer type.
|
||||
func notificationEnumMapping[T ~string]() map[T]new_gtsmodel.NotificationType {
|
||||
return map[T]new_gtsmodel.NotificationType{
|
||||
T(old_gtsmodel.NotificationFollow): new_gtsmodel.NotificationFollow,
|
||||
T(old_gtsmodel.NotificationFollowRequest): new_gtsmodel.NotificationFollowRequest,
|
||||
T(old_gtsmodel.NotificationMention): new_gtsmodel.NotificationMention,
|
||||
T(old_gtsmodel.NotificationReblog): new_gtsmodel.NotificationReblog,
|
||||
T(old_gtsmodel.NotificationFave): new_gtsmodel.NotificationFave,
|
||||
T(old_gtsmodel.NotificationPoll): new_gtsmodel.NotificationPoll,
|
||||
T(old_gtsmodel.NotificationStatus): new_gtsmodel.NotificationStatus,
|
||||
T(old_gtsmodel.NotificationSignup): new_gtsmodel.NotificationSignup,
|
||||
T(old_gtsmodel.NotificationPendingFave): new_gtsmodel.NotificationPendingFave,
|
||||
T(old_gtsmodel.NotificationPendingReply): new_gtsmodel.NotificationPendingReply,
|
||||
T(old_gtsmodel.NotificationPendingReblog): new_gtsmodel.NotificationPendingReblog,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// AccountSettings models settings / preferences for a local, non-instance account.
|
||||
type AccountSettings struct {
|
||||
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
|
||||
Privacy Visibility `bun:",nullzero,default:3"` // Default post privacy for this account
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
|
||||
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
|
||||
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
|
||||
Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
|
||||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile.
|
||||
InteractionPolicyDirect *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
||||
InteractionPolicyMutualsOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyFollowersOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyUnlocked *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyPublic *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy.
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc.
|
||||
type Notification struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
NotificationType NotificationType `bun:",nullzero,notnull"` // Type of this notification
|
||||
TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account targeted by the notification (ie., who will receive the notification?)
|
||||
TargetAccount *gtsmodel.Account `bun:"-"` // Account corresponding to TargetAccountID. Can be nil, always check first + select using ID if necessary.
|
||||
OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account that performed the action that created the notification.
|
||||
OriginAccount *gtsmodel.Account `bun:"-"` // Account corresponding to OriginAccountID. Can be nil, always check first + select using ID if necessary.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero"` // If the notification pertains to a status, what is the database ID of that status?
|
||||
Status *Status `bun:"-"` // Status corresponding to StatusID. Can be nil, always check first + select using ID if necessary.
|
||||
Read *bool `bun:",nullzero,notnull,default:false"` // Notification has been seen/read
|
||||
}
|
||||
|
||||
// NotificationType describes the reason/type of this notification.
|
||||
type NotificationType string
|
||||
|
||||
// Notification Types
|
||||
const (
|
||||
NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you
|
||||
NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you
|
||||
NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status
|
||||
NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses
|
||||
NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses
|
||||
NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended
|
||||
NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status.
|
||||
NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance.
|
||||
NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you.
|
||||
NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you.
|
||||
NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you.
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// SinBinStatus represents a status that's been rejected and/or reported + quarantined.
|
||||
//
|
||||
// Automatically rejected statuses are not put in the sin bin, only statuses that were
|
||||
// stored on the instance and which someone (local or remote) has subsequently rejected.
|
||||
type SinBinStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item.
|
||||
URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status.
|
||||
URL string `bun:",nullzero"` // Web url for viewing this status.
|
||||
Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`.
|
||||
AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status.
|
||||
InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to.
|
||||
Content string `bun:",nullzero"` // Content of this status.
|
||||
AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status.
|
||||
MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts.
|
||||
EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status.
|
||||
PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status.
|
||||
ContentWarning string `bun:",nullzero"` // CW / subject string for this status.
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive.
|
||||
Language string `bun:",nullzero"` // Language code for this status.
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status.
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID
|
||||
InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes.
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||
BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
Poll *gtsmodel.Poll `bun:"-"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
InteractionPolicy *gtsmodel.InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"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 {
|
||||
|
||||
// Check for 'updated_at' column on media attachments table, else return.
|
||||
exists, err := doesColumnExist(ctx, tx, "media_attachments", "updated_at")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 'updated_at' column.
|
||||
log.Info(ctx, "removing unused updated_at column from media attachments to save space, please wait...")
|
||||
_, err = tx.NewDropColumn().
|
||||
Model((*gtsmodel.MediaAttachment)(nil)).
|
||||
Column("updated_at").
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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,104 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
oldmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat"
|
||||
newmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
var newStatus *newmodel.Status
|
||||
newStatusType := reflect.TypeOf(newStatus)
|
||||
|
||||
// Generate new Status.EditedAt column definition from bun.
|
||||
colDef, err := getBunColumnDef(tx, newStatusType, "EditedAt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making column def: %w", err)
|
||||
}
|
||||
|
||||
log.Info(ctx, "adding statuses.edited_at column...")
|
||||
_, err = tx.NewAddColumn().Model(newStatus).
|
||||
ColumnExpr(colDef).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding column: %w", err)
|
||||
}
|
||||
|
||||
var whereSQL string
|
||||
var whereArg []any
|
||||
|
||||
// Check for an empty length
|
||||
// EditIDs JSON array, with different
|
||||
// SQL depending on connected database.
|
||||
switch tx.Dialect().Name() {
|
||||
case dialect.SQLite:
|
||||
whereSQL = "NOT (json_array_length(?) = 0 OR ? IS NULL)"
|
||||
whereArg = []any{bun.Ident("edits"), bun.Ident("edits")}
|
||||
case dialect.PG:
|
||||
whereSQL = "NOT (CARDINALITY(?) = 0 OR ? IS NULL)"
|
||||
whereArg = []any{bun.Ident("edits"), bun.Ident("edits")}
|
||||
default:
|
||||
panic("unsupported db type")
|
||||
}
|
||||
|
||||
log.Info(ctx, "setting edited_at = updated_at where not empty(edits)...")
|
||||
res, err := tx.NewUpdate().Model(newStatus).Where(whereSQL, whereArg...).
|
||||
Set("? = ?",
|
||||
bun.Ident("edited_at"),
|
||||
bun.Ident("updated_at"),
|
||||
).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating columns: %w", err)
|
||||
}
|
||||
|
||||
count, _ := res.RowsAffected()
|
||||
log.Infof(ctx, "updated %d statuses", count)
|
||||
|
||||
log.Info(ctx, "removing statuses.updated_at column...")
|
||||
_, err = tx.NewDropColumn().Model((*oldmodel.Status)(nil)).
|
||||
Column("updated_at").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dropping column: %w", 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,99 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
// A policy URI is GoToSocial's internal representation of
|
||||
// one ActivityPub URI for an Actor or a Collection of Actors,
|
||||
// specific to the domain of enforcing interaction policies.
|
||||
//
|
||||
// A PolicyValue can be stored in the database either as one
|
||||
// of the Value constants defined below (to save space), OR as
|
||||
// a full-fledged ActivityPub URI.
|
||||
//
|
||||
// A PolicyValue should be translated to the canonical string
|
||||
// value of the represented URI when federating an item, or
|
||||
// from the canonical string value of the URI when receiving
|
||||
// or retrieving an item.
|
||||
//
|
||||
// For example, if the PolicyValue `followers` was being
|
||||
// federated outwards in an interaction policy attached to an
|
||||
// item created by the actor `https://example.org/users/someone`,
|
||||
// then it should be translated to their followers URI when sent,
|
||||
// eg., `https://example.org/users/someone/followers`.
|
||||
//
|
||||
// Likewise, if GoToSocial receives an item with an interaction
|
||||
// policy containing `https://example.org/users/someone/followers`,
|
||||
// and the item was created by `https://example.org/users/someone`,
|
||||
// then the followers URI would be converted to `followers`
|
||||
// for internal storage.
|
||||
type PolicyValue string
|
||||
|
||||
const (
|
||||
// Stand-in for ActivityPub magic public URI,
|
||||
// which encompasses every possible Actor URI.
|
||||
PolicyValuePublic PolicyValue = "public"
|
||||
// Stand-in for the Followers Collection of
|
||||
// the item owner's Actor.
|
||||
PolicyValueFollowers PolicyValue = "followers"
|
||||
// Stand-in for the Following Collection of
|
||||
// the item owner's Actor.
|
||||
PolicyValueFollowing PolicyValue = "following"
|
||||
// Stand-in for the Mutuals Collection of
|
||||
// the item owner's Actor.
|
||||
//
|
||||
// (TODO: Reserved, currently unused).
|
||||
PolicyValueMutuals PolicyValue = "mutuals"
|
||||
// Stand-in for Actor URIs tagged in the item.
|
||||
PolicyValueMentioned PolicyValue = "mentioned"
|
||||
// Stand-in for the Actor URI of the item owner.
|
||||
PolicyValueAuthor PolicyValue = "author"
|
||||
)
|
||||
|
||||
type PolicyValues []PolicyValue
|
||||
|
||||
// An InteractionPolicy determines which
|
||||
// interactions will be accepted for an
|
||||
// item, and according to what rules.
|
||||
type InteractionPolicy struct {
|
||||
// Conditions in which a Like
|
||||
// interaction will be accepted
|
||||
// for an item with this policy.
|
||||
CanLike PolicyRules
|
||||
// Conditions in which a Reply
|
||||
// interaction will be accepted
|
||||
// for an item with this policy.
|
||||
CanReply PolicyRules
|
||||
// Conditions in which an Announce
|
||||
// interaction will be accepted
|
||||
// for an item with this policy.
|
||||
CanAnnounce PolicyRules
|
||||
}
|
||||
|
||||
// PolicyRules represents the rules according
|
||||
// to which a certain interaction is permitted
|
||||
// to various Actor and Actor Collection URIs.
|
||||
type PolicyRules struct {
|
||||
// Always is for PolicyValues who are
|
||||
// permitted to do an interaction
|
||||
// without requiring approval.
|
||||
Always PolicyValues
|
||||
// WithApproval is for PolicyValues who
|
||||
// are conditionally permitted to do
|
||||
// an interaction, pending approval.
|
||||
WithApproval PolicyValues
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Status represents a user-created 'post' or
|
||||
// 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes.
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
EditIDs []string `bun:"edits,array"` //
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
InteractionPolicy *InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
// GetID implements timeline.Timelineable{}.
|
||||
func (s *Status) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// IsLocal returns true if this is a local
|
||||
// status (ie., originating from this instance).
|
||||
func (s *Status) IsLocal() bool {
|
||||
return s.Local != nil && *s.Local
|
||||
}
|
||||
|
||||
// enumType is the type we (at least, should) use
|
||||
// for database enum types. it is the largest size
|
||||
// supported by a PostgreSQL SMALLINT, since an
|
||||
// SQLite SMALLINT is actually variable in size.
|
||||
type enumType int16
|
||||
|
||||
// Visibility represents the
|
||||
// visibility granularity of a status.
|
||||
type Visibility enumType
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = 1
|
||||
|
||||
// VisibilityPublic means this status will
|
||||
// be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = 2
|
||||
|
||||
// VisibilityUnlocked means this status will be visible to everyone,
|
||||
// but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = 3
|
||||
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = 4
|
||||
|
||||
// VisibilityMutualsOnly means this status
|
||||
// is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = 5
|
||||
|
||||
// VisibilityDirect means this status is
|
||||
// visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = 6
|
||||
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"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 {
|
||||
var edits []*gtsmodel.StatusEdit
|
||||
|
||||
// Select all status edits that
|
||||
// are not actually connected to
|
||||
// the status they reference.
|
||||
if err := tx.NewSelect().
|
||||
Model(&edits).
|
||||
Join("JOIN ? AS ? ON ? = ?",
|
||||
bun.Ident("statuses"),
|
||||
bun.Ident("status"),
|
||||
bun.Ident("status.id"),
|
||||
bun.Ident("status_edit.status_id"),
|
||||
).
|
||||
Where("CAST(? AS TEXT) NOT LIKE CONCAT(?, ?, ?)",
|
||||
bun.Ident("status.edits"),
|
||||
"%", bun.Ident("status_edit.id"), "%",
|
||||
).
|
||||
Column("id", "status_id", "created_at").
|
||||
Scan(ctx, &edits); err != nil {
|
||||
return fmt.Errorf("error selecting unlinked edits: %w", err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "relinking %d unlinked status edits", len(edits))
|
||||
|
||||
for _, edit := range edits {
|
||||
var status gtsmodel.Status
|
||||
|
||||
// Select the list of edits
|
||||
// CURRENTLY attached to the
|
||||
// status that edit references.
|
||||
if err := tx.NewSelect().
|
||||
Model(&status).
|
||||
Column("edits").
|
||||
Where("? = ?",
|
||||
bun.Ident("id"),
|
||||
edit.StatusID,
|
||||
).
|
||||
Scan(ctx); err != nil {
|
||||
return fmt.Errorf("error selecting status.edits: %w", err)
|
||||
}
|
||||
|
||||
// Select only the ID and creation
|
||||
// dates of all the other edits that
|
||||
// are attached to referenced status.
|
||||
if err := tx.NewSelect().
|
||||
Model(&status.Edits).
|
||||
Column("id", "created_at").
|
||||
Where("? IN (?)",
|
||||
bun.Ident("id"),
|
||||
bun.In(status.EditIDs),
|
||||
).
|
||||
Scan(ctx); err != nil {
|
||||
return fmt.Errorf("error selecting other status edits: %w", err)
|
||||
}
|
||||
|
||||
editID := func(e *gtsmodel.StatusEdit) string { return e.ID }
|
||||
|
||||
// Append this unlinked edit to status' list
|
||||
// of edits and then sort edits by creation.
|
||||
//
|
||||
// On tiny off-chance we end up with dupes,
|
||||
// we still deduplicate these status edits.
|
||||
status.Edits = append(status.Edits, edit)
|
||||
status.Edits = xslices.DeduplicateFunc(status.Edits, editID)
|
||||
slices.SortFunc(status.Edits, func(e1, e2 *gtsmodel.StatusEdit) int {
|
||||
const k = -1 // oldest at 0th, newest at nth
|
||||
switch c1, c2 := e1.CreatedAt, e2.CreatedAt; {
|
||||
case c1.Before(c2):
|
||||
return +k
|
||||
case c2.Before(c1):
|
||||
return -k
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
// Extract the IDs from edits to update the status edit IDs.
|
||||
status.EditIDs = xslices.Gather(nil, status.Edits, editID)
|
||||
|
||||
// Update the relevant status
|
||||
// edit IDs column in database.
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(&status).
|
||||
Column("edits").
|
||||
Where("? = ?",
|
||||
bun.Ident("id"),
|
||||
edit.StatusID,
|
||||
).
|
||||
Exec(ctx); err != nil {
|
||||
return fmt.Errorf("error updating status.edits: %w", 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,11 +19,209 @@ package migrations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/gruf/go-byteutil"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/dialect/feature"
|
||||
"github.com/uptrace/bun/dialect/sqltype"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
// convertEnums performs a transaction that converts
|
||||
// a table's column of our old-style enums (strings) to
|
||||
// more performant and space-saving integer types.
|
||||
func convertEnums[OldType ~string, NewType ~int16](
|
||||
ctx context.Context,
|
||||
tx bun.Tx,
|
||||
table string,
|
||||
column string,
|
||||
mapping map[OldType]NewType,
|
||||
defaultValue *NewType,
|
||||
) error {
|
||||
if len(mapping) == 0 {
|
||||
return errors.New("empty mapping")
|
||||
}
|
||||
|
||||
// Generate new column name.
|
||||
newColumn := column + "_new"
|
||||
|
||||
log.Infof(ctx, "converting %s.%s enums; "+
|
||||
"this may take a while, please don't interrupt!",
|
||||
table, column,
|
||||
)
|
||||
|
||||
// Ensure a default value.
|
||||
if defaultValue == nil {
|
||||
var zero NewType
|
||||
defaultValue = &zero
|
||||
}
|
||||
|
||||
// Add new column to database.
|
||||
if _, err := tx.NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
|
||||
bun.Ident(newColumn),
|
||||
*defaultValue).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error adding new column: %w", err)
|
||||
}
|
||||
|
||||
// Get a count of all in table.
|
||||
total, err := tx.NewSelect().
|
||||
Table(table).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error selecting total count: %w", err)
|
||||
}
|
||||
|
||||
var updated int
|
||||
for old, new := range mapping {
|
||||
|
||||
// Update old to new values.
|
||||
res, err := tx.NewUpdate().
|
||||
Table(table).
|
||||
Where("? = ?", bun.Ident(column), old).
|
||||
Set("? = ?", bun.Ident(newColumn), new).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error updating old column values: %w", err)
|
||||
}
|
||||
|
||||
// Count number items updated.
|
||||
n, _ := res.RowsAffected()
|
||||
updated += int(n)
|
||||
}
|
||||
|
||||
// Check total updated.
|
||||
if total != updated {
|
||||
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
|
||||
}
|
||||
|
||||
// Drop the old column from table.
|
||||
if _, err := tx.NewDropColumn().
|
||||
Table(table).
|
||||
ColumnExpr("?", bun.Ident(column)).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error dropping old column: %w", err)
|
||||
}
|
||||
|
||||
// Rename new to old name.
|
||||
if _, err := tx.NewRaw(
|
||||
"ALTER TABLE ? RENAME COLUMN ? TO ?",
|
||||
bun.Ident(table),
|
||||
bun.Ident(newColumn),
|
||||
bun.Ident(column),
|
||||
).Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error renaming new column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBunColumnDef generates a column definition string for the SQL table represented by
|
||||
// Go type, with the SQL column represented by the given Go field name. This ensures when
|
||||
// adding a new column for table by migration that it will end up as bun would create it.
|
||||
//
|
||||
// NOTE: this function must stay in sync with (*bun.CreateTableQuery{}).AppendQuery(),
|
||||
// specifically where it loops over table fields appending each column definition.
|
||||
func getBunColumnDef(db bun.IDB, rtype reflect.Type, fieldName string) (string, error) {
|
||||
d := db.Dialect()
|
||||
f := d.Features()
|
||||
|
||||
// Get bun schema definitions for Go type and its field.
|
||||
field, table, err := getModelField(db, rtype, fieldName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Start with reasonable buf.
|
||||
buf := make([]byte, 0, 64)
|
||||
|
||||
// Start with the SQL column name.
|
||||
buf = append(buf, field.SQLName...)
|
||||
buf = append(buf, " "...)
|
||||
|
||||
// Append the SQL
|
||||
// type information.
|
||||
switch {
|
||||
|
||||
// Most of the time these two will match, but for the cases where DiscoveredSQLType is dialect-specific,
|
||||
// e.g. pgdialect would change sqltype.SmallInt to pgTypeSmallSerial for columns that have `bun:",autoincrement"`
|
||||
case !strings.EqualFold(field.CreateTableSQLType, field.DiscoveredSQLType):
|
||||
buf = append(buf, field.CreateTableSQLType...)
|
||||
|
||||
// For all common SQL types except VARCHAR, both UserDefinedSQLType and DiscoveredSQLType specify the correct type,
|
||||
// and we needn't modify it. For VARCHAR columns, we will stop to check if a valid length has been set in .Varchar(int).
|
||||
case !strings.EqualFold(field.CreateTableSQLType, sqltype.VarChar):
|
||||
buf = append(buf, field.CreateTableSQLType...)
|
||||
|
||||
// All else falls back
|
||||
// to a default varchar.
|
||||
default:
|
||||
if d.Name() == dialect.Oracle {
|
||||
buf = append(buf, "VARCHAR2"...)
|
||||
} else {
|
||||
buf = append(buf, sqltype.VarChar...)
|
||||
}
|
||||
buf = append(buf, "("...)
|
||||
buf = strconv.AppendInt(buf, int64(d.DefaultVarcharLen()), 10)
|
||||
buf = append(buf, ")"...)
|
||||
}
|
||||
|
||||
// Append not null definition if field requires.
|
||||
if field.NotNull && d.Name() != dialect.Oracle {
|
||||
buf = append(buf, " NOT NULL"...)
|
||||
}
|
||||
|
||||
// Append autoincrement definition if field requires.
|
||||
if field.Identity && f.Has(feature.GeneratedIdentity) ||
|
||||
(field.AutoIncrement && (f.Has(feature.AutoIncrement) || f.Has(feature.Identity))) {
|
||||
buf = d.AppendSequence(buf, table, field)
|
||||
}
|
||||
|
||||
// Append any default value.
|
||||
if field.SQLDefault != "" {
|
||||
buf = append(buf, " DEFAULT "...)
|
||||
buf = append(buf, field.SQLDefault...)
|
||||
}
|
||||
|
||||
return byteutil.B2S(buf), nil
|
||||
}
|
||||
|
||||
// getModelField returns the uptrace/bun schema details for given Go type and field name.
|
||||
func getModelField(db bun.IDB, rtype reflect.Type, fieldName string) (*schema.Field, *schema.Table, error) {
|
||||
|
||||
// Get the associated table for Go type.
|
||||
table := db.Dialect().Tables().Get(rtype)
|
||||
if table == nil {
|
||||
return nil, nil, fmt.Errorf("no table found for type: %s", rtype)
|
||||
}
|
||||
|
||||
var field *schema.Field
|
||||
|
||||
// Look for field matching Go name.
|
||||
for i := range table.Fields {
|
||||
if table.Fields[i].GoName == fieldName {
|
||||
field = table.Fields[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if field == nil {
|
||||
return nil, nil, fmt.Errorf("no bun field found on %s with name: %s", rtype, fieldName)
|
||||
}
|
||||
|
||||
return field, table, nil
|
||||
}
|
||||
|
||||
// doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately.
|
||||
func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) {
|
||||
var n int
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ func (n *notificationDB) GetNotificationsByIDs(ctx context.Context, ids []string
|
|||
// Reorder the notifs by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(n *gtsmodel.Notification) string { return n.ID }
|
||||
util.OrderBy(notifs, ids, getID)
|
||||
xslices.OrderBy(notifs, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -192,22 +192,19 @@ func (n *notificationDB) PopulateNotification(ctx context.Context, notif *gtsmod
|
|||
func (n *notificationDB) GetAccountNotifications(
|
||||
ctx context.Context,
|
||||
accountID string,
|
||||
maxID string,
|
||||
sinceID string,
|
||||
minID string,
|
||||
limit int,
|
||||
types []string,
|
||||
excludeTypes []string,
|
||||
page *paging.Page,
|
||||
types []gtsmodel.NotificationType,
|
||||
excludeTypes []gtsmodel.NotificationType,
|
||||
) ([]*gtsmodel.Notification, error) {
|
||||
// Ensure reasonable
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Make educated guess for slice size
|
||||
var (
|
||||
notifIDs = make([]string, 0, limit)
|
||||
frontToBack = true
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
notifIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := n.db.
|
||||
|
|
@ -215,23 +212,14 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Column("notification.id")
|
||||
|
||||
if maxID == "" {
|
||||
maxID = id.Highest
|
||||
}
|
||||
|
||||
// Return only notifs LOWER (ie., older) than maxID.
|
||||
q = q.Where("? < ?", bun.Ident("notification.id"), maxID)
|
||||
|
||||
if sinceID != "" {
|
||||
// Return only notifs HIGHER (ie., newer) than sinceID.
|
||||
q = q.Where("? > ?", bun.Ident("notification.id"), sinceID)
|
||||
if maxID != "" {
|
||||
// Return only notifs LOWER (ie., older) than maxID.
|
||||
q = q.Where("? < ?", bun.Ident("notification.id"), maxID)
|
||||
}
|
||||
|
||||
if minID != "" {
|
||||
// Return only notifs HIGHER (ie., newer) than minID.
|
||||
q = q.Where("? > ?", bun.Ident("notification.id"), minID)
|
||||
|
||||
frontToBack = false // page up
|
||||
}
|
||||
|
||||
if len(types) > 0 {
|
||||
|
|
@ -251,12 +239,12 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if frontToBack {
|
||||
// Page down.
|
||||
q = q.Order("notification.id DESC")
|
||||
} else {
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.Order("notification.id ASC")
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.Order("notification.id DESC")
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, ¬ifIDs); err != nil {
|
||||
|
|
@ -269,11 +257,8 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
|
||||
// If we're paging up, we still want notifications
|
||||
// to be sorted by ID desc, so reverse ids slice.
|
||||
// https://zchee.github.io/golang-wiki/SliceTricks/#reversing
|
||||
if !frontToBack {
|
||||
for l, r := 0, len(notifIDs)-1; l < r; l, r = l+1, r-1 {
|
||||
notifIDs[l], notifIDs[r] = notifIDs[r], notifIDs[l]
|
||||
}
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(notifIDs)
|
||||
}
|
||||
|
||||
// Fetch notification models by their IDs.
|
||||
|
|
@ -303,7 +288,7 @@ func (n *notificationDB) DeleteNotificationByID(ctx context.Context, id string)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) error {
|
||||
func (n *notificationDB) DeleteNotifications(ctx context.Context, types []gtsmodel.NotificationType, targetAccountID string, originAccountID string) error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return gtserror.New("one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -92,10 +93,11 @@ func (suite *NotificationTestSuite) TestGetAccountNotificationsWithSpam() {
|
|||
notifications, err := suite.db.GetAccountNotifications(
|
||||
gtscontext.SetBarebones(context.Background()),
|
||||
testAccount.ID,
|
||||
id.Highest,
|
||||
id.Lowest,
|
||||
"",
|
||||
20,
|
||||
&paging.Page{
|
||||
Min: paging.EitherMinID("", id.Lowest),
|
||||
Max: paging.MaxID(id.Highest),
|
||||
Limit: 20,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
|
@ -115,10 +117,11 @@ func (suite *NotificationTestSuite) TestGetAccountNotificationsWithoutSpam() {
|
|||
notifications, err := suite.db.GetAccountNotifications(
|
||||
gtscontext.SetBarebones(context.Background()),
|
||||
testAccount.ID,
|
||||
id.Highest,
|
||||
id.Lowest,
|
||||
"",
|
||||
20,
|
||||
&paging.Page{
|
||||
Min: paging.EitherMinID("", id.Lowest),
|
||||
Max: paging.MaxID(id.Highest),
|
||||
Limit: 20,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
|
@ -140,10 +143,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() {
|
|||
notifications, err := suite.db.GetAccountNotifications(
|
||||
gtscontext.SetBarebones(context.Background()),
|
||||
testAccount.ID,
|
||||
id.Highest,
|
||||
id.Lowest,
|
||||
"",
|
||||
20,
|
||||
&paging.Page{
|
||||
Min: paging.EitherMinID("", id.Lowest),
|
||||
Max: paging.MaxID(id.Highest),
|
||||
Limit: 20,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
|
@ -161,10 +165,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() {
|
|||
notifications, err = suite.db.GetAccountNotifications(
|
||||
gtscontext.SetBarebones(context.Background()),
|
||||
testAccount.ID,
|
||||
id.Highest,
|
||||
id.Lowest,
|
||||
"",
|
||||
20,
|
||||
&paging.Page{
|
||||
Min: paging.EitherMinID("", id.Lowest),
|
||||
Max: paging.MaxID(id.Highest),
|
||||
Limit: 20,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
|
@ -183,10 +188,11 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsWithTwoAccounts() {
|
|||
notifications, err := suite.db.GetAccountNotifications(
|
||||
gtscontext.SetBarebones(context.Background()),
|
||||
testAccount.ID,
|
||||
id.Highest,
|
||||
id.Lowest,
|
||||
"",
|
||||
20,
|
||||
&paging.Page{
|
||||
Min: paging.EitherMinID("", id.Lowest),
|
||||
Max: paging.MaxID(id.Highest),
|
||||
Limit: 20,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
|
|
@ -29,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -88,12 +87,15 @@ func (p *pollDB) getPoll(ctx context.Context, lookup string, dbQuery func(*gtsmo
|
|||
func (p *pollDB) GetOpenPolls(ctx context.Context) ([]*gtsmodel.Poll, error) {
|
||||
var pollIDs []string
|
||||
|
||||
// Select all polls with unset `closed_at` time.
|
||||
// Select all polls with:
|
||||
// - UNSET `closed_at`
|
||||
// - SET `expires_at`
|
||||
if err := p.db.NewSelect().
|
||||
Table("polls").
|
||||
Column("polls.id").
|
||||
Join("JOIN ? ON ? = ?", bun.Ident("statuses"), bun.Ident("polls.id"), bun.Ident("statuses.poll_id")).
|
||||
Where("? = true", bun.Ident("statuses.local")).
|
||||
Where("? IS NOT NULL", bun.Ident("polls.expires_at")).
|
||||
Where("? IS NULL", bun.Ident("polls.closed_at")).
|
||||
Scan(ctx, &pollIDs); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -155,17 +157,6 @@ func (p *pollDB) UpdatePoll(ctx context.Context, poll *gtsmodel.Poll, cols ...st
|
|||
|
||||
return p.state.Caches.DB.Poll.Store(poll, func() error {
|
||||
return p.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
// Update the status' "updated_at" field.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("statuses").
|
||||
Where("? = ?", bun.Ident("id"), poll.StatusID).
|
||||
SetColumn("updated_at", "?", time.Now()).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Finally, update poll
|
||||
// columns in database.
|
||||
_, err := tx.NewUpdate().
|
||||
Model(poll).
|
||||
Column(cols...).
|
||||
|
|
@ -315,7 +306,7 @@ func (p *pollDB) GetPollVotes(ctx context.Context, pollID string) ([]*gtsmodel.P
|
|||
// Reorder the poll votes by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(v *gtsmodel.PollVote) string { return v.ID }
|
||||
util.OrderBy(votes, voteIDs, getID)
|
||||
xslices.OrderBy(votes, voteIDs, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ func (r *relationshipDB) GetBlocksByIDs(ctx context.Context, ids []string) ([]*g
|
|||
// Reorder the blocks by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(b *gtsmodel.Block) string { return b.ID }
|
||||
util.OrderBy(blocks, ids, getID)
|
||||
xslices.OrderBy(blocks, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ func (r *relationshipDB) GetFollowsByIDs(ctx context.Context, ids []string) ([]*
|
|||
// Reorder the follows by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(f *gtsmodel.Follow) string { return f.ID }
|
||||
util.OrderBy(follows, ids, getID)
|
||||
xslices.OrderBy(follows, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -376,7 +376,7 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str
|
|||
}
|
||||
|
||||
// Gather the follow IDs that were deleted for removing related list entries.
|
||||
followIDs := util.Gather(nil, deleted, func(follow *gtsmodel.Follow) string {
|
||||
followIDs := xslices.Gather(nil, deleted, func(follow *gtsmodel.Follow) string {
|
||||
return follow.ID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ func (r *relationshipDB) GetFollowRequestsByIDs(ctx context.Context, ids []strin
|
|||
// Reorder the requests by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(f *gtsmodel.FollowRequest) string { return f.ID }
|
||||
util.OrderBy(follows, ids, getID)
|
||||
xslices.OrderBy(follows, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -265,8 +265,8 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI
|
|||
}
|
||||
|
||||
// Delete original follow request notification
|
||||
if err := r.state.DB.DeleteNotifications(ctx, []string{
|
||||
string(gtsmodel.NotificationFollowRequest),
|
||||
if err := r.state.DB.DeleteNotifications(ctx, []gtsmodel.NotificationType{
|
||||
gtsmodel.NotificationFollowRequest,
|
||||
}, targetAccountID, sourceAccountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -281,8 +281,8 @@ func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountI
|
|||
}
|
||||
|
||||
// Delete follow request notification
|
||||
return r.state.DB.DeleteNotifications(ctx, []string{
|
||||
string(gtsmodel.NotificationFollowRequest),
|
||||
return r.state.DB.DeleteNotifications(ctx, []gtsmodel.NotificationType{
|
||||
gtsmodel.NotificationFollowRequest,
|
||||
}, targetAccountID, sourceAccountID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -109,7 +109,7 @@ func (r *relationshipDB) getMutesByIDs(ctx context.Context, ids []string) ([]*gt
|
|||
// Reorder the mutes by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(b *gtsmodel.UserMute) string { return b.ID }
|
||||
util.OrderBy(mutes, ids, getID)
|
||||
xslices.OrderBy(mutes, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
|
|
@ -29,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ func (s *statusDB) GetStatusesByIDs(ctx context.Context, ids []string) ([]*gtsmo
|
|||
// Reorder the statuses by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(s *gtsmodel.Status) string { return s.ID }
|
||||
util.OrderBy(statuses, ids, getID)
|
||||
xslices.OrderBy(statuses, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -181,7 +180,7 @@ func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*g
|
|||
func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(9)
|
||||
errs gtserror.MultiError
|
||||
)
|
||||
|
||||
if status.Account == nil {
|
||||
|
|
@ -257,7 +256,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.AttachmentsPopulated() {
|
||||
// Status attachments are out-of-date with IDs, repopulate.
|
||||
status.Attachments, err = s.state.DB.GetAttachmentsByIDs(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.AttachmentIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -268,7 +267,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.TagsPopulated() {
|
||||
// Status tags are out-of-date with IDs, repopulate.
|
||||
status.Tags, err = s.state.DB.GetTags(
|
||||
ctx,
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.TagIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -279,7 +278,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.MentionsPopulated() {
|
||||
// Status mentions are out-of-date with IDs, repopulate.
|
||||
status.Mentions, err = s.state.DB.GetMentions(
|
||||
ctx, // leave fully populated for now
|
||||
ctx, // TODO: manually populate mentions for places expecting these populated
|
||||
status.MentionIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -290,7 +289,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.EmojisPopulated() {
|
||||
// Status emojis are out-of-date with IDs, repopulate.
|
||||
status.Emojis, err = s.state.DB.GetEmojisByIDs(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.EmojiIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -301,7 +300,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if status.CreatedWithApplicationID != "" && status.CreatedWithApplication == nil {
|
||||
// Populate the status' expected CreatedWithApplication (not always set).
|
||||
status.CreatedWithApplication, err = s.state.DB.GetApplicationByID(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.CreatedWithApplicationID,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -312,6 +311,23 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *statusDB) PopulateStatusEdits(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var err error
|
||||
|
||||
if !status.EditsPopulated() {
|
||||
// Status edits are out-of-date with IDs, repopulate.
|
||||
status.Edits, err = s.state.DB.GetStatusEditsByIDs(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.EditIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error populating status edits: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
return s.state.Caches.DB.Status.Store(status, func() error {
|
||||
// It is safe to run this database transaction within cache.Store
|
||||
|
|
@ -350,14 +366,14 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
|
|||
}
|
||||
}
|
||||
|
||||
// change the status ID of the media attachments to the new status
|
||||
// change the status ID of the media
|
||||
// attachments to the current status
|
||||
for _, a := range status.Attachments {
|
||||
a.StatusID = status.ID
|
||||
a.UpdatedAt = time.Now()
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Model(a).
|
||||
Column("status_id", "updated_at").
|
||||
Column("status_id").
|
||||
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
|
@ -384,19 +400,15 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
|
|||
}
|
||||
|
||||
// Finally, insert the status
|
||||
_, err := tx.NewInsert().Model(status).Exec(ctx)
|
||||
_, err := tx.NewInsert().
|
||||
Model(status).
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) error {
|
||||
status.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column, ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
return s.state.Caches.DB.Status.Store(status, func() error {
|
||||
// It is safe to run this database transaction within cache.Store
|
||||
// as the cache does not attempt a mutex lock until AFTER hook.
|
||||
|
|
@ -434,13 +446,14 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
|
|||
}
|
||||
}
|
||||
|
||||
// change the status ID of the media attachments to the new status
|
||||
// change the status ID of the media
|
||||
// attachments to the current status.
|
||||
for _, a := range status.Attachments {
|
||||
a.StatusID = status.ID
|
||||
a.UpdatedAt = time.Now()
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Model(a).
|
||||
Column("status_id").
|
||||
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
|
@ -467,8 +480,7 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
|
|||
}
|
||||
|
||||
// Finally, update the status
|
||||
_, err := tx.
|
||||
NewUpdate().
|
||||
_, err := tx.NewUpdate().
|
||||
Model(status).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("status.id"), status.ID).
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ func (s *statusBookmarkDB) GetStatusBookmarksByIDs(ctx context.Context, ids []st
|
|||
// Reorder the bookmarks by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(b *gtsmodel.StatusBookmark) string { return b.ID }
|
||||
util.OrderBy(bookmarks, ids, getID)
|
||||
xslices.OrderBy(bookmarks, ids, getID)
|
||||
|
||||
// Populate all loaded bookmarks, removing those we fail
|
||||
// to populate (removes needing so many later nil checks).
|
||||
|
|
|
|||
198
internal/db/bundb/statusedit.go
Normal file
198
internal/db/bundb/statusedit.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type statusEditDB struct {
|
||||
db *bun.DB
|
||||
state *state.State
|
||||
}
|
||||
|
||||
func (s *statusEditDB) GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error) {
|
||||
// Fetch edit from database cache with loader callback.
|
||||
edit, err := s.state.Caches.DB.StatusEdit.LoadOne("ID",
|
||||
func() (*gtsmodel.StatusEdit, error) {
|
||||
var edit gtsmodel.StatusEdit
|
||||
|
||||
// Not cached, load edit
|
||||
// from database by its ID.
|
||||
if err := s.db.NewSelect().
|
||||
Model(&edit).
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &edit, nil
|
||||
}, id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return edit, nil
|
||||
}
|
||||
|
||||
// Further populate the edit fields where applicable.
|
||||
if err := s.PopulateStatusEdit(ctx, edit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return edit, nil
|
||||
}
|
||||
|
||||
func (s *statusEditDB) GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error) {
|
||||
// Load status edits for IDs via cache loader callbacks.
|
||||
edits, err := s.state.Caches.DB.StatusEdit.LoadIDs("ID",
|
||||
ids,
|
||||
func(uncached []string) ([]*gtsmodel.StatusEdit, error) {
|
||||
// Preallocate expected length of uncached edits.
|
||||
edits := make([]*gtsmodel.StatusEdit, 0, len(uncached))
|
||||
|
||||
// Perform database query scanning
|
||||
// the remaining (uncached) edit IDs.
|
||||
if err := s.db.NewSelect().
|
||||
Model(&edits).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(uncached)).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return edits, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reorder the edits by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(e *gtsmodel.StatusEdit) string { return e.ID }
|
||||
xslices.OrderBy(edits, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
// Populate all loaded edits, removing those we fail to
|
||||
// populate (removes needing so many nil checks everywhere).
|
||||
edits = slices.DeleteFunc(edits, func(edit *gtsmodel.StatusEdit) bool {
|
||||
if err := s.PopulateStatusEdit(ctx, edit); err != nil {
|
||||
log.Errorf(ctx, "error populating edit %s: %v", edit.ID, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
func (s *statusEditDB) PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error {
|
||||
var err error
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// For sub-models we only want
|
||||
// barebones versions of them.
|
||||
ctx = gtscontext.SetBarebones(ctx)
|
||||
|
||||
if !edit.AttachmentsPopulated() {
|
||||
// Fetch all attachments for status edit's IDs.
|
||||
edit.Attachments, err = s.state.DB.GetAttachmentsByIDs(
|
||||
ctx,
|
||||
edit.AttachmentIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating edit attachments: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *statusEditDB) PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error {
|
||||
return s.state.Caches.DB.StatusEdit.Store(edit, func() error {
|
||||
_, err := s.db.NewInsert().Model(edit).Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) error {
|
||||
// Gather necessary fields from
|
||||
// deleted for cache invalidation.
|
||||
deleted := make([]*gtsmodel.StatusEdit, 0, len(ids))
|
||||
|
||||
// Delete all edits with IDs pertaining
|
||||
// to given slice, returning status IDs.
|
||||
if _, err := s.db.NewDelete().
|
||||
Model(&deleted).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(ids)).
|
||||
Returning("?", bun.Ident("status_id")).
|
||||
Exec(ctx); err != nil &&
|
||||
!errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for no deletes.
|
||||
if len(deleted) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate all the cached status edits with IDs.
|
||||
s.state.Caches.DB.StatusEdit.InvalidateIDs("ID", ids)
|
||||
|
||||
// With each invalidate hook mark status ID of
|
||||
// edit we just called for. We only want to call
|
||||
// invalidate hooks of edits from unique statuses.
|
||||
invalidated := make(map[string]struct{}, 1)
|
||||
|
||||
// Invalidate the first delete manually, this
|
||||
// opt negates need for initial hashmap lookup.
|
||||
s.state.Caches.OnInvalidateStatusEdit(deleted[0])
|
||||
invalidated[deleted[0].StatusID] = struct{}{}
|
||||
|
||||
for _, edit := range deleted {
|
||||
// Check not already called for status.
|
||||
_, ok := invalidated[edit.StatusID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Manually call status edit invalidate hook.
|
||||
s.state.Caches.OnInvalidateStatusEdit(edit)
|
||||
invalidated[edit.StatusID] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
168
internal/db/bundb/statusedit_test.go
Normal file
168
internal/db/bundb/statusedit_test.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type StatusEditTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestGetStatusEditBy() {
|
||||
t := suite.T()
|
||||
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// Sentinel error to mark avoiding a test case.
|
||||
sentinelErr := errors.New("sentinel")
|
||||
|
||||
for _, edit := range suite.testStatusEdits {
|
||||
for lookup, dbfunc := range map[string]func() (*gtsmodel.StatusEdit, error){
|
||||
"id": func() (*gtsmodel.StatusEdit, error) {
|
||||
return suite.db.GetStatusEditByID(ctx, edit.ID)
|
||||
},
|
||||
} {
|
||||
// Clear database caches.
|
||||
suite.state.Caches.Init()
|
||||
|
||||
t.Logf("checking database lookup %q", lookup)
|
||||
|
||||
// Perform database function.
|
||||
checkEdit, err := dbfunc()
|
||||
if err != nil {
|
||||
if err == sentinelErr {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Errorf("error encountered for database lookup %q: %v", lookup, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check received account data.
|
||||
if !areEditsEqual(edit, checkEdit) {
|
||||
t.Errorf("edit does not contain expected data: %+v", checkEdit)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestGetStatusEditsByIDs() {
|
||||
t := suite.T()
|
||||
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// editsByStatus returns all test edits by the given status with ID.
|
||||
editsByStatus := func(status *gtsmodel.Status) []*gtsmodel.StatusEdit {
|
||||
var edits []*gtsmodel.StatusEdit
|
||||
for _, edit := range suite.testStatusEdits {
|
||||
if edit.StatusID == status.ID {
|
||||
edits = append(edits, edit)
|
||||
}
|
||||
}
|
||||
return edits
|
||||
}
|
||||
|
||||
for _, status := range suite.testStatuses {
|
||||
// Get test status edit models
|
||||
// that should be found for status.
|
||||
check := editsByStatus(status)
|
||||
|
||||
// Fetch edits for the slice of IDs attached to status from database.
|
||||
edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure both slices
|
||||
// sorted the same.
|
||||
sortEdits(check)
|
||||
sortEdits(edits)
|
||||
|
||||
// Check whether slices of status edits match.
|
||||
if !slices.EqualFunc(check, edits, areEditsEqual) {
|
||||
t.Error("status edit slices do not match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestDeleteStatusEdits() {
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
for _, status := range suite.testStatuses {
|
||||
// Delete all edits for status with given IDs from database.
|
||||
err := suite.state.DB.DeleteStatusEdits(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
|
||||
// Now attempt to fetch these edits from database, should be empty.
|
||||
edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
suite.Empty(edits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusEditTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusEditTestSuite))
|
||||
}
|
||||
|
||||
func areEditsEqual(e1, e2 *gtsmodel.StatusEdit) bool {
|
||||
// Clone the 1st status edit.
|
||||
e1Copy := new(gtsmodel.StatusEdit)
|
||||
*e1Copy = *e1
|
||||
e1 = e1Copy
|
||||
|
||||
// Clone the 2nd status edit.
|
||||
e2Copy := new(gtsmodel.StatusEdit)
|
||||
*e2Copy = *e2
|
||||
e2 = e2Copy
|
||||
|
||||
// Clear populated sub-models.
|
||||
e1.Attachments = nil
|
||||
e2.Attachments = nil
|
||||
|
||||
// Clear database-set fields.
|
||||
e1.CreatedAt = time.Time{}
|
||||
e2.CreatedAt = time.Time{}
|
||||
|
||||
return reflect.DeepEqual(*e1, *e2)
|
||||
}
|
||||
|
||||
func sortEdits(edits []*gtsmodel.StatusEdit) {
|
||||
slices.SortFunc(edits, func(a, b *gtsmodel.StatusEdit) int {
|
||||
if a.CreatedAt.Before(b.CreatedAt) {
|
||||
return +1
|
||||
} else if b.CreatedAt.Before(a.CreatedAt) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ func (s *statusFaveDB) GetStatusFaves(ctx context.Context, statusID string) ([]*
|
|||
// Reorder the statuses by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(f *gtsmodel.StatusFave) string { return f.ID }
|
||||
util.OrderBy(faves, faveIDs, getID)
|
||||
xslices.OrderBy(faves, faveIDs, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -339,7 +339,7 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st
|
|||
}
|
||||
|
||||
// Deduplicate determined status IDs.
|
||||
statusIDs = util.Deduplicate(statusIDs)
|
||||
statusIDs = xslices.Deduplicate(statusIDs)
|
||||
|
||||
// Invalidate any cached status faves for this status ID.
|
||||
s.state.Caches.DB.StatusFave.InvalidateIDs("ID", statusIDs)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func (t *tagDB) GetTags(ctx context.Context, ids []string) ([]*gtsmodel.Tag, err
|
|||
// Reorder the tags by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(t *gtsmodel.Tag) string { return t.ID }
|
||||
util.OrderBy(tags, ids, getID)
|
||||
xslices.OrderBy(tags, ids, getID)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
|
@ -301,5 +301,5 @@ func (t *tagDB) GetAccountIDsFollowingTagIDs(ctx context.Context, tagIDs []strin
|
|||
|
||||
// Accounts might be following multiple tags in list,
|
||||
// but we only want to return each account once.
|
||||
return util.Deduplicate(accountIDs), nil
|
||||
return xslices.Deduplicate(accountIDs), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,13 +123,8 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
|
@ -223,13 +218,8 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceI
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
|
@ -409,13 +399,8 @@ func (t *timelineDB) GetListTimeline(
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
|
@ -508,13 +493,8 @@ func (t *timelineDB) GetTagTimeline(
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
|
|
|||
|
|
@ -37,10 +37,7 @@ type TimelineTestSuite struct {
|
|||
|
||||
func getFutureStatus() *gtsmodel.Status {
|
||||
theDistantFuture := time.Now().Add(876600 * time.Hour)
|
||||
id, err := id.NewULIDFromTime(theDistantFuture)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
id := id.NewULIDFromTime(theDistantFuture)
|
||||
|
||||
return >smodel.Status{
|
||||
ID: id,
|
||||
|
|
@ -53,7 +50,6 @@ func getFutureStatus() *gtsmodel.Status {
|
|||
MentionIDs: []string{},
|
||||
EmojiIDs: []string{},
|
||||
CreatedAt: theDistantFuture,
|
||||
UpdatedAt: theDistantFuture,
|
||||
Local: util.Ptr(true),
|
||||
AccountURI: "http://localhost:8080/users/admin",
|
||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
|
|
@ -74,7 +70,8 @@ func (suite *TimelineTestSuite) publicCount() int {
|
|||
var publicCount int
|
||||
for _, status := range suite.testStatuses {
|
||||
if status.Visibility == gtsmodel.VisibilityPublic &&
|
||||
status.BoostOfID == "" {
|
||||
status.BoostOfID == "" &&
|
||||
!util.PtrOrZero(status.PendingApproval) {
|
||||
publicCount++
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +178,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 9)
|
||||
|
||||
// Remove admin account from the exclusive list.
|
||||
listEntry := suite.testListEntries["local_account_1_list_1_entry_2"]
|
||||
|
|
@ -195,7 +192,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 12)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 13)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
|
||||
|
|
@ -227,7 +224,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 9)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
|
||||
|
|
@ -280,8 +277,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() {
|
|||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 5)
|
||||
suite.Equal("01J2M1HPFSS54S60Y0KYV23KJE", s[0].ID)
|
||||
suite.Equal("01G36SF3V6Y6V5BF9P4R7PQG7G", s[len(s)-1].ID)
|
||||
suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID)
|
||||
suite.Equal("01HEN2RZ8BG29Y5Z9VJC73HZW7", s[len(s)-1].ID)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {
|
||||
|
|
@ -295,7 +292,7 @@ func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 12)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 13)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineMaxID() {
|
||||
|
|
@ -310,8 +307,8 @@ func (suite *TimelineTestSuite) TestGetListTimelineMaxID() {
|
|||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 5)
|
||||
suite.Equal("01HEN2PRXT0TF4YDRA64FZZRN7", s[0].ID)
|
||||
suite.Equal("01FF25D5Q0DH7CHD57CTRS6WK0", s[len(s)-1].ID)
|
||||
suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID)
|
||||
suite.Equal("01FN3VJGFH10KR7S2PB0GFJZYG", s[len(s)-1].ID)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineMinID() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue