mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 08:22:27 -05:00 
			
		
		
		
	Block/unblock (#96)
* remote + local block logic, incl. federation * improve blocking stuff * fiddle with display of blocked profiles * go fmt
This commit is contained in:
		
					parent
					
						
							
								c7da64922f
							
						
					
				
			
			
				commit
				
					
						846057f0d6
					
				
			
		
					 45 changed files with 1405 additions and 63 deletions
				
			
		|  | @ -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) |     * [ ] /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/follow POST                  (Follow this account) | ||||||
|     * [x] /api/v1/accounts/:id/unfollow POST                (Unfollow this account) |     * [x] /api/v1/accounts/:id/unfollow POST                (Unfollow this account) | ||||||
|     * [ ] /api/v1/accounts/:id/block POST                   (Block this account) |     * [x] /api/v1/accounts/:id/block POST                   (Block this account) | ||||||
|     * [ ] /api/v1/accounts/:id/unblock POST                 (Unblock 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/mute POST                    (Mute this account) | ||||||
|     * [ ] /api/v1/accounts/:id/unmute POST                  (Unmute this account) |     * [ ] /api/v1/accounts/:id/unmute POST                  (Unmute this account) | ||||||
|     * [ ] /api/v1/accounts/:id/pin POST                     (Feature this account on profile) |     * [ ] /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) |     * [x] /api/v1/favourites GET                            (See faved statuses) | ||||||
|   * [ ] Mutes |   * [ ] Mutes | ||||||
|     * [ ] /api/v1/mutes GET                                 (See list of muted accounts) |     * [ ] /api/v1/mutes GET                                 (See list of muted accounts) | ||||||
|   * [ ] Blocks |   * [x] Blocks | ||||||
|     * [ ] /api/v1/blocks GET                                (See list of blocked accounts) |     * [x] /api/v1/blocks GET                                (See list of blocked accounts) | ||||||
|   * [ ] Domain Blocks |   * [ ] Domain Blocks | ||||||
|     * [x] /api/v1/domain_blocks GET                         (See list of domain blocks) |     * [x] /api/v1/domain_blocks GET                         (See list of domain blocks) | ||||||
|     * [x] /api/v1/domain_blocks POST                        (Create a domain block) |     * [x] /api/v1/domain_blocks POST                        (Create a domain block) | ||||||
|  |  | ||||||
|  | @ -61,10 +61,14 @@ const ( | ||||||
| 	GetFollowingPath = BasePathWithID + "/following" | 	GetFollowingPath = BasePathWithID + "/following" | ||||||
| 	// GetRelationshipsPath is for showing an account's relationship with other accounts | 	// GetRelationshipsPath is for showing an account's relationship with other accounts | ||||||
| 	GetRelationshipsPath = BasePath + "/relationships" | 	GetRelationshipsPath = BasePath + "/relationships" | ||||||
| 	// PostFollowPath is for POSTing new follows to, and updating existing follows | 	// FollowPath is for POSTing new follows to, and updating existing follows | ||||||
| 	PostFollowPath = BasePathWithID + "/follow" | 	FollowPath = BasePathWithID + "/follow" | ||||||
| 	// PostUnfollowPath is for POSTing an unfollow | 	// UnfollowPath is for POSTing an unfollow | ||||||
| 	PostUnfollowPath = BasePathWithID + "/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 | // 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 | // Route attaches all routes from this module to the given router | ||||||
| func (m *Module) Route(r router.Router) error { | func (m *Module) Route(r router.Router) error { | ||||||
|  | 	// create account | ||||||
| 	r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) | 	r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) | ||||||
|  | 
 | ||||||
|  | 	// get account | ||||||
| 	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) | 	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) | ||||||
|  | 
 | ||||||
|  | 	// modify account | ||||||
| 	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) | 	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) | ||||||
|  | 
 | ||||||
|  | 	// get account's statuses | ||||||
| 	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) | 	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) | ||||||
|  | 
 | ||||||
|  | 	// get following or followers | ||||||
| 	r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) | 	r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) | ||||||
| 	r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) | 	r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) | ||||||
|  | 
 | ||||||
