summaryrefslogtreecommitdiff
path: root/internal/db
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db')
-rw-r--r--internal/db/bundb/account.go22
-rw-r--r--internal/db/bundb/basic_test.go2
-rw-r--r--internal/db/bundb/bundb_test.go46
-rw-r--r--internal/db/bundb/instance.go3
-rw-r--r--internal/db/bundb/interaction.go289
-rw-r--r--internal/db/bundb/interaction_test.go261
-rw-r--r--internal/db/bundb/migrations/20240716151327_interaction_policy.go29
-rw-r--r--internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go154
-rw-r--r--internal/db/bundb/timeline.go38
-rw-r--r--internal/db/interaction.go46
10 files changed, 762 insertions, 128 deletions
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 94fd054bf..b57dcb57b 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -1285,34 +1285,40 @@ func (a *accountDB) RegenerateAccountStats(ctx context.Context, account *gtsmode
if err := a.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
var err error
- // Scan database for account statuses.
+ // Scan database for account statuses, ignoring
+ // statuses that are currently pending approval.
statusesCount, err := tx.NewSelect().
- Table("statuses").
- Where("? = ?", bun.Ident("account_id"), account.ID).
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Count(ctx)
if err != nil {
return err
}
stats.StatusesCount = &statusesCount
- // Scan database for pinned statuses.
+ // Scan database for pinned statuses, ignoring
+ // statuses that are currently pending approval.
statusesPinnedCount, err := tx.NewSelect().
- Table("statuses").
- Where("? = ?", bun.Ident("account_id"), account.ID).
- Where("? IS NOT NULL", bun.Ident("pinned_at")).
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Count(ctx)
if err != nil {
return err
}
stats.StatusesPinnedCount = &statusesPinnedCount
- // Scan database for last status.
+ // Scan database for last status, ignoring
+ // statuses that are currently pending approval.
lastStatusAt := time.Time{}
err = tx.
NewSelect().
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
Column("status.created_at").
Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Order("status.id DESC").
Limit(1).
Scan(ctx, &lastStatusAt)
diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go
index f6647c1f5..56159dc25 100644
--- a/internal/db/bundb/basic_test.go
+++ b/internal/db/bundb/basic_test.go
@@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
s := []*gtsmodel.Status{}
err := suite.db.GetAll(context.Background(), &s)
suite.NoError(err)
- suite.Len(s, 24)
+ suite.Len(s, 25)
}
func (suite *BasicTestSuite) TestGetAllNotNull() {
diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go
index adfd605a5..e976199e4 100644
--- a/internal/db/bundb/bundb_test.go
+++ b/internal/db/bundb/bundb_test.go
@@ -34,28 +34,29 @@ type BunDBStandardTestSuite struct {
state state.State
// standard suite models
- testTokens map[string]*gtsmodel.Token
- testClients map[string]*gtsmodel.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
- testTags map[string]*gtsmodel.Tag
- testMentions map[string]*gtsmodel.Mention
- testFollows map[string]*gtsmodel.Follow
- testEmojis map[string]*gtsmodel.Emoji
- testReports map[string]*gtsmodel.Report
- testBookmarks map[string]*gtsmodel.StatusBookmark
- testFaves map[string]*gtsmodel.StatusFave
- testLists map[string]*gtsmodel.List
- testListEntries map[string]*gtsmodel.ListEntry
- testAccountNotes map[string]*gtsmodel.AccountNote
- testMarkers map[string]*gtsmodel.Marker
- testRules map[string]*gtsmodel.Rule
- testThreads map[string]*gtsmodel.Thread
- testPolls map[string]*gtsmodel.Poll
- testPollVotes map[string]*gtsmodel.PollVote
+ testTokens map[string]*gtsmodel.Token
+ testClients map[string]*gtsmodel.Client
+ testApplications map[string]*gtsmodel.Application
+ testUsers map[string]*gtsmodel.User
+ testAccounts map[string]*gtsmodel.Account
+ testAttachments map[string]*gtsmodel.MediaAttachment
+ testStatuses map[string]*gtsmodel.Status
+ testTags map[string]*gtsmodel.Tag
+ testMentions map[string]*gtsmodel.Mention
+ testFollows map[string]*gtsmodel.Follow
+ testEmojis map[string]*gtsmodel.Emoji
+ testReports map[string]*gtsmodel.Report
+ testBookmarks map[string]*gtsmodel.StatusBookmark
+ testFaves map[string]*gtsmodel.StatusFave
+ testLists map[string]*gtsmodel.List
+ testListEntries map[string]*gtsmodel.ListEntry
+ testAccountNotes map[string]*gtsmodel.AccountNote
+ testMarkers map[string]*gtsmodel.Marker
+ testRules map[string]*gtsmodel.Rule
+ testThreads map[string]*gtsmodel.Thread
+ testPolls map[string]*gtsmodel.Poll
+ testPollVotes map[string]*gtsmodel.PollVote
+ testInteractionRequests map[string]*gtsmodel.InteractionRequest
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@@ -81,6 +82,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testThreads = testrig.NewTestThreads()
suite.testPolls = testrig.NewTestPolls()
suite.testPollVotes = testrig.NewTestPollVotes()
+ suite.testInteractionRequests = testrig.NewTestInteractionRequests()
}
func (suite *BunDBStandardTestSuite) SetupTest() {
diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go
index 6d98c8db7..008b6c8f3 100644
--- a/internal/db/bundb/instance.go
+++ b/internal/db/bundb/instance.go
@@ -76,6 +76,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
Where("? = ?", bun.Ident("account.domain"), domain)
}
+ // Ignore statuses that are currently pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
count, err := q.Count(ctx)
if err != nil {
return 0, err
diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go
index 2ded70311..78abcc763 100644
--- a/internal/db/bundb/interaction.go
+++ b/internal/db/bundb/interaction.go
@@ -19,10 +19,14 @@ 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/paging"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/uptrace/bun"
)
@@ -32,56 +36,70 @@ type interactionDB struct {
state *state.State
}
-func (r *interactionDB) newInteractionApprovalQ(approval interface{}) *bun.SelectQuery {
- return r.db.
+func (i *interactionDB) newInteractionRequestQ(request interface{}) *bun.SelectQuery {
+ return i.db.
NewSelect().
- Model(approval)
+ Model(request)
}
-func (r *interactionDB) GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) {
- return r.getInteractionApproval(
+func (i *interactionDB) GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
ctx,
"ID",
- func(approval *gtsmodel.InteractionApproval) error {
- return r.
- newInteractionApprovalQ(approval).
- Where("? = ?", bun.Ident("interaction_approval.id"), id).
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.id"), id).
Scan(ctx)
},
id,
)
}
-func (r *interactionDB) GetInteractionApprovalByURI(ctx context.Context, uri string) (*gtsmodel.InteractionApproval, error) {
- return r.getInteractionApproval(
+func (i *interactionDB) GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
+ ctx,
+ "InteractionURI",
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.interaction_uri"), uri).
+ Scan(ctx)
+ },
+ uri,
+ )
+}
+
+func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
ctx,
"URI",
- func(approval *gtsmodel.InteractionApproval) error {
- return r.
- newInteractionApprovalQ(approval).
- Where("? = ?", bun.Ident("interaction_approval.uri"), uri).
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.uri"), uri).
Scan(ctx)
},
uri,
)
}
-func (r *interactionDB) getInteractionApproval(
+func (i *interactionDB) getInteractionRequest(
ctx context.Context,
lookup string,
- dbQuery func(*gtsmodel.InteractionApproval) error,
+ dbQuery func(*gtsmodel.InteractionRequest) error,
keyParts ...any,
-) (*gtsmodel.InteractionApproval, error) {
- // Fetch approval from database cache with loader callback
- approval, err := r.state.Caches.DB.InteractionApproval.LoadOne(lookup, func() (*gtsmodel.InteractionApproval, error) {
- var approval gtsmodel.InteractionApproval
+) (*gtsmodel.InteractionRequest, error) {
+ // Fetch request from database cache with loader callback
+ request, err := i.state.Caches.DB.InteractionRequest.LoadOne(lookup, func() (*gtsmodel.InteractionRequest, error) {
+ var request gtsmodel.InteractionRequest
// Not cached! Perform database query
- if err := dbQuery(&approval); err != nil {
+ if err := dbQuery(&request); err != nil {
return nil, err
}
- return &approval, nil
+ return &request, nil
}, keyParts...)
if err != nil {
// Error already processed.
@@ -90,60 +108,241 @@ func (r *interactionDB) getInteractionApproval(
if gtscontext.Barebones(ctx) {
// Only a barebones model was requested.
- return approval, nil
+ return request, nil
}
- if err := r.PopulateInteractionApproval(ctx, approval); err != nil {
+ if err := i.PopulateInteractionRequest(ctx, request); err != nil {
return nil, err
}
- return approval, nil
+ return request, nil
}
-func (r *interactionDB) PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error {
+func (i *interactionDB) PopulateInteractionRequest(ctx context.Context, req *gtsmodel.InteractionRequest) error {
var (
err error
- errs = gtserror.NewMultiError(2)
+ errs = gtserror.NewMultiError(4)
)
- if approval.Account == nil {
- // Account is not set, fetch from the database.
- approval.Account, err = r.state.DB.GetAccountByID(
+ if req.Status == nil {
+ // Target status is not set, fetch from the database.
+ req.Status, err = i.state.DB.GetStatusByID(
+ gtscontext.SetBarebones(ctx),
+ req.StatusID,
+ )
+ if err != nil {
+ errs.Appendf("error populating interactionRequest target: %w", err)
+ }
+ }
+
+ if req.TargetAccount == nil {
+ // Target account is not set, fetch from the database.
+ req.TargetAccount, err = i.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
- approval.AccountID,
+ req.TargetAccountID,
)
if err != nil {
- errs.Appendf("error populating interactionApproval account: %w", err)
+ errs.Appendf("error populating interactionRequest target account: %w", err)
}
}
- if approval.InteractingAccount == nil {
+ if req.InteractingAccount == nil {
// InteractingAccount is not set, fetch from the database.
- approval.InteractingAccount, err = r.state.DB.GetAccountByID(
+ req.InteractingAccount, err = i.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
- approval.InteractingAccountID,
+ req.InteractingAccountID,
)
if err != nil {
- errs.Appendf("error populating interactionApproval interacting account: %w", err)
+ errs.Appendf("error populating interactionRequest interacting account: %w", err)
+ }
+ }
+
+ // Depending on the interaction type, *try* to populate
+ // the related model, but don't error if this is not
+ // possible, as it may have just already been deleted
+ // by its owner and we haven't cleaned up yet.
+ switch req.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ req.Like, err = i.state.DB.GetStatusFaveByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Like: %w", err)
+ }
+
+ case gtsmodel.InteractionReply:
+ req.Reply, err = i.state.DB.GetStatusByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Reply: %w", err)
+ }
+
+ case gtsmodel.InteractionAnnounce:
+ req.Announce, err = i.state.DB.GetStatusByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Announce: %w", err)
}
}
return errs.Combine()
}
-func (r *interactionDB) PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error {
- return r.state.Caches.DB.InteractionApproval.Store(approval, func() error {
- _, err := r.db.NewInsert().Model(approval).Exec(ctx)
+func (i *interactionDB) PutInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error {
+ return i.state.Caches.DB.InteractionRequest.Store(request, func() error {
+ _, err := i.db.NewInsert().Model(request).Exec(ctx)
return err
})
}
-func (r *interactionDB) DeleteInteractionApprovalByID(ctx context.Context, id string) error {
- defer r.state.Caches.DB.InteractionApproval.Invalidate("ID", id)
+func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest, columns ...string) error {
+ return i.state.Caches.DB.InteractionRequest.Store(request, func() error {
+ _, err := i.db.
+ NewUpdate().
+ Model(request).
+ Where("? = ?", bun.Ident("interaction_request.id"), request.ID).
+ Column(columns...).
+ Exec(ctx)
+ return err
+ })
+}
+
+func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
+ defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
- _, err := r.db.NewDelete().
- TableExpr("? AS ?", bun.Ident("interaction_approvals"), bun.Ident("interaction_approval")).
- Where("? = ?", bun.Ident("interaction_approval.id"), id).
+ _, 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
}
+
+func (i *interactionDB) GetInteractionsRequestsForAcct(
+ ctx context.Context,
+ acctID string,
+ statusID string,
+ likes bool,
+ replies bool,
+ boosts bool,
+ page *paging.Page,
+) ([]*gtsmodel.InteractionRequest, error) {
+ if !likes && !replies && !boosts {
+ return nil, gtserror.New("at least one of likes, replies, or boosts must be true")
+ }
+
+ var (
+ // Get paging params.
+ minID = page.GetMin()
+ maxID = page.GetMax()
+ limit = page.GetLimit()
+ order = page.GetOrder()
+
+ // Make educated guess for slice size
+ reqIDs = make([]string, 0, limit)
+ )
+
+ // Create the basic select query.
+ q := i.db.
+ NewSelect().
+ Column("id").
+ TableExpr(
+ "? AS ?",
+ bun.Ident("interaction_requests"),
+ 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"))
+
+ // Select interactions targeting status.
+ if statusID != "" {
+ q = q.Where("? = ?", bun.Ident("status_id"), statusID)
+ }
+
+ // Select interactions targeting account.
+ if acctID != "" {
+ q = q.Where("? = ?", bun.Ident("target_account_id"), acctID)
+ }
+
+ // Figure out which types of interaction are
+ // being sought, and add them to the query.
+ wantTypes := make([]gtsmodel.InteractionType, 0, 3)
+ if likes {
+ wantTypes = append(wantTypes, gtsmodel.InteractionLike)
+ }
+ if replies {
+ wantTypes = append(wantTypes, gtsmodel.InteractionReply)
+ }
+ if boosts {
+ wantTypes = append(wantTypes, gtsmodel.InteractionAnnounce)
+ }
+ q = q.Where("? IN (?)", bun.Ident("interaction_type"), bun.In(wantTypes))
+
+ // Add paging param max ID.
+ if maxID != "" {
+ q = q.Where("? < ?", bun.Ident("id"), maxID)
+ }
+
+ // Add paging param min ID.
+ if minID != "" {
+ q = q.Where("? > ?", bun.Ident("id"), minID)
+ }
+
+ // Add paging param order.
+ if order == paging.OrderAscending {
+ // Page up.
+ q = q.OrderExpr("? ASC", bun.Ident("id"))
+ } else {
+ // Page down.
+ q = q.OrderExpr("? DESC", bun.Ident("id"))
+ }
+
+ // Add paging param limit.
+ if limit > 0 {
+ q = q.Limit(limit)
+ }
+
+ // Execute the query and scan into IDs.
+ err := q.Scan(ctx, &reqIDs)
+ if err != nil {
+ return nil, err
+ }
+
+ // Catch case of no items early
+ if len(reqIDs) == 0 {
+ return nil, db.ErrNoEntries
+ }
+
+ // If we're paging up, we still want interactions
+ // to be sorted by ID desc, so reverse ids slice.
+ if order == paging.OrderAscending {
+ 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
+}
+
+func (i *interactionDB) IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error) {
+ req, err := i.GetInteractionRequestByInteractionURI(ctx, interactionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return false, gtserror.Newf("db error getting interaction request: %w", err)
+ }
+
+ if req == nil {
+ // No interaction req at all with this
+ // interactionURI so it can't be rejected.
+ return false, nil
+ }
+
+ return req.IsRejected(), nil
+}
diff --git a/internal/db/bundb/interaction_test.go b/internal/db/bundb/interaction_test.go
new file mode 100644
index 000000000..37684f18c
--- /dev/null
+++ b/internal/db/bundb/interaction_test.go
@@ -0,0 +1,261 @@
+// 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"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+type InteractionTestSuite struct {
+ BunDBStandardTestSuite
+}
+
+func (suite *InteractionTestSuite) markInteractionsPending(
+ ctx context.Context,
+ statusID string,
+) (pendingCount int) {
+ // Get replies of given status.
+ replies, err := suite.state.DB.GetStatusReplies(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each reply as pending approval.
+ for _, reply := range replies {
+ reply.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatus(
+ ctx,
+ reply,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this reply.
+ req, err := typeutils.StatusToInteractionRequest(ctx, reply)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ // Get boosts of given status.
+ boosts, err := suite.state.DB.GetStatusBoosts(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each boost as pending approval.
+ for _, boost := range boosts {
+ boost.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatus(
+ ctx,
+ boost,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this boost.
+ req, err := typeutils.StatusToInteractionRequest(ctx, boost)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ // Get faves of given status.
+ faves, err := suite.state.DB.GetStatusFaves(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each fave as pending approval.
+ for _, fave := range faves {
+ fave.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatusFave(
+ ctx,
+ fave,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this fave.
+ req, err := typeutils.StatusFaveToInteractionRequest(ctx, fave)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ return pendingCount
+}
+
+func (suite *InteractionTestSuite) TestGetPending() {
+ var (
+ testStatus = suite.testStatuses["local_account_1_status_1"]
+ ctx = context.Background()
+ acctID = suite.testAccounts["local_account_1"].ID
+ statusID = ""
+ likes = true
+ replies = true
+ boosts = true
+ page = &paging.Page{
+ Max: paging.MaxID(id.Highest),
+ Limit: 20,
+ }
+ )
+
+ // Update target test status to mark
+ // all interactions with it pending.
+ pendingCount := suite.markInteractionsPending(ctx, testStatus.ID)
+
+ // Get pendingInts interactions.
+ pendingInts, err := suite.state.DB.GetInteractionsRequestsForAcct(
+ ctx,
+ acctID,
+ statusID,
+ likes,
+ replies,
+ boosts,
+ page,
+ )
+ suite.NoError(err)
+ suite.Len(pendingInts, pendingCount)
+
+ // Ensure relevant model populated.
+ for _, pendingInt := range pendingInts {
+ switch pendingInt.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ suite.NotNil(pendingInt.Like)
+
+ case gtsmodel.InteractionReply:
+ suite.NotNil(pendingInt.Reply)
+
+ case gtsmodel.InteractionAnnounce:
+ suite.NotNil(pendingInt.Announce)
+ }
+ }
+}
+
+func (suite *InteractionTestSuite) TestGetPendingRepliesOnly() {
+ var (
+ testStatus = suite.testStatuses["local_account_1_status_1"]
+ ctx = context.Background()
+ acctID = suite.testAccounts["local_account_1"].ID
+ statusID = ""
+ likes = false
+ replies = true
+ boosts = false
+ page = &paging.Page{
+ Max: paging.MaxID(id.Highest),
+ Limit: 20,
+ }
+ )
+
+ // Update target test status to mark
+ // all interactions with it pending.
+ suite.markInteractionsPending(ctx, testStatus.ID)
+
+ // Get pendingInts interactions.
+ pendingInts, err := suite.state.DB.GetInteractionsRequestsForAcct(
+ ctx,
+ acctID,
+ statusID,
+ likes,
+ replies,
+ boosts,
+ page,
+ )
+ suite.NoError(err)
+
+ // Ensure only replies returned.
+ for _, pendingInt := range pendingInts {
+ suite.Equal(gtsmodel.InteractionReply, pendingInt.InteractionType)
+ }
+}
+
+func (suite *InteractionTestSuite) TestInteractionRejected() {
+ var (
+ ctx = context.Background()
+ req = new(gtsmodel.InteractionRequest)
+ )
+
+ // Make a copy of the request we'll modify.
+ *req = *suite.testInteractionRequests["admin_account_reply_turtle"]
+
+ // No rejection in the db for this interaction URI so it should be OK.
+ rejected, err := suite.state.DB.IsInteractionRejected(ctx, req.InteractionURI)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ if rejected {
+ suite.FailNow("wanted rejected = false, got true")
+ }
+
+ // Update the interaction request to mark it rejected.
+ req.RejectedAt = time.Now()
+ req.URI = "https://some.reject.uri"
+ if err := suite.state.DB.UpdateInteractionRequest(ctx, req, "uri", "rejected_at"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Rejection in the db for this interaction URI now so it should be très mauvais.
+ rejected, err = suite.state.DB.IsInteractionRejected(ctx, req.InteractionURI)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ if !rejected {
+ suite.FailNow("wanted rejected = true, got false")
+ }
+}
+
+func TestInteractionTestSuite(t *testing.T) {
+ suite.Run(t, new(InteractionTestSuite))
+}
diff --git a/internal/db/bundb/migrations/20240716151327_interaction_policy.go b/internal/db/bundb/migrations/20240716151327_interaction_policy.go
index fb0d1d752..210d4b2c5 100644
--- a/internal/db/bundb/migrations/20240716151327_interaction_policy.go
+++ b/internal/db/bundb/migrations/20240716151327_interaction_policy.go
@@ -20,41 +20,12 @@ package migrations
import (
"context"
- gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
- if _, err := tx.
- NewCreateTable().
- Model(&gtsmodel.InteractionApproval{}).
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
- if _, err := tx.
- NewCreateIndex().
- Table("interaction_approvals").
- Index("interaction_approvals_account_id_idx").
- Column("account_id").
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
- if _, err := tx.
- NewCreateIndex().
- Table("interaction_approvals").
- Index("interaction_approvals_interacting_account_id_idx").
- Column("interacting_account_id").
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
return nil
})
}
diff --git a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go
new file mode 100644
index 000000000..82c2b4016
--- /dev/null
+++ b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go
@@ -0,0 +1,154 @@
+// 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/typeutils"
+
+ "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 {
+
+ // Drop interaction approvals table if it exists,
+ // ie., if instance was running on main between now
+ // and 2024-07-16.
+ //
+ // We might lose some interaction approvals this way,
+ // but since they weren't *really* used much yet this
+ // it's not a big deal, that's the running-on-main life!
+ if _, err := tx.NewDropTable().
+ Table("interaction_approvals").
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Add `interaction_requests`
+ // table and new indexes.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.InteractionRequest{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ for idx, col := range map[string]string{
+ "interaction_requests_status_id_idx": "status_id",
+ "interaction_requests_target_account_id_idx": "target_account_id",
+ "interaction_requests_interacting_account_id_idx": "interacting_account_id",
+ } {
+ if _, err := tx.
+ NewCreateIndex().
+ Table("interaction_requests").
+ Index(idx).
+ Column(col).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ // Select all pending statuses (replies or boosts).
+ pendingStatuses := []*gtsmodel.Status{}
+ err := tx.
+ NewSelect().
+ Model(&pendingStatuses).
+ Column(
+ "created_at",
+ "in_reply_to_id",
+ "boost_of_id",
+ "in_reply_to_account_id",
+ "boost_of_account_id",
+ "account_id",
+ "uri",
+ ).
+ Where("? = ?", bun.Ident("pending_approval"), true).
+ Scan(ctx)
+ if err != nil {
+ return err
+ }
+
+ // 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
+ }
+
+ if _, err := tx.
+ NewInsert().
+ Model(req).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ // Now do the same thing for pending faves.
+ pendingFaves := []*gtsmodel.StatusFave{}
+ err = tx.
+ NewSelect().
+ Model(&pendingFaves).
+ Column(
+ "created_at",
+ "status_id",
+ "target_account_id",
+ "account_id",
+ "uri",
+ ).
+ Where("? = ?", bun.Ident("pending_approval"), true).
+ Scan(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, pendingFave := range pendingFaves {
+ req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave)
+ if err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewInsert().
+ Model(req).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go
index e6c7e482d..995c4e84f 100644
--- a/internal/db/bundb/timeline.go
+++ b/internal/db/bundb/timeline.go
@@ -89,19 +89,6 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
q = q.Where("? = ?", bun.Ident("status.local"), local)
}
- if limit > 0 {
- // limit amount of statuses returned
- q = q.Limit(limit)
- }
-
- if frontToBack {
- // Page down.
- q = q.Order("status.id DESC")
- } else {
- // Page up.
- q = q.Order("status.id ASC")
- }
-
// As this is the home timeline, it should be
// populated by statuses from accounts followed
// by accountID, and posts from accountID itself.
@@ -137,6 +124,22 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
bun.In(targetAccountIDs),
)
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
+ if limit > 0 {
+ // limit amount of statuses returned
+ q = q.Limit(limit)
+ }
+
+ if frontToBack {
+ // Page down.
+ q = q.Order("status.id DESC")
+ } else {
+ // Page up.
+ q = q.Order("status.id ASC")
+ }
+
if err := q.Scan(ctx, &statusIDs); err != nil {
return nil, err
}
@@ -213,6 +216,9 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceI
q = q.Where("? = ?", bun.Ident("status.local"), local)
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
@@ -395,6 +401,9 @@ func (t *timelineDB) GetListTimeline(
frontToBack = false
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
@@ -491,6 +500,9 @@ func (t *timelineDB) GetTagTimeline(
frontToBack = false
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
diff --git a/internal/db/interaction.go b/internal/db/interaction.go
index 6f595c54e..a3a3afde9 100644
--- a/internal/db/interaction.go
+++ b/internal/db/interaction.go
@@ -21,21 +21,47 @@ import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
)
type Interaction interface {
- // GetInteractionApprovalByID gets one approval with the given id.
- GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
+ // GetInteractionRequestByID gets one request with the given id.
+ GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error)
- // GetInteractionApprovalByID gets one approval with the given uri.
- GetInteractionApprovalByURI(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
+ // GetInteractionRequestByID gets one request with the given interaction uri.
+ GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error)
- // PopulateInteractionApproval ensures that the approval's struct fields are populated.
- PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
+ // GetInteractionRequestByURI returns one accepted or rejected
+ // interaction request with the given URI, if it exists in the db.
+ GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error)
- // PutInteractionApproval puts a new approval in the database.
- PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
+ // PopulateInteractionRequest ensures that the request's struct fields are populated.
+ PopulateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error
- // DeleteInteractionApprovalByID deletes one approval with the given ID.
- DeleteInteractionApprovalByID(ctx context.Context, id string) error
+ // PutInteractionRequest puts a new request in the database.
+ PutInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error
+
+ // UpdateInteractionRequest updates the given interaction request.
+ UpdateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest, columns ...string) error
+
+ // DeleteInteractionRequestByID deletes one request with the given ID.
+ DeleteInteractionRequestByID(ctx context.Context, id string) error
+
+ // GetInteractionsRequestsForAcct returns pending interactions targeting
+ // the given (optional) account ID and the given (optional) status ID.
+ //
+ // At least one of `likes`, `replies`, or `boosts` must be true.
+ GetInteractionsRequestsForAcct(
+ ctx context.Context,
+ acctID string,
+ statusID string,
+ likes bool,
+ replies bool,
+ boosts bool,
+ page *paging.Page,
+ ) ([]*gtsmodel.InteractionRequest, error)
+
+ // IsInteractionRejected returns true if an rejection exists in the database for an
+ // object with the given interactionURI (ie., a status or announce or fave uri).
+ IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error)
}