gotosocial/internal/processing/account/statuses.go

226 lines
6.8 KiB
Go
Raw Permalink Normal View History

// 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"
[performance] refactoring + add fave / follow / request / visibility caching (#1607) * refactor visibility checking, add caching for visibility * invalidate visibility cache items on account / status deletes * fix requester ID passed to visibility cache nil ptr * de-interface caches, fix home / public timeline caching + visibility * finish adding code comments for visibility filter * fix angry goconst linter warnings * actually finish adding filter visibility code comments for timeline functions * move home timeline status author check to after visibility * remove now-unused code * add more code comments * add TODO code comment, update printed cache start names * update printed cache names on stop * start adding separate follow(request) delete db functions, add specific visibility cache tests * add relationship type caching * fix getting local account follows / followed-bys, other small codebase improvements * simplify invalidation using cache hooks, add more GetAccountBy___() functions * fix boosting to return 404 if not boostable but no error (to not leak status ID) * remove dead code * improved placement of cache invalidation * update license headers * add example follow, follow-request config entries * add example visibility cache configuration to config file * use specific PutFollowRequest() instead of just Put() * add tests for all GetAccountBy() * add GetBlockBy() tests * update block to check primitive fields * update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests * fix copy-pasted code * update envparsing test * whitespace * fix bun struct tag * add license header to gtscontext * fix old license header * improved error creation to not use fmt.Errorf() when not needed * fix various rebase conflicts, fix account test * remove commented-out code, fix-up mention caching * fix mention select bun statement * ensure mention target account populated, pass in context to customrenderer logging * remove more uncommented code, fix typeutil test * add statusfave database model caching * add status fave cache configuration * add status fave cache example config * woops, catch missed error. nice catch linter! * add back testrig panic on nil db * update example configuration to match defaults, slight tweak to cache configuration defaults * update envparsing test with new defaults * fetch followingget to use the follow target account * use accounnt.IsLocal() instead of empty domain check * use constants for the cache visibility type check * use bun.In() for notification type restriction in db query * include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable()) * use bun query building for nested select statements to ensure working with postgres * update public timeline future status checks to match visibility filter * same as previous, for home timeline * update public timeline tests to dynamically check for appropriate statuses * migrate accounts to allow unique constraint on public_key * provide minimal account with publicKey --------- Signed-off-by: kim <grufwub@gmail.com> Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-03-28 13:03:14 +00:00
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// StatusesGet fetches a number of statuses (in time descending order) from the
// target account, filtered by visibility according to the requesting account.
func (p *Processor) StatusesGet(
ctx context.Context,
requestingAccount *gtsmodel.Account,
targetAccountID string,
limit int,
excludeReplies bool,
excludeReblogs bool,
maxID string,
minID string,
pinned bool,
mediaOnly bool,
publicOnly bool,
) (*apimodel.PageableResponse, gtserror.WithCode) {
if requestingAccount != nil {
blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
// Block exists between accounts.
// Just return empty statuses.
return util.EmptyPageableResponse(), nil
}
}
var (
statuses []*gtsmodel.Status
err error
)
if pinned {
// Get *ONLY* pinned statuses.
statuses, err = p.state.DB.GetAccountPinnedStatuses(ctx, targetAccountID)
} else {
// Get account statuses which *may* include pinned ones.
statuses, err = p.state.DB.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, mediaOnly, publicOnly)
}
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(err)
}
count := len(statuses)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
var (
items = make([]interface{}, 0, count)
// Set next + prev values before filtering and API
// converting, so caller can still page properly.
nextMaxIDValue = statuses[count-1].ID
prevMinIDValue = statuses[0].ID
)
// Filtering + serialization process is the same for
// both pinned status queries and 'normal' ones.
filtered, err := p.filter.StatusesVisible(ctx, requestingAccount, statuses)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
for _, s := range filtered {
// Convert filtered statuses to API statuses.
item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
}
items = append(items, item)
}
if pinned {
// We don't page on pinned status responses,
// so we can save some work + just return items.
return &apimodel.PageableResponse{
Items: items,
}, nil
}
return util.PackagePageableResponse(util.PageableResponseParams{
Items: items,
Path: "/api/v1/accounts/" + targetAccountID + "/statuses",
NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: prevMinIDValue,
Limit: limit,
ExtraQueryParams: []string{
fmt.Sprintf("exclude_replies=%t", excludeReplies),
fmt.Sprintf("exclude_reblogs=%t", excludeReblogs),
fmt.Sprintf("pinned=%t", pinned),
fmt.Sprintf("only_media=%t", mediaOnly),
fmt.Sprintf("only_public=%t", publicOnly),
},
})
}
// WebStatusesGet fetches a number of statuses (in descending order)
// from the given account. It selects only statuses which are suitable
// for showing on the public web profile of an account.
func (p *Processor) WebStatusesGet(
ctx context.Context,
targetAccountID string,
maxID string,
) (*apimodel.PageableResponse, gtserror.WithCode) {
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err := fmt.Errorf("account %s not found in the db, not getting web statuses for it", targetAccountID)
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
if account.Domain != "" {
err := fmt.Errorf("account %s was not a local account, not getting web statuses for it", targetAccountID)
return nil, gtserror.NewErrorNotFound(err)
}
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(err)
}
count := len(statuses)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
var (
items = make([]interface{}, 0, count)
// Set next value before API converting,
// so caller can still page properly.
nextMaxIDValue = statuses[count-1].ID
)
for _, s := range statuses {
// Convert fetched statuses to web view statuses.
item, err := p.converter.StatusToWebStatus(ctx, s, nil)
if err != nil {
log.Errorf(ctx, "error convering to web status: %v", err)
continue
}
items = append(items, item)
}
return util.PackagePageableResponse(util.PageableResponseParams{
Items: items,
Path: "/@" + account.Username,
NextMaxIDValue: nextMaxIDValue,
})
}
// WebStatusesGetPinned returns web versions of pinned statuses.
func (p *Processor) WebStatusesGetPinned(
ctx context.Context,
targetAccountID string,
) ([]*apimodel.Status, gtserror.WithCode) {
statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, targetAccountID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(err)
}
webStatuses := make([]*apimodel.Status, 0, len(statuses))
for _, status := range statuses {
if status.Visibility != gtsmodel.VisibilityPublic {
// Skip non-public
// pinned status.
continue
}
webStatus, err := p.converter.StatusToWebStatus(ctx, status, nil)
if err != nil {
log.Errorf(ctx, "error convering to web status: %v", err)
continue
}
// Normally when viewed via the API, 'pinned' is
// only true if the *viewing account* has pinned
// the status being viewed. For web statuses,
// however, we still want to be able to indicate
// a pinned status, so bodge this in here.
webStatus.Pinned = true
webStatuses = append(webStatuses, webStatus)
}
return webStatuses, nil
}