|  | 	// get relationship with account | ||||||
| 	r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) | 	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 | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										49
									
								
								internal/api/client/account/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/api/client/account/block.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								internal/api/client/account/unblock.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/api/client/account/unblock.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								internal/api/client/blocks/blocks.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/api/client/blocks/blocks.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								internal/api/client/blocks/blocksget.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/api/client/blocks/blocksget.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
|  | @ -67,23 +67,23 @@ sendLoop: | ||||||
| 		select { | 		select { | ||||||
| 		case m := <-stream.Messages: | 		case m := <-stream.Messages: | ||||||
| 			// we've got a streaming message!! | 			// 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 { | 			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 | 				// if something is wrong we want to bail and drop the connection -- the client will create a new one | ||||||
| 				break sendLoop | 				break sendLoop | ||||||
| 			} | 			} | ||||||
| 			l.Debug("wrote message into websocket connection") | 			l.Trace("wrote message into websocket connection") | ||||||
| 		case <-t.C: | 		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 { | 			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 | 				// if something is wrong we want to bail and drop the connection -- the client will create a new one | ||||||
| 				break sendLoop | 				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") | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								internal/api/model/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/api/model/block.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -14,6 +14,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/app" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/app" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" | 	"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/emoji" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" | 	"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) | 	securityModule := security.New(c, dbService, log) | ||||||
| 	streamingModule := streaming.New(c, processor, log) | 	streamingModule := streaming.New(c, processor, log) | ||||||
| 	favouritesModule := favourites.New(c, processor, log) | 	favouritesModule := favourites.New(c, processor, log) | ||||||
|  | 	blocksModule := blocks.New(c, processor, log) | ||||||
| 
 | 
 | ||||||
