diff options
| author | 2021-07-11 16:22:21 +0200 | |
|---|---|---|
| committer | 2021-07-11 16:22:21 +0200 | |
| commit | 846057f0d696fded87d105dec1245e9ba32763ce (patch) | |
| tree | 9a4914c07bcf189a3eea0a2c091567c56cdf4963 | |
| parent | favourites GET implementation (#95) (diff) | |
| download | gotosocial-846057f0d696fded87d105dec1245e9ba32763ce.tar.xz | |
Block/unblock (#96)
* remote + local block logic, incl. federation
* improve blocking stuff
* fiddle with display of blocked profiles
* go fmt
45 files changed, 1405 insertions, 63 deletions
diff --git a/PROGRESS.md b/PROGRESS.md index 54e11d2b5..1d824c42b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -56,8 +56,8 @@ Things are moving on the project! As of July 2021 you can now:      * [ ] /api/v1/accounts/:id/identity_proofs GET          (Get identity proofs for this account)      * [x] /api/v1/accounts/:id/follow POST                  (Follow this account)      * [x] /api/v1/accounts/:id/unfollow POST                (Unfollow this account) -    * [ ] /api/v1/accounts/:id/block POST                   (Block this account) -    * [ ] /api/v1/accounts/:id/unblock POST                 (Unblock this account) +    * [x] /api/v1/accounts/:id/block POST                   (Block this account) +    * [x] /api/v1/accounts/:id/unblock POST                 (Unblock this account)      * [ ] /api/v1/accounts/:id/mute POST                    (Mute this account)      * [ ] /api/v1/accounts/:id/unmute POST                  (Unmute this account)      * [ ] /api/v1/accounts/:id/pin POST                     (Feature this account on profile) @@ -71,8 +71,8 @@ Things are moving on the project! As of July 2021 you can now:      * [x] /api/v1/favourites GET                            (See faved statuses)    * [ ] Mutes      * [ ] /api/v1/mutes GET                                 (See list of muted accounts) -  * [ ] Blocks -    * [ ] /api/v1/blocks GET                                (See list of blocked accounts) +  * [x] Blocks +    * [x] /api/v1/blocks GET                                (See list of blocked accounts)    * [ ] Domain Blocks      * [x] /api/v1/domain_blocks GET                         (See list of domain blocks)      * [x] /api/v1/domain_blocks POST                        (Create a domain block) diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go index 3a820c0ea..42aca3283 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/account/account.go @@ -61,10 +61,14 @@ const (  	GetFollowingPath = BasePathWithID + "/following"  	// GetRelationshipsPath is for showing an account's relationship with other accounts  	GetRelationshipsPath = BasePath + "/relationships" -	// PostFollowPath is for POSTing new follows to, and updating existing follows -	PostFollowPath = BasePathWithID + "/follow" -	// PostUnfollowPath is for POSTing an unfollow -	PostUnfollowPath = BasePathWithID + "/unfollow" +	// FollowPath is for POSTing new follows to, and updating existing follows +	FollowPath = BasePathWithID + "/follow" +	// UnfollowPath is for POSTing an unfollow +	UnfollowPath = BasePathWithID + "/unfollow" +	// BlockPath is for creating a block of an account +	BlockPath = BasePathWithID + "/block" +	// UnblockPath is for removing a block of an account +	UnblockPath = BasePathWithID + "/unblock"  )  // Module implements the ClientAPIModule interface for account-related actions @@ -85,15 +89,33 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg  // Route attaches all routes from this module to the given router  func (m *Module) Route(r router.Router) error { +	// create account  	r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) + +	// get account  	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) + +	// modify account  	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) + +	// get account's statuses  	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) + +	// get following or followers  	r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)  	r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) + +	// get relationship with account  	r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) -	r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler) -	r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler) + +	// follow or unfollow account +	r.AttachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler) +	r.AttachHandler(http.MethodPost, UnfollowPath, m.AccountUnfollowPOSTHandler) + +	// block or unblock account +	r.AttachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler) +	r.AttachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler) +  	return nil  } diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go new file mode 100644 index 000000000..c83837c2a --- /dev/null +++ b/internal/api/client/account/block.go @@ -0,0 +1,49 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package account + +import ( +	"net/http" + +	"github.com/gin-gonic/gin" +	"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 { +		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) +		return +	} + +	targetAcctID := c.Param(IDKey) +	if targetAcctID == "" { +		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) +		return +	} + +	relationship, errWithCode := m.processor.AccountBlockCreate(authed, targetAcctID) +	if errWithCode != nil { +		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) +		return +	} + +	c.JSON(http.StatusOK, relationship) +} diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go new file mode 100644 index 000000000..1cb959db9 --- /dev/null +++ b/internal/api/client/account/unblock.go @@ -0,0 +1,49 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package account + +import ( +	"net/http" + +	"github.com/gin-gonic/gin" +	"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 { +		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) +		return +	} + +	targetAcctID := c.Param(IDKey) +	if targetAcctID == "" { +		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) +		return +	} + +	relationship, errWithCode := m.processor.AccountBlockRemove(authed, targetAcctID) +	if errWithCode != nil { +		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) +		return +	} + +	c.JSON(http.StatusOK, relationship) +} diff --git a/internal/api/client/blocks/blocks.go b/internal/api/client/blocks/blocks.go new file mode 100644 index 000000000..de87e892f --- /dev/null +++ b/internal/api/client/blocks/blocks.go @@ -0,0 +1,63 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package blocks + +import ( +	"net/http" + +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/api" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/processing" +	"github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( +	// BasePath is the base URI path for serving favourites +	BasePath = "/api/v1/blocks" + +	// MaxIDKey is the url query for setting a max ID to return +	MaxIDKey = "max_id" +	// SinceIDKey is the url query for returning results newer than the given ID +	SinceIDKey = "since_id" +	// LimitKey is for specifying maximum number of results to return. +	LimitKey = "limit" +) + +// Module implements the ClientAPIModule interface for everything relating to viewing blocks +type Module struct { +	config    *config.Config +	processor processing.Processor +	log       *logrus.Logger +} + +// New returns a new blocks module +func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule { +	return &Module{ +		config:    config, +		processor: processor, +		log:       log, +	} +} + +// Route attaches all routes from this module to the given router +func (m *Module) Route(r router.Router) error { +	r.AttachHandler(http.MethodGet, BasePath, m.BlocksGETHandler) +	return nil +} diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go new file mode 100644 index 000000000..bf5f41e40 --- /dev/null +++ b/internal/api/client/blocks/blocksget.go @@ -0,0 +1,75 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package blocks + +import ( +	"net/http" +	"strconv" + +	"github.com/gin-gonic/gin" +	"github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// BlocksGETHandler handles GETting blocks. +func (m *Module) BlocksGETHandler(c *gin.Context) { +	l := m.log.WithField("func", "PublicTimelineGETHandler") + +	authed, err := oauth.Authed(c, true, true, true, true) +	if err != nil { +		l.Debugf("error authing: %s", err) +		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) +		return +	} + +	maxID := "" +	maxIDString := c.Query(MaxIDKey) +	if maxIDString != "" { +		maxID = maxIDString +	} + +	sinceID := "" +	sinceIDString := c.Query(SinceIDKey) +	if sinceIDString != "" { +		sinceID = sinceIDString +	} + +	limit := 20 +	limitString := c.Query(LimitKey) +	if limitString != "" { +		i, err := strconv.ParseInt(limitString, 10, 64) +		if err != nil { +			l.Debugf("error parsing limit string: %s", err) +			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) +			return +		} +		limit = int(i) +	} + +	resp, errWithCode := m.processor.BlocksGet(authed, maxID, sinceID, limit) +	if errWithCode != nil { +		l.Debugf("error from processor BlocksGet: %s", errWithCode) +		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) +		return +	} + +	if resp.LinkHeader != "" { +		c.Header("Link", resp.LinkHeader) +	} +	c.JSON(http.StatusOK, resp.Accounts) +} diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go index a5b8a2d99..20a12fefe 100644 --- a/internal/api/client/streaming/stream.go +++ b/internal/api/client/streaming/stream.go @@ -67,23 +67,23 @@ sendLoop:  		select {  		case m := <-stream.Messages:  			// we've got a streaming message!! -			l.Debug("received message from stream") +			l.Trace("received message from stream")  			if err := conn.WriteJSON(m); err != nil { -				l.Infof("error writing json to websocket connection: %s", err) +				l.Debugf("error writing json to websocket connection: %s", err)  				// if something is wrong we want to bail and drop the connection -- the client will create a new one  				break sendLoop  			} -			l.Debug("wrote message into websocket connection") +			l.Trace("wrote message into websocket connection")  		case <-t.C: -			l.Debug("received TICK from ticker") +			l.Trace("received TICK from ticker")  			if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { -				l.Infof("error writing ping to websocket connection: %s", err) +				l.Debugf("error writing ping to websocket connection: %s", err)  				// if something is wrong we want to bail and drop the connection -- the client will create a new one  				break sendLoop  			} -			l.Debug("wrote ping message into websocket connection") +			l.Trace("wrote ping message into websocket connection")  		}  	} -	l.Debug("leaving StreamGETHandler") +	l.Trace("leaving StreamGETHandler")  } diff --git a/internal/api/model/block.go b/internal/api/model/block.go new file mode 100644 index 000000000..6d6b15833 --- /dev/null +++ b/internal/api/model/block.go @@ -0,0 +1,26 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package model + +// BlocksResponse wraps a slice of accounts, ready to be serialized, along with the Link +// header for the previous and next queries, to be returned to the client. +type BlocksResponse struct { +	Accounts   []*Account +	LinkHeader string +} diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index b8eb2e381..316be614e 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -14,6 +14,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/app"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" @@ -143,6 +144,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log  	securityModule := security.New(c, dbService, log)  	streamingModule := streaming.New(c, processor, log)  	favouritesModule := favourites.New(c, processor, log) +	blocksModule := blocks.New(c, processor, log)  	apis := []api.ClientModule{  		// modules with middleware go first @@ -170,6 +172,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log  		listsModule,  		streamingModule,  		favouritesModule, +		blocksModule,  	}  	for _, m := range apis { diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go index 312d19a62..c669dd851 100644 --- a/internal/cliactions/testrig/testrig.go +++ b/internal/cliactions/testrig/testrig.go @@ -16,6 +16,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/app"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" @@ -88,6 +89,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log  	securityModule := security.New(c, dbService, log)  	streamingModule := streaming.New(c, processor, log)  	favouritesModule := favourites.New(c, processor, log) +	blocksModule := blocks.New(c, processor, log)  	apis := []api.ClientModule{  		// modules with middleware go first @@ -115,6 +117,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log  		listsModule,  		streamingModule,  		favouritesModule, +		blocksModule,  	}  	for _, m := range apis { diff --git a/internal/db/db.go b/internal/db/db.go index 0a3979df6..bbe780e80 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -159,6 +159,8 @@ type DB interface {  	// In case of no entries, a 'no entries' error will be returned  	GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) +	GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) +  	// GetLastStatusForAccountID simply gets the most recent status by the given account.  	// The given slice 'status' pointer will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned diff --git a/internal/db/pg/blocks.go b/internal/db/pg/blocks.go new file mode 100644 index 000000000..a6fc1f859 --- /dev/null +++ b/internal/db/pg/blocks.go @@ -0,0 +1,67 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"github.com/go-pg/pg/v10" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (ps *postgresService) GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) { +	blocks := []*gtsmodel.Block{} + +	fq := ps.conn.Model(&blocks). +		Where("block.account_id = ?", accountID). +		Relation("TargetAccount"). +		Order("block.id DESC") + +	if maxID != "" { +		fq = fq.Where("block.id < ?", maxID) +	} + +	if sinceID != "" { +		fq = fq.Where("block.id > ?", sinceID) +	} + +	if limit > 0 { +		fq = fq.Limit(limit) +	} + +	err := fq.Select() +	if err != nil { +		if err == pg.ErrNoRows { +			return nil, "", "", db.ErrNoEntries{} +		} +		return nil, "", "", err +	} + +	if len(blocks) == 0 { +		return nil, "", "", db.ErrNoEntries{} +	} + +	accounts := []*gtsmodel.Account{} +	for _, b := range blocks { +		accounts = append(accounts, b.TargetAccount) +	} + +	nextMaxID := blocks[len(blocks)-1].ID +	prevMinID := blocks[0].ID +	return accounts, nextMaxID, prevMinID, 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/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 02a5dfd72..3562e7f4c 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -129,6 +129,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			}  		}  	case gtsmodel.ActivityStreamsFollow: +		// FOLLOW SOMETHING  		follow, ok := asType.(vocab.ActivityStreamsFollow)  		if !ok {  			return errors.New("could not convert type to follow") @@ -156,6 +157,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			ReceivingAccount: targetAcct,  		}  	case gtsmodel.ActivityStreamsLike: +		// LIKE SOMETHING  		like, ok := asType.(vocab.ActivityStreamsLike)  		if !ok {  			return errors.New("could not convert type to like") @@ -182,6 +184,34 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  			GTSModel:         fave,  			ReceivingAccount: targetAcct,  		} +	case gtsmodel.ActivityStreamsBlock: +		// BLOCK SOMETHING +		blockable, ok := asType.(vocab.ActivityStreamsBlock) +		if !ok { +			return errors.New("could not convert type to block") +		} + +		block, err := f.typeConverter.ASBlockToBlock(blockable) +		if err != nil { +			return fmt.Errorf("could not convert Block to gts model block") +		} + +		newID, err := id.NewULID() +		if err != nil { +			return err +		} +		block.ID = newID + +		if err := f.db.Put(block); err != nil { +			return fmt.Errorf("database error inserting block: %s", err) +		} + +		fromFederatorChan <- gtsmodel.FromFederator{ +			APObjectType:     gtsmodel.ActivityStreamsBlock, +			APActivityType:   gtsmodel.ActivityStreamsCreate, +			GTSModel:         block, +			ReceivingAccount: targetAcct, +		}  	}  	return nil  } diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go index fe7160021..51b20151a 100644 --- a/internal/federation/federatingdb/owns.go +++ b/internal/federation/federatingdb/owns.go @@ -39,16 +39,15 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			"id":   id.String(),  		},  	) -	l.Debugf("entering OWNS function with id %s", id.String()) +	l.Tracef("entering OWNS function with id %s", id.String())  	// if the id host isn't this instance host, we don't own this IRI  	if id.Host != f.config.Host { -		l.Debugf("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host) +		l.Tracef("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host)  		return false, nil  	}  	// apparently it belongs to this host, so what *is* it? -  	// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS  	if util.IsStatusesPath(id) {  		_, uid, err := util.ParseStatusesPath(id) @@ -63,11 +62,10 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			// an actual error happened  			return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)  		} -		l.Debug("we DO own this") +		l.Debugf("we own url %s", id.String())  		return true, nil  	} -	// check if it's a user, eg /users/example_username  	if util.IsUserPath(id) {  		username, err := util.ParseUserPath(id)  		if err != nil { @@ -81,7 +79,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			// an actual error happened  			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)  		} -		l.Debug("we DO own this") +		l.Debugf("we own url %s", id.String())  		return true, nil  	} @@ -98,7 +96,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			// an actual error happened  			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)  		} -		l.Debug("we DO own this") +		l.Debugf("we own url %s", id.String())  		return true, nil  	} @@ -115,7 +113,57 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			// an actual error happened  			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)  		} -		l.Debug("we DO own this") +		l.Debugf("we own url %s", id.String()) +		return true, nil +	} + +	if util.IsLikePath(id) { +		username, likeID, err := util.ParseLikedPath(id) +		if err != nil { +			return false, fmt.Errorf("error parsing like path for url %s: %s", id.String(), err) +		} +		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { +			if _, ok := err.(db.ErrNoEntries); ok { +				// there are no entries for this username +				return false, nil +			} +			// an actual error happened +			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) +		} +		if err := f.db.GetByID(likeID, >smodel.StatusFave{}); err != nil { +			if _, ok := err.(db.ErrNoEntries); ok { +				// there are no entries +				return false, nil +			} +			// an actual error happened +			return false, fmt.Errorf("database error fetching like with id %s: %s", likeID, err) +		} +		l.Debugf("we own url %s", id.String()) +		return true, nil +	} + +	if util.IsBlockPath(id) { +		username, blockID, err := util.ParseBlockPath(id) +		if err != nil { +			return false, fmt.Errorf("error parsing block path for url %s: %s", id.String(), err) +		} +		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { +			if _, ok := err.(db.ErrNoEntries); ok { +				// there are no entries for this username +				return false, nil +			} +			// an actual error happened +			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) +		} +		if err := f.db.GetByID(blockID, >smodel.Block{}); err != nil { +			if _, ok := err.(db.ErrNoEntries); ok { +				// there are no entries +				return false, nil +			} +			// an actual error happened +			return false, fmt.Errorf("database error fetching block with id %s: %s", blockID, err) +		} +		l.Debugf("we own url %s", id.String())  		return true, nil  	} diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index 3feee6457..063adaedd 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -85,6 +85,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)  			// UNDO LIKE  		case string(gtsmodel.ActivityStreamsAnnounce):  			// UNDO BOOST/REBLOG/ANNOUNCE +		case string(gtsmodel.ActivityStreamsBlock): +			// UNDO BLOCK +			ASBlock, ok := iter.GetType().(vocab.ActivityStreamsBlock) +			if !ok { +				return errors.New("UNDO: couldn't parse block into vocab.ActivityStreamsBlock") +			} +			// make sure the actor owns the follow +			if !sameActor(undo.GetActivityStreamsActor(), ASBlock.GetActivityStreamsActor()) { +				return errors.New("UNDO: block actor and activity actor not the same") +			} +			// convert the block to something we can understand +			gtsBlock, err := f.typeConverter.ASBlockToBlock(ASBlock) +			if err != nil { +				return fmt.Errorf("UNDO: error converting asblock to gtsblock: %s", err) +			} +			// make sure the addressee of the original block is the same as whatever inbox this landed in +			if gtsBlock.TargetAccountID != targetAcct.ID { +				return errors.New("UNDO: block object account and inbox account were not the same") +			} +			// delete any existing BLOCK +			if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsBlock.URI}}, >smodel.Block{}); err != nil { +				return fmt.Errorf("UNDO: db error removing block: %s", err) +			} +			l.Debug("block undone") +			return nil  		}  	} diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index ed3c252d9..28f4c5a21 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -139,7 +139,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e  		// ID might already be set on an announce we've created, so check it here and return it if it is  		announce, ok := t.(vocab.ActivityStreamsAnnounce)  		if !ok { -			return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsAnnounce") +			return nil, errors.New("newid: announce couldn't be parsed into vocab.ActivityStreamsAnnounce")  		}  		idProp := announce.GetJSONLDId()  		if idProp != nil { @@ -152,7 +152,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e  		// ID might already be set on an update we've created, so check it here and return it if it is  		update, ok := t.(vocab.ActivityStreamsUpdate)  		if !ok { -			return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsUpdate") +			return nil, errors.New("newid: update couldn't be parsed into vocab.ActivityStreamsUpdate")  		}  		idProp := update.GetJSONLDId()  		if idProp != nil { @@ -160,6 +160,32 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e  				return idProp.GetIRI(), nil  			}  		} +	case gtsmodel.ActivityStreamsBlock: +		// BLOCK +		// ID might already be set on a block we've created, so check it here and return it if it is +		block, ok := t.(vocab.ActivityStreamsBlock) +		if !ok { +			return nil, errors.New("newid: block couldn't be parsed into vocab.ActivityStreamsBlock") +		} +		idProp := block.GetJSONLDId() +		if idProp != nil { +			if idProp.IsIRI() { +				return idProp.GetIRI(), nil +			} +		} +	case gtsmodel.ActivityStreamsUndo: +		// UNDO +		// ID might already be set on an undo we've created, so check it here and return it if it is +		undo, ok := t.(vocab.ActivityStreamsUndo) +		if !ok { +			return nil, errors.New("newid: undo couldn't be parsed into vocab.ActivityStreamsUndo") +		} +		idProp := undo.GetJSONLDId() +		if idProp != nil { +			if idProp.IsIRI() { +				return idProp.GetIRI(), nil +			} +		}  	}  	// fallback default behavior: just return a random ULID after our protocol and host diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index c0943a328..1acdb6cb1 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -243,8 +243,8 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er  			return true, nil  		} -		a := >smodel.Account{} -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { +		requestingAccount := >smodel.Account{} +		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil {  			_, ok := err.(db.ErrNoEntries)  			if ok {  				// we don't have an entry for this account so it's not blocked @@ -253,11 +253,13 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er  			}  			return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)  		} -		blocked, err := f.db.Blocked(requestedAccount.ID, a.ID) -		if err != nil { -			return false, fmt.Errorf("error checking account blocks: %s", err) -		} -		if blocked { + +		// check if requested account blocks requesting account +		if err := f.db.GetWhere([]db.Where{ +			{Key: "account_id", Value: requestedAccount.ID}, +			{Key: "target_account_id", Value: requestingAccount.ID}, +		}, >smodel.Block{}); err == nil { +			// a block exists  			return true, nil  		}  	} diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go index 27b39727c..32afede55 100644 --- a/internal/gtsmodel/block.go +++ b/internal/gtsmodel/block.go @@ -11,9 +11,11 @@ type Block struct {  	// When was this block updated  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Who created this block? -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:has-one"`  	// Who is targeted by this block? -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// Activitypub URI for this block -	URI string +	URI string `pg:",notnull"`  } 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.go b/internal/processing/account.go index ec58846d1..f722c88eb 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -59,3 +59,11 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou  func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {  	return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)  } + +func (p *processor) AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { +	return p.accountProcessor.BlockCreate(authed.Account, targetAccountID) +} + +func (p *processor) AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { +	return p.accountProcessor.BlockRemove(authed.Account, targetAccountID) +} diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index efdac5d3e..7b8910149 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -59,6 +59,11 @@ type Processor interface {  	FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)  	// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.  	FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) +	// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local. +	BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) +	// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local. +	BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) +  	// UpdateHeader does the dirty work of checking the header part of an account update form,  	// parsing and checking the image, and doing the necessary updates in the database for this to become  	// the account's new header image. diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go new file mode 100644 index 000000000..79ce03805 --- /dev/null +++ b/internal/processing/account/createblock.go @@ -0,0 +1,155 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package account + +import ( +	"fmt" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { +	// make sure the target account actually exists in our db +	targetAcct := >smodel.Account{} +	if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { +		if _, ok := err.(db.ErrNoEntries); ok { +			return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: account %s not found in the db: %s", targetAccountID, err)) +		} +	} + +	// if requestingAccount already blocks target account, we don't need to do anything +	block := >smodel.Block{} +	if err := p.db.GetWhere([]db.Where{ +		{Key: "account_id", Value: requestingAccount.ID}, +		{Key: "target_account_id", Value: targetAccountID}, +	}, block); err == nil { +		// block already exists, just return relationship +		return p.RelationshipGet(requestingAccount, targetAccountID) +	} + +	// make the block +	newBlockID, err := id.NewULID() +	if err != nil { +		return nil, gtserror.NewErrorInternalError(err) +	} +	block.ID = newBlockID +	block.AccountID = requestingAccount.ID +	block.Account = requestingAccount +	block.TargetAccountID = targetAccountID +	block.TargetAccount = targetAcct +	block.URI = util.GenerateURIForBlock(requestingAccount.Username, p.config.Protocol, p.config.Host, newBlockID) + +	// whack it in the database +	if err := p.db.Put(block); err != nil { +		return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err)) +	} + +	// clear any follows or follow requests from the blocked account to the target account -- this is a simple delete +	if err := p.db.DeleteWhere([]db.Where{ +		{Key: "account_id", Value: targetAccountID}, +		{Key: "target_account_id", Value: requestingAccount.ID}, +	}, >smodel.Follow{}); err != nil { +		return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) +	} +	if err := p.db.DeleteWhere([]db.Where{ +		{Key: "account_id", Value: targetAccountID}, +		{Key: "target_account_id", Value: requestingAccount.ID}, +	}, >smodel.FollowRequest{}); err != nil { +		return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) +	} + +	// clear any follows or follow requests from the requesting account to the target account -- +	// this might require federation so we need to pass some messages around + +	// check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later) +	var frChanged bool +	var frURI string +	fr := >smodel.FollowRequest{} +	if err := p.db.GetWhere([]db.Where{ +		{Key: "account_id", Value: requestingAccount.ID}, +		{Key: "target_account_id", Value: targetAccountID}, +	}, fr); err == nil { +		frURI = fr.URI +		if err := p.db.DeleteByID(fr.ID, fr); err != nil { +			return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err)) +		} +		frChanged = true +	} + +	// now do the same thing for any existing follow +	var fChanged bool +	var fURI string +	f := >smodel.Follow{} +	if err := p.db.GetWhere([]db.Where{ +		{Key: "account_id", Value: requestingAccount.ID}, +		{Key: "target_account_id", Value: targetAccountID}, +	}, f); err == nil { +		fURI = f.URI +		if err := p.db.DeleteByID(f.ID, f); err != nil { +			return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err)) +		} +		fChanged = true +	} + +	// follow request status changed so send the UNDO activity to the channel for async processing +	if frChanged { +		p.fromClientAPI <- gtsmodel.FromClientAPI{ +			APObjectType:   gtsmodel.ActivityStreamsFollow, +			APActivityType: gtsmodel.ActivityStreamsUndo, +			GTSModel: >smodel.Follow{ +				AccountID:       requestingAccount.ID, +				TargetAccountID: targetAccountID, +				URI:             frURI, +			}, +			OriginAccount: requestingAccount, +			TargetAccount: targetAcct, +		} +	} + +	// follow status changed so send the UNDO activity to the channel for async processing +	if fChanged { +		p.fromClientAPI <- gtsmodel.FromClientAPI{ +			APObjectType:   gtsmodel.ActivityStreamsFollow, +			APActivityType: gtsmodel.ActivityStreamsUndo, +			GTSModel: >smodel.Follow{ +				AccountID:       requestingAccount.ID, +				TargetAccountID: targetAccountID, +				URI:             fURI, +			}, +			OriginAccount: requestingAccount, +			TargetAccount: targetAcct, +		} +	} + +	// handle the rest of the block process asynchronously +	p.fromClientAPI <- gtsmodel.FromClientAPI{ +		APObjectType:   gtsmodel.ActivityStreamsBlock, +		APActivityType: gtsmodel.ActivityStreamsCreate, +		GTSModel:       block, +		OriginAccount:  requestingAccount, +		TargetAccount:  targetAcct, +	} + +	return p.RelationshipGet(requestingAccount, targetAccountID) +} 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/account/removeblock.go b/internal/processing/account/removeblock.go new file mode 100644 index 000000000..03b0c6750 --- /dev/null +++ b/internal/processing/account/removeblock.go @@ -0,0 +1,67 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package account + +import ( +	"fmt" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { +	// make sure the target account actually exists in our db +	targetAcct := >smodel.Account{} +	if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { +		if _, ok := err.(db.ErrNoEntries); ok { +			return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockRemove: account %s not found in the db: %s", targetAccountID, err)) +		} +	} + +	// check if a block exists, and remove it if it does (storing the URI for later) +	var blockChanged bool +	block := >smodel.Block{} +	if err := p.db.GetWhere([]db.Where{ +		{Key: "account_id", Value: requestingAccount.ID}, +		{Key: "target_account_id", Value: targetAccountID}, +	}, block); err == nil { +		block.Account = requestingAccount +		block.TargetAccount = targetAcct +		if err := p.db.DeleteByID(block.ID, >smodel.Block{}); err != nil { +			return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err)) +		} +		blockChanged = true +	} + +	// block status changed so send the UNDO activity to the channel for async processing +	if blockChanged { +		p.fromClientAPI <- gtsmodel.FromClientAPI{ +			APObjectType:   gtsmodel.ActivityStreamsBlock, +			APActivityType: gtsmodel.ActivityStreamsUndo, +			GTSModel:       block, +			OriginAccount:  requestingAccount, +			TargetAccount:  targetAcct, +		} +	} + +	// return whatever relationship results from all this +	return p.RelationshipGet(requestingAccount, targetAccountID) +} diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go new file mode 100644 index 000000000..509600ca6 --- /dev/null +++ b/internal/processing/blocks.go @@ -0,0 +1,83 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package processing + +import ( +	"fmt" +	"net/url" + +	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/oauth" +) + +func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { +	accounts, nextMaxID, prevMinID, err := p.db.GetBlocksForAccount(authed.Account.ID, maxID, sinceID, limit) +	if err != nil { +		if _, ok := err.(db.ErrNoEntries); ok { +			// there are just no entries +			return &apimodel.BlocksResponse{ +				Accounts: []*apimodel.Account{}, +			}, nil +		} +		// there's an actual error +		return nil, gtserror.NewErrorInternalError(err) +	} + +	apiAccounts := []*apimodel.Account{} +	for _, a := range accounts { +		apiAccount, err := p.tc.AccountToMastoBlocked(a) +		if err != nil { +			continue +		} +		apiAccounts = append(apiAccounts, apiAccount) +	} + +	return p.packageBlocksResponse(apiAccounts, "/api/v1/blocks", nextMaxID, prevMinID, limit) +} + +func (p *processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { +	resp := &apimodel.BlocksResponse{ +		Accounts: []*apimodel.Account{}, +	} +	resp.Accounts = accounts + +	// prepare the next and previous links +	if len(accounts) != 0 { +		nextLink := &url.URL{ +			Scheme:   p.config.Protocol, +			Host:     p.config.Host, +			Path:     path, +			RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, nextMaxID), +		} +		next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String()) + +		prevLink := &url.URL{ +			Scheme:   p.config.Protocol, +			Host:     p.config.Host, +			Path:     path, +			RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, prevMinID), +		} +		prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String()) +		resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev) +	} + +	return resp, nil +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index cfed3b5e4..6755a9d82 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -76,7 +76,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  			}  			return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount) -  		case gtsmodel.ActivityStreamsAnnounce:  			// CREATE BOOST/ANNOUNCE  			boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) @@ -93,6 +92,25 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  			}  			return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) +		case gtsmodel.ActivityStreamsBlock: +			// CREATE BLOCK +			block, ok := clientMsg.GTSModel.(*gtsmodel.Block) +			if !ok { +				return errors.New("block was not parseable as *gtsmodel.Block") +			} + +			// 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 + +			return p.federateBlock(block)  		}  	case gtsmodel.ActivityStreamsUpdate:  		// UPDATE @@ -132,6 +150,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  				return errors.New("undo was not parseable as *gtsmodel.Follow")  			}  			return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) +		case gtsmodel.ActivityStreamsBlock: +			// UNDO BLOCK +			block, ok := clientMsg.GTSModel.(*gtsmodel.Block) +			if !ok { +				return errors.New("undo was not parseable as *gtsmodel.Block") +			} +			return p.federateUnblock(block)  		case gtsmodel.ActivityStreamsLike:  			// UNDO LIKE/FAVE  			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) @@ -530,3 +555,93 @@ func (p *processor) federateAccountUpdate(updatedAccount *gtsmodel.Account, orig  	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)  	return err  } + +func (p *processor) federateBlock(block *gtsmodel.Block) error { +	if block.Account == nil { +		a := >smodel.Account{} +		if err := p.db.GetByID(block.AccountID, a); err != nil { +			return fmt.Errorf("federateBlock: error getting block account from database: %s", err) +		} +		block.Account = a +	} + +	if block.TargetAccount == nil { +		a := >smodel.Account{} +		if err := p.db.GetByID(block.TargetAccountID, a); err != nil { +			return fmt.Errorf("federateBlock: error getting block target account from database: %s", err) +		} +		block.TargetAccount = a +	} + +	// if both accounts are local there's nothing to do here +	if block.Account.Domain == "" && block.TargetAccount.Domain == "" { +		return nil +	} + +	asBlock, err := p.tc.BlockToAS(block) +	if err != nil { +		return fmt.Errorf("federateBlock: error converting block to AS format: %s", err) +	} + +	outboxIRI, err := url.Parse(block.Account.OutboxURI) +	if err != nil { +		return fmt.Errorf("federateBlock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err) +	} + +	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asBlock) +	return err +} + +func (p *processor) federateUnblock(block *gtsmodel.Block) error { +	if block.Account == nil { +		a := >smodel.Account{} +		if err := p.db.GetByID(block.AccountID, a); err != nil { +			return fmt.Errorf("federateUnblock: error getting block account from database: %s", err) +		} +		block.Account = a +	} + +	if block.TargetAccount == nil { +		a := >smodel.Account{} +		if err := p.db.GetByID(block.TargetAccountID, a); err != nil { +			return fmt.Errorf("federateUnblock: error getting block target account from database: %s", err) +		} +		block.TargetAccount = a +	} + +	// if both accounts are local there's nothing to do here +	if block.Account.Domain == "" && block.TargetAccount.Domain == "" { +		return nil +	} + +	asBlock, err := p.tc.BlockToAS(block) +	if err != nil { +		return fmt.Errorf("federateUnblock: error converting block to AS format: %s", err) +	} + +	targetAccountURI, err := url.Parse(block.TargetAccount.URI) +	if err != nil { +		return fmt.Errorf("federateUnblock: error parsing uri %s: %s", block.TargetAccount.URI, err) +	} + +	// create an Undo and set the appropriate actor on it +	undo := streams.NewActivityStreamsUndo() +	undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor()) + +	// Set the block as the 'object' property. +	undoObject := streams.NewActivityStreamsObjectProperty() +	undoObject.AppendActivityStreamsBlock(asBlock) +	undo.SetActivityStreamsObject(undoObject) + +	// Set the To of the undo as the target of the block +	undoTo := streams.NewActivityStreamsToProperty() +	undoTo.AppendIRI(targetAccountURI) +	undo.SetActivityStreamsTo(undoTo) + +	outboxIRI, err := url.Parse(block.Account.OutboxURI) +	if err != nil { +		return fmt.Errorf("federateUnblock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err) +	} +	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo) +	return err +} diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 36568cf13..94a4e5af8 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -34,7 +34,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  		"federatorMsg": fmt.Sprintf("%+v", federatorMsg),  	}) -	l.Debug("entering function PROCESS FROM FEDERATOR") +	l.Trace("entering function PROCESS FROM FEDERATOR")  	switch federatorMsg.APActivityType {  	case gtsmodel.ActivityStreamsCreate: @@ -47,7 +47,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  				return errors.New("note was not parseable as *gtsmodel.Status")  			} -			l.Debug("will now derefence incoming status") +			l.Trace("will now derefence incoming status")  			if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {  				return fmt.Errorf("error dereferencing status from federator: %s", err)  			} @@ -70,7 +70,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  				return errors.New("profile was not parseable as *gtsmodel.Account")  			} -			l.Debug("will now derefence incoming account") +			l.Trace("will now derefence incoming account")  			if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {  				return fmt.Errorf("error dereferencing account from federator: %s", err)  			} @@ -127,6 +127,22 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  			if err := p.notifyAnnounce(incomingAnnounce); err != nil {  				return err  			} +		case gtsmodel.ActivityStreamsBlock: +			// CREATE A BLOCK +			block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) +			if !ok { +				return errors.New("block was not parseable as *gtsmodel.Block") +			} + +			// 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  		}  	case gtsmodel.ActivityStreamsUpdate:  		// UPDATE @@ -138,7 +154,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  				return errors.New("profile was not parseable as *gtsmodel.Account")  			} -			l.Debug("will now derefence incoming account") +			l.Trace("will now derefence incoming account")  			if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {  				return fmt.Errorf("error dereferencing account from federator: %s", err)  			} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index bb4cd2da7..a09a370e9 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -82,6 +82,10 @@ type Processor interface {  	AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)  	// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.  	AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) +	// AccountBlockCreate handles the creation of a block from authed account to target account, either remote or local. +	AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) +	// AccountBlockRemove handles the removal of a block from authed account to target account, either remote or local. +	AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)  	// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.  	AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) @@ -99,6 +103,9 @@ type Processor interface {  	// AppCreate processes the creation of a new API application  	AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) +	// BlocksGet returns a list of accounts blocked by the requesting account. +	BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) +  	// FileGet handles the fetching of a media attachment file via the fileserver.  	FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) @@ -275,14 +282,14 @@ func (p *processor) Start() error {  		for {  			select {  			case clientMsg := <-p.fromClientAPI: -				p.log.Infof("received message FROM client API: %+v", clientMsg) +				p.log.Tracef("received message FROM client API: %+v", clientMsg)  				go func() {  					if err := p.processFromClientAPI(clientMsg); err != nil {  						p.log.Error(err)  					}  				}()  			case federatorMsg := <-p.fromFederator: -				p.log.Infof("received message FROM federator: %+v", federatorMsg) +				p.log.Tracef("received message FROM federator: %+v", federatorMsg)  				go func() {  					if err := p.processFromFederator(federatorMsg); err != nil {  						p.log.Error(err) 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 <http://www.gnu.org/licenses/>. +*/ +  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 <http://www.gnu.org/licenses/>. +*/ +  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 <http://www.gnu.org/licenses/>. +*/ +  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 <http://www.gnu.org/licenses/>. +*/ +  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 <http://www.gnu.org/licenses/>. +*/ +  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..cf0b0b617 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 <http://www.gnu.org/licenses/>. +*/ +  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/asinterfaces.go b/internal/typeutils/asinterfaces.go index aae3ecf93..d0b1cf617 100644 --- a/internal/typeutils/asinterfaces.go +++ b/internal/typeutils/asinterfaces.go @@ -111,6 +111,15 @@ type Likeable interface {  	withObject  } +// Blockable represents the minimum interface for an activitystreams 'block' activity. +type Blockable interface { +	withJSONLDId +	withTypeName + +	withActor +	withObject +} +  // Announceable represents the minimum interface for an activitystreams 'announce' activity.  type Announceable interface {  	withJSONLDId diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index dc58346fb..394de6e82 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -426,6 +426,41 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error  	}, nil  } +func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error) { +	idProp := blockable.GetJSONLDId() +	if idProp == nil || !idProp.IsIRI() { +		return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri") +	} +	uri := idProp.GetIRI().String() + +	origin, err := extractActor(blockable) +	if err != nil { +		return nil, errors.New("ASBlockToBlock: error extracting actor property from block") +	} +	originAccount := >smodel.Account{} +	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { +		return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", origin.String(), err) +	} + +	target, err := extractObject(blockable) +	if err != nil { +		return nil, errors.New("ASBlockToBlock: error extracting object property from block") +	} + +	targetAccount := >smodel.Account{} +	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String(), CaseInsensitive: true}}, targetAccount); err != nil { +		return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", target.String(), err) +	} + +	return >smodel.Block{ +		AccountID:       originAccount.ID, +		Account:         originAccount, +		TargetAccountID: targetAccount.ID, +		TargetAccount:   targetAccount, +		URI:             uri, +	}, nil +} +  func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) {  	status := >smodel.Status{}  	isNew := true diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 30c1c7d2c..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. @@ -104,6 +108,8 @@ type TypeConverter interface {  	ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)  	// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.  	ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) +	// ASBlockToBlock converts a remote activity streams 'block' representation into a gts model block. +	ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)  	// ASAnnounceToStatus converts an activitystreams 'announce' into a status.  	//  	// The returned bool indicates whether this status is new (true) or not new (false). @@ -124,6 +130,11 @@ type TypeConverter interface {  	// AccountToAS converts a gts model account into an activity streams person, suitable for federation  	AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) +	// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation. +	// +	// The returned account will just have the Type, Username, PublicKey, and ID properties set. This is +	// suitable for serving to requesters to whom we want to give as little information as possible because +	// we don't trust them (yet).  	AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)  	// StatusToAS converts a gts model status into an activity streams note, suitable for federation  	StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) @@ -137,6 +148,8 @@ type TypeConverter interface {  	FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error)  	// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation  	BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) +	// BlockToAS converts a gts model block into an activityStreams BLOCK, suitable for federation. +	BlockToAS(block *gtsmodel.Block) (vocab.ActivityStreamsBlock, error)  	/*  		INTERNAL (gts) MODEL TO INTERNAL MODEL 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/internaltoas.go b/internal/typeutils/internaltoas.go index 1760b8731..809cedc45 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -780,3 +780,73 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou  	return announce, nil  } + +/* +	we want to end up with something like this: + +	{ +		"@context": "https://www.w3.org/ns/activitystreams", +		"actor": "https://example.org/users/some_user", +		"id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK", +		"object":"https://some_other.instance/users/some_other_user", +		"type":"Block" +	} +*/ +func (c *converter) BlockToAS(b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) { +	if b.Account == nil { +		a := >smodel.Account{} +		if err := c.db.GetByID(b.AccountID, a); err != nil { +			return nil, fmt.Errorf("BlockToAS: error getting block account from database: %s", err) +		} +		b.Account = a +	} + +	if b.TargetAccount == nil { +		a := >smodel.Account{} +		if err := c.db.GetByID(b.TargetAccountID, a); err != nil { +			return nil, fmt.Errorf("BlockToAS: error getting block target account from database: %s", err) +		} +		b.TargetAccount = a +	} + +	// create the block +	block := streams.NewActivityStreamsBlock() + +	// set the actor property to the block-ing account's URI +	actorProp := streams.NewActivityStreamsActorProperty() +	actorIRI, err := url.Parse(b.Account.URI) +	if err != nil { +		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.Account.URI, err) +	} +	actorProp.AppendIRI(actorIRI) +	block.SetActivityStreamsActor(actorProp) + +	// set the ID property to the blocks's URI +	idProp := streams.NewJSONLDIdProperty() +	idIRI, err := url.Parse(b.URI) +	if err != nil { +		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.URI, err) +	} +	idProp.Set(idIRI) +	block.SetJSONLDId(idProp) + +	// set the object property to the target account's URI +	objectProp := streams.NewActivityStreamsObjectProperty() +	targetIRI, err := url.Parse(b.TargetAccount.URI) +	if err != nil { +		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err) +	} +	objectProp.AppendIRI(targetIRI) +	block.SetActivityStreamsObject(objectProp) + +	// set the TO property to the target account's IRI +	toProp := streams.NewActivityStreamsToProperty() +	toIRI, err := url.Parse(b.TargetAccount.URI) +	if err != nil { +		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err) +	} +	toProp.AppendIRI(toIRI) +	block.SetActivityStreamsTo(toProp) + +	return block, nil +} diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 61c11b8ef..03e071981 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -150,6 +150,11 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		acct = a.Username  	} +	var suspended bool +	if !a.SuspendedAt.IsZero() { +		suspended = true +	} +  	return &model.Account{  		ID:             a.ID,  		Username:       a.Username, @@ -170,6 +175,34 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		LastStatusAt:   lastStatusAt,  		Emojis:         emojis, // TODO: implement this  		Fields:         fields, +		Suspended:      suspended, +	}, 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 +	} + +	var suspended bool +	if !a.SuspendedAt.IsZero() { +		suspended = true +	} + +	return &model.Account{ +		ID:          a.ID, +		Username:    a.Username, +		Acct:        acct, +		DisplayName: a.DisplayName, +		Bot:         a.Bot, +		CreatedAt:   a.CreatedAt.Format(time.RFC3339), +		URL:         a.URL, +		Suspended:   suspended,  	}, nil  } diff --git a/internal/util/regexes.go b/internal/util/regexes.go index 25d90417c..1ca34708f 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -104,4 +104,9 @@ var (  	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH  	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1  	statusesPathRegex = regexp.MustCompile(statusesPathRegexString) + +	blockPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, BlocksPath, ulidRegexString) +	// blockPathRegex parses a path that validates and captures the username part and the ulid part +	// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH +	blockPathRegex = regexp.MustCompile(blockPathRegexString)  ) diff --git a/internal/util/uri.go b/internal/util/uri.go index 5eb291628..370b2fa6f 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -50,6 +50,8 @@ const (  	FollowPath = "follow"  	// UpdatePath is used to generate the URI for an account update  	UpdatePath = "updates" +	// BlocksPath is used to generate the URI for a block +	BlocksPath = "blocks"  )  // APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains @@ -124,6 +126,12 @@ func GenerateURIForUpdate(username string, protocol string, host string, thisUpd  	return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)  } +// GenerateURIForBlock returns the AP URI for a new block activity -- something like: +// https://example.org/users/whatever_user/blocks/01F7XTH1QGBAPMGF49WJZ91XGC +func GenerateURIForBlock(username string, protocol string, host string, thisBlockID string) string { +	return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, BlocksPath, thisBlockID) +} +  // GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.  func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {  	// The below URLs are used for serving web requests @@ -214,6 +222,11 @@ func IsPublicKeyPath(id *url.URL) bool {  	return userPublicKeyPathRegex.MatchString(id.Path)  } +// IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK +func IsBlockPath(id *url.URL) bool { +	return blockPathRegex.MatchString(id.Path) +} +  // ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS  func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {  	matches := statusesPathRegex.FindStringSubmatch(id.Path) @@ -292,3 +305,15 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {  	ulid = matches[2]  	return  } + +// ParseBlockPath returns the username and ulid from a path such as /users/example_username/blocks/SOME_ULID_OF_A_BLOCK +func ParseBlockPath(id *url.URL) (username string, ulid string, err error) { +	matches := blockPathRegex.FindStringSubmatch(id.Path) +	if len(matches) != 3 { +		err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches)) +		return +	} +	username = matches[1] +	ulid = matches[2] +	return +}  | 
