From f4dc4d0aa0c05b7392eb9b9150e431bdd990b683 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sun, 11 Jul 2021 13:40:48 +0200 Subject: [PATCH] improve blocking stuff --- internal/api/client/account/block.go | 1 + internal/api/client/account/unblock.go | 1 + internal/federation/dereference.go | 2 + internal/gtsmodel/status.go | 2 + internal/processing/account/get.go | 14 ++++- internal/processing/blocks.go | 2 +- internal/processing/fromclientapi.go | 9 ++- internal/processing/fromfederator.go | 12 +++- internal/timeline/get.go | 18 ++++++ internal/timeline/index.go | 37 ++++++++++--- internal/timeline/manager.go | 16 +++++- internal/timeline/postindex.go | 24 +++++++- internal/timeline/prepare.go | 25 ++++++++- internal/timeline/preparedposts.go | 25 ++++++++- internal/timeline/remove.go | 70 ++++++++++++++++++++++++ internal/timeline/timeline.go | 8 ++- internal/typeutils/converter.go | 4 ++ internal/typeutils/internal.go | 1 + internal/typeutils/internaltofrontend.go | 21 +++++++ 19 files changed, 270 insertions(+), 22 deletions(-) diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go index c8cf099f9..c83837c2a 100644 --- a/internal/api/client/account/block.go +++ b/internal/api/client/account/block.go @@ -25,6 +25,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) +// AccountBlockPOSTHandler handles the creation of a block from the authed account targeting the given account ID. func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go index ec237dd06..1cb959db9 100644 --- a/internal/api/client/account/unblock.go +++ b/internal/api/client/account/unblock.go @@ -25,6 +25,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) +// AccountUnblockPOSTHandler handles the removal of a block from the authed account targeting the given account ID. func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go index 20ffa3a8d..b87462acd 100644 --- a/internal/federation/dereference.go +++ b/internal/federation/dereference.go @@ -393,6 +393,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse announce.Language = boostedStatus.Language announce.Text = boostedStatus.Text announce.BoostOfID = boostedStatus.ID + announce.BoostOfAccountID = boostedStatus.AccountID announce.Visibility = boostedStatus.Visibility announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced announce.GTSBoostedStatus = boostedStatus @@ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse announce.Language = boostedStatus.Language announce.Text = boostedStatus.Text announce.BoostOfID = boostedStatus.ID + announce.BoostOfAccountID = boostedStatus.AccountID announce.Visibility = boostedStatus.Visibility announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced announce.GTSBoostedStatus = boostedStatus diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index caa5a2a25..84b3dfc7c 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -56,6 +56,8 @@ type Status struct { InReplyToAccountID string `pg:"type:CHAR(26)"` // id of the status this status is a boost of BoostOfID string `pg:"type:CHAR(26)"` + // id of the account that owns the boosted status + BoostOfAccountID string `pg:"type:CHAR(26)"` // cw string for this status ContentWarning string // visibility entry for this status diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index aba1ed14a..b937ace5b 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -45,9 +45,19 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) } - var mastoAccount *apimodel.Account + var blocked bool var err error - if requestingAccount != nil && targetAccount.ID == requestingAccount.ID { + if requestingAccount != nil { + blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID) + if err != nil { + return nil, fmt.Errorf("error checking account block: %s", err) + } + } + + var mastoAccount *apimodel.Account + if blocked { + mastoAccount, err = p.tc.AccountToMastoBlocked(targetAccount) + } else if requestingAccount != nil && targetAccount.ID == requestingAccount.ID { mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount) } else { mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount) diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go index 31f57fc7c..509600ca6 100644 --- a/internal/processing/blocks.go +++ b/internal/processing/blocks.go @@ -43,7 +43,7 @@ func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string, apiAccounts := []*apimodel.Account{} for _, a := range accounts { - apiAccount, err := p.tc.AccountToMastoPublic(a) + apiAccount, err := p.tc.AccountToMastoBlocked(a) if err != nil { continue } diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index e7f757f10..6755a9d82 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -99,7 +99,14 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("block was not parseable as *gtsmodel.Block") } - // TODO: remove any of the blocking account's statuses from the blocked account's timeline + // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa + if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil { + return err + } + if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil { + return err + } + // TODO: same with notifications // TODO: same with bookmarks diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index f324af15f..94a4e5af8 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -129,8 +129,18 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er } case gtsmodel.ActivityStreamsBlock: // CREATE A BLOCK + block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) + if !ok { + return errors.New("block was not parseable as *gtsmodel.Block") + } - // TODO: remove any of the blocking account's statuses from the blocked account's timeline + // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa + if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil { + return err + } + if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil { + return err + } // TODO: same with notifications // TODO: same with bookmarks } diff --git a/internal/timeline/get.go b/internal/timeline/get.go index f07d81d55..d7ebb7766 100644 --- a/internal/timeline/get.go +++ b/internal/timeline/get.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( diff --git a/internal/timeline/index.go b/internal/timeline/index.go index 8c6b0d578..8dd7fee97 100644 --- a/internal/timeline/index.go +++ b/internal/timeline/index.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( @@ -44,7 +62,7 @@ grabloop: } for _, s := range filtered { - if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil { + if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil { return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err) } } @@ -79,7 +97,7 @@ grabloop: } for _, s := range filtered { - if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil { + if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil { return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err) } } @@ -91,24 +109,29 @@ func (t *timeline) IndexOneByID(statusID string) error { return nil } -func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) { +func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) { t.Lock() defer t.Unlock() postIndexEntry := &postIndexEntry{ - statusID: statusID, - boostOfID: boostOfID, + statusID: statusID, + boostOfID: boostOfID, + accountID: accountID, + boostOfAccountID: boostOfAccountID, } return t.postIndex.insertIndexed(postIndexEntry) } -func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) { +func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) { t.Lock() defer t.Unlock() postIndexEntry := &postIndexEntry{ - statusID: statusID, + statusID: statusID, + boostOfID: boostOfID, + accountID: accountID, + boostOfAccountID: boostOfAccountID, } inserted, err := t.postIndex.insertIndexed(postIndexEntry) diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go index d50c9f783..00d87bb26 100644 --- a/internal/timeline/manager.go +++ b/internal/timeline/manager.go @@ -78,6 +78,8 @@ type Manager interface { Remove(statusID string, timelineAccountID string) (int, error) // WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines WipeStatusFromAllTimelines(statusID string) error + // WipeStatusesFromAccountID removes all statuses by the given accountID from the timelineAccountID's timelines. + WipeStatusesFromAccountID(accountID string, timelineAccountID string) error } // NewManager returns a new timeline manager with the given database, typeconverter, config, and log. @@ -112,7 +114,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) (boo } l.Trace("ingesting status") - return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID) + return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID) } func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) { @@ -128,7 +130,7 @@ func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID st } l.Trace("ingesting status") - return t.IndexAndPrepareOne(status.CreatedAt, status.ID) + return t.IndexAndPrepareOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID) } func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) { @@ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error { return err } +func (m *manager) WipeStatusesFromAccountID(accountID string, timelineAccountID string) error { + t, err := m.getOrCreateTimeline(timelineAccountID) + if err != nil { + return err + } + + _, err = t.RemoveAllBy(accountID) + return err +} + func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) { var t Timeline i, ok := m.accountTimelines.Load(timelineAccountID) diff --git a/internal/timeline/postindex.go b/internal/timeline/postindex.go index 44765bf50..db155d0fe 100644 --- a/internal/timeline/postindex.go +++ b/internal/timeline/postindex.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( @@ -10,8 +28,10 @@ type postIndex struct { } type postIndexEntry struct { - statusID string - boostOfID string + statusID string + boostOfID string + accountID string + boostOfAccountID string } func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) { diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go index ac85d92e9..0fbd8ebba 100644 --- a/internal/timeline/prepare.go +++ b/internal/timeline/prepare.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( @@ -207,8 +225,11 @@ func (t *timeline) prepare(statusID string) error { // shove it in prepared posts as a prepared posts entry preparedPostsEntry := &preparedPostsEntry{ - statusID: statusID, - prepared: apiModelStatus, + statusID: gtsStatus.ID, + boostOfID: gtsStatus.BoostOfID, + accountID: gtsStatus.AccountID, + boostOfAccountID: gtsStatus.BoostOfAccountID, + prepared: apiModelStatus, } return t.preparedPosts.insertPrepared(preparedPostsEntry) diff --git a/internal/timeline/preparedposts.go b/internal/timeline/preparedposts.go index 1976189c8..7f8d84357 100644 --- a/internal/timeline/preparedposts.go +++ b/internal/timeline/preparedposts.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( @@ -12,8 +30,11 @@ type preparedPosts struct { } type preparedPostsEntry struct { - statusID string - prepared *apimodel.Status + statusID string + boostOfID string + accountID string + boostOfAccountID string + prepared *apimodel.Status } func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error { diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go index 8842c60cb..ee4f112c5 100644 --- a/internal/timeline/remove.go +++ b/internal/timeline/remove.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package timeline import ( @@ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) { l.Debugf("removed %d entries", removed) return removed, nil } + +func (t *timeline) RemoveAllBy(accountID string) (int, error) { + l := t.log.WithFields(logrus.Fields{ + "func": "RemoveAllBy", + "accountTimeline": t.accountID, + "accountID": accountID, + }) + t.Lock() + defer t.Unlock() + var removed int + + // remove entr(ies) from the post index + removeIndexes := []*list.Element{} + if t.postIndex != nil && t.postIndex.data != nil { + for e := t.postIndex.data.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*postIndexEntry) + if !ok { + return removed, errors.New("Remove: could not parse e as a postIndexEntry") + } + if entry.accountID == accountID || entry.boostOfAccountID == accountID { + l.Debug("found status in postIndex") + removeIndexes = append(removeIndexes, e) + } + } + } + for _, e := range removeIndexes { + t.postIndex.data.Remove(e) + removed = removed + 1 + } + + // remove entr(ies) from prepared posts + removePrepared := []*list.Element{} + if t.preparedPosts != nil && t.preparedPosts.data != nil { + for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*preparedPostsEntry) + if !ok { + return removed, errors.New("Remove: could not parse e as a preparedPostsEntry") + } + if entry.accountID == accountID || entry.boostOfAccountID == accountID { + l.Debug("found status in preparedPosts") + removePrepared = append(removePrepared, e) + } + } + } + for _, e := range removePrepared { + t.preparedPosts.data.Remove(e) + removed = removed + 1 + } + + l.Debugf("removed %d entries", removed) + return removed, nil +} diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go index d0fadb19e..fe811a303 100644 --- a/internal/timeline/timeline.go +++ b/internal/timeline/timeline.go @@ -65,7 +65,7 @@ type Timeline interface { // // The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false // if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline. - IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) + IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, 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. @@ -85,7 +85,7 @@ type Timeline interface { // // The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false // if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline. - IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) + IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) // OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared 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. OldestPreparedPostID() (string, error) @@ -109,6 +109,10 @@ type Timeline interface { // // The returned int indicates the amount of entries that were removed. Remove(statusID string) (int, error) + // RemoveAllBy removes all statuses by the given accountID, from both the index and prepared posts. + // + // The returned int indicates the amount of entries that were removed. + RemoveAllBy(accountID string) (int, error) } // timeline fulfils the Timeline interface diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 203b4019a..57c2a1f6d 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -48,6 +48,10 @@ type TypeConverter interface { // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. // In other words, this is the public record that the server has of an account. AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) + // AccountToMastoBlocked takes a db model account as a param, and returns a mastotype account, or an error if + // something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used + // when someone wants to view an account they've blocked. + AccountToMastoBlocked(account *gtsmodel.Account) (*model.Account, error) // AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error // if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields // (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index b081708a2..a46ad7fbd 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel. Language: s.Language, Text: s.Text, BoostOfID: s.ID, + BoostOfAccountID: s.AccountID, Visibility: s.Visibility, VisibilityAdvanced: s.VisibilityAdvanced, diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 61c11b8ef..088e30d68 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -173,6 +173,27 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e }, nil } +func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) { + var acct string + if a.Domain != "" { + // this is a remote user + acct = fmt.Sprintf("%s@%s", a.Username, a.Domain) + } else { + // this is a local user + acct = a.Username + } + + return &model.Account{ + ID: a.ID, + Username: a.Username, + Acct: acct, + DisplayName: a.Username, + Bot: a.Bot, + CreatedAt: a.CreatedAt.Format(time.RFC3339), + URL: a.URL, + }, nil +} + func (c *converter) AppToMastoSensitive(a *gtsmodel.Application) (*model.Application, error) { return &model.Application{ ID: a.ID,