| 	apis := []api.ClientModule{ | 	apis := []api.ClientModule{ | ||||||
| 		// modules with middleware go first | 		// modules with middleware go first | ||||||
|  | @ -170,6 +172,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log | ||||||
| 		listsModule, | 		listsModule, | ||||||
| 		streamingModule, | 		streamingModule, | ||||||
| 		favouritesModule, | 		favouritesModule, | ||||||
|  | 		blocksModule, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, m := range apis { | 	for _, m := range apis { | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/app" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/app" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" | 	"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/emoji" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" | 	"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) | 	securityModule := security.New(c, dbService, log) | ||||||
| 	streamingModule := streaming.New(c, processor, log) | 	streamingModule := streaming.New(c, processor, log) | ||||||
| 	favouritesModule := favourites.New(c, processor, log) | 	favouritesModule := favourites.New(c, processor, log) | ||||||
|  | 	blocksModule := blocks.New(c, processor, log) | ||||||
| 
 | 
 | ||||||
| 	apis := []api.ClientModule{ | 	apis := []api.ClientModule{ | ||||||
| 		// modules with middleware go first | 		// modules with middleware go first | ||||||
|  | @ -115,6 +117,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log | ||||||
| 		listsModule, | 		listsModule, | ||||||
| 		streamingModule, | 		streamingModule, | ||||||
| 		favouritesModule, | 		favouritesModule, | ||||||
|  | 		blocksModule, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, m := range apis { | 	for _, m := range apis { | ||||||
|  |  | ||||||
|  | @ -159,6 +159,8 @@ type DB interface { | ||||||
| 	// In case of no entries, a 'no entries' error will be returned | 	// 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) | 	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. | 	// 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. | 	// 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 | 	// In case of no entries, a 'no entries' error will be returned | ||||||
|  |  | ||||||
							
								
								
									
										67
									
								
								internal/db/pg/blocks.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/db/pg/blocks.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -393,6 +393,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse | ||||||
| 		announce.Language = boostedStatus.Language | 		announce.Language = boostedStatus.Language | ||||||
| 		announce.Text = boostedStatus.Text | 		announce.Text = boostedStatus.Text | ||||||
| 		announce.BoostOfID = boostedStatus.ID | 		announce.BoostOfID = boostedStatus.ID | ||||||
|  | 		announce.BoostOfAccountID = boostedStatus.AccountID | ||||||
| 		announce.Visibility = boostedStatus.Visibility | 		announce.Visibility = boostedStatus.Visibility | ||||||
| 		announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced | 		announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced | ||||||
| 		announce.GTSBoostedStatus = boostedStatus | 		announce.GTSBoostedStatus = boostedStatus | ||||||
|  | @ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse | ||||||
| 	announce.Language = boostedStatus.Language | 	announce.Language = boostedStatus.Language | ||||||
| 	announce.Text = boostedStatus.Text | 	announce.Text = boostedStatus.Text | ||||||
| 	announce.BoostOfID = boostedStatus.ID | 	announce.BoostOfID = boostedStatus.ID | ||||||
|  | 	announce.BoostOfAccountID = boostedStatus.AccountID | ||||||
| 	announce.Visibility = boostedStatus.Visibility | 	announce.Visibility = boostedStatus.Visibility | ||||||
| 	announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced | 	announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced | ||||||
| 	announce.GTSBoostedStatus = boostedStatus | 	announce.GTSBoostedStatus = boostedStatus | ||||||
|  |  | ||||||
|  | @ -129,6 +129,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	case gtsmodel.ActivityStreamsFollow: | 	case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 		// FOLLOW SOMETHING | ||||||
| 		follow, ok := asType.(vocab.ActivityStreamsFollow) | 		follow, ok := asType.(vocab.ActivityStreamsFollow) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return errors.New("could not convert type to follow") | 			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, | 			ReceivingAccount: targetAcct, | ||||||
| 		} | 		} | ||||||
| 	case gtsmodel.ActivityStreamsLike: | 	case gtsmodel.ActivityStreamsLike: | ||||||
|  | 		// LIKE SOMETHING | ||||||
| 		like, ok := asType.(vocab.ActivityStreamsLike) | 		like, ok := asType.(vocab.ActivityStreamsLike) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return errors.New("could not convert type to like") | 			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, | 			GTSModel:         fave, | ||||||
| 			ReceivingAccount: targetAcct, | 			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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -39,16 +39,15 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
| 			"id":   id.String(), | 			"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 the id host isn't this instance host, we don't own this IRI | ||||||
| 	if id.Host != f.config.Host { | 	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 | 		return false, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// apparently it belongs to this host, so what *is* it? | 	// 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 | 	// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS | ||||||
| 	if util.IsStatusesPath(id) { | 	if util.IsStatusesPath(id) { | ||||||
| 		_, uid, err := util.ParseStatusesPath(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 | 			// an actual error happened | ||||||
| 			return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err) | 			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 | 		return true, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check if it's a user, eg /users/example_username |  | ||||||
| 	if util.IsUserPath(id) { | 	if util.IsUserPath(id) { | ||||||
| 		username, err := util.ParseUserPath(id) | 		username, err := util.ParseUserPath(id) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -81,7 +79,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
| 			// an actual error happened | 			// an actual error happened | ||||||
| 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | 			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 | 		return true, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -98,7 +96,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
| 			// an actual error happened | 			// an actual error happened | ||||||
| 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | 			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 | 		return true, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -115,7 +113,57 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
| 			// an actual error happened | 			// an actual error happened | ||||||
| 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | 			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 | 		return true, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -85,6 +85,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) | ||||||
| 			// UNDO LIKE | 			// UNDO LIKE | ||||||
| 		case string(gtsmodel.ActivityStreamsAnnounce): | 		case string(gtsmodel.ActivityStreamsAnnounce): | ||||||
| 			// UNDO BOOST/REBLOG/ANNOUNCE | 			// 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 | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | 		// 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) | 		announce, ok := t.(vocab.ActivityStreamsAnnounce) | ||||||
| 		if !ok { | 		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() | 		idProp := announce.GetJSONLDId() | ||||||
| 		if idProp != nil { | 		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 | 		// 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) | 		update, ok := t.(vocab.ActivityStreamsUpdate) | ||||||
| 		if !ok { | 		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() | 		idProp := update.GetJSONLDId() | ||||||
| 		if idProp != nil { | 		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 | 				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 | 	// fallback default behavior: just return a random ULID after our protocol and host | ||||||
|  |  | ||||||
|  | @ -243,8 +243,8 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er | ||||||
| 			return true, nil | 			return true, nil | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		a := >smodel.Account{} | 		requestingAccount := >smodel.Account{} | ||||||
| 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil { | ||||||
| 			_, ok := err.(db.ErrNoEntries) | 			_, ok := err.(db.ErrNoEntries) | ||||||
| 			if ok { | 			if ok { | ||||||
| 				// we don't have an entry for this account so it's not blocked | 				// 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) | 			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 { | 		// check if requested account blocks requesting account | ||||||
| 			return false, fmt.Errorf("error checking account blocks: %s", err) | 		if err := f.db.GetWhere([]db.Where{ | ||||||
| 		} | 			{Key: "account_id", Value: requestedAccount.ID}, | ||||||
| 		if blocked { | 			{Key: "target_account_id", Value: requestingAccount.ID}, | ||||||
|  | 		}, >smodel.Block{}); err == nil { | ||||||
|  | 			// a block exists | ||||||
| 			return true, nil | 			return true, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -12,8 +12,10 @@ type Block struct { | ||||||
| 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
| 	// Who created this block? | 	// 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? | 	// 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 | 	// Activitypub URI for this block | ||||||
| 	URI string | 	URI string `pg:",notnull"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -56,6 +56,8 @@ type Status struct { | ||||||
| 	InReplyToAccountID string `pg:"type:CHAR(26)"` | 	InReplyToAccountID string `pg:"type:CHAR(26)"` | ||||||
| 	// id of the status this status is a boost of | 	// id of the status this status is a boost of | ||||||
| 	BoostOfID string `pg:"type:CHAR(26)"` | 	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 | 	// cw string for this status | ||||||
| 	ContentWarning string | 	ContentWarning string | ||||||
| 	// visibility entry for this status | 	// visibility entry for this status | ||||||
|  |  | ||||||
|  | @ -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) { | func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { | ||||||
| 	return p.accountProcessor.FollowRemove(authed.Account, targetAccountID) | 	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) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -59,6 +59,11 @@ type Processor interface { | ||||||
| 	FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) | 	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 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) | 	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, | 	// 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 | 	// parsing and checking the image, and doing the necessary updates in the database for this to become | ||||||
| 	// the account's new header image. | 	// the account's new header image. | ||||||
|  |  | ||||||
							
								
								
									
										155
									
								
								internal/processing/account/createblock.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								internal/processing/account/createblock.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
|  | @ -45,9 +45,19 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str | ||||||
| 		p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) | 		p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var mastoAccount *apimodel.Account | 	var blocked bool | ||||||
| 	var err error | 	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) | 		mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount) | ||||||
| 	} else { | 	} else { | ||||||
| 		mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount) | 		mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount) | ||||||
|  |  | ||||||
							
								
								
									
										67
									
								
								internal/processing/account/removeblock.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/processing/account/removeblock.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								internal/processing/blocks.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								internal/processing/blocks.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -76,7 +76,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount) | 			return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
| 
 |  | ||||||
| 		case gtsmodel.ActivityStreamsAnnounce: | 		case gtsmodel.ActivityStreamsAnnounce: | ||||||
| 			// CREATE BOOST/ANNOUNCE | 			// CREATE BOOST/ANNOUNCE | ||||||
| 			boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) | 			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) | 			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: | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
| 		// UPDATE | 		// UPDATE | ||||||
|  | @ -132,6 +150,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | ||||||
| 				return errors.New("undo was not parseable as *gtsmodel.Follow") | 				return errors.New("undo was not parseable as *gtsmodel.Follow") | ||||||
| 			} | 			} | ||||||
| 			return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | 			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: | 		case gtsmodel.ActivityStreamsLike: | ||||||
| 			// UNDO LIKE/FAVE | 			// UNDO LIKE/FAVE | ||||||
| 			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) | 			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) | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update) | ||||||
| 	return err | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 		"federatorMsg": fmt.Sprintf("%+v", federatorMsg), | 		"federatorMsg": fmt.Sprintf("%+v", federatorMsg), | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	l.Debug("entering function PROCESS FROM FEDERATOR") | 	l.Trace("entering function PROCESS FROM FEDERATOR") | ||||||
| 
 | 
 | ||||||
| 	switch federatorMsg.APActivityType { | 	switch federatorMsg.APActivityType { | ||||||
| 	case gtsmodel.ActivityStreamsCreate: | 	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") | 				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 { | 			if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil { | ||||||
| 				return fmt.Errorf("error dereferencing status from federator: %s", err) | 				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") | 				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 { | 			if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil { | ||||||
| 				return fmt.Errorf("error dereferencing account from federator: %s", err) | 				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 { | 			if err := p.notifyAnnounce(incomingAnnounce); err != nil { | ||||||
| 				return err | 				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: | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
| 		// UPDATE | 		// UPDATE | ||||||
|  | @ -138,7 +154,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 				return errors.New("profile was not parseable as *gtsmodel.Account") | 				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 { | 			if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil { | ||||||
| 				return fmt.Errorf("error dereferencing account from federator: %s", err) | 				return fmt.Errorf("error dereferencing account from federator: %s", err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -82,6 +82,10 @@ type Processor interface { | ||||||
| 	AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) | 	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 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) | 	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 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) | 	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 processes the creation of a new API application | ||||||
| 	AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) | 	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 handles the fetching of a media attachment file via the fileserver. | ||||||
| 	FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) | 	FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) | ||||||
| 
 | 
 | ||||||
|  | @ -275,14 +282,14 @@ func (p *processor) Start() error { | ||||||
| 		for { | 		for { | ||||||
| 			select { | 			select { | ||||||
| 			case clientMsg := <-p.fromClientAPI: | 			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() { | 				go func() { | ||||||
| 					if err := p.processFromClientAPI(clientMsg); err != nil { | 					if err := p.processFromClientAPI(clientMsg); err != nil { | ||||||
| 						p.log.Error(err) | 						p.log.Error(err) | ||||||
| 					} | 					} | ||||||
| 				}() | 				}() | ||||||
| 			case federatorMsg := <-p.fromFederator: | 			case federatorMsg := <-p.fromFederator: | ||||||
| 				p.log.Infof("received message FROM federator: %+v", federatorMsg) | 				p.log.Tracef("received message FROM federator: %+v", federatorMsg) | ||||||
| 				go func() { | 				go func() { | ||||||
| 					if err := p.processFromFederator(federatorMsg); err != nil { | 					if err := p.processFromFederator(federatorMsg); err != nil { | ||||||
| 						p.log.Error(err) | 						p.log.Error(err) | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -44,7 +62,7 @@ grabloop: | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, s := range filtered { | 	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) | 			return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -79,7 +97,7 @@ grabloop: | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, s := range filtered { | 	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) | 			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 | 	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() | 	t.Lock() | ||||||
| 	defer t.Unlock() | 	defer t.Unlock() | ||||||
| 
 | 
 | ||||||
| 	postIndexEntry := &postIndexEntry{ | 	postIndexEntry := &postIndexEntry{ | ||||||
| 		statusID:         statusID, | 		statusID:         statusID, | ||||||
| 		boostOfID:        boostOfID, | 		boostOfID:        boostOfID, | ||||||
|  | 		accountID:        accountID, | ||||||
|  | 		boostOfAccountID: boostOfAccountID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return t.postIndex.insertIndexed(postIndexEntry) | 	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() | 	t.Lock() | ||||||
| 	defer t.Unlock() | 	defer t.Unlock() | ||||||
| 
 | 
 | ||||||
| 	postIndexEntry := &postIndexEntry{ | 	postIndexEntry := &postIndexEntry{ | ||||||
| 		statusID:         statusID, | 		statusID:         statusID, | ||||||
|  | 		boostOfID:        boostOfID, | ||||||
|  | 		accountID:        accountID, | ||||||
|  | 		boostOfAccountID: boostOfAccountID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	inserted, err := t.postIndex.insertIndexed(postIndexEntry) | 	inserted, err := t.postIndex.insertIndexed(postIndexEntry) | ||||||
|  |  | ||||||
|  | @ -78,6 +78,8 @@ type Manager interface { | ||||||
| 	Remove(statusID string, timelineAccountID string) (int, error) | 	Remove(statusID string, timelineAccountID string) (int, error) | ||||||
| 	// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines | 	// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines | ||||||
| 	WipeStatusFromAllTimelines(statusID string) error | 	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. | // 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") | 	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) { | 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") | 	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) { | func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) { | ||||||
|  | @ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error { | ||||||
| 	return err | 	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) { | func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) { | ||||||
| 	var t Timeline | 	var t Timeline | ||||||
| 	i, ok := m.accountTimelines.Load(timelineAccountID) | 	i, ok := m.accountTimelines.Load(timelineAccountID) | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -12,6 +30,8 @@ type postIndex struct { | ||||||
| type postIndexEntry struct { | type postIndexEntry struct { | ||||||
| 	statusID         string | 	statusID         string | ||||||
| 	boostOfID        string | 	boostOfID        string | ||||||
|  | 	accountID        string | ||||||
|  | 	boostOfAccountID string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) { | func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) { | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -207,7 +225,10 @@ func (t *timeline) prepare(statusID string) error { | ||||||
| 
 | 
 | ||||||
| 	// shove it in prepared posts as a prepared posts entry | 	// shove it in prepared posts as a prepared posts entry | ||||||
| 	preparedPostsEntry := &preparedPostsEntry{ | 	preparedPostsEntry := &preparedPostsEntry{ | ||||||
| 		statusID: statusID, | 		statusID:         gtsStatus.ID, | ||||||
|  | 		boostOfID:        gtsStatus.BoostOfID, | ||||||
|  | 		accountID:        gtsStatus.AccountID, | ||||||
|  | 		boostOfAccountID: gtsStatus.BoostOfAccountID, | ||||||
| 		prepared:         apiModelStatus, | 		prepared:         apiModelStatus, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -13,6 +31,9 @@ type preparedPosts struct { | ||||||
| 
 | 
 | ||||||
| type preparedPostsEntry struct { | type preparedPostsEntry struct { | ||||||
| 	statusID         string | 	statusID         string | ||||||
|  | 	boostOfID        string | ||||||
|  | 	accountID        string | ||||||
|  | 	boostOfAccountID string | ||||||
| 	prepared         *apimodel.Status | 	prepared         *apimodel.Status | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | @ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) { | ||||||
| 	l.Debugf("removed %d entries", removed) | 	l.Debugf("removed %d entries", removed) | ||||||
| 	return removed, nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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 | 	// 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. | 	// 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. | 	// 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. | 	// 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 | 	// 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. | 	// 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. | 	// 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. | 	// 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) | 	OldestPreparedPostID() (string, error) | ||||||
|  | @ -109,6 +109,10 @@ type Timeline interface { | ||||||
| 	// | 	// | ||||||
| 	// The returned int indicates the amount of entries that were removed. | 	// The returned int indicates the amount of entries that were removed. | ||||||
| 	Remove(statusID string) (int, error) | 	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 | // timeline fulfils the Timeline interface | ||||||
|  |  | ||||||
|  | @ -111,6 +111,15 @@ type Likeable interface { | ||||||
| 	withObject | 	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. | // Announceable represents the minimum interface for an activitystreams 'announce' activity. | ||||||
| type Announceable interface { | type Announceable interface { | ||||||
| 	withJSONLDId | 	withJSONLDId | ||||||
|  |  | ||||||
|  | @ -426,6 +426,41 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error | ||||||
| 	}, nil | 	}, 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) { | func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) { | ||||||
| 	status := >smodel.Status{} | 	status := >smodel.Status{} | ||||||
| 	isNew := true | 	isNew := true | ||||||
|  |  | ||||||
|  | @ -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. | 	// 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. | 	// In other words, this is the public record that the server has of an account. | ||||||
| 	AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) | 	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 | 	// 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 | 	// 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. | 	// (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) | 	ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) | ||||||
| 	// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave. | 	// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave. | ||||||
| 	ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) | 	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. | 	// ASAnnounceToStatus converts an activitystreams 'announce' into a status. | ||||||
| 	// | 	// | ||||||
| 	// The returned bool indicates whether this status is new (true) or not new (false). | 	// 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 converts a gts model account into an activity streams person, suitable for federation | ||||||
| 	AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) | 	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) | 	AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) | ||||||
| 	// StatusToAS converts a gts model status into an activity streams note, suitable for federation | 	// StatusToAS converts a gts model status into an activity streams note, suitable for federation | ||||||
| 	StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) | 	StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) | ||||||
|  | @ -137,6 +148,8 @@ type TypeConverter interface { | ||||||
| 	FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) | 	FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) | ||||||
| 	// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation | 	// 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) | 	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 | 		INTERNAL (gts) MODEL TO INTERNAL MODEL | ||||||
|  |  | ||||||
|  | @ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel. | ||||||
| 		Language:            s.Language, | 		Language:            s.Language, | ||||||
| 		Text:                s.Text, | 		Text:                s.Text, | ||||||
| 		BoostOfID:           s.ID, | 		BoostOfID:           s.ID, | ||||||
|  | 		BoostOfAccountID:    s.AccountID, | ||||||
| 		Visibility:          s.Visibility, | 		Visibility:          s.Visibility, | ||||||
| 		VisibilityAdvanced:  s.VisibilityAdvanced, | 		VisibilityAdvanced:  s.VisibilityAdvanced, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -780,3 +780,73 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou | ||||||
| 
 | 
 | ||||||
| 	return announce, nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -150,6 +150,11 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e | ||||||
| 		acct = a.Username | 		acct = a.Username | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	var suspended bool | ||||||
|  | 	if !a.SuspendedAt.IsZero() { | ||||||
|  | 		suspended = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return &model.Account{ | 	return &model.Account{ | ||||||
| 		ID:             a.ID, | 		ID:             a.ID, | ||||||
| 		Username:       a.Username, | 		Username:       a.Username, | ||||||
|  | @ -170,6 +175,34 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e | ||||||
| 		LastStatusAt:   lastStatusAt, | 		LastStatusAt:   lastStatusAt, | ||||||
| 		Emojis:         emojis, // TODO: implement this | 		Emojis:         emojis, // TODO: implement this | ||||||
| 		Fields:         fields, | 		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 | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -104,4 +104,9 @@ var ( | ||||||
| 	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH | 	// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH | ||||||
| 	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 | 	// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 | ||||||
| 	statusesPathRegex = regexp.MustCompile(statusesPathRegexString) | 	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) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -50,6 +50,8 @@ const ( | ||||||
| 	FollowPath = "follow" | 	FollowPath = "follow" | ||||||
| 	// UpdatePath is used to generate the URI for an account update | 	// UpdatePath is used to generate the URI for an account update | ||||||
| 	UpdatePath = "updates" | 	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 | // 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) | 	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. | // 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 { | func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs { | ||||||
| 	// The below URLs are used for serving web requests | 	// The below URLs are used for serving web requests | ||||||
|  | @ -214,6 +222,11 @@ func IsPublicKeyPath(id *url.URL) bool { | ||||||
| 	return userPublicKeyPathRegex.MatchString(id.Path) | 	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 | // 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) { | func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) { | ||||||
| 	matches := statusesPathRegex.FindStringSubmatch(id.Path) | 	matches := statusesPathRegex.FindStringSubmatch(id.Path) | ||||||
|  | @ -292,3 +305,15 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) { | ||||||
| 	ulid = matches[2] | 	ulid = matches[2] | ||||||
| 	return | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue