Files
GoToSocial/internal/processing/timeline/notification.go
kim 6a6a499333 [performance] rewrite timelines to rely on new timeline cache type (#3941)
* start work rewriting timeline cache type

* further work rewriting timeline caching

* more work integration new timeline code

* remove old code

* add local timeline, fix up merge conflicts

* remove old use of go-bytes

* implement new timeline code into more areas of codebase, pull in latest go-mangler, go-mutexes, go-structr

* remove old timeline package, add local timeline cache

* remove references to old timeline types that needed starting up in tests

* start adding page validation

* fix test-identified timeline cache package issues

* fix up more tests, fix missing required changes, etc

* add exclusion for test.out in gitignore

* clarify some things better in code comments

* tweak cache size limits

* fix list timeline cache fetching

* further list timeline fixes

* linter, ssssssssshhhhhhhhhhhh please

* fix linter hints

* reslice the output if it's beyond length of 'lim'

* remove old timeline initialization code, bump go-structr to v0.9.4

* continued from previous commit

* improved code comments

* don't allow multiple entries for BoostOfID values to prevent repeated boosts of same boosts

* finish writing more code comments

* some variable renaming, for ease of following

* change the way we update lo,hi paging values during timeline load

* improved code comments for updated / returned lo , hi paging values

* finish writing code comments for the StatusTimeline{} type itself

* fill in more code comments

* update go-structr version to latest with changed timeline unique indexing logic

* have a local and public timeline *per user*

* rewrite calls to public / local timeline calls

* remove the zero length check, as lo, hi values might still be set

* simplify timeline cache loading, fix lo/hi returns, fix timeline invalidation side-effects missing for some federated actions

* swap the lo, hi values 🤦

* add (now) missing slice reverse of tag timeline statuses when paging ASC

* remove local / public caches (is out of scope for this work), share more timeline code

* remove unnecessary change

* again, remove more unused code

* remove unused function to appease the linter

* move boost checking to prepare function

* fix use of timeline.lastOrder, fix incorrect range functions used

* remove comments for repeat code

* remove the boost logic from prepare function

* do a maximum of 5 loads, not 10

* add repeat boost filtering logic, update go-structr, general improvements

* more code comments

* add important note

* fix timeline tests now that timelines are returned in page order

* remove unused field

* add StatusTimeline{} tests

* add more status timeline tests

* start adding preloading support

* ensure repeat boosts are marked in preloaded entries

* share a bunch of the database load code in timeline cache, don't clear timelines on relationship change

* add logic to allow dynamic clear / preloading of timelines

* comment-out unused functions, but leave in place as we might end-up using them

* fix timeline preload state check

* much improved status timeline code comments

* more code comments, don't bother inserting statuses if timeline not preloaded

* shift around some logic to make sure things aren't accidentally left set

* finish writing code comments

* remove trim-after-insert behaviour

* fix-up some comments referring to old logic

* remove unsetting of lo, hi

* fix preload repeatBoost checking logic

* don't return on status filter errors, these are usually transient

* better concurrency safety in Clear() and Done()

* fix test broken due to addition of preloader

* fix repeatBoost logic that doesn't account for already-hidden repeatBoosts

* ensure edit submodels are dropped on cache insertion

* update code-comment to expand CAS accronym

* use a plus1hULID() instead of 24h

* remove unused functions

* add note that public / local timeline requester can be nil

* fix incorrect visibility filtering of tag timeline statuses

* ensure we filter home timeline statuses on local only

* some small re-orderings to confirm query params in correct places

* fix the local only home timeline filter func
2025-04-26 09:56:15 +00:00

217 lines
6.5 KiB
Go

// 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 timeline
import (
"context"
"errors"
"fmt"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
"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"
)
// NotificationsGet ...
func (p *Processor) NotificationsGet(
ctx context.Context,
authed *apiutil.Auth,
page *paging.Page,
types []gtsmodel.NotificationType,
excludeTypes []gtsmodel.NotificationType,
) (*apimodel.PageableResponse, gtserror.WithCode) {
notifs, err := p.state.DB.GetAccountNotifications(
ctx,
authed.Account.ID,
page,
types,
excludeTypes,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("NotificationsGet: db error getting notifications: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(notifs)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
filters, err := p.state.DB.GetFiltersForAccountID(ctx, authed.Account.ID)
if err != nil {
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", authed.Account.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), authed.Account.ID, nil)
if err != nil {
err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", authed.Account.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
compiledMutes := usermute.NewCompiledUserMuteList(mutes)
var (
items = make([]interface{}, 0, count)
// Get the lowest and highest
// ID values, used for paging.
lo = notifs[count-1].ID
hi = notifs[0].ID
)
for _, n := range notifs {
visible, err := p.notifVisible(ctx, n, authed.Account)
if err != nil {
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err)
continue
}
if !visible {
continue
}
item, err := p.converter.NotificationToAPINotification(ctx, n, filters, compiledMutes)
if err != nil {
if !errors.Is(err, status.ErrHideStatus) {
log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err)
}
continue
}
items = append(items, item)
}
// Build type query string.
query := make(url.Values)
for _, typ := range types {
query.Add("types[]", typ.String())
}
for _, typ := range excludeTypes {
query.Add("exclude_types[]", typ.String())
}
return paging.PackageResponse(paging.ResponseParams{
Items: items,
Path: "/api/v1/notifications",
Next: page.Next(lo, hi),
Prev: page.Prev(lo, hi),
Query: query,
}), nil
}
func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Account, targetNotifID string) (*apimodel.Notification, gtserror.WithCode) {
notif, err := p.state.DB.GetNotificationByID(ctx, targetNotifID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorNotFound(err)
}
// Real error.
return nil, gtserror.NewErrorInternalError(err)
}
if notifTargetAccountID := notif.TargetAccountID; notifTargetAccountID != account.ID {
err = fmt.Errorf("account %s does not have permission to view notification belong to account %s", account.ID, notifTargetAccountID)
return nil, gtserror.NewErrorNotFound(err)
}
filters, err := p.state.DB.GetFiltersForAccountID(ctx, account.ID)
if err != nil {
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", account.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), account.ID, nil)
if err != nil {
err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", account.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
compiledMutes := usermute.NewCompiledUserMuteList(mutes)
apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorNotFound(err)
}
// Real error.
return nil, gtserror.NewErrorInternalError(err)
}
return apiNotif, nil
}
func (p *Processor) NotificationsClear(ctx context.Context, authed *apiutil.Auth) gtserror.WithCode {
// Delete all notifications of all types that target the authorized account.
if err := p.state.DB.DeleteNotifications(ctx, nil, authed.Account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.NewErrorInternalError(err)
}
return nil
}
func (p *Processor) notifVisible(
ctx context.Context,
n *gtsmodel.Notification,
acct *gtsmodel.Account,
) (bool, error) {
// If account is set, ensure it's
// visible to notif target.
if n.OriginAccount != nil {
// If this is a new local account sign-up,
// skip normal visibility checking because
// origin account won't be confirmed yet.
if n.NotificationType == gtsmodel.NotificationAdminSignup {
return true, nil
}
visible, err := p.visFilter.AccountVisible(ctx, acct, n.OriginAccount)
if err != nil {
return false, err
}
if !visible {
return false, nil
}
}
// If status is set, ensure it's
// visible to notif target.
if n.Status != nil {
visible, err := p.visFilter.StatusVisible(ctx, acct, n.Status)
if err != nil {
return false, err
}
if !visible {
return false, nil
}
}
return true, nil
}