From 82d9f88e424fffacfa9a9c1c26f2f702b97f3e3a Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 17 Jun 2021 18:02:33 +0200 Subject: [PATCH] Timeline improvements (#41) Tidying up. Parent/child statuses now display correctly in status/id/context. --- PROGRESS.md | 2 +- internal/db/db.go | 24 +- internal/db/pg/pg.go | 258 +----------------- internal/db/pg/statuscontext.go | 75 +++++ internal/federation/federatingdb/create.go | 2 + internal/gtsmodel/status.go | 27 +- internal/processing/account.go | 30 +- internal/processing/federation.go | 12 + internal/processing/fromclientapi.go | 4 + internal/processing/fromcommon.go | 24 +- internal/processing/fromfederator.go | 4 + internal/processing/processor.go | 3 + internal/processing/search.go | 8 +- .../processing/synchronous/status/boost.go | 10 +- .../synchronous/status/boostedby.go | 8 +- .../processing/synchronous/status/context.go | 59 +++- .../processing/synchronous/status/create.go | 3 +- .../processing/synchronous/status/delete.go | 8 +- .../processing/synchronous/status/fave.go | 10 +- .../processing/synchronous/status/favedby.go | 8 +- internal/processing/synchronous/status/get.go | 10 +- .../processing/synchronous/status/status.go | 3 + .../processing/synchronous/status/unfave.go | 19 +- internal/processing/timeline.go | 49 +--- internal/timeline/index.go | 50 +--- internal/timeline/manager.go | 2 +- internal/timeline/postindex.go | 15 +- internal/timeline/prepare.go | 18 +- internal/timeline/preparedposts.go | 18 ++ internal/timeline/timeline.go | 7 +- internal/typeutils/astointernal.go | 6 +- internal/typeutils/converter.go | 4 +- internal/typeutils/internal.go | 9 +- internal/typeutils/internaltofrontend.go | 120 +++----- internal/typeutils/util.go | 46 ++++ internal/visibility/filter.go | 33 +++ internal/visibility/statushometimelineable.go | 75 +++++ internal/visibility/statusvisible.go | 197 +++++++++++++ internal/visibility/util.go | 81 ++++++ 39 files changed, 739 insertions(+), 602 deletions(-) create mode 100644 internal/db/pg/statuscontext.go create mode 100644 internal/typeutils/util.go create mode 100644 internal/visibility/filter.go create mode 100644 internal/visibility/statushometimelineable.go create mode 100644 internal/visibility/statusvisible.go create mode 100644 internal/visibility/util.go diff --git a/PROGRESS.md b/PROGRESS.md index 4ccf1e99f..c25887d09 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -72,7 +72,7 @@ * [x] /api/v1/statuses POST (Create a new status) * [x] /api/v1/statuses/:id GET (View an existing status) * [x] /api/v1/statuses/:id DELETE (Delete a status) - * [ ] /api/v1/statuses/:id/context GET (View statuses above and below status ID) + * [x] /api/v1/statuses/:id/context GET (View statuses above and below status ID) * [x] /api/v1/statuses/:id/reblogged_by GET (See who has reblogged a status) * [x] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) * [x] /api/v1/statuses/:id/favourite POST (Fave a status) diff --git a/internal/db/db.go b/internal/db/db.go index 51685f024..4e21358c3 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -199,21 +199,6 @@ type DB interface { // GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) - // StatusVisible returns true if targetStatus is visible to requestingAccount, based on the - // privacy settings of the status, and any blocks/mutes that might exist between the two accounts - // or account domains. - // - // StatusVisible will also check through the given slice of 'otherRelevantAccounts', which should include: - // - // 1. Accounts mentioned in the targetStatus - // - // 2. Accounts replied to by the target status - // - // 3. Accounts boosted by the target status - // - // Will return an error if something goes wrong while pulling stuff out of the database. - StatusVisible(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) - // Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) @@ -223,9 +208,6 @@ type DB interface { // Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) - // PullRelevantAccountsFromStatus returns all accounts mentioned in a status, replied to by a status, or boosted by a status - PullRelevantAccountsFromStatus(status *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) - // GetReplyCountForStatus returns the amount of replies recorded for a status, or an error if something goes wrong GetReplyCountForStatus(status *gtsmodel.Status) (int, error) @@ -235,6 +217,12 @@ type DB interface { // GetFaveCountForStatus returns the amount of faves/likes recorded for a status, or an error if something goes wrong GetFaveCountForStatus(status *gtsmodel.Status) (int, error) + // StatusParents get the parent statuses of a given status. + StatusParents(status *gtsmodel.Status) ([]*gtsmodel.Status, error) + + // StatusChildren gets the child statuses of a given status. + StatusChildren(status *gtsmodel.Status) ([]*gtsmodel.Status, error) + // StatusFavedBy checks if a given status has been faved by a given account ID StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 2866a1157..851501334 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -806,196 +806,27 @@ func (ps *postgresService) GetRelationship(requestingAccount string, targetAccou return r, nil } -func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { - l := ps.log.WithField("func", "StatusVisible") - - targetAccount := relevantAccounts.StatusAuthor - - // if target account is suspended then don't show the status - if !targetAccount.SuspendedAt.IsZero() { - l.Trace("target account suspended at is not zero") - return false, nil - } - - // if the target user doesn't exist (anymore) then the status also shouldn't be visible - // note: we only do this for local users - if targetAccount.Domain == "" { - targetUser := >smodel.User{} - if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { - l.Debug("target user could not be selected") - if err == pg.ErrNoRows { - return false, db.ErrNoEntries{} - } - return false, err - } - - // if target user is disabled, not yet approved, or not confirmed then don't show the status - // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) - if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { - l.Trace("target user is disabled, not approved, or not confirmed") - return false, nil - } - } - - // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. - // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. - if requestingAccount == nil { - if targetStatus.Visibility == gtsmodel.VisibilityPublic { - return true, nil - } - l.Trace("requesting account is nil but the target status isn't public") - return false, nil - } - - // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten - // this far (ie., been authed) in the first place: this is just for safety. - if !requestingAccount.SuspendedAt.IsZero() { - l.Trace("requesting account is suspended") - return false, nil - } - - // check if we have a local account -- if so we can check the user for that account in the DB - if requestingAccount.Domain == "" { - requestingUser := >smodel.User{} - if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { - // if the requesting account is local but doesn't have a corresponding user in the db this is a problem - if err == pg.ErrNoRows { - l.Debug("requesting account is local but there's no corresponding user") - return false, nil - } - l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) - return false, err - } - // okay, user exists, so make sure it has full privileges/is confirmed/approved - if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { - l.Trace("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") - return false, nil - } - } - - // if the target status belongs to the requesting account, they should always be able to view it at this point - if targetStatus.AccountID == requestingAccount.ID { - return true, nil - } - - // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou - // First check if a block exists directly between the target account (which authored the status) and the requesting account. - if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { - l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) - return false, err - } else if blocked { - // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please - l.Trace("a block exists between requesting account and target account") - return false, nil - } - - // check other accounts mentioned/boosted by/replied to by the status, if they exist - if relevantAccounts != nil { - // status replies to account id - if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID { - if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - l.Trace("a block exists between requesting account and reply to account") - return false, nil - } - - // check reply to ID - if targetStatus.InReplyToID != "" { - followsRepliedAccount, err := ps.Follows(requestingAccount, relevantAccounts.ReplyToAccount) - if err != nil { - return false, err - } - if !followsRepliedAccount { - l.Trace("target status is a followers-only reply to an account that is not followed by the requesting account") - return false, nil - } - } - } - - // status boosts accounts id - if relevantAccounts.BoostedAccount != nil { - if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - l.Trace("a block exists between requesting account and boosted account") - return false, nil - } - } - - // status boosts a reply to account id - if relevantAccounts.BoostedReplyToAccount != nil { - if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - l.Trace("a block exists between requesting account and boosted reply to account") - return false, nil - } - } - - // status mentions accounts - for _, a := range relevantAccounts.MentionedAccounts { - if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - l.Trace("a block exists between requesting account and a mentioned account") - return false, nil - } - } - - // if the requesting account is mentioned in the status it should always be visible - for _, acct := range relevantAccounts.MentionedAccounts { - if acct.ID == requestingAccount.ID { - return true, nil // yep it's mentioned! - } - } - } - - // at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status - // that means it's now just a matter of checking the visibility settings of the status itself - switch targetStatus.Visibility { - case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: - // no problem here, just return OK - return true, nil - case gtsmodel.VisibilityFollowersOnly: - // check one-way follow - follows, err := ps.Follows(requestingAccount, targetAccount) - if err != nil { - return false, err - } - if !follows { - l.Trace("requested status is followers only but requesting account is not a follower") - return false, nil - } - return true, nil - case gtsmodel.VisibilityMutualsOnly: - // check mutual follow - mutuals, err := ps.Mutuals(requestingAccount, targetAccount) - if err != nil { - return false, err - } - if !mutuals { - l.Trace("requested status is mutuals only but accounts aren't mufos") - return false, nil - } - return true, nil - case gtsmodel.VisibilityDirect: - l.Trace("requesting account requests a status it's not mentioned in") - return false, nil // it's not mentioned -_- - } - - return false, errors.New("reached the end of StatusVisible with no result") -} - func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { + if sourceAccount == nil || targetAccount == nil { + return false, nil + } + return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() } func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { + if sourceAccount == nil || targetAccount == nil { + return false, nil + } + return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() } func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { + if account1 == nil || account2 == nil { + return false, nil + } + // make sure account 1 follows account 2 f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() if err != nil { @@ -1017,71 +848,6 @@ func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmode return f1 && f2, nil } -func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) { - accounts := >smodel.RelevantAccounts{ - MentionedAccounts: []*gtsmodel.Account{}, - } - - // get the author account - if targetStatus.GTSAuthorAccount == nil { - statusAuthor := >smodel.Account{} - if err := ps.conn.Model(statusAuthor).Where("id = ?", targetStatus.AccountID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err) - } - targetStatus.GTSAuthorAccount = statusAuthor - } - accounts.StatusAuthor = targetStatus.GTSAuthorAccount - - // get the replied to account from the status and add it to the pile - if targetStatus.InReplyToAccountID != "" { - repliedToAccount := >smodel.Account{} - if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s", targetStatus.InReplyToAccountID, err) - } - accounts.ReplyToAccount = repliedToAccount - } - - // get the boosted account from the status and add it to the pile - if targetStatus.BoostOfID != "" { - // retrieve the boosted status first - boostedStatus := >smodel.Status{} - if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err) - } - boostedAccount := >smodel.Account{} - if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err) - } - accounts.BoostedAccount = boostedAccount - - // the boosted status might be a reply to another account so we should get that too - if boostedStatus.InReplyToAccountID != "" { - boostedStatusRepliedToAccount := >smodel.Account{} - if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err) - } - accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount - } - } - - // now get all accounts with IDs that are mentioned in the status - for _, mentionID := range targetStatus.Mentions { - - mention := >smodel.Mention{} - if err := ps.conn.Model(mention).Where("id = ?", mentionID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mention with id %s: %s", mentionID, err) - } - - mentionedAccount := >smodel.Account{} - if err := ps.conn.Model(mentionedAccount).Where("id = ?", mention.TargetAccountID).Select(); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mentioned account: %s", err) - } - accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) - } - - return accounts, nil -} - func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() } diff --git a/internal/db/pg/statuscontext.go b/internal/db/pg/statuscontext.go new file mode 100644 index 000000000..e907a2d6f --- /dev/null +++ b/internal/db/pg/statuscontext.go @@ -0,0 +1,75 @@ +package pg + +import ( + "container/list" + "errors" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (ps *postgresService) StatusParents(status *gtsmodel.Status) ([]*gtsmodel.Status, error) { + parents := []*gtsmodel.Status{} + ps.statusParent(status, &parents) + + return parents, nil +} + +func (ps *postgresService) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status) { + if status.InReplyToID == "" { + return + } + + parentStatus := >smodel.Status{} + if err := ps.conn.Model(parentStatus).Where("id = ?", status.InReplyToID).Select(); err == nil { + *foundStatuses = append(*foundStatuses, parentStatus) + } + + ps.statusParent(parentStatus, foundStatuses) +} + +func (ps *postgresService) StatusChildren(status *gtsmodel.Status) ([]*gtsmodel.Status, error) { + foundStatuses := &list.List{} + foundStatuses.PushFront(status) + ps.statusChildren(status, foundStatuses) + + children := []*gtsmodel.Status{} + for e := foundStatuses.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*gtsmodel.Status) + if !ok { + panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) + } + + // only append children, not the overall parent status + if entry.ID != status.ID { + children = append(children, entry) + } + } + + return children, nil +} + +func (ps *postgresService) statusChildren(status *gtsmodel.Status, foundStatuses *list.List) { + immediateChildren := []*gtsmodel.Status{} + + err := ps.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID).Select() + if err != nil { + return + } + + for _, child := range immediateChildren { + insertLoop: + for e := foundStatuses.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*gtsmodel.Status) + if !ok { + panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) + } + + if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { + foundStatuses.InsertAfter(child, e) + break insertLoop + } + } + + ps.statusChildren(child, foundStatuses) + } +} diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 3ab6e2eca..02a5dfd72 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -87,6 +87,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { switch asType.GetTypeName() { case gtsmodel.ActivityStreamsCreate: + // CREATE SOMETHING create, ok := asType.(vocab.ActivityStreamsCreate) if !ok { return errors.New("could not convert type to create") @@ -95,6 +96,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { switch objectIter.GetType().GetTypeName() { case gtsmodel.ActivityStreamsNote: + // CREATE A NOTE note := objectIter.GetActivityStreamsNote() status, err := f.typeConverter.ASStatusToStatus(note) if err != nil { diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index f5e332978..caa5a2a25 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -46,8 +46,12 @@ type Status struct { Local bool // which account posted this status? AccountID string `pg:"type:CHAR(26),notnull"` + // AP uri of the owner of this status + AccountURI string // id of the status this status is a reply to InReplyToID string `pg:"type:CHAR(26)"` + // AP uri of the status this status is a reply to + InReplyToURI string // id of the account that this status replies to InReplyToAccountID string `pg:"type:CHAR(26)"` // id of the status this status is a boost of @@ -97,20 +101,6 @@ type Status struct { GTSBoostedStatus *Status `pg:"-"` // Account of the boosted status GTSBoostedAccount *Account `pg:"-"` - - /* - AP NON-DATABASE FIELDS - - These are for convenience while passing the status around internally, - but these fields should *never* be put in the db. - */ - - // AP URI of the status being replied to. - // Useful when that status doesn't exist in the database yet and we still need to dereference it. - APReplyToStatusURI string `pg:"-"` - // The AP URI of the owner/creator of the status. - // Useful when that account doesn't exist in the database yet and we still need to dereference it. - APStatusOwnerURI string `pg:"-"` } // Visibility represents the visibility granularity of a status. @@ -150,12 +140,3 @@ type VisibilityAdvanced struct { // This status can be liked/faved Likeable bool `pg:"default:true"` } - -// RelevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. -type RelevantAccounts struct { - StatusAuthor *Account - ReplyToAccount *Account - BoostedAccount *Account - BoostedReplyToAccount *Account - MentionedAccounts []*Account -} diff --git a/internal/processing/account.go b/internal/processing/account.go index 870734184..0e7dbbad3 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -222,12 +222,7 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin } for _, s := range statuses { - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err)) - } - - visible, err := p.db.StatusVisible(&s, authed.Account, relevantAccounts) + visible, err := p.filter.StatusVisible(&s, authed.Account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err)) } @@ -235,28 +230,7 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin continue } - var boostedStatus *gtsmodel.Status - if s.BoostOfID != "" { - bs := >smodel.Status{} - if err := p.db.GetByID(s.BoostOfID, bs); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err)) - } - boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err)) - } - - boostedVisible, err := p.db.StatusVisible(bs, authed.Account, boostedRelevantAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err)) - } - - if boostedVisible { - boostedStatus = bs - } - } - - apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus) + apiStatus, err := p.tc.StatusToMasto(&s, authed.Account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err)) } diff --git a/internal/processing/federation.go b/internal/processing/federation.go index 1c0d67fc8..5693caf90 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -223,6 +223,8 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st return nil, gtserror.NewErrorNotAuthorized(err) } + // authorize the request: + // 1. check if a block exists between the requester and the requestee blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) if err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -232,6 +234,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } + // get the status out of the database here s := >smodel.Status{} if err := p.db.GetWhere([]db.Where{ {Key: "id", Value: requestedStatusID}, @@ -240,6 +243,15 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) } + visible, err := p.filter.StatusVisible(s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if !visible { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) + } + + // requester is authorized to view the status, so convert it to AP representation and serialize it asStatus, err := p.tc.StatusToAS(s) if err != nil { return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index d171e593a..8c4a1692e 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -83,6 +83,10 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("boost was not parseable as *gtsmodel.Status") } + if err := p.timelineStatus(boostWrapperStatus); err != nil { + return err + } + if err := p.notifyAnnounce(boostWrapperStatus); err != nil { return err } diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index 85531d20b..65ccef45d 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -255,12 +255,6 @@ func (p *processor) timelineStatus(status *gtsmodel.Status) error { status.GTSAuthorAccount = a } - // get all relevant accounts here once - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(status) - if err != nil { - return fmt.Errorf("timelineStatus: error getting relevant accounts from status: %s", err) - } - // get local followers of the account that posted the status followers := []gtsmodel.Follow{} if err := p.db.GetFollowersByAccountID(status.AccountID, &followers, true); err != nil { @@ -279,7 +273,7 @@ func (p *processor) timelineStatus(status *gtsmodel.Status) error { errors := make(chan error, len(followers)) for _, f := range followers { - go p.timelineStatusForAccount(status, f.AccountID, relevantAccounts, errors, &wg) + go p.timelineStatusForAccount(status, f.AccountID, errors, &wg) } // read any errors that come in from the async functions @@ -306,29 +300,29 @@ func (p *processor) timelineStatus(status *gtsmodel.Status) error { return nil } -func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID string, relevantAccounts *gtsmodel.RelevantAccounts, errors chan error, wg *sync.WaitGroup) { +func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) { defer wg.Done() - // get the targetAccount + // get the timeline owner account timelineAccount := >smodel.Account{} if err := p.db.GetByID(accountID, timelineAccount); err != nil { - errors <- fmt.Errorf("timelineStatus: error getting account for timeline with id %s: %s", accountID, err) + errors <- fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %s", accountID, err) return } - // make sure the status is visible - visible, err := p.db.StatusVisible(status, timelineAccount, relevantAccounts) + // make sure the status is timelineable + timelineable, err := p.filter.StatusHometimelineable(status, timelineAccount) if err != nil { - errors <- fmt.Errorf("timelineStatus: error getting visibility for status for timeline with id %s: %s", accountID, err) + errors <- fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %s", accountID, err) return } - if !visible { + if !timelineable { return } if err := p.timelineManager.IngestAndPrepare(status, timelineAccount.ID); err != nil { - errors <- fmt.Errorf("initTimelineFor: error ingesting status %s: %s", status.ID, err) + errors <- fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %s", status.ID, err) } } diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index f010a7aa1..cc3ffa153 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -121,6 +121,10 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er } } + if err := p.timelineStatus(incomingAnnounce); err != nil { + return err + } + if err := p.notifyAnnounce(incomingAnnounce); err != nil { return err } diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 1ccf71e34..301cb5707 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -35,6 +35,7 @@ "github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/visibility" ) // Processor should be passed to api modules (see internal/apimodule/...). It is used for @@ -185,6 +186,7 @@ type processor struct { storage blob.Storage timelineManager timeline.Manager db db.DB + filter visibility.Filter /* SUB-PROCESSORS @@ -214,6 +216,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f storage: storage, timelineManager: timelineManager, db: db, + filter: visibility.NewFilter(db, log), statusProcessor: statusProcessor, } diff --git a/internal/processing/search.go b/internal/processing/search.go index d518a0310..a0a48145b 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -106,15 +106,11 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu continue } - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(foundStatus) - if err != nil { - continue - } - if visible, err := p.db.StatusVisible(foundStatus, authed.Account, relevantAccounts); !visible || err != nil { + if visible, err := p.filter.StatusVisible(foundStatus, authed.Account); !visible || err != nil { continue } - statusMasto, err := p.tc.StatusToMasto(foundStatus, statusOwner, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, nil) + statusMasto, err := p.tc.StatusToMasto(foundStatus, authed.Account) if err != nil { continue } diff --git a/internal/processing/synchronous/status/boost.go b/internal/processing/synchronous/status/boost.go index a746e9fd8..93d0f19de 100644 --- a/internal/processing/synchronous/status/boost.go +++ b/internal/processing/synchronous/status/boost.go @@ -24,14 +24,8 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) + visible, err := p.filter.StatusVisible(targetStatus, account) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } @@ -70,7 +64,7 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli } // return the frontend representation of the new status to the submitter - mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account, account, targetAccount, nil, targetStatus) + mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/boostedby.go b/internal/processing/synchronous/status/boostedby.go index 8ebfcebc0..b352178e3 100644 --- a/internal/processing/synchronous/status/boostedby.go +++ b/internal/processing/synchronous/status/boostedby.go @@ -24,14 +24,8 @@ func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching related accounts for status %s: %s", targetStatusID, err)) - } - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) + visible, err := p.filter.StatusVisible(targetStatus, account) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/context.go b/internal/processing/synchronous/status/context.go index cac86815e..72b9b5623 100644 --- a/internal/processing/synchronous/status/context.go +++ b/internal/processing/synchronous/status/context.go @@ -1,14 +1,69 @@ package status import ( + "fmt" + "sort" + 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" ) func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { - return &apimodel.Context{ + + context := &apimodel.Context{ Ancestors: []apimodel.Status{}, Descendants: []apimodel.Status{}, - }, nil + } + + targetStatus := >smodel.Status{} + if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + visible, err := p.filter.StatusVisible(targetStatus, account) + if err != nil { + return nil, gtserror.NewErrorNotFound(err) + } + if !visible { + return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID)) + } + + parents, err := p.db.StatusParents(targetStatus) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + for _, status := range parents { + if v, err := p.filter.StatusVisible(status, account); err == nil && v { + mastoStatus, err := p.tc.StatusToMasto(status, account) + if err == nil { + context.Ancestors = append(context.Ancestors, *mastoStatus) + } + } + } + + sort.Slice(context.Ancestors, func(i int, j int) bool { + return context.Ancestors[i].ID < context.Ancestors[j].ID + }) + + children, err := p.db.StatusChildren(targetStatus) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + for _, status := range children { + if v, err := p.filter.StatusVisible(status, account); err == nil && v { + mastoStatus, err := p.tc.StatusToMasto(status, account) + if err == nil { + context.Descendants = append(context.Descendants, *mastoStatus) + } + } + } + + return context, nil } diff --git a/internal/processing/synchronous/status/create.go b/internal/processing/synchronous/status/create.go index 07f670d1a..aa7468ae5 100644 --- a/internal/processing/synchronous/status/create.go +++ b/internal/processing/synchronous/status/create.go @@ -28,6 +28,7 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl UpdatedAt: time.Now(), Local: true, AccountID: account.ID, + AccountURI: account.URI, ContentWarning: form.SpoilerText, ActivityStreamsType: gtsmodel.ActivityStreamsNote, Sensitive: form.Sensitive, @@ -96,7 +97,7 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl } // return the frontend representation of the new status to the submitter - mastoStatus, err := p.tc.StatusToMasto(newStatus, account, account, nil, newStatus.GTSReplyToAccount, nil) + mastoStatus, err := p.tc.StatusToMasto(newStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/delete.go b/internal/processing/synchronous/status/delete.go index 7e251080a..5da196a9f 100644 --- a/internal/processing/synchronous/status/delete.go +++ b/internal/processing/synchronous/status/delete.go @@ -26,12 +26,6 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account")) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - var boostOfStatus *gtsmodel.Status if targetStatus.BoostOfID != "" { boostOfStatus = >smodel.Status{} @@ -40,7 +34,7 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a } } - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/fave.go b/internal/processing/synchronous/status/fave.go index b4622abbc..23f0d2944 100644 --- a/internal/processing/synchronous/status/fave.go +++ b/internal/processing/synchronous/status/fave.go @@ -26,12 +26,6 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - var boostOfStatus *gtsmodel.Status if targetStatus.BoostOfID != "" { boostOfStatus = >smodel.Status{} @@ -41,7 +35,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api } l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } @@ -98,7 +92,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api } // return the mastodon representation of the target status - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/favedby.go b/internal/processing/synchronous/status/favedby.go index bda47d581..5194cc258 100644 --- a/internal/processing/synchronous/status/favedby.go +++ b/internal/processing/synchronous/status/favedby.go @@ -24,14 +24,8 @@ func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, account) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/get.go b/internal/processing/synchronous/status/get.go index 7dbbb4e7d..9a70185b0 100644 --- a/internal/processing/synchronous/status/get.go +++ b/internal/processing/synchronous/status/get.go @@ -24,14 +24,8 @@ func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apim return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } @@ -48,7 +42,7 @@ func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apim } } - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/synchronous/status/status.go b/internal/processing/synchronous/status/status.go index 5dd26a2f0..cfc48ff30 100644 --- a/internal/processing/synchronous/status/status.go +++ b/internal/processing/synchronous/status/status.go @@ -8,6 +8,7 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/visibility" ) // Processor wraps a bunch of functions for processing statuses. @@ -36,6 +37,7 @@ type processor struct { tc typeutils.TypeConverter config *config.Config db db.DB + filter visibility.Filter fromClientAPI chan gtsmodel.FromClientAPI log *logrus.Logger } @@ -46,6 +48,7 @@ func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClient tc: tc, config: config, db: db, + filter: visibility.NewFilter(db, log), fromClientAPI: fromClientAPI, log: log, } diff --git a/internal/processing/synchronous/status/unfave.go b/internal/processing/synchronous/status/unfave.go index 54cbbf509..b51daacb9 100644 --- a/internal/processing/synchronous/status/unfave.go +++ b/internal/processing/synchronous/status/unfave.go @@ -24,14 +24,8 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) } - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, account) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } @@ -74,16 +68,7 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a } } - // return the status (whatever its state) back to the caller - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 80e63317f..a8f42d64c 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -94,47 +94,15 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err)) } - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s) - if err != nil { - l.Debugf("skipping status %s because we couldn't pull relevant accounts from the db", s.ID) - continue - } - - visible, err := p.db.StatusVisible(s, authed.Account, relevantAccounts) + timelineable, err := p.filter.StatusHometimelineable(s, authed.Account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err)) } - if !visible { + if !timelineable { continue } - var boostedStatus *gtsmodel.Status - if s.BoostOfID != "" { - bs := >smodel.Status{} - if err := p.db.GetByID(s.BoostOfID, bs); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - l.Debugf("skipping status %s because status %s can't be found in the db", s.ID, s.BoostOfID) - continue - } - return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err)) - } - boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs) - if err != nil { - l.Debugf("skipping status %s because we couldn't pull relevant accounts from the db", s.ID) - continue - } - - boostedVisible, err := p.db.StatusVisible(bs, authed.Account, boostedRelevantAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err)) - } - - if boostedVisible { - boostedStatus = bs - } - } - - apiStatus, err := p.tc.StatusToMasto(s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus) + apiStatus, err := p.tc.StatusToMasto(s, authed.Account) if err != nil { l.Debugf("skipping status %s because it couldn't be converted to its mastodon representation: %s", s.ID, err) continue @@ -227,17 +195,12 @@ func (p *processor) indexAndIngest(statuses []*gtsmodel.Status, timelineAccount }) for _, s := range statuses { - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s) + timelineable, err := p.filter.StatusHometimelineable(s, timelineAccount) if err != nil { - l.Error(fmt.Errorf("initTimelineFor: error getting relevant accounts from status %s: %s", s.ID, err)) + l.Error(fmt.Errorf("initTimelineFor: error checking home timelineability of status %s: %s", s.ID, err)) continue } - visible, err := p.db.StatusVisible(s, timelineAccount, relevantAccounts) - if err != nil { - l.Error(fmt.Errorf("initTimelineFor: error checking visibility of status %s: %s", s.ID, err)) - continue - } - if visible { + if timelineable { if err := p.timelineManager.Ingest(s, timelineAccount.ID); err != nil { l.Error(fmt.Errorf("initTimelineFor: error ingesting status %s: %s", s.ID, err)) continue diff --git a/internal/timeline/index.go b/internal/timeline/index.go index 56f5c14df..bc1bf996b 100644 --- a/internal/timeline/index.go +++ b/internal/timeline/index.go @@ -10,41 +10,6 @@ ) func (t *timeline) IndexBefore(statusID string, include bool, amount int) error { - // filtered := []*gtsmodel.Status{} - // offsetStatus := statusID - - // grabloop: - // for len(filtered) < amount { - // statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, amount, offsetStatus, include, true) - // if err != nil { - // if _, ok := err.(db.ErrNoEntries); !ok { - // return fmt.Errorf("IndexBeforeAndIncluding: error getting statuses from db: %s", err) - // } - // break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail - // } - - // for _, s := range statuses { - // relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s) - // if err != nil { - // continue - // } - // visible, err := t.db.StatusVisible(s, t.account, relevantAccounts) - // if err != nil { - // continue - // } - // if visible { - // filtered = append(filtered, s) - // } - // offsetStatus = s.ID - // } - // } - - // for _, s := range filtered { - // if err := t.IndexOne(s.CreatedAt, s.ID); err != nil { - // return fmt.Errorf("IndexBeforeAndIncluding: error indexing status with id %s: %s", s.ID, err) - // } - // } - return nil } @@ -63,15 +28,11 @@ func (t *timeline) IndexBehind(statusID string, amount int) error { } for _, s := range statuses { - relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s) + timelineable, err := t.filter.StatusHometimelineable(s, t.account) if err != nil { continue } - visible, err := t.db.StatusVisible(s, t.account, relevantAccounts) - if err != nil { - continue - } - if visible { + if timelineable { filtered = append(filtered, s) } offsetStatus = s.ID @@ -79,7 +40,7 @@ func (t *timeline) IndexBehind(statusID string, amount int) error { } for _, s := range filtered { - if err := t.IndexOne(s.CreatedAt, s.ID); err != nil { + if err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil { return fmt.Errorf("IndexBehindAndIncluding: error indexing status with id %s: %s", s.ID, err) } } @@ -91,12 +52,13 @@ func (t *timeline) IndexOneByID(statusID string) error { return nil } -func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string) error { +func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) error { t.Lock() defer t.Unlock() postIndexEntry := &postIndexEntry{ - statusID: statusID, + statusID: statusID, + boostOfID: boostOfID, } return t.postIndex.insertIndexed(postIndexEntry) diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go index 9d28b5060..c389a6b8a 100644 --- a/internal/timeline/manager.go +++ b/internal/timeline/manager.go @@ -105,7 +105,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) erro t := m.getOrCreateTimeline(timelineAccountID) l.Trace("ingesting status") - return t.IndexOne(status.CreatedAt, status.ID) + return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID) } func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error { diff --git a/internal/timeline/postindex.go b/internal/timeline/postindex.go index 2ab65e087..7142035a7 100644 --- a/internal/timeline/postindex.go +++ b/internal/timeline/postindex.go @@ -10,7 +10,8 @@ type postIndex struct { } type postIndexEntry struct { - statusID string + statusID string + boostOfID string } func (p *postIndex) insertIndexed(i *postIndexEntry) error { @@ -25,14 +26,26 @@ func (p *postIndex) insertIndexed(i *postIndexEntry) error { } var insertMark *list.Element + var position int // We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created. // We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*). for e := p.data.Front(); e != nil; e = e.Next() { + position = position + 1 + entry, ok := e.Value.(*postIndexEntry) if !ok { return errors.New("index: could not parse e as a postIndexEntry") } + // don't insert this if it's a boost of a status we've seen recently + if i.boostOfID != "" { + if i.boostOfID == entry.boostOfID || i.boostOfID == entry.statusID { + if position < boostReinsertionDepth { + return nil + } + } + } + // if the post to index is newer than e, insert it before e in the list if insertMark == nil { if i.statusID > entry.statusID { diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go index 1fb1cd714..cd740993c 100644 --- a/internal/timeline/prepare.go +++ b/internal/timeline/prepare.go @@ -163,24 +163,8 @@ func (t *timeline) prepare(statusID string) error { t.account = timelineOwnerAccount } - // to convert the status we need relevant accounts from it, so pull them out here - relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(gtsStatus) - if err != nil { - return err - } - - // check if this is a boost... - var reblogOfStatus *gtsmodel.Status - if gtsStatus.BoostOfID != "" { - s := >smodel.Status{} - if err := t.db.GetByID(gtsStatus.BoostOfID, s); err != nil { - return err - } - reblogOfStatus = s - } - // serialize the status (or, at least, convert it to a form that's ready to be serialized) - apiModelStatus, err := t.tc.StatusToMasto(gtsStatus, relevantAccounts.StatusAuthor, t.account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, reblogOfStatus) + apiModelStatus, err := t.tc.StatusToMasto(gtsStatus, t.account) if err != nil { return err } diff --git a/internal/timeline/preparedposts.go b/internal/timeline/preparedposts.go index 429ce5415..1976189c8 100644 --- a/internal/timeline/preparedposts.go +++ b/internal/timeline/preparedposts.go @@ -28,14 +28,32 @@ func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error { } var insertMark *list.Element + var position int // We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created. // We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*). for e := p.data.Front(); e != nil; e = e.Next() { + position = position + 1 + entry, ok := e.Value.(*preparedPostsEntry) if !ok { return errors.New("index: could not parse e as a preparedPostsEntry") } + // don't insert this if it's a boost of a status we've seen recently + if i.prepared.Reblog != nil { + if entry.prepared.Reblog != nil && i.prepared.Reblog.ID == entry.prepared.Reblog.ID { + if position < boostReinsertionDepth { + return nil + } + } + + if i.prepared.Reblog.ID == entry.statusID { + if position < boostReinsertionDepth { + return nil + } + } + } + // if the post to index is newer than e, insert it before e in the list if insertMark == nil { if i.statusID > entry.statusID { diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go index 7408436dc..363c0999c 100644 --- a/internal/timeline/timeline.go +++ b/internal/timeline/timeline.go @@ -27,8 +27,11 @@ "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/visibility" ) +const boostReinsertionDepth = 50 + // Timeline represents a timeline for one account, and contains indexed and prepared posts. type Timeline interface { /* @@ -59,7 +62,7 @@ type Timeline interface { */ // IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property. - IndexOne(statusCreatedAt time.Time, statusID string) error + IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) error // OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong. // If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this. @@ -109,6 +112,7 @@ type timeline struct { accountID string account *gtsmodel.Account db db.DB + filter visibility.Filter tc typeutils.TypeConverter log *logrus.Logger sync.Mutex @@ -121,6 +125,7 @@ func NewTimeline(accountID string, db db.DB, typeConverter typeutils.TypeConvert preparedPosts: &preparedPosts{}, accountID: accountID, db: db, + filter: visibility.NewFilter(db, log), tc: typeConverter, log: log, } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 5990e750f..dc58346fb 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -222,13 +222,14 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e if err != nil { return nil, errors.New("attributedTo was empty") } - status.APStatusOwnerURI = attributedTo.String() + status.AccountURI = attributedTo.String() statusOwner := >smodel.Account{} if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String(), CaseInsensitive: true}}, statusOwner); err != nil { return nil, fmt.Errorf("couldn't get status owner from db: %s", err) } status.AccountID = statusOwner.ID + status.AccountURI = statusOwner.URI status.GTSAuthorAccount = statusOwner // check if there's a post that this is a reply to @@ -236,7 +237,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e if err == nil { // something is set so we can at least set this field on the // status and dereference using this later if we need to - status.APReplyToStatusURI = inReplyToURI.String() + status.InReplyToURI = inReplyToURI.String() // now we can check if we have the replied-to status in our db already inReplyToStatus := >smodel.Status{} @@ -475,6 +476,7 @@ func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Sta return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error in db fetching account with uri %s: %s", actor.String(), err) } status.AccountID = boostingAccount.ID + status.AccountURI = boostingAccount.URI // these will all be wrapped in the boosted status so set them empty here status.Attachments = []string{} diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index ab680fbdd..806090f66 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -65,7 +65,9 @@ type TypeConverter interface { // TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API. TagToMasto(t *gtsmodel.Tag) (model.Tag, error) // StatusToMasto converts a gts model status into its mastodon (frontend) representation for serialization on the API. - StatusToMasto(s *gtsmodel.Status, statusAuthor *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*model.Status, error) + // + // Requesting account can be nil. + StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) // VisToMasto converts a gts visibility into its mastodon equivalent VisToMasto(m gtsmodel.Visibility) model.Visibility // InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index 3b3c8bd1b..b081708a2 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -43,10 +43,11 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel. URL: boostWrapperStatusURL, // the boosted status is not created now, but the boost certainly is - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Local: local, - AccountID: boostingAccount.ID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Local: local, + AccountID: boostingAccount.ID, + AccountURI: boostingAccount.URI, // replies can be boosted, but boosts are never replies InReplyToID: "", diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 1c283e9b8..90460ecdd 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -268,14 +268,7 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) { }, nil } -func (c *converter) StatusToMasto( - s *gtsmodel.Status, - statusAuthor *gtsmodel.Account, - requestingAccount *gtsmodel.Account, - boostOfAccount *gtsmodel.Account, - replyToAccount *gtsmodel.Account, - reblogOfStatus *gtsmodel.Status) (*model.Status, error) { - +func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) { repliesCount, err := c.db.GetReplyCountForStatus(s) if err != nil { return nil, fmt.Errorf("error counting replies: %s", err) @@ -291,82 +284,32 @@ func (c *converter) StatusToMasto( return nil, fmt.Errorf("error counting faves: %s", err) } - var faved bool - var reblogged bool - var bookmarked bool - var muted bool - - // requestingAccount will be nil for public requests without auth - // But if it's not nil, we can also get information about the requestingAccount's interaction with this status - if requestingAccount != nil { - faved, err = c.db.StatusFavedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err) - } - - reblogged, err = c.db.StatusRebloggedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) - } - - muted, err = c.db.StatusMutedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err) - } - - bookmarked, err = c.db.StatusBookmarkedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err) - } - } - var mastoRebloggedStatus *model.Status if s.BoostOfID != "" { // the boosted status might have been set on this struct already so check first before doing db calls - var gtsBoostedStatus *gtsmodel.Status - if s.GTSBoostedStatus != nil { - // it's set, great! - gtsBoostedStatus = s.GTSBoostedStatus - } else { + if s.GTSBoostedStatus == nil { // it's not set so fetch it from the db - gtsBoostedStatus = >smodel.Status{} - if err := c.db.GetByID(s.BoostOfID, gtsBoostedStatus); err != nil { + bs := >smodel.Status{} + if err := c.db.GetByID(s.BoostOfID, bs); err != nil { return nil, fmt.Errorf("error getting boosted status with id %s: %s", s.BoostOfID, err) } + s.GTSBoostedStatus = bs } // the boosted account might have been set on this struct already or passed as a param so check first before doing db calls - var gtsBoostedAccount *gtsmodel.Account - if s.GTSBoostedAccount != nil { - // it's set, great! - gtsBoostedAccount = s.GTSBoostedAccount - } else if boostOfAccount != nil { - // it's been given as a param, great! - gtsBoostedAccount = boostOfAccount - } else if boostOfAccount == nil && s.GTSBoostedAccount == nil { + if s.GTSBoostedAccount == nil { // it's not set so fetch it from the db - gtsBoostedAccount = >smodel.Account{} - if err := c.db.GetByID(gtsBoostedStatus.AccountID, gtsBoostedAccount); err != nil { - return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", gtsBoostedStatus.AccountID, s.BoostOfID, err) + ba := >smodel.Account{} + if err := c.db.GetByID(s.GTSBoostedStatus.AccountID, ba); err != nil { + return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", s.GTSBoostedStatus.AccountID, s.BoostOfID, err) } + s.GTSBoostedAccount = ba + s.GTSBoostedStatus.GTSAuthorAccount = ba } - // the boosted status might be a reply so check this - var gtsBoostedReplyToAccount *gtsmodel.Account - if gtsBoostedStatus.InReplyToAccountID != "" { - gtsBoostedReplyToAccount = >smodel.Account{} - if err := c.db.GetByID(gtsBoostedStatus.InReplyToAccountID, gtsBoostedReplyToAccount); err != nil { - return nil, fmt.Errorf("error getting account that boosted status was a reply to: %s", err) - } - } - - if gtsBoostedStatus != nil || gtsBoostedAccount != nil { - mastoRebloggedStatus, err = c.StatusToMasto(gtsBoostedStatus, gtsBoostedAccount, requestingAccount, nil, gtsBoostedReplyToAccount, nil) - if err != nil { - return nil, fmt.Errorf("error converting boosted status to mastotype: %s", err) - } - } else { - return nil, fmt.Errorf("boost of id was set to %s but that status or account was nil", s.BoostOfID) + mastoRebloggedStatus, err = c.StatusToMasto(s.GTSBoostedStatus, requestingAccount) + if err != nil { + return nil, fmt.Errorf("error converting boosted status to mastotype: %s", err) } } @@ -382,7 +325,15 @@ func (c *converter) StatusToMasto( } } - mastoAuthorAccount, err := c.AccountToMastoPublic(statusAuthor) + if s.GTSAuthorAccount == nil { + a := >smodel.Account{} + if err := c.db.GetByID(s.AccountID, a); err != nil { + return nil, fmt.Errorf("error getting status author: %s", err) + } + s.GTSAuthorAccount = a + } + + mastoAuthorAccount, err := c.AccountToMastoPublic(s.GTSAuthorAccount) if err != nil { return nil, fmt.Errorf("error parsing account of status author: %s", err) } @@ -498,6 +449,12 @@ func (c *converter) StatusToMasto( var mastoCard *model.Card var mastoPoll *model.Poll + statusInteractions := &statusInteractions{} + si, err := c.interactionsWithStatusForAccount(s, requestingAccount) + if err == nil { + statusInteractions = si + } + return &model.Status{ ID: s.ID, CreatedAt: s.CreatedAt.Format(time.RFC3339), @@ -512,10 +469,10 @@ func (c *converter) StatusToMasto( RepliesCount: repliesCount, ReblogsCount: reblogsCount, FavouritesCount: favesCount, - Favourited: faved, - Reblogged: reblogged, - Muted: muted, - Bookmarked: bookmarked, + Favourited: statusInteractions.Faved, + Bookmarked: statusInteractions.Bookmarked, + Muted: statusInteractions.Muted, + Reblogged: statusInteractions.Reblogged, Pinned: s.Pinned, Content: s.Content, Reblog: mastoRebloggedStatus, @@ -630,15 +587,6 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi n.GTSStatus = status } - var replyToAccount *gtsmodel.Account - if n.GTSStatus.InReplyToAccountID != "" { - r := >smodel.Account{} - if err := c.db.GetByID(n.GTSStatus.InReplyToAccountID, r); err != nil { - return nil, fmt.Errorf("NotificationToMasto: error getting replied to account with id %s from the db: %s", n.GTSStatus.InReplyToAccountID, err) - } - replyToAccount = r - } - if n.GTSStatus.GTSAuthorAccount == nil { if n.GTSStatus.AccountID == n.GTSTargetAccount.ID { n.GTSStatus.GTSAuthorAccount = n.GTSTargetAccount @@ -648,7 +596,7 @@ func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notifi } var err error - mastoStatus, err = c.StatusToMasto(n.GTSStatus, n.GTSStatus.GTSAuthorAccount, n.GTSTargetAccount, nil, replyToAccount, nil) + mastoStatus, err = c.StatusToMasto(n.GTSStatus, nil) if err != nil { return nil, fmt.Errorf("NotificationToMasto: error converting status to masto: %s", err) } diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go new file mode 100644 index 000000000..1e13f0713 --- /dev/null +++ b/internal/typeutils/util.go @@ -0,0 +1,46 @@ +package typeutils + +import ( + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (c *converter) interactionsWithStatusForAccount(s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*statusInteractions, error) { + si := &statusInteractions{} + + if requestingAccount != nil { + faved, err := c.db.StatusFavedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err) + } + si.Faved = faved + + reblogged, err := c.db.StatusRebloggedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) + } + si.Reblogged = reblogged + + muted, err := c.db.StatusMutedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err) + } + si.Muted = muted + + bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err) + } + si.Bookmarked = bookmarked + } + return si, nil +} + +// StatusInteractions denotes interactions with a status on behalf of an account. +type statusInteractions struct { + Faved bool + Muted bool + Bookmarked bool + Reblogged bool +} diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go new file mode 100644 index 000000000..d12ad0ff6 --- /dev/null +++ b/internal/visibility/filter.go @@ -0,0 +1,33 @@ +package visibility + +import ( + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Filter packages up a bunch of logic for checking whether given statuses or accounts are visible to a requester. +type Filter interface { + // StatusVisible returns true if targetStatus is visible to requestingAccount, based on the + // privacy settings of the status, and any blocks/mutes that might exist between the two accounts + // or account domains, and other relevant accounts mentioned in or replied to by the status. + StatusVisible(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) + + // StatusHometimelineable returns true if targetStatus should be in the home timeline of the requesting account. + // + // This function will call StatusVisible internally, so it's not necessary to call it beforehand. + StatusHometimelineable(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) +} + +type filter struct { + db db.DB + log *logrus.Logger +} + +// NewFilter returns a new Filter interface that will use the provided database and logger. +func NewFilter(db db.DB, log *logrus.Logger) Filter { + return &filter{ + db: db, + log: log, + } +} diff --git a/internal/visibility/statushometimelineable.go b/internal/visibility/statushometimelineable.go new file mode 100644 index 000000000..bc5f7bcb8 --- /dev/null +++ b/internal/visibility/statushometimelineable.go @@ -0,0 +1,75 @@ +package visibility + +import ( + "fmt" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineOwnerAccount *gtsmodel.Account) (bool, error) { + l := f.log.WithFields(logrus.Fields{ + "func": "StatusHometimelineable", + "statusID": targetStatus.ID, + }) + + // status owner should always be able to see their own status in their timeline so we can return early if this is the case + if timelineOwnerAccount != nil && targetStatus.AccountID == timelineOwnerAccount.ID { + return true, nil + } + + v, err := f.StatusVisible(targetStatus, timelineOwnerAccount) + if err != nil { + return false, fmt.Errorf("StatusHometimelineable: error checking visibility of status with id %s: %s", targetStatus.ID, err) + } + + if !v { + l.Debug("status is not hometimelineable because it's not visible to the requester") + return false, nil + } + + // Don't timeline a status whose parent hasn't been dereferenced yet or can't be dereferenced. + // If we have the reply to URI but don't have an ID for the replied-to account or the replied-to status in our database, we haven't dereferenced it yet. + if targetStatus.InReplyToURI != "" && (targetStatus.InReplyToID == "" || targetStatus.InReplyToAccountID == "") { + return false, nil + } + + // if a status replies to an ID we know in the database, we need to make sure we also follow the replied-to status owner account + if targetStatus.InReplyToID != "" { + // pin the reply to status on to this status if it hasn't been done already + if targetStatus.GTSReplyToStatus == nil { + rs := >smodel.Status{} + if err := f.db.GetByID(targetStatus.InReplyToID, rs); err != nil { + return false, fmt.Errorf("StatusHometimelineable: error getting replied to status with id %s: %s", targetStatus.InReplyToID, err) + } + targetStatus.GTSReplyToStatus = rs + } + + // pin the reply to account on to this status if it hasn't been done already + if targetStatus.GTSReplyToAccount == nil { + ra := >smodel.Account{} + if err := f.db.GetByID(targetStatus.InReplyToAccountID, ra); err != nil { + return false, fmt.Errorf("StatusHometimelineable: error getting replied to account with id %s: %s", targetStatus.InReplyToAccountID, err) + } + targetStatus.GTSReplyToAccount = ra + } + + // if it's a reply to the timelineOwnerAccount, we don't need to check if the timelineOwnerAccount follows itself, just return true, they can see it + if targetStatus.AccountID == timelineOwnerAccount.ID { + return true, nil + } + + // the replied-to account != timelineOwnerAccount, so make sure the timelineOwnerAccount follows the replied-to account + follows, err := f.db.Follows(timelineOwnerAccount, targetStatus.GTSReplyToAccount) + if err != nil { + return false, fmt.Errorf("StatusHometimelineable: error checking follow from account %s to account %s: %s", timelineOwnerAccount.ID, targetStatus.InReplyToAccountID, err) + } + + // we don't want to timeline a reply to a status whose owner isn't followed by the requesting account + if !follows { + return false, nil + } + } + + return true, nil +} diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go new file mode 100644 index 000000000..caf5cfcfd --- /dev/null +++ b/internal/visibility/statusvisible.go @@ -0,0 +1,197 @@ +package visibility + +import ( + "errors" + + "fmt" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) { + l := f.log.WithFields(logrus.Fields{ + "func": "StatusVisible", + "statusID": targetStatus.ID, + "requestingAccountID": requestingAccount.ID, + }) + + relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) + if err != nil { + l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) + } + targetAccount := relevantAccounts.StatusAuthor + + // if target account is suspended then don't show the status + if !targetAccount.SuspendedAt.IsZero() { + l.Trace("target account suspended at is not zero") + return false, nil + } + + // if the target user doesn't exist (anymore) then the status also shouldn't be visible + // note: we only do this for local users + if targetAccount.Domain == "" { + targetUser := >smodel.User{} + if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: targetAccount.ID}}, targetUser); err != nil { + l.Debug("target user could not be selected") + if _, ok := err.(db.ErrNoEntries); ok { + return false, nil + } + return false, fmt.Errorf("StatusVisible: db error selecting user for local target account %s: %s", targetAccount.ID, err) + } + + // if target user is disabled, not yet approved, or not confirmed then don't show the status + // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) + if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { + l.Trace("target user is disabled, not approved, or not confirmed") + return false, nil + } + } + + // if the requesting user doesn't exist (anymore) then the status also shouldn't be visible + // note: we only do this for local users + if requestingAccount.Domain == "" { + requestingUser := >smodel.User{} + if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: requestingAccount.ID}}, requestingUser); err != nil { + // if the requesting account is local but doesn't have a corresponding user in the db this is a problem + l.Debug("requesting user could not be selected") + if _, ok := err.(db.ErrNoEntries); ok { + return false, nil + } + return false, fmt.Errorf("StatusVisible: db error selecting user for local requesting account %s: %s", requestingAccount.ID, err) + } + // okay, user exists, so make sure it has full privileges/is confirmed/approved + if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { + l.Trace("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") + return false, nil + } + } + + // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. + // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. + if requestingAccount == nil { + if targetStatus.Visibility == gtsmodel.VisibilityPublic { + return true, nil + } + l.Trace("requesting account is nil but the target status isn't public") + return false, nil + } + + // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten + // this far (ie., been authed) in the first place: this is just for safety. + if !requestingAccount.SuspendedAt.IsZero() { + l.Trace("requesting account is suspended") + return false, nil + } + + // if the target status belongs to the requesting account, they should always be able to view it at this point + if targetStatus.AccountID == requestingAccount.ID { + return true, nil + } + + // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou + // First check if a block exists directly between the target account (which authored the status) and the requesting account. + if blocked, err := f.db.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { + l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) + return false, err + } else if blocked { + // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please + l.Trace("a block exists between requesting account and target account") + return false, nil + } + + // status replies to account id + if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID { + if blocked, err := f.db.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + l.Trace("a block exists between requesting account and reply to account") + return false, nil + } + + // check reply to ID + if targetStatus.InReplyToID != "" && (targetStatus.Visibility == gtsmodel.VisibilityFollowersOnly || targetStatus.Visibility == gtsmodel.VisibilityDirect) { + followsRepliedAccount, err := f.db.Follows(requestingAccount, relevantAccounts.ReplyToAccount) + if err != nil { + return false, err + } + if !followsRepliedAccount { + l.Trace("target status is a followers-only reply to an account that is not followed by the requesting account") + return false, nil + } + } + } + + // status boosts accounts id + if relevantAccounts.BoostedAccount != nil { + if blocked, err := f.db.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + l.Trace("a block exists between requesting account and boosted account") + return false, nil + } + } + + // status boosts a reply to account id + if relevantAccounts.BoostedReplyToAccount != nil { + if blocked, err := f.db.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + l.Trace("a block exists between requesting account and boosted reply to account") + return false, nil + } + } + + // status mentions accounts + for _, a := range relevantAccounts.MentionedAccounts { + if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + l.Trace("a block exists between requesting account and a mentioned account") + return false, nil + } + } + + // if the requesting account is mentioned in the status it should always be visible + for _, acct := range relevantAccounts.MentionedAccounts { + if acct.ID == requestingAccount.ID { + return true, nil // yep it's mentioned! + } + } + + // at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status + // that means it's now just a matter of checking the visibility settings of the status itself + switch targetStatus.Visibility { + case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: + // no problem here, just return OK + return true, nil + case gtsmodel.VisibilityFollowersOnly: + // check one-way follow + follows, err := f.db.Follows(requestingAccount, targetAccount) + if err != nil { + return false, err + } + if !follows { + l.Trace("requested status is followers only but requesting account is not a follower") + return false, nil + } + return true, nil + case gtsmodel.VisibilityMutualsOnly: + // check mutual follow + mutuals, err := f.db.Mutuals(requestingAccount, targetAccount) + if err != nil { + return false, err + } + if !mutuals { + l.Trace("requested status is mutuals only but accounts aren't mufos") + return false, nil + } + return true, nil + case gtsmodel.VisibilityDirect: + l.Trace("requesting account requests a status it's not mentioned in") + return false, nil // it's not mentioned -_- + } + + return false, errors.New("reached the end of StatusVisible with no result") +} diff --git a/internal/visibility/util.go b/internal/visibility/util.go new file mode 100644 index 000000000..f52661d0b --- /dev/null +++ b/internal/visibility/util.go @@ -0,0 +1,81 @@ +package visibility + +import ( + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { + accounts := &relevantAccounts{ + MentionedAccounts: []*gtsmodel.Account{}, + } + + // get the author account + if targetStatus.GTSAuthorAccount == nil { + statusAuthor := >smodel.Account{} + if err := f.db.GetByID(targetStatus.AccountID, statusAuthor); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err) + } + targetStatus.GTSAuthorAccount = statusAuthor + } + accounts.StatusAuthor = targetStatus.GTSAuthorAccount + + // get the replied to account from the status and add it to the pile + if targetStatus.InReplyToAccountID != "" { + repliedToAccount := >smodel.Account{} + if err := f.db.GetByID(targetStatus.InReplyToAccountID, repliedToAccount); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s", targetStatus.InReplyToAccountID, err) + } + accounts.ReplyToAccount = repliedToAccount + } + + // get the boosted account from the status and add it to the pile + if targetStatus.BoostOfID != "" { + // retrieve the boosted status first + boostedStatus := >smodel.Status{} + if err := f.db.GetByID(targetStatus.BoostOfID, boostedStatus); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err) + } + boostedAccount := >smodel.Account{} + if err := f.db.GetByID(boostedStatus.AccountID, boostedAccount); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err) + } + accounts.BoostedAccount = boostedAccount + + // the boosted status might be a reply to another account so we should get that too + if boostedStatus.InReplyToAccountID != "" { + boostedStatusRepliedToAccount := >smodel.Account{} + if err := f.db.GetByID(boostedStatus.InReplyToAccountID, boostedStatusRepliedToAccount); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err) + } + accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount + } + } + + // now get all accounts with IDs that are mentioned in the status + for _, mentionID := range targetStatus.Mentions { + + mention := >smodel.Mention{} + if err := f.db.GetByID(mentionID, mention); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mention with id %s: %s", mentionID, err) + } + + mentionedAccount := >smodel.Account{} + if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil { + return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mentioned account: %s", err) + } + accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) + } + + return accounts, nil +} + +// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. +type relevantAccounts struct { + StatusAuthor *gtsmodel.Account + ReplyToAccount *gtsmodel.Account + BoostedAccount *gtsmodel.Account + BoostedReplyToAccount *gtsmodel.Account + MentionedAccounts []*gtsmodel.Account +}