[feature] Support setting private notes on accounts (#1982)

* Support setting private notes on accounts

* Reformat comment whitespace

* Add missing license headers

* Use apiutil.ParseID

* Rename Note model and cache to AccountNote

* Update golden cache config in test/envparsing.sh

* Rename gtsmodel/note.go to gtsmodel/accountnote.go

* Update AccountNote uniqueness constraint name

Now has same prefix as other indexes on this table.

---------

Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com>
This commit is contained in:
Vyr Cossont 2023-07-27 01:30:39 -07:00 committed by GitHub
commit 22ac4607a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 597 additions and 2 deletions

View file

@ -49,6 +49,7 @@ type BunDBStandardTestSuite struct {
testFaves map[string]*gtsmodel.StatusFave
testLists map[string]*gtsmodel.List
testListEntries map[string]*gtsmodel.ListEntry
testAccountNotes map[string]*gtsmodel.AccountNote
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@ -68,6 +69,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testFaves = testrig.NewTestFaves()
suite.testLists = testrig.NewTestLists()
suite.testListEntries = testrig.NewTestListEntries()
suite.testAccountNotes = testrig.NewTestAccountNotes()
}
func (suite *BunDBStandardTestSuite) SetupTest() {

View file

@ -0,0 +1,62 @@
// 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/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 {
// Account note table.
if _, err := tx.
NewCreateTable().
Model(&gtsmodel.AccountNote{}).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Add IDs index to the account note table.
if _, err := tx.
NewCreateIndex().
Model(&gtsmodel.AccountNote{}).
Index("account_notes_account_id_target_account_id_idx").
Column("account_id", "target_account_id").
Exec(ctx); err != nil {
return err
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -85,6 +85,19 @@ func (r *relationshipDB) GetRelationship(ctx context.Context, requestingAccount
return nil, fmt.Errorf("GetRelationship: error checking blockedBy: %w", err)
}
// retrieve a note by the requesting account on the target account, if there is one
note, err := r.GetNote(
gtscontext.SetBarebones(ctx),
requestingAccount,
targetAccount,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, fmt.Errorf("GetRelationship: error fetching note: %w", err)
}
if note != nil {
rel.Note = note.Comment
}
return &rel, nil
}

View 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
import (
"context"
"fmt"
"time"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
)
func (r *relationshipDB) GetNote(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.AccountNote, error) {
return r.getNote(
ctx,
"AccountID.TargetAccountID",
func(note *gtsmodel.AccountNote) error {
return r.conn.NewSelect().Model(note).
Where("? = ?", bun.Ident("account_id"), sourceAccountID).
Where("? = ?", bun.Ident("target_account_id"), targetAccountID).
Scan(ctx)
},
sourceAccountID,
targetAccountID,
)
}
func (r *relationshipDB) getNote(ctx context.Context, lookup string, dbQuery func(*gtsmodel.AccountNote) error, keyParts ...any) (*gtsmodel.AccountNote, error) {
// Fetch note from cache with loader callback
note, err := r.state.Caches.GTS.AccountNote().Load(lookup, func() (*gtsmodel.AccountNote, error) {
var note gtsmodel.AccountNote
// Not cached! Perform database query
if err := dbQuery(&note); err != nil {
return nil, r.conn.ProcessError(err)
}
return &note, nil
}, keyParts...)
if err != nil {
// already processed
return nil, err
}
if gtscontext.Barebones(ctx) {
// Only a barebones model was requested.
return note, nil
}
// Set the note source account
note.Account, err = r.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
note.AccountID,
)
if err != nil {
return nil, fmt.Errorf("error getting note source account: %w", err)
}
// Set the note target account
note.TargetAccount, err = r.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
note.TargetAccountID,
)
if err != nil {
return nil, fmt.Errorf("error getting note target account: %w", err)
}
return note, nil
}
func (r *relationshipDB) PutNote(ctx context.Context, note *gtsmodel.AccountNote) error {
note.UpdatedAt = time.Now()
return r.state.Caches.GTS.AccountNote().Store(note, func() error {
_, err := r.conn.
NewInsert().
Model(note).
On("CONFLICT (?, ?) DO UPDATE", bun.Ident("account_id"), bun.Ident("target_account_id")).
Set("? = ?, ? = ?", bun.Ident("updated_at"), note.UpdatedAt, bun.Ident("comment"), note.Comment).
Exec(ctx)
return r.conn.ProcessError(err)
})
}

View file

@ -912,6 +912,53 @@ func (suite *RelationshipTestSuite) TestUpdateFollow() {
suite.True(relationship.Notifying)
}
func (suite *RelationshipTestSuite) TestGetNote() {
ctx := context.Background()
// Retrieve a fixture note
account1 := suite.testAccounts["local_account_1"].ID
account2 := suite.testAccounts["local_account_2"].ID
expectedNote := suite.testAccountNotes["local_account_2_note_on_1"]
note, err := suite.db.GetNote(ctx, account2, account1)
suite.NoError(err)
suite.NotNil(note)
suite.Equal(expectedNote.ID, note.ID)
suite.Equal(expectedNote.Comment, note.Comment)
}
func (suite *RelationshipTestSuite) TestPutNote() {
ctx := context.Background()
// put a note in
account1 := suite.testAccounts["local_account_1"].ID
account2 := suite.testAccounts["local_account_2"].ID
err := suite.db.PutNote(ctx, &gtsmodel.AccountNote{
ID: "01H539R2NA0M83JX15Y5RWKE97",
AccountID: account1,
TargetAccountID: account2,
Comment: "foo",
})
suite.NoError(err)
// make sure the note is in the db
note, err := suite.db.GetNote(ctx, account1, account2)
suite.NoError(err)
suite.NotNil(note)
suite.Equal("01H539R2NA0M83JX15Y5RWKE97", note.ID)
suite.Equal("foo", note.Comment)
// update the note
note.Comment = "bar"
err = suite.db.PutNote(ctx, note)
suite.NoError(err)
// make sure the comment changes
note, err = suite.db.GetNote(ctx, account1, account2)
suite.NoError(err)
suite.NotNil(note)
suite.Equal("bar", note.Comment)
}
func TestRelationshipTestSuite(t *testing.T) {
suite.Run(t, new(RelationshipTestSuite))
}

View file

@ -165,4 +165,10 @@ type Relationship interface {
// CountAccountFollowerRequests returns number of follow requests originating from the given account.
CountAccountFollowRequesting(ctx context.Context, accountID string) (int, error)
// GetNote gets a private note from a source account on a target account, if it exists.
GetNote(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.AccountNote, error)
// PutNote creates or updates a private note.
PutNote(ctx context.Context, note *gtsmodel.AccountNote) error
}