mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-26 06:10:23 +00:00
[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:
parent
5f3e095717
commit
22ac4607a1
19 changed files with 597 additions and 2 deletions
|
@ -2944,6 +2944,45 @@ paths:
|
|||
summary: See all lists of yours that contain requested account.
|
||||
tags:
|
||||
- accounts
|
||||
/api/v1/accounts/{id}/note:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
operationId: accountNote
|
||||
parameters:
|
||||
- description: The id of the account for which to set a note.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- default: ""
|
||||
description: The text of the note. Omit this parameter or send an empty string to clear the note.
|
||||
in: formData
|
||||
name: comment
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Your relationship to the account.
|
||||
schema:
|
||||
$ref: '#/definitions/accountRelationship'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:accounts
|
||||
summary: Set a private note for an account with the given id.
|
||||
tags:
|
||||
- accounts
|
||||
/api/v1/accounts/{id}/statuses:
|
||||
get:
|
||||
description: The statuses will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
|
||||
|
|
|
@ -45,6 +45,7 @@ const (
|
|||
FollowPath = BasePathWithID + "/follow"
|
||||
ListsPath = BasePathWithID + "/lists"
|
||||
LookupPath = BasePath + "/lookup"
|
||||
NotePath = BasePathWithID + "/note"
|
||||
RelationshipsPath = BasePath + "/relationships"
|
||||
SearchPath = BasePath + "/search"
|
||||
StatusesPath = BasePathWithID + "/statuses"
|
||||
|
@ -101,6 +102,9 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
|||
// account lists
|
||||
attachHandler(http.MethodGet, ListsPath, m.AccountListsGETHandler)
|
||||
|
||||
// account note
|
||||
attachHandler(http.MethodPost, NotePath, m.AccountNotePOSTHandler)
|
||||
|
||||
// search for accounts
|
||||
attachHandler(http.MethodGet, SearchPath, m.AccountSearchGETHandler)
|
||||
attachHandler(http.MethodGet, LookupPath, m.AccountLookupGETHandler)
|
||||
|
|
108
internal/api/client/accounts/note.go
Normal file
108
internal/api/client/accounts/note.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
// 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 accounts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountNotePOSTHandler swagger:operation POST /api/v1/accounts/{id}/note accountNote
|
||||
//
|
||||
// Set a private note for an account with the given id.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - accounts
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the account for which to set a note.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// name: comment
|
||||
// type: string
|
||||
// description: The text of the note. Omit this parameter or send an empty string to clear the note.
|
||||
// in: formData
|
||||
// default: ""
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:accounts
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Your relationship to the account.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountNotePOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.AccountNoteRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.Account().PutNote(c.Request.Context(), authed.Account, targetAcctID, form.Comment)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, relationship)
|
||||
}
|
|
@ -231,3 +231,11 @@ const (
|
|||
AccountRoleAdmin AccountRoleName = "admin" // Instance admin
|
||||
AccountRoleUnknown AccountRoleName = "" // We don't know / remote account
|
||||
)
|
||||
|
||||
// AccountNoteRequest models a request to update the private note for an account.
|
||||
//
|
||||
// swagger:ignore
|
||||
type AccountNoteRequest struct {
|
||||
// Comment to use for the note text.
|
||||
Comment string `form:"comment" json:"comment" xml:"comment"`
|
||||
}
|
||||
|
|
26
internal/cache/gts.go
vendored
26
internal/cache/gts.go
vendored
|
@ -26,8 +26,9 @@ import (
|
|||
)
|
||||
|
||||
type GTSCaches struct {
|
||||
account *result.Cache[*gtsmodel.Account]
|
||||
block *result.Cache[*gtsmodel.Block]
|
||||
account *result.Cache[*gtsmodel.Account]
|
||||
accountNote *result.Cache[*gtsmodel.AccountNote]
|
||||
block *result.Cache[*gtsmodel.Block]
|
||||
// TODO: maybe should be moved out of here since it's
|
||||
// not actually doing anything with gtsmodel.DomainBlock.
|
||||
domainBlock *domain.BlockCache
|
||||
|
@ -54,6 +55,7 @@ type GTSCaches struct {
|
|||
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
|
||||
func (c *GTSCaches) Init() {
|
||||
c.initAccount()
|
||||
c.initAccountNote()
|
||||
c.initBlock()
|
||||
c.initDomainBlock()
|
||||
c.initEmoji()
|
||||
|
@ -77,6 +79,7 @@ func (c *GTSCaches) Init() {
|
|||
// Start will attempt to start all of the gtsmodel caches, or panic.
|
||||
func (c *GTSCaches) Start() {
|
||||
tryStart(c.account, config.GetCacheGTSAccountSweepFreq())
|
||||
tryStart(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq())
|
||||
tryStart(c.block, config.GetCacheGTSBlockSweepFreq())
|
||||
tryStart(c.emoji, config.GetCacheGTSEmojiSweepFreq())
|
||||
tryStart(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
|
||||
|
@ -104,6 +107,7 @@ func (c *GTSCaches) Start() {
|
|||
// Stop will attempt to stop all of the gtsmodel caches, or panic.
|
||||
func (c *GTSCaches) Stop() {
|
||||
tryStop(c.account, config.GetCacheGTSAccountSweepFreq())
|
||||
tryStop(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq())
|
||||
tryStop(c.block, config.GetCacheGTSBlockSweepFreq())
|
||||
tryStop(c.emoji, config.GetCacheGTSEmojiSweepFreq())
|
||||
tryStop(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
|
||||
|
@ -128,6 +132,11 @@ func (c *GTSCaches) Account() *result.Cache[*gtsmodel.Account] {
|
|||
return c.account
|
||||
}
|
||||
|
||||
// AccountNote provides access to the gtsmodel Note database cache.
|
||||
func (c *GTSCaches) AccountNote() *result.Cache[*gtsmodel.AccountNote] {
|
||||
return c.accountNote
|
||||
}
|
||||
|
||||
// Block provides access to the gtsmodel Block (account) database cache.
|
||||
func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] {
|
||||
return c.block
|
||||
|
@ -238,6 +247,19 @@ func (c *GTSCaches) initAccount() {
|
|||
c.account.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initAccountNote() {
|
||||
c.accountNote = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "AccountID.TargetAccountID"},
|
||||
}, func(n1 *gtsmodel.AccountNote) *gtsmodel.AccountNote {
|
||||
n2 := new(gtsmodel.AccountNote)
|
||||
*n2 = *n1
|
||||
return n2
|
||||
}, config.GetCacheGTSAccountNoteMaxSize())
|
||||
c.accountNote.SetTTL(config.GetCacheGTSAccountNoteTTL(), true)
|
||||
c.accountNote.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initBlock() {
|
||||
c.block = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
|
|
|
@ -186,6 +186,10 @@ type GTSCacheConfiguration struct {
|
|||
AccountTTL time.Duration `name:"account-ttl"`
|
||||
AccountSweepFreq time.Duration `name:"account-sweep-freq"`
|
||||
|
||||
AccountNoteMaxSize int `name:"account-note-max-size"`
|
||||
AccountNoteTTL time.Duration `name:"account-note-ttl"`
|
||||
AccountNoteSweepFreq time.Duration `name:"account-note-sweep-freq"`
|
||||
|
||||
BlockMaxSize int `name:"block-max-size"`
|
||||
BlockTTL time.Duration `name:"block-ttl"`
|
||||
BlockSweepFreq time.Duration `name:"block-sweep-freq"`
|
||||
|
|
|
@ -131,6 +131,10 @@ var Defaults = Configuration{
|
|||
AccountTTL: time.Minute * 30,
|
||||
AccountSweepFreq: time.Minute,
|
||||
|
||||
AccountNoteMaxSize: 1000,
|
||||
AccountNoteTTL: time.Minute * 30,
|
||||
AccountNoteSweepFreq: time.Minute,
|
||||
|
||||
BlockMaxSize: 1000,
|
||||
BlockTTL: time.Minute * 30,
|
||||
BlockSweepFreq: time.Minute,
|
||||
|
|
|
@ -2474,6 +2474,81 @@ func GetCacheGTSAccountSweepFreq() time.Duration { return global.GetCacheGTSAcco
|
|||
// SetCacheGTSAccountSweepFreq safely sets the value for global configuration 'Cache.GTS.AccountSweepFreq' field
|
||||
func SetCacheGTSAccountSweepFreq(v time.Duration) { global.SetCacheGTSAccountSweepFreq(v) }
|
||||
|
||||
// GetCacheGTSAccountNoteMaxSize safely fetches the Configuration value for state's 'Cache.GTS.AccountNoteMaxSize' field
|
||||
func (st *ConfigState) GetCacheGTSAccountNoteMaxSize() (v int) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.GTS.AccountNoteMaxSize
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheGTSAccountNoteMaxSize safely sets the Configuration value for state's 'Cache.GTS.AccountNoteMaxSize' field
|
||||
func (st *ConfigState) SetCacheGTSAccountNoteMaxSize(v int) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.GTS.AccountNoteMaxSize = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheGTSAccountNoteMaxSizeFlag returns the flag name for the 'Cache.GTS.AccountNoteMaxSize' field
|
||||
func CacheGTSAccountNoteMaxSizeFlag() string { return "cache-gts-account-note-max-size" }
|
||||
|
||||
// GetCacheGTSAccountNoteMaxSize safely fetches the value for global configuration 'Cache.GTS.AccountNoteMaxSize' field
|
||||
func GetCacheGTSAccountNoteMaxSize() int { return global.GetCacheGTSAccountNoteMaxSize() }
|
||||
|
||||
// SetCacheGTSAccountNoteMaxSize safely sets the value for global configuration 'Cache.GTS.AccountNoteMaxSize' field
|
||||
func SetCacheGTSAccountNoteMaxSize(v int) { global.SetCacheGTSAccountNoteMaxSize(v) }
|
||||
|
||||
// GetCacheGTSAccountNoteTTL safely fetches the Configuration value for state's 'Cache.GTS.AccountNoteTTL' field
|
||||
func (st *ConfigState) GetCacheGTSAccountNoteTTL() (v time.Duration) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.GTS.AccountNoteTTL
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheGTSAccountNoteTTL safely sets the Configuration value for state's 'Cache.GTS.AccountNoteTTL' field
|
||||
func (st *ConfigState) SetCacheGTSAccountNoteTTL(v time.Duration) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.GTS.AccountNoteTTL = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheGTSAccountNoteTTLFlag returns the flag name for the 'Cache.GTS.AccountNoteTTL' field
|
||||
func CacheGTSAccountNoteTTLFlag() string { return "cache-gts-account-note-ttl" }
|
||||
|
||||
// GetCacheGTSAccountNoteTTL safely fetches the value for global configuration 'Cache.GTS.AccountNoteTTL' field
|
||||
func GetCacheGTSAccountNoteTTL() time.Duration { return global.GetCacheGTSAccountNoteTTL() }
|
||||
|
||||
// SetCacheGTSAccountNoteTTL safely sets the value for global configuration 'Cache.GTS.AccountNoteTTL' field
|
||||
func SetCacheGTSAccountNoteTTL(v time.Duration) { global.SetCacheGTSAccountNoteTTL(v) }
|
||||
|
||||
// GetCacheGTSAccountNoteSweepFreq safely fetches the Configuration value for state's 'Cache.GTS.AccountNoteSweepFreq' field
|
||||
func (st *ConfigState) GetCacheGTSAccountNoteSweepFreq() (v time.Duration) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.GTS.AccountNoteSweepFreq
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheGTSAccountNoteSweepFreq safely sets the Configuration value for state's 'Cache.GTS.AccountNoteSweepFreq' field
|
||||
func (st *ConfigState) SetCacheGTSAccountNoteSweepFreq(v time.Duration) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.GTS.AccountNoteSweepFreq = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheGTSAccountNoteSweepFreqFlag returns the flag name for the 'Cache.GTS.AccountNoteSweepFreq' field
|
||||
func CacheGTSAccountNoteSweepFreqFlag() string { return "cache-gts-account-note-sweep-freq" }
|
||||
|
||||
// GetCacheGTSAccountNoteSweepFreq safely fetches the value for global configuration 'Cache.GTS.AccountNoteSweepFreq' field
|
||||
func GetCacheGTSAccountNoteSweepFreq() time.Duration { return global.GetCacheGTSAccountNoteSweepFreq() }
|
||||
|
||||
// SetCacheGTSAccountNoteSweepFreq safely sets the value for global configuration 'Cache.GTS.AccountNoteSweepFreq' field
|
||||
func SetCacheGTSAccountNoteSweepFreq(v time.Duration) { global.SetCacheGTSAccountNoteSweepFreq(v) }
|
||||
|
||||
// GetCacheGTSBlockMaxSize safely fetches the Configuration value for state's 'Cache.GTS.BlockMaxSize' field
|
||||
func (st *ConfigState) GetCacheGTSBlockMaxSize() (v int) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
@ -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() {
|
||||
|
|
62
internal/db/bundb/migrations/20230711214815_account_notes.go
Normal file
62
internal/db/bundb/migrations/20230711214815_account_notes.go
Normal 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(>smodel.AccountNote{}).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add IDs index to the account note table.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Model(>smodel.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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
99
internal/db/bundb/relationship_note.go
Normal file
99
internal/db/bundb/relationship_note.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
|
||||
|
||||
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(¬e); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return ¬e, 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)
|
||||
})
|
||||
}
|
|
@ -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, >smodel.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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
32
internal/gtsmodel/accountnote.go
Normal file
32
internal/gtsmodel/accountnote.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// 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"
|
||||
|
||||
// AccountNote stores a private note from a local account related to any account.
|
||||
type AccountNote struct {
|
||||
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
AccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:account_notes_account_id_target_account_id_uniq,notnull,nullzero"` // ID of the local account that created the note
|
||||
Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
|
||||
TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:account_notes_account_id_target_account_id_uniq,notnull,nullzero"` // Who is the target of this note?
|
||||
TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
|
||||
Comment string `validate:"-" bun:""` // The text of the note.
|
||||
}
|
48
internal/processing/account/note.go
Normal file
48
internal/processing/account/note.go
Normal file
|
@ -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 account
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
// PutNote updates the requesting account's private note on the target account.
|
||||
func (p *Processor) PutNote(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, comment string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||
targetAccount, errWithCode := p.Get(ctx, requestingAccount, targetAccountID)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
note := >smodel.AccountNote{
|
||||
ID: id.NewULID(),
|
||||
AccountID: requestingAccount.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
Comment: comment,
|
||||
}
|
||||
err := p.state.DB.PutNote(ctx, note)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.RelationshipGet(ctx, requestingAccount, targetAccount.ID)
|
||||
}
|
|
@ -20,6 +20,9 @@ EXPECT=$(cat <<"EOF"
|
|||
"cache": {
|
||||
"gts": {
|
||||
"account-max-size": 99,
|
||||
"account-note-max-size": 1000,
|
||||
"account-note-sweep-freq": 60000000000,
|
||||
"account-note-ttl": 1800000000000,
|
||||
"account-sweep-freq": 1000000000,
|
||||
"account-ttl": 10800000000000,
|
||||
"block-max-size": 1000,
|
||||
|
|
|
@ -60,6 +60,7 @@ var testModels = []interface{}{
|
|||
>smodel.EmojiCategory{},
|
||||
>smodel.Tombstone{},
|
||||
>smodel.Report{},
|
||||
>smodel.AccountNote{},
|
||||
}
|
||||
|
||||
// NewTestDB returns a new initialized, empty database for testing.
|
||||
|
@ -280,6 +281,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestAccountNotes() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.CreateInstanceAccount(ctx); err != nil {
|
||||
log.Panic(nil, err)
|
||||
}
|
||||
|
|
|
@ -1893,6 +1893,18 @@ func NewTestFaves() map[string]*gtsmodel.StatusFave {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTestAccountNotes returns some account notes for use in testing.
|
||||
func NewTestAccountNotes() map[string]*gtsmodel.AccountNote {
|
||||
return map[string]*gtsmodel.AccountNote{
|
||||
"local_account_2_note_on_1": {
|
||||
ID: "01H53TM628GNC4ZDNRGQGPK8S0",
|
||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
Comment: "extremely average poster",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestNotifications returns some notifications for use in testing.
|
||||
func NewTestNotifications() map[string]*gtsmodel.Notification {
|
||||
return map[string]*gtsmodel.Notification{
|
||||
|
|
Loading…
Reference in a new issue