mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 20:02:24 -05:00 
			
		
		
		
	Follows and relationships (#27)
* Follows -- create and undo, both remote and local * Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
This commit is contained in:
		
					parent
					
						
							
								dc06e71b76
							
						
					
				
			
			
				commit
				
					
						d839f27c30
					
				
			
		
					 54 changed files with 2260 additions and 299 deletions
				
			
		|  | @ -17,12 +17,12 @@ | ||||||
|     * [x] /api/v1/accounts/:id GET                          (Get account information) |     * [x] /api/v1/accounts/:id GET                          (Get account information) | ||||||
|     * [x] /api/v1/accounts/:id/statuses GET                 (Get an account's statuses) |     * [x] /api/v1/accounts/:id/statuses GET                 (Get an account's statuses) | ||||||
|     * [x] /api/v1/accounts/:id/followers GET                (Get an account's followers) |     * [x] /api/v1/accounts/:id/followers GET                (Get an account's followers) | ||||||
|     * [ ] /api/v1/accounts/:id/following GET                (Get an account's following) |     * [x] /api/v1/accounts/:id/following GET                (Get an account's following) | ||||||
|     * [ ] /api/v1/accounts/:id/featured_tags GET            (Get an account's featured tags) |     * [ ] /api/v1/accounts/:id/featured_tags GET            (Get an account's featured tags) | ||||||
|     * [ ] /api/v1/accounts/:id/lists GET                    (Get lists containing this account) |     * [ ] /api/v1/accounts/:id/lists GET                    (Get lists containing this account) | ||||||
|     * [ ] /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) | ||||||
|     * [ ] /api/v1/accounts/:id/follow POST                  (Follow this account) |     * [x] /api/v1/accounts/:id/follow POST                  (Follow this account) | ||||||
|     * [ ] /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) |     * [ ] /api/v1/accounts/:id/block POST                   (Block this account) | ||||||
|     * [ ] /api/v1/accounts/:id/unblock POST                 (Unblock this account) |     * [ ] /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) | ||||||
|  | @ -30,7 +30,7 @@ | ||||||
|     * [ ] /api/v1/accounts/:id/pin POST                     (Feature this account on profile) |     * [ ] /api/v1/accounts/:id/pin POST                     (Feature this account on profile) | ||||||
|     * [ ] /api/v1/accounts/:id/unpin POST                   (Remove this account from profile) |     * [ ] /api/v1/accounts/:id/unpin POST                   (Remove this account from profile) | ||||||
|     * [ ] /api/v1/accounts/:id/note POST                    (Make a personal note about this account) |     * [ ] /api/v1/accounts/:id/note POST                    (Make a personal note about this account) | ||||||
|     * [ ] /api/v1/accounts/relationships GET                (Check relationships with accounts) |     * [x] /api/v1/accounts/relationships GET                (Check relationships with accounts) | ||||||
|     * [ ] /api/v1/accounts/search GET                       (Search for an account) |     * [ ] /api/v1/accounts/search GET                       (Search for an account) | ||||||
|   * [ ] Bookmarks |   * [ ] Bookmarks | ||||||
|     * [ ] /api/v1/bookmarks GET                             (See bookmarked statuses) |     * [ ] /api/v1/bookmarks GET                             (See bookmarked statuses) | ||||||
|  | @ -177,6 +177,7 @@ | ||||||
|     * [ ] 'Greedy' federation |     * [ ] 'Greedy' federation | ||||||
|     * [ ] No federation (insulate this instance from the Fediverse) |     * [ ] No federation (insulate this instance from the Fediverse) | ||||||
|       * [ ] Allowlist |       * [ ] Allowlist | ||||||
|  |   * [x] Secure HTTP signatures (creation and validation) | ||||||
| * [ ] Storage | * [ ] Storage | ||||||
|   * [x] Internal/statuses/preferences etc |   * [x] Internal/statuses/preferences etc | ||||||
|     * [x] Postgres interface |     * [x] Postgres interface | ||||||
|  |  | ||||||
|  | @ -57,6 +57,14 @@ const ( | ||||||
| 	GetStatusesPath = BasePathWithID + "/statuses" | 	GetStatusesPath = BasePathWithID + "/statuses" | ||||||
| 	// GetFollowersPath is for showing an account's followers | 	// GetFollowersPath is for showing an account's followers | ||||||
| 	GetFollowersPath = BasePathWithID + "/followers" | 	GetFollowersPath = BasePathWithID + "/followers" | ||||||
|  | 	// GetFollowingPath is for showing account's that an account follows. | ||||||
|  | 	GetFollowingPath = BasePathWithID + "/following" | ||||||
|  | 	// GetRelationshipsPath is for showing an account's relationship with other accounts | ||||||
|  | 	GetRelationshipsPath = BasePath + "/relationships" | ||||||
|  | 	// FollowPath is for POSTing new follows to, and updating existing follows | ||||||
|  | 	PostFollowPath = BasePathWithID + "/follow" | ||||||
|  | 	// PostUnfollowPath is for POSTing an unfollow | ||||||
|  | 	PostUnfollowPath = BasePathWithID + "/unfollow" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Module implements the ClientAPIModule interface for account-related actions | // Module implements the ClientAPIModule interface for account-related actions | ||||||
|  | @ -82,6 +90,10 @@ func (m *Module) Route(r router.Router) error { | ||||||
| 	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) | 	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) | ||||||
| 	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) | 	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) | ||||||
| 	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, GetRelationshipsPath, m.AccountRelationshipsGETHandler) | ||||||
|  | 	r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler) | ||||||
|  | 	r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								internal/api/client/account/follow.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								internal/api/client/account/follow.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | /* | ||||||
|  |    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/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AccountFollowPOSTHandler is the endpoint for creating a new follow request to the target account | ||||||
|  | func (m *Module) AccountFollowPOSTHandler(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 | ||||||
|  | 	} | ||||||
|  | 	form := &model.AccountFollowRequest{} | ||||||
|  | 	if err := c.ShouldBind(form); err != nil { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	form.TargetAccountID = targetAcctID | ||||||
|  | 
 | ||||||
|  | 	relationship, errWithCode := m.processor.AccountFollowCreate(authed, form) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, relationship) | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								internal/api/client/account/following.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/api/client/account/following.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" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester. | ||||||
|  | func (m *Module) AccountFollowingGETHandler(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 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	following, errWithCode := m.processor.AccountFollowingGet(authed, targetAcctID) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, following) | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								internal/api/client/account/relationships.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/api/client/account/relationships.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | package account | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AccountRelationshipsGETHandler serves the relationship of the requesting account with one or more requested account IDs. | ||||||
|  | func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { | ||||||
|  | 	l := m.log.WithField("func", "AccountRelationshipsGETHandler") | ||||||
|  | 
 | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	targetAccountIDs := c.QueryArray("id[]") | ||||||
|  | 	if len(targetAccountIDs) == 0 { | ||||||
|  | 		// check fallback -- let's be generous and see if maybe it's just set as 'id'? | ||||||
|  | 		id := c.Query("id") | ||||||
|  | 		if id == "" { | ||||||
|  | 			l.Debug("no account id specified in query") | ||||||
|  | 			c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		targetAccountIDs = append(targetAccountIDs, id) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	relationships := []model.Relationship{} | ||||||
|  | 
 | ||||||
|  | 	for _, targetAccountID := range targetAccountIDs { | ||||||
|  | 		r, errWithCode := m.processor.AccountRelationshipGet(authed, targetAccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		relationships = append(relationships, *r) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, relationships) | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								internal/api/client/account/unfollow.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								internal/api/client/account/unfollow.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | /* | ||||||
|  |    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" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AccountUnfollowPOSTHandler is the endpoint for removing a follow and/or follow request to the target account | ||||||
|  | func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { | ||||||
|  | 	l := m.log.WithField("func", "AccountUnfollowPOSTHandler") | ||||||
|  | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Debug(err) | ||||||
|  | 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	targetAcctID := c.Param(IDKey) | ||||||
|  | 	if targetAcctID == "" { | ||||||
|  | 		l.Debug(err) | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	relationship, errWithCode := m.processor.AccountFollowRemove(authed, targetAcctID) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		l.Debug(errWithCode.Error()) | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, relationship) | ||||||
|  | } | ||||||
|  | @ -28,6 +28,7 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -60,7 +61,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 	app := >smodel.Application{ | 	app := >smodel.Application{ | ||||||
| 		ClientID: clientID, | 		ClientID: clientID, | ||||||
| 	} | 	} | ||||||
| 	if err := m.db.GetWhere("client_id", app.ClientID, app); err != nil { | 	if err := m.db.GetWhere([]db.Where{{Key: "client_id", Value: app.ClientID}}, app); err != nil { | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) | 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ package auth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
|  | @ -68,7 +69,7 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) { | ||||||
| 	if cid := ti.GetClientID(); cid != "" { | 	if cid := ti.GetClientID(); cid != "" { | ||||||
| 		l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope()) | 		l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope()) | ||||||
| 		app := >smodel.Application{} | 		app := >smodel.Application{} | ||||||
| 		if err := m.db.GetWhere("client_id", cid, app); err != nil { | 		if err := m.db.GetWhere([]db.Where{{Key: "client_id",Value: cid}}, app); err != nil { | ||||||
| 			l.Tracef("no app found for client %s", cid) | 			l.Tracef("no app found for client %s", cid) | ||||||
| 		} | 		} | ||||||
| 		c.Set(oauth.SessionAuthorizedApplication, app) | 		c.Set(oauth.SessionAuthorizedApplication, app) | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  | @ -87,7 +88,7 @@ func (m *Module) ValidatePassword(email string, password string) (userid string, | ||||||
| 	// first we select the user from the database based on email address, bail if no user found for that email | 	// first we select the user from the database based on email address, bail if no user found for that email | ||||||
| 	gtsUser := >smodel.User{} | 	gtsUser := >smodel.User{} | ||||||
| 
 | 
 | ||||||
| 	if err := m.db.GetWhere("email", email, gtsUser); err != nil { | 	if err := m.db.GetWhere([]db.Where{{Key: "email", Value: email}}, gtsUser); err != nil { | ||||||
| 		l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) | 		l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) | ||||||
| 		return incorrectPassword() | 		return incorrectPassword() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -48,10 +48,11 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil { | 	r, errWithCode := m.processor.FollowRequestAccept(authed, originAccountID) | ||||||
|  | 	if errWithCode != nil { | ||||||
| 		l.Debug(errWithCode.Error()) | 		l.Debug(errWithCode.Error()) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	c.Status(http.StatusOK) | 	c.JSON(http.StatusOK, r) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ import ( | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
|  | @ -118,7 +119,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { | ||||||
| 	}, statusReply.Tags[0]) | 	}, statusReply.Tags[0]) | ||||||
| 
 | 
 | ||||||
| 	gtsTag := >smodel.Tag{} | 	gtsTag := >smodel.Tag{} | ||||||
| 	err = suite.db.GetWhere("name", "helloworld", gtsTag) | 	err = suite.db.GetWhere([]db.Where{{Key: "name", Value: "helloworld"}}, gtsTag) | ||||||
| 	assert.NoError(suite.T(), err) | 	assert.NoError(suite.T(), err) | ||||||
| 	assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID) | 	assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -77,18 +77,18 @@ type Account struct { | ||||||
| // See https://docs.joinmastodon.org/methods/accounts/ | // See https://docs.joinmastodon.org/methods/accounts/ | ||||||
| type AccountCreateRequest struct { | type AccountCreateRequest struct { | ||||||
| 	// Text that will be reviewed by moderators if registrations require manual approval. | 	// Text that will be reviewed by moderators if registrations require manual approval. | ||||||
| 	Reason string `form:"reason"` | 	Reason string `form:"reason" json:"reason" xml:"reason"` | ||||||
| 	// The desired username for the account | 	// The desired username for the account | ||||||
| 	Username string `form:"username" binding:"required"` | 	Username string `form:"username" json:"username" xml:"username" binding:"required"` | ||||||
| 	// The email address to be used for login | 	// The email address to be used for login | ||||||
| 	Email string `form:"email" binding:"required"` | 	Email string `form:"email" json:"email" xml:"email" binding:"required"` | ||||||
| 	// The password to be used for login | 	// The password to be used for login | ||||||
| 	Password string `form:"password" binding:"required"` | 	Password string `form:"password" json:"password" xml:"password" binding:"required"` | ||||||
| 	// Whether the user agrees to the local rules, terms, and policies. | 	// Whether the user agrees to the local rules, terms, and policies. | ||||||
| 	// These should be presented to the user in order to allow them to consent before setting this parameter to TRUE. | 	// These should be presented to the user in order to allow them to consent before setting this parameter to TRUE. | ||||||
| 	Agreement bool `form:"agreement" binding:"required"` | 	Agreement bool `form:"agreement"  json:"agreement" xml:"agreement" binding:"required"` | ||||||
| 	// The language of the confirmation email that will be sent | 	// The language of the confirmation email that will be sent | ||||||
| 	Locale string `form:"locale" binding:"required"` | 	Locale string `form:"locale" json:"locale" xml:"locale" binding:"required"` | ||||||
| 	// The IP of the sign up request, will not be parsed from the form but must be added manually | 	// The IP of the sign up request, will not be parsed from the form but must be added manually | ||||||
| 	IP net.IP `form:"-"` | 	IP net.IP `form:"-"` | ||||||
| } | } | ||||||
|  | @ -97,40 +97,51 @@ type AccountCreateRequest struct { | ||||||
| // See https://docs.joinmastodon.org/methods/accounts/ | // See https://docs.joinmastodon.org/methods/accounts/ | ||||||
| type UpdateCredentialsRequest struct { | type UpdateCredentialsRequest struct { | ||||||
| 	// Whether the account should be shown in the profile directory. | 	// Whether the account should be shown in the profile directory. | ||||||
| 	Discoverable *bool `form:"discoverable"` | 	Discoverable *bool `form:"discoverable" json:"discoverable" xml:"discoverable"` | ||||||
| 	// Whether the account has a bot flag. | 	// Whether the account has a bot flag. | ||||||
| 	Bot *bool `form:"bot"` | 	Bot *bool `form:"bot" json:"bot" xml:"bot"` | ||||||
| 	// The display name to use for the profile. | 	// The display name to use for the profile. | ||||||
| 	DisplayName *string `form:"display_name"` | 	DisplayName *string `form:"display_name" json:"display_name" xml:"display_name"` | ||||||
| 	// The account bio. | 	// The account bio. | ||||||
| 	Note *string `form:"note"` | 	Note *string `form:"note" json:"note" xml:"note"` | ||||||
| 	// Avatar image encoded using multipart/form-data | 	// Avatar image encoded using multipart/form-data | ||||||
| 	Avatar *multipart.FileHeader `form:"avatar"` | 	Avatar *multipart.FileHeader `form:"avatar" json:"avatar" xml:"avatar"` | ||||||
| 	// Header image encoded using multipart/form-data | 	// Header image encoded using multipart/form-data | ||||||
| 	Header *multipart.FileHeader `form:"header"` | 	Header *multipart.FileHeader `form:"header" json:"header" xml:"header"` | ||||||
| 	// Whether manual approval of follow requests is required. | 	// Whether manual approval of follow requests is required. | ||||||
| 	Locked *bool `form:"locked"` | 	Locked *bool `form:"locked" json:"locked" xml:"locked"` | ||||||
| 	// New Source values for this account | 	// New Source values for this account | ||||||
| 	Source *UpdateSource `form:"source"` | 	Source *UpdateSource `form:"source" json:"source" xml:"source"` | ||||||
| 	// Profile metadata name and value | 	// Profile metadata name and value | ||||||
| 	FieldsAttributes *[]UpdateField `form:"fields_attributes"` | 	FieldsAttributes *[]UpdateField `form:"fields_attributes" json:"fields_attributes" xml:"fields_attributes"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UpdateSource is to be used specifically in an UpdateCredentialsRequest. | // UpdateSource is to be used specifically in an UpdateCredentialsRequest. | ||||||
| type UpdateSource struct { | type UpdateSource struct { | ||||||
| 	// Default post privacy for authored statuses. | 	// Default post privacy for authored statuses. | ||||||
| 	Privacy *string `form:"privacy"` | 	Privacy *string `form:"privacy" json:"privacy" xml:"privacy"` | ||||||
| 	// Whether to mark authored statuses as sensitive by default. | 	// Whether to mark authored statuses as sensitive by default. | ||||||
| 	Sensitive *bool `form:"sensitive"` | 	Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"` | ||||||
| 	// Default language to use for authored statuses. (ISO 6391) | 	// Default language to use for authored statuses. (ISO 6391) | ||||||
| 	Language *string `form:"language"` | 	Language *string `form:"language" json:"language" xml:"language"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UpdateField is to be used specifically in an UpdateCredentialsRequest. | // UpdateField is to be used specifically in an UpdateCredentialsRequest. | ||||||
| // By default, max 4 fields and 255 characters per property/value. | // By default, max 4 fields and 255 characters per property/value. | ||||||
| type UpdateField struct { | type UpdateField struct { | ||||||
| 	// Name of the field | 	// Name of the field | ||||||
| 	Name *string `form:"name"` | 	Name *string `form:"name" json:"name" xml:"name"` | ||||||
| 	// Value of the field | 	// Value of the field | ||||||
| 	Value *string `form:"value"` | 	Value *string `form:"value" json:"value" xml:"value"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AccountFollowRequest is for parsing requests at /api/v1/accounts/:id/follow | ||||||
|  | type AccountFollowRequest struct { | ||||||
|  | 	// ID of the account to follow request | ||||||
|  | 	// This should be a URL parameter not a form field | ||||||
|  | 	TargetAccountID string `form:"-"` | ||||||
|  | 	// Show reblogs for this account? | ||||||
|  | 	Reblogs *bool `form:"reblogs" json:"reblogs" xml:"reblogs"` | ||||||
|  | 	// Notify when this account posts? | ||||||
|  | 	Notify *bool `form:"notify" json:"notify" xml:"notify"` | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										58
									
								
								internal/api/s2s/user/followers.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/api/s2s/user/followers.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | /* | ||||||
|  |    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 user | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (m *Module) FollowersGETHandler(c *gin.Context) { | ||||||
|  | 	l := m.log.WithFields(logrus.Fields{ | ||||||
|  | 		"func": "FollowersGETHandler", | ||||||
|  | 		"url":  c.Request.RequestURI, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	requestedUsername := c.Param(UsernameKey) | ||||||
|  | 	if requestedUsername == "" { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// make sure this actually an AP request | ||||||
|  | 	format := c.NegotiateFormat(ActivityPubAcceptHeaders...) | ||||||
|  | 	if format == "" { | ||||||
|  | 		c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	l.Tracef("negotiated format: %s", format) | ||||||
|  | 
 | ||||||
|  | 	// make a copy of the context to pass along so we don't break anything | ||||||
|  | 	cp := c.Copy() | ||||||
|  | 	user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Info(err.Error()) | ||||||
|  | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, user) | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								internal/api/s2s/user/statusget.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/api/s2s/user/statusget.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | package user | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (m *Module) StatusGETHandler(c *gin.Context) { | ||||||
|  | 	l := m.log.WithFields(logrus.Fields{ | ||||||
|  | 		"func": "StatusGETHandler", | ||||||
|  | 		"url":  c.Request.RequestURI, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	requestedUsername := c.Param(UsernameKey) | ||||||
|  | 	if requestedUsername == "" { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestedStatusID := c.Param(StatusIDKey) | ||||||
|  | 	if requestedStatusID == "" { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// make sure this actually an AP request | ||||||
|  | 	format := c.NegotiateFormat(ActivityPubAcceptHeaders...) | ||||||
|  | 	if format == "" { | ||||||
|  | 		c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	l.Tracef("negotiated format: %s", format) | ||||||
|  | 
 | ||||||
|  | 	// make a copy of the context to pass along so we don't break anything | ||||||
|  | 	cp := c.Copy() | ||||||
|  | 	status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Info(err.Error()) | ||||||
|  | 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, status) | ||||||
|  | } | ||||||
|  | @ -32,6 +32,8 @@ import ( | ||||||
| const ( | const ( | ||||||
| 	// UsernameKey is for account usernames. | 	// UsernameKey is for account usernames. | ||||||
| 	UsernameKey = "username" | 	UsernameKey = "username" | ||||||
|  | 	// StatusIDKey is for status IDs | ||||||
|  | 	StatusIDKey = "status" | ||||||
| 	// UsersBasePath is the base path for serving information about Users eg https://example.org/users | 	// UsersBasePath is the base path for serving information about Users eg https://example.org/users | ||||||
| 	UsersBasePath = "/" + util.UsersPath | 	UsersBasePath = "/" + util.UsersPath | ||||||
| 	// UsersBasePathWithUsername is just the users base path with the Username key in it. | 	// UsersBasePathWithUsername is just the users base path with the Username key in it. | ||||||
|  | @ -40,6 +42,10 @@ const ( | ||||||
| 	UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey | 	UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey | ||||||
| 	// UsersInboxPath is for serving POST requests to a user's inbox with the given username key. | 	// UsersInboxPath is for serving POST requests to a user's inbox with the given username key. | ||||||
| 	UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath | 	UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath | ||||||
|  | 	// UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key. | ||||||
|  | 	UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath | ||||||
|  | 	// UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID | ||||||
|  | 	UsersStatusPath = UsersBasePathWithUsername + "/" + util.StatusesPath + "/:" + StatusIDKey | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ActivityPubAcceptHeaders represents the Accept headers mentioned here: | // ActivityPubAcceptHeaders represents the Accept headers mentioned here: | ||||||
|  | @ -69,5 +75,7 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger) | ||||||
| func (m *Module) Route(s router.Router) error { | func (m *Module) Route(s router.Router) error { | ||||||
| 	s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) | 	s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) | ||||||
| 	s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) | 	s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) | ||||||
|  | 	s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler) | ||||||
|  | 	s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -43,5 +43,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule { | ||||||
| func (m *Module) Route(s router.Router) error { | func (m *Module) Route(s router.Router) error { | ||||||
| 	s.AttachMiddleware(m.FlocBlock) | 	s.AttachMiddleware(m.FlocBlock) | ||||||
| 	s.AttachMiddleware(m.ExtraHeaders) | 	s.AttachMiddleware(m.ExtraHeaders) | ||||||
|  | 	s.AttachMiddleware(m.UserAgentBlock) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										43
									
								
								internal/api/security/useragentblock.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/api/security/useragentblock.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | /* | ||||||
|  |    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 security | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // UserAgentBlock is a middleware that prevents google chrome cohort tracking by | ||||||
|  | // writing the Permissions-Policy header after all other parts of the request have been completed. | ||||||
|  | // See: https://plausible.io/blog/google-floc | ||||||
|  | func (m *Module) UserAgentBlock(c *gin.Context) { | ||||||
|  | 
 | ||||||
|  | 	ua := c.Request.UserAgent() | ||||||
|  | 	if ua == "" { | ||||||
|  | 		c.AbortWithStatus(http.StatusTeapot) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) { | ||||||
|  | 		c.AbortWithStatus(http.StatusTeapot) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -22,7 +22,6 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"net" | 	"net" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/pub" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -33,18 +32,28 @@ const ( | ||||||
| 
 | 
 | ||||||
| // ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. | // ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. | ||||||
| type ErrNoEntries struct{} | type ErrNoEntries struct{} | ||||||
| 
 |  | ||||||
| func (e ErrNoEntries) Error() string { | func (e ErrNoEntries) Error() string { | ||||||
| 	return "no entries" | 	return "no entries" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints. | ||||||
|  | type ErrAlreadyExists struct{} | ||||||
|  | func (e ErrAlreadyExists) Error() string { | ||||||
|  | 	return "already exists" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Where struct { | ||||||
|  | 	Key string | ||||||
|  | 	Value interface{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres). | // DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres). | ||||||
| // Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated | // Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated | ||||||
| // by whatever is returned from the database. | // by whatever is returned from the database. | ||||||
| type DB interface { | type DB interface { | ||||||
| 	// Federation returns an interface that's compatible with go-fed, for performing federation storage/retrieval functions. | 	// Federation returns an interface that's compatible with go-fed, for performing federation storage/retrieval functions. | ||||||
| 	// See: https://pkg.go.dev/github.com/go-fed/activity@v1.0.0/pub?utm_source=gopls#Database | 	// See: https://pkg.go.dev/github.com/go-fed/activity@v1.0.0/pub?utm_source=gopls#Database | ||||||
| 	Federation() pub.Database | 	// Federation() federatingdb.FederatingDB | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		BASIC DB FUNCTIONALITY | 		BASIC DB FUNCTIONALITY | ||||||
|  | @ -75,7 +84,7 @@ type DB interface { | ||||||
| 	// name of the key to select from. | 	// name of the key to select from. | ||||||
| 	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. | 	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. | ||||||
| 	// In case of no entries, a 'no entries' error will be returned | 	// In case of no entries, a 'no entries' error will be returned | ||||||
| 	GetWhere(key string, value interface{}, i interface{}) error | 	GetWhere(where []Where, i interface{}) error | ||||||
| 
 | 
 | ||||||
| 	// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where". | 	// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where". | ||||||
| 	// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second | 	// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second | ||||||
|  | @ -109,7 +118,7 @@ type DB interface { | ||||||
| 
 | 
 | ||||||
| 	// DeleteWhere deletes i where key = value | 	// DeleteWhere deletes i where key = value | ||||||
| 	// If i didn't exist anyway, then no error should be returned. | 	// If i didn't exist anyway, then no error should be returned. | ||||||
| 	DeleteWhere(key string, value interface{}, i interface{}) error | 	DeleteWhere(where []Where, i interface{}) error | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		HANDY SHORTCUTS | 		HANDY SHORTCUTS | ||||||
|  | @ -117,7 +126,9 @@ type DB interface { | ||||||
| 
 | 
 | ||||||
| 	// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. | 	// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. | ||||||
| 	// In other words, it should create the follow, and delete the existing follow request. | 	// In other words, it should create the follow, and delete the existing follow request. | ||||||
| 	AcceptFollowRequest(originAccountID string, targetAccountID string) error | 	// | ||||||
|  | 	// It will return the newly created follow for further processing. | ||||||
|  | 	AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) | ||||||
| 
 | 
 | ||||||
| 	// CreateInstanceAccount creates an account in the database with the same username as the instance host value. | 	// CreateInstanceAccount creates an account in the database with the same username as the instance host value. | ||||||
| 	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. | 	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. | ||||||
|  | @ -204,6 +215,9 @@ type DB interface { | ||||||
| 	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. | 	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. | ||||||
| 	Blocked(account1 string, account2 string) (bool, error) | 	Blocked(account1 string, account2 string) (bool, error) | ||||||
| 
 | 
 | ||||||
|  | 	// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. | ||||||
|  | 	GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) | ||||||
|  | 
 | ||||||
| 	// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the | 	// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the | ||||||
| 	// privacy settings of the status, and any blocks/mutes that might exist between the two accounts | 	// privacy settings of the status, and any blocks/mutes that might exist between the two accounts | ||||||
| 	// or account domains. | 	// or account domains. | ||||||
|  | @ -222,6 +236,9 @@ type DB interface { | ||||||
| 	// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. | 	// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. | ||||||
| 	Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) | 	Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) | ||||||
| 
 | 
 | ||||||
|  | 	// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. | ||||||
|  | 	FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) | ||||||
|  | 
 | ||||||
| 	// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. | 	// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. | ||||||
| 	Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) | 	Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,6 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/pub" |  | ||||||
| 	"github.com/go-pg/pg/extra/pgdebug" | 	"github.com/go-pg/pg/extra/pgdebug" | ||||||
| 	"github.com/go-pg/pg/v10" | 	"github.com/go-pg/pg/v10" | ||||||
| 	"github.com/go-pg/pg/v10/orm" | 	"github.com/go-pg/pg/v10/orm" | ||||||
|  | @ -38,7 +37,6 @@ import ( | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
|  | @ -46,11 +44,11 @@ import ( | ||||||
| 
 | 
 | ||||||
| // postgresService satisfies the DB interface | // postgresService satisfies the DB interface | ||||||
| type postgresService struct { | type postgresService struct { | ||||||
| 	config       *config.Config | 	config *config.Config | ||||||
| 	conn         *pg.DB | 	conn   *pg.DB | ||||||
| 	log          *logrus.Logger | 	log    *logrus.Logger | ||||||
| 	cancel       context.CancelFunc | 	cancel context.CancelFunc | ||||||
| 	federationDB pub.Database | 	// federationDB pub.Database | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. | // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. | ||||||
|  | @ -97,9 +95,6 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge | ||||||
| 		cancel: cancel, | 		cancel: cancel, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	federatingDB := federation.NewFederatingDB(ps, c, log) |  | ||||||
| 	ps.federationDB = federatingDB |  | ||||||
| 
 |  | ||||||
| 	// we can confidently return this useable postgres service now | 	// we can confidently return this useable postgres service now | ||||||
| 	return ps, nil | 	return ps, nil | ||||||
| } | } | ||||||
|  | @ -159,14 +154,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) { | ||||||
| 	return options, nil | 	return options, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* |  | ||||||
| 	FEDERATION FUNCTIONALITY |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| func (ps *postgresService) Federation() pub.Database { |  | ||||||
| 	return ps.federationDB |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* | /* | ||||||
| 	BASIC DB FUNCTIONALITY | 	BASIC DB FUNCTIONALITY | ||||||
| */ | */ | ||||||
|  | @ -229,8 +216,17 @@ func (ps *postgresService) GetByID(id string, i interface{}) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error { | func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error { | ||||||
| 	if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil { | 	if len(where) == 0 { | ||||||
|  | 		return errors.New("no queries provided") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	q := ps.conn.Model(i) | ||||||
|  | 	for _, w := range where { | ||||||
|  | 		q = q.Where("? = ?", pg.Safe(w.Key), w.Value) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := q.Select(); err != nil { | ||||||
| 		if err == pg.ErrNoRows { | 		if err == pg.ErrNoRows { | ||||||
| 			return db.ErrNoEntries{} | 			return db.ErrNoEntries{} | ||||||
| 		} | 		} | ||||||
|  | @ -255,6 +251,9 @@ func (ps *postgresService) GetAll(i interface{}) error { | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) Put(i interface{}) error { | func (ps *postgresService) Put(i interface{}) error { | ||||||
| 	_, err := ps.conn.Model(i).Insert(i) | 	_, err := ps.conn.Model(i).Insert(i) | ||||||
|  | 	if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { | ||||||
|  | 		return db.ErrAlreadyExists{} | ||||||
|  | 	} | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -285,20 +284,31 @@ func (ps *postgresService) UpdateOneByID(id string, key string, value interface{ | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) DeleteByID(id string, i interface{}) error { | func (ps *postgresService) DeleteByID(id string, i interface{}) error { | ||||||
| 	if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil { | 	if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil { | ||||||
| 		if err == pg.ErrNoRows { | 		// if there are no rows *anyway* then that's fine | ||||||
| 			return db.ErrNoEntries{} | 		// just return err if there's an actual error | ||||||
|  | 		if err != pg.ErrNoRows { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 		return err |  | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error { | func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error { | ||||||
| 	if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil { | 	if len(where) == 0 { | ||||||
| 		if err == pg.ErrNoRows { | 		return errors.New("no queries provided") | ||||||
| 			return db.ErrNoEntries{} | 	} | ||||||
|  | 
 | ||||||
|  | 	q := ps.conn.Model(i) | ||||||
|  | 	for _, w := range where { | ||||||
|  | 		q = q.Where("? = ?", pg.Safe(w.Key), w.Value) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := q.Delete(); err != nil { | ||||||
|  | 		// if there are no rows *anyway* then that's fine | ||||||
|  | 		// just return err if there's an actual error | ||||||
|  | 		if err != pg.ErrNoRows { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 		return err |  | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | @ -307,30 +317,34 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac | ||||||
| 	HANDY SHORTCUTS | 	HANDY SHORTCUTS | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error { | func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) { | ||||||
|  | 	// make sure the original follow request exists | ||||||
| 	fr := >smodel.FollowRequest{} | 	fr := >smodel.FollowRequest{} | ||||||
| 	if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { | 	if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { | ||||||
| 		if err == pg.ErrMultiRows { | 		if err == pg.ErrMultiRows { | ||||||
| 			return db.ErrNoEntries{} | 			return nil, db.ErrNoEntries{} | ||||||
| 		} | 		} | ||||||
| 		return err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// create a new follow to 'replace' the request with | ||||||
| 	follow := >smodel.Follow{ | 	follow := >smodel.Follow{ | ||||||
| 		AccountID:       originAccountID, | 		AccountID:       originAccountID, | ||||||
| 		TargetAccountID: targetAccountID, | 		TargetAccountID: targetAccountID, | ||||||
| 		URI:             fr.URI, | 		URI:             fr.URI, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := ps.conn.Model(follow).Insert(); err != nil { | 	// if the follow already exists, just update the URI -- we don't need to do anything else | ||||||
| 		return err | 	if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { | ||||||
|  | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// now remove the follow request | ||||||
| 	if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { | 	if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { | ||||||
| 		return err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return follow, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) CreateInstanceAccount() error { | func (ps *postgresService) CreateInstanceAccount() error { | ||||||
|  | @ -681,6 +695,60 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro | ||||||
| 	return blocked, nil | 	return blocked, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) { | ||||||
|  | 	r := >smodel.Relationship{ | ||||||
|  | 		ID: targetAccount, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if the requesting account follows the target account | ||||||
|  | 	follow := >smodel.Follow{} | ||||||
|  | 	if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { | ||||||
|  | 		if err != pg.ErrNoRows { | ||||||
|  | 			// a proper error | ||||||
|  | 			return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) | ||||||
|  | 		} | ||||||
|  | 		// no follow exists so these are all false | ||||||
|  | 		r.Following = false | ||||||
|  | 		r.ShowingReblogs = false | ||||||
|  | 		r.Notifying = false | ||||||
|  | 	} else { | ||||||
|  | 		// follow exists so we can fill these fields out... | ||||||
|  | 		r.Following = true | ||||||
|  | 		r.ShowingReblogs = follow.ShowReblogs | ||||||
|  | 		r.Notifying = follow.Notify | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if the target account follows the requesting account | ||||||
|  | 	followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) | ||||||
|  | 	} | ||||||
|  | 	r.FollowedBy = followedBy | ||||||
|  | 
 | ||||||
|  | 	// check if the requesting account blocks the target account | ||||||
|  | 	blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) | ||||||
|  | 	} | ||||||
|  | 	r.Blocking = blocking | ||||||
|  | 
 | ||||||
|  | 	// check if the target account blocks the requesting account | ||||||
|  | 	blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) | ||||||
|  | 	} | ||||||
|  | 	r.BlockedBy = blockedBy | ||||||
|  | 
 | ||||||
|  | 	// check if there's a pending following request from requesting account to target account | ||||||
|  | 	requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) | ||||||
|  | 	} | ||||||
|  | 	r.Requested = requested | ||||||
|  | 
 | ||||||
|  | 	return r, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { | func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { | ||||||
| 	l := ps.log.WithField("func", "StatusVisible") | 	l := ps.log.WithField("func", "StatusVisible") | ||||||
| 
 | 
 | ||||||
|  | @ -853,6 +921,10 @@ func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccoun | ||||||
| 	return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() | 	return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { | ||||||
|  | 	return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { | func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { | ||||||
| 	// make sure account 1 follows account 2 | 	// make sure account 1 follows account 2 | ||||||
| 	f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() | 	f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() | ||||||
|  | @ -1036,6 +1108,11 @@ func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { | func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { | ||||||
|  | 	ogAccount := >smodel.Account{} | ||||||
|  | 	if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	menchies := []*gtsmodel.Mention{} | 	menchies := []*gtsmodel.Mention{} | ||||||
| 	for _, a := range targetAccounts { | 	for _, a := range targetAccounts { | ||||||
| 		// A mentioned account looks like "@test@example.org" or just "@test" for a local account | 		// A mentioned account looks like "@test@example.org" or just "@test" for a local account | ||||||
|  | @ -1093,9 +1170,13 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori | ||||||
| 
 | 
 | ||||||
| 		// id, createdAt and updatedAt will be populated by the db, so we have everything we need! | 		// id, createdAt and updatedAt will be populated by the db, so we have everything we need! | ||||||
| 		menchies = append(menchies, >smodel.Mention{ | 		menchies = append(menchies, >smodel.Mention{ | ||||||
| 			StatusID:        statusID, | 			StatusID:            statusID, | ||||||
| 			OriginAccountID: originAccountID, | 			OriginAccountID:     ogAccount.ID, | ||||||
| 			TargetAccountID: mentionedAccount.ID, | 			OriginAccountURI:    ogAccount.URI, | ||||||
|  | 			TargetAccountID:     mentionedAccount.ID, | ||||||
|  | 			NameString:          a, | ||||||
|  | 			MentionedAccountURI: mentionedAccount.URI, | ||||||
|  | 			GTSAccount:          mentionedAccount, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	return menchies, nil | 	return menchies, nil | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ package federation | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | @ -37,6 +38,12 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | type FederatingDB interface { | ||||||
|  | 	pub.Database | ||||||
|  | 	Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error | ||||||
|  | 	Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface. | // FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface. | ||||||
| // It doesn't care what the underlying implementation of the DB interface is, as long as it works. | // It doesn't care what the underlying implementation of the DB interface is, as long as it works. | ||||||
| type federatingDB struct { | type federatingDB struct { | ||||||
|  | @ -47,8 +54,8 @@ type federatingDB struct { | ||||||
| 	typeConverter typeutils.TypeConverter | 	typeConverter typeutils.TypeConverter | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewFederatingDB returns a pub.Database interface using the given database, config, and logger. | // NewFederatingDB returns a FederatingDB interface using the given database, config, and logger. | ||||||
| func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) pub.Database { | func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) FederatingDB { | ||||||
| 	return &federatingDB{ | 	return &federatingDB{ | ||||||
| 		locks:         new(sync.Map), | 		locks:         new(sync.Map), | ||||||
| 		db:            db, | 		db:            db, | ||||||
|  | @ -204,7 +211,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | ||||||
| 		} | 		} | ||||||
| 		if err := f.db.GetWhere("uri", uid, >smodel.Status{}); err != nil { | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil { | ||||||
| 			if _, ok := err.(db.ErrNoEntries); ok { | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
| 				// there are no entries for this status | 				// there are no entries for this status | ||||||
| 				return false, nil | 				return false, nil | ||||||
|  | @ -253,7 +260,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac | ||||||
| 		return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) | 		return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) | ||||||
| 	} | 	} | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil { | ||||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
| 			return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) | 			return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) | ||||||
| 		} | 		} | ||||||
|  | @ -278,7 +285,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto | ||||||
| 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | ||||||
| 	} | 	} | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { | ||||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
| 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | ||||||
| 		} | 		} | ||||||
|  | @ -304,7 +311,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out | ||||||
| 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | ||||||
| 	} | 	} | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { | ||||||
| 		if _, ok := err.(db.ErrNoEntries); ok { | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
| 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | ||||||
| 		} | 		} | ||||||
|  | @ -343,9 +350,10 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er | ||||||
| 
 | 
 | ||||||
| 	if util.IsUserPath(id) { | 	if util.IsUserPath(id) { | ||||||
| 		acct := >smodel.Account{} | 		acct := >smodel.Account{} | ||||||
| 		if err := f.db.GetWhere("uri", id.String(), acct); err != nil { | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | 		l.Debug("is user path! returning account") | ||||||
| 		return f.typeConverter.AccountToAS(acct) | 		return f.typeConverter.AccountToAS(acct) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -371,27 +379,40 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 			"asType": asType.GetTypeName(), | 			"asType": asType.GetTypeName(), | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| 	l.Debugf("received CREATE asType %+v", asType) | 	m, err := streams.Serialize(asType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	l.Debugf("received CREATE asType %s", string(b)) | ||||||
| 
 | 
 | ||||||
| 	targetAcctI := ctx.Value(util.APAccount) | 	targetAcctI := ctx.Value(util.APAccount) | ||||||
| 	if targetAcctI == nil { | 	if targetAcctI == nil { | ||||||
| 		l.Error("target account wasn't set on context") | 		l.Error("target account wasn't set on context") | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		l.Error("target account was set on context but couldn't be parsed") | 		l.Error("target account was set on context but couldn't be parsed") | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | 	fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | ||||||
| 	if fromFederatorChanI == nil { | 	if fromFederatorChanI == nil { | ||||||
| 		l.Error("from federator channel wasn't set on context") | 		l.Error("from federator channel wasn't set on context") | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| 	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | 	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		l.Error("from federator channel was set on context but couldn't be parsed") | 		l.Error("from federator channel was set on context but couldn't be parsed") | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) { | 	switch asType.GetTypeName() { | ||||||
| 	case gtsmodel.ActivityStreamsCreate: | 	case gtsmodel.ActivityStreamsCreate: | ||||||
| 		create, ok := asType.(vocab.ActivityStreamsCreate) | 		create, ok := asType.(vocab.ActivityStreamsCreate) | ||||||
| 		if !ok { | 		if !ok { | ||||||
|  | @ -399,7 +420,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 		} | 		} | ||||||
| 		object := create.GetActivityStreamsObject() | 		object := create.GetActivityStreamsObject() | ||||||
| 		for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { | 		for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { | ||||||
| 			switch gtsmodel.ActivityStreamsObject(objectIter.GetType().GetTypeName()) { | 			switch objectIter.GetType().GetTypeName() { | ||||||
| 			case gtsmodel.ActivityStreamsNote: | 			case gtsmodel.ActivityStreamsNote: | ||||||
| 				note := objectIter.GetActivityStreamsNote() | 				note := objectIter.GetActivityStreamsNote() | ||||||
| 				status, err := f.typeConverter.ASStatusToStatus(note) | 				status, err := f.typeConverter.ASStatusToStatus(note) | ||||||
|  | @ -407,13 +428,17 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 					return fmt.Errorf("error converting note to status: %s", err) | 					return fmt.Errorf("error converting note to status: %s", err) | ||||||
| 				} | 				} | ||||||
| 				if err := f.db.Put(status); err != nil { | 				if err := f.db.Put(status); err != nil { | ||||||
|  | 					if _, ok := err.(db.ErrAlreadyExists); ok { | ||||||
|  | 						return nil | ||||||
|  | 					} | ||||||
| 					return fmt.Errorf("database error inserting status: %s", err) | 					return fmt.Errorf("database error inserting status: %s", err) | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				fromFederatorChan <- gtsmodel.FromFederator{ | 				fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
| 					APObjectType:   gtsmodel.ActivityStreamsNote, | 					APObjectType:     gtsmodel.ActivityStreamsNote, | ||||||
| 					APActivityType: gtsmodel.ActivityStreamsCreate, | 					APActivityType:   gtsmodel.ActivityStreamsCreate, | ||||||
| 					GTSModel:       status, | 					GTSModel:         status, | ||||||
|  | 					ReceivingAccount: targetAcct, | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | @ -433,7 +458,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !targetAcct.Locked { | 		if !targetAcct.Locked { | ||||||
| 			if err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { | 			if _, err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { | ||||||
| 				return fmt.Errorf("database error accepting follow request: %s", err) | 				return fmt.Errorf("database error accepting follow request: %s", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | @ -450,14 +475,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
| // the entire value. | // the entire value. | ||||||
| // | // | ||||||
| // The library makes this call only after acquiring a lock first. | // The library makes this call only after acquiring a lock first. | ||||||
| func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { | func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { | ||||||
| 	l := f.log.WithFields( | 	l := f.log.WithFields( | ||||||
| 		logrus.Fields{ | 		logrus.Fields{ | ||||||
| 			"func":   "Update", | 			"func":   "Update", | ||||||
| 			"asType": asType.GetTypeName(), | 			"asType": asType.GetTypeName(), | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| 	l.Debugf("received UPDATE asType %+v", asType) | 	m, err := streams.Serialize(asType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	l.Debugf("received UPDATE asType %s", string(b)) | ||||||
|  | 
 | ||||||
|  | 	receivingAcctI := ctx.Value(util.APAccount) | ||||||
|  | 	if receivingAcctI == nil { | ||||||
|  | 		l.Error("receiving account wasn't set on context") | ||||||
|  | 	} | ||||||
|  | 	receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("receiving account was set on context but couldn't be parsed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | ||||||
|  | 	if fromFederatorChanI == nil { | ||||||
|  | 		l.Error("from federator channel wasn't set on context") | ||||||
|  | 	} | ||||||
|  | 	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("from federator channel was set on context but couldn't be parsed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch asType.GetTypeName() { | ||||||
|  | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
|  | 		update, ok := asType.(vocab.ActivityStreamsCreate) | ||||||
|  | 		if !ok { | ||||||
|  | 			return errors.New("could not convert type to create") | ||||||
|  | 		} | ||||||
|  | 		object := update.GetActivityStreamsObject() | ||||||
|  | 		for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { | ||||||
|  | 			switch objectIter.GetType().GetTypeName() { | ||||||
|  | 			case string(gtsmodel.ActivityStreamsPerson): | ||||||
|  | 				person := objectIter.GetActivityStreamsPerson() | ||||||
|  | 				updatedAcct, err := f.typeConverter.ASRepresentationToAccount(person) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return fmt.Errorf("error converting person to account: %s", err) | ||||||
|  | 				} | ||||||
|  | 				if err := f.db.Put(updatedAcct); err != nil { | ||||||
|  | 					return fmt.Errorf("database error inserting updated account: %s", err) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 					APObjectType:     gtsmodel.ActivityStreamsProfile, | ||||||
|  | 					APActivityType:   gtsmodel.ActivityStreamsUpdate, | ||||||
|  | 					GTSModel:         updatedAcct, | ||||||
|  | 					ReceivingAccount: receivingAcct, | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 			case string(gtsmodel.ActivityStreamsApplication): | ||||||
|  | 				application := objectIter.GetActivityStreamsApplication() | ||||||
|  | 				updatedAcct, err := f.typeConverter.ASRepresentationToAccount(application) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return fmt.Errorf("error converting person to account: %s", err) | ||||||
|  | 				} | ||||||
|  | 				if err := f.db.Put(updatedAcct); err != nil { | ||||||
|  | 					return fmt.Errorf("database error inserting updated account: %s", err) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 					APObjectType:     gtsmodel.ActivityStreamsProfile, | ||||||
|  | 					APActivityType:   gtsmodel.ActivityStreamsUpdate, | ||||||
|  | 					GTSModel:         updatedAcct, | ||||||
|  | 					ReceivingAccount: receivingAcct, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -490,7 +588,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v | ||||||
| 	) | 	) | ||||||
| 	l.Debug("entering GETOUTBOX function") | 	l.Debug("entering GETOUTBOX function") | ||||||
| 
 | 
 | ||||||
| 	return nil, nil | 	return streams.NewActivityStreamsOrderedCollectionPage(), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SetOutbox saves the outbox value given from GetOutbox, with new items | // SetOutbox saves the outbox value given from GetOutbox, with new items | ||||||
|  | @ -522,9 +620,60 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err | ||||||
| 			"asType": t.GetTypeName(), | 			"asType": t.GetTypeName(), | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| 	l.Debugf("received NEWID request for asType %+v", t) | 	m, err := streams.Serialize(t) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	b, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	l.Debugf("received NEWID request for asType %s", string(b)) | ||||||
| 
 | 
 | ||||||
| 	return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString())) | 	switch t.GetTypeName() { | ||||||
|  | 	case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 		// FOLLOW | ||||||
|  | 		// ID might already be set on a follow we've created, so check it here and return it if it is | ||||||
|  | 		follow, ok := t.(vocab.ActivityStreamsFollow) | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsFollow") | ||||||
|  | 		} | ||||||
|  | 		idProp := follow.GetJSONLDId() | ||||||
|  | 		if idProp != nil { | ||||||
|  | 			if idProp.IsIRI() { | ||||||
|  | 				return idProp.GetIRI(), nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		// it's not set so create one based on the actor set on the follow (ie., the followER not the followEE) | ||||||
|  | 		actorProp := follow.GetActivityStreamsActor() | ||||||
|  | 		if actorProp != nil { | ||||||
|  | 			for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() { | ||||||
|  | 				// take the IRI of the first actor we can find (there should only be one) | ||||||
|  | 				if iter.IsIRI() { | ||||||
|  | 					actorAccount := >smodel.Account{} | ||||||
|  | 					if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here | ||||||
|  | 						return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host)) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsNote: | ||||||
|  | 		// NOTE aka STATUS | ||||||
|  | 		// ID might already be set on a note we've created, so check it here and return it if it is | ||||||
|  | 		note, ok := t.(vocab.ActivityStreamsNote) | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote") | ||||||
|  | 		} | ||||||
|  | 		idProp := note.GetJSONLDId() | ||||||
|  | 		if idProp != nil { | ||||||
|  | 			if idProp.IsIRI() { | ||||||
|  | 				return idProp.GetIRI(), nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// fallback default behavior: just return a random UUID after our protocol and host | ||||||
|  | 	return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString())) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Followers obtains the Followers Collection for an actor with the | // Followers obtains the Followers Collection for an actor with the | ||||||
|  | @ -543,7 +692,7 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower | ||||||
| 	l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) | 	l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) | ||||||
| 
 | 
 | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { | ||||||
| 		return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) | 		return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -585,7 +734,7 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin | ||||||
| 	l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) | 	l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) | ||||||
| 
 | 
 | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { | ||||||
| 		return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) | 		return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -627,3 +776,139 @@ func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab. | ||||||
| 	l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String()) | 	l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String()) | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 	CUSTOM FUNCTIONALITY FOR GTS | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":   "Undo", | ||||||
|  | 			"asType": undo.GetTypeName(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	m, err := streams.Serialize(undo) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	l.Debugf("received UNDO asType %s", string(b)) | ||||||
|  | 
 | ||||||
|  | 	targetAcctI := ctx.Value(util.APAccount) | ||||||
|  | 	if targetAcctI == nil { | ||||||
|  | 		l.Error("UNDO: target account wasn't set on context") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("UNDO: target account was set on context but couldn't be parsed") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	undoObject := undo.GetActivityStreamsObject() | ||||||
|  | 	if undoObject == nil { | ||||||
|  | 		return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() { | ||||||
|  | 		switch iter.GetType().GetTypeName() { | ||||||
|  | 		case string(gtsmodel.ActivityStreamsFollow): | ||||||
|  | 			// UNDO FOLLOW | ||||||
|  | 			ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow") | ||||||
|  | 			} | ||||||
|  | 			// make sure the actor owns the follow | ||||||
|  | 			if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) { | ||||||
|  | 				return errors.New("UNDO: follow actor and activity actor not the same") | ||||||
|  | 			} | ||||||
|  | 			// convert the follow to something we can understand | ||||||
|  | 			gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err) | ||||||
|  | 			} | ||||||
|  | 			// make sure the addressee of the original follow is the same as whatever inbox this landed in | ||||||
|  | 			if gtsFollow.TargetAccountID != targetAcct.ID { | ||||||
|  | 				return errors.New("UNDO: follow object account and inbox account were not the same") | ||||||
|  | 			} | ||||||
|  | 			// delete any existing FOLLOW | ||||||
|  | 			if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.Follow{}); err != nil { | ||||||
|  | 				return fmt.Errorf("UNDO: db error removing follow: %s", err) | ||||||
|  | 			} | ||||||
|  | 			// delete any existing FOLLOW REQUEST | ||||||
|  | 			if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.FollowRequest{}); err != nil { | ||||||
|  | 				return fmt.Errorf("UNDO: db error removing follow request: %s", err) | ||||||
|  | 			} | ||||||
|  | 			l.Debug("follow undone") | ||||||
|  | 			return nil | ||||||
|  | 		case string(gtsmodel.ActivityStreamsLike): | ||||||
|  | 			// UNDO LIKE | ||||||
|  | 		case string(gtsmodel.ActivityStreamsAnnounce): | ||||||
|  | 			// UNDO BOOST/REBLOG/ANNOUNCE | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":   "Accept", | ||||||
|  | 			"asType": accept.GetTypeName(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	m, err := streams.Serialize(accept) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	l.Debugf("received ACCEPT asType %s", string(b)) | ||||||
|  | 
 | ||||||
|  | 	inboxAcctI := ctx.Value(util.APAccount) | ||||||
|  | 	if inboxAcctI == nil { | ||||||
|  | 		l.Error("ACCEPT: inbox account wasn't set on context") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	inboxAcct, ok := inboxAcctI.(*gtsmodel.Account) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("ACCEPT: inbox account was set on context but couldn't be parsed") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	acceptObject := accept.GetActivityStreamsObject() | ||||||
|  | 	if acceptObject == nil { | ||||||
|  | 		return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() { | ||||||
|  | 		switch iter.GetType().GetTypeName() { | ||||||
|  | 		case string(gtsmodel.ActivityStreamsFollow): | ||||||
|  | 			// ACCEPT FOLLOW | ||||||
|  | 			asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow") | ||||||
|  | 			} | ||||||
|  | 			// convert the follow to something we can understand | ||||||
|  | 			gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err) | ||||||
|  | 			} | ||||||
|  | 			// make sure the addressee of the original follow is the same as whatever inbox this landed in | ||||||
|  | 			if gtsFollow.AccountID != inboxAcct.ID { | ||||||
|  | 				return errors.New("ACCEPT: follow object account and inbox account were not the same") | ||||||
|  | 			} | ||||||
|  | 			_, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestingAccount := >smodel.Account{} | 	requestingAccount := >smodel.Account{} | ||||||
| 	if err := f.db.GetWhere("uri", publicKeyOwnerURI.String(), requestingAccount); err != nil { | 	if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil { | ||||||
| 		// there's been a proper error so return it | 		// there's been a proper error so return it | ||||||
| 		if _, ok := err.(db.ErrNoEntries); !ok { | 		if _, ok := err.(db.ErrNoEntries); !ok { | ||||||
| 			return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) | 			return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) | ||||||
|  | @ -146,6 +146,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		requestingAccount = a | 		requestingAccount = a | ||||||
|  | 
 | ||||||
|  | 		// send the newly dereferenced account into the processor channel for further async processing | ||||||
|  | 		fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | ||||||
|  | 		if fromFederatorChanI == nil { | ||||||
|  | 			l.Error("from federator channel wasn't set on context") | ||||||
|  | 		} | ||||||
|  | 		fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | ||||||
|  | 		if !ok { | ||||||
|  | 			l.Error("from federator channel was set on context but couldn't be parsed") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 			APObjectType:   gtsmodel.ActivityStreamsProfile, | ||||||
|  | 			APActivityType: gtsmodel.ActivityStreamsCreate, | ||||||
|  | 			GTSModel:       requestingAccount, | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) | 	withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) | ||||||
|  | @ -184,7 +200,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er | ||||||
| 
 | 
 | ||||||
| 	for _, uri := range actorIRIs { | 	for _, uri := range actorIRIs { | ||||||
| 		a := >smodel.Account{} | 		a := >smodel.Account{} | ||||||
| 		if err := f.db.GetWhere("uri", uri.String(), a); err != nil { | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); 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 | ||||||
|  | @ -228,17 +244,19 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa | ||||||
| 		"func": "FederatingCallbacks", | 		"func": "FederatingCallbacks", | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	targetAcctI := ctx.Value(util.APAccount) | 	receivingAcctI := ctx.Value(util.APAccount) | ||||||
| 	if targetAcctI == nil { | 	if receivingAcctI == nil { | ||||||
| 		l.Error("target account wasn't set on context") | 		l.Error("receiving account wasn't set on context") | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | 	receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		l.Error("target account was set on context but couldn't be parsed") | 		l.Error("receiving account was set on context but couldn't be parsed") | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept | 	var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept | ||||||
| 	if targetAcct.Locked { | 	if receivingAcct.Locked { | ||||||
| 		onFollow = pub.OnFollowDoNothing | 		onFollow = pub.OnFollowDoNothing | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -248,6 +266,17 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa | ||||||
| 		OnFollow: onFollow, | 		OnFollow: onFollow, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	other = []interface{}{ | ||||||
|  | 		// override default undo behavior | ||||||
|  | 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | ||||||
|  | 			return f.FederatingDB().Undo(ctx, undo) | ||||||
|  | 		}, | ||||||
|  | 		// override default accept behavior | ||||||
|  | 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | ||||||
|  | 			return f.FederatingDB().Accept(ctx, accept) | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,6 +34,8 @@ import ( | ||||||
| type Federator interface { | type Federator interface { | ||||||
| 	// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes. | 	// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes. | ||||||
| 	FederatingActor() pub.FederatingActor | 	FederatingActor() pub.FederatingActor | ||||||
|  | 	// FederatingDB returns the underlying FederatingDB interface. | ||||||
|  | 	FederatingDB() FederatingDB | ||||||
| 	// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources. | 	// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources. | ||||||
| 	// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments. | 	// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments. | ||||||
| 	AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error) | 	AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error) | ||||||
|  | @ -52,6 +54,7 @@ type Federator interface { | ||||||
| type federator struct { | type federator struct { | ||||||
| 	config              *config.Config | 	config              *config.Config | ||||||
| 	db                  db.DB | 	db                  db.DB | ||||||
|  | 	federatingDB        FederatingDB | ||||||
| 	clock               pub.Clock | 	clock               pub.Clock | ||||||
| 	typeConverter       typeutils.TypeConverter | 	typeConverter       typeutils.TypeConverter | ||||||
| 	transportController transport.Controller | 	transportController transport.Controller | ||||||
|  | @ -60,18 +63,19 @@ type federator struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewFederator returns a new federator | // NewFederator returns a new federator | ||||||
| func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator { | func NewFederator(db db.DB, federatingDB FederatingDB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator { | ||||||
| 
 | 
 | ||||||
| 	clock := &Clock{} | 	clock := &Clock{} | ||||||
| 	f := &federator{ | 	f := &federator{ | ||||||
| 		config:              config, | 		config:              config, | ||||||
| 		db:                  db, | 		db:                  db, | ||||||
|  | 		federatingDB:        federatingDB, | ||||||
| 		clock:               &Clock{}, | 		clock:               &Clock{}, | ||||||
| 		typeConverter:       typeConverter, | 		typeConverter:       typeConverter, | ||||||
| 		transportController: transportController, | 		transportController: transportController, | ||||||
| 		log:                 log, | 		log:                 log, | ||||||
| 	} | 	} | ||||||
| 	actor := newFederatingActor(f, f, db.Federation(), clock) | 	actor := newFederatingActor(f, f, federatingDB, clock) | ||||||
| 	f.actor = actor | 	f.actor = actor | ||||||
| 	return f | 	return f | ||||||
| } | } | ||||||
|  | @ -79,3 +83,7 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co | ||||||
| func (f *federator) FederatingActor() pub.FederatingActor { | func (f *federator) FederatingActor() pub.FederatingActor { | ||||||
| 	return f.actor | 	return f.actor | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (f *federator) FederatingDB() FederatingDB { | ||||||
|  | 	return f.federatingDB | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	})) | 	})) | ||||||
| 	// setup module being tested | 	// setup module being tested | ||||||
| 	federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter) | 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter) | ||||||
| 
 | 
 | ||||||
| 	// setup request | 	// setup request | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  | @ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	// now setup module being tested, with the mock transport controller | 	// now setup module being tested, with the mock transport controller | ||||||
| 	federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter) | 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter) | ||||||
| 
 | 
 | ||||||
| 	// setup request | 	// setup request | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | @ -27,11 +27,13 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/pub" | 	"github.com/go-fed/activity/pub" | ||||||
| 	"github.com/go-fed/activity/streams" | 	"github.com/go-fed/activity/streams" | ||||||
| 	"github.com/go-fed/activity/streams/vocab" | 	"github.com/go-fed/activity/streams/vocab" | ||||||
| 	"github.com/go-fed/httpsig" | 	"github.com/go-fed/httpsig" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
|  | @ -128,57 +130,73 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques | ||||||
| 		return nil, fmt.Errorf("could not parse key id into a url: %s", err) | 		return nil, fmt.Errorf("could not parse key id into a url: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	transport, err := f.GetTransportForUser(username) | 	var publicKey interface{} | ||||||
| 	if err != nil { | 	var pkOwnerURI *url.URL | ||||||
| 		return nil, fmt.Errorf("transport err: %s", err) | 	if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) { | ||||||
| 	} | 		// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing | ||||||
|  | 		requestingLocalAccount := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err) | ||||||
|  | 		} | ||||||
|  | 		publicKey = requestingLocalAccount.PublicKey | ||||||
|  | 		pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// the request is remote, so we need to authenticate the request properly by dereferencing the remote key | ||||||
|  | 		transport, err := f.GetTransportForUser(username) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("transport err: %s", err) | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 	// The actual http call to the remote server is made right here in the Dereference function. | 		// The actual http call to the remote server is made right here in the Dereference function. | ||||||
| 	b, err := transport.Dereference(context.Background(), requestingPublicKeyID) | 		b, err := transport.Dereference(context.Background(), requestingPublicKeyID) | ||||||
| 	if err != nil { | 		if err != nil { | ||||||
| 		return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) | 			return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) | ||||||
| 	} | 		} | ||||||
| 
 | 
 | ||||||
| 	// if the key isn't in the response, we can't authenticate the request | 		// if the key isn't in the response, we can't authenticate the request | ||||||
| 	requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) | 		requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) | ||||||
| 	if err != nil { | 		if err != nil { | ||||||
| 		return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) | 			return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) | ||||||
| 	} | 		} | ||||||
| 
 | 
 | ||||||
| 	// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey | 		// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey | ||||||
| 	pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | 		pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | ||||||
| 	if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { | 		if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { | ||||||
| 		return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") | 			return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") | ||||||
| 	} | 		} | ||||||
| 
 | 
 | ||||||
| 	// and decode the PEM so that we can parse it as a golang public key | 		// and decode the PEM so that we can parse it as a golang public key | ||||||
| 	pubKeyPem := pkPemProp.Get() | 		pubKeyPem := pkPemProp.Get() | ||||||
| 	block, _ := pem.Decode([]byte(pubKeyPem)) | 		block, _ := pem.Decode([]byte(pubKeyPem)) | ||||||
| 	if block == nil || block.Type != "PUBLIC KEY" { | 		if block == nil || block.Type != "PUBLIC KEY" { | ||||||
| 		return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") | 			return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") | ||||||
| 	} | 		} | ||||||
| 
 | 
 | ||||||
| 	p, err := x509.ParsePKIXPublicKey(block.Bytes) | 		publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) | ||||||
| 	if err != nil { | 		if err != nil { | ||||||
| 		return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) | 			return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// all good! we just need the URI of the key owner to return | ||||||
|  | 		pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() | ||||||
|  | 		if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { | ||||||
|  | 			return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") | ||||||
|  | 		} | ||||||
|  | 		pkOwnerURI = pkOwnerProp.GetIRI() | ||||||
| 	} | 	} | ||||||
| 	if p == nil { | 	if publicKey == nil { | ||||||
| 		return nil, errors.New("returned public key was empty") | 		return nil, errors.New("returned public key was empty") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// do the actual authentication here! | 	// do the actual authentication here! | ||||||
| 	algo := httpsig.RSA_SHA256 // TODO: make this more robust | 	algo := httpsig.RSA_SHA256 // TODO: make this more robust | ||||||
| 	if err := verifier.Verify(p, algo); err != nil { | 	if err := verifier.Verify(publicKey, algo); err != nil { | ||||||
| 		return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err) | 		return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// all good! we just need the URI of the key owner to return |  | ||||||
| 	pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() |  | ||||||
| 	if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { |  | ||||||
| 		return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") |  | ||||||
| 	} |  | ||||||
| 	pkOwnerURI := pkOwnerProp.GetIRI() |  | ||||||
| 
 |  | ||||||
| 	return pkOwnerURI, nil | 	return pkOwnerURI, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -217,6 +235,12 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u | ||||||
| 			return nil, errors.New("error resolving type as activitystreams application") | 			return nil, errors.New("error resolving type as activitystreams application") | ||||||
| 		} | 		} | ||||||
| 		return p, nil | 		return p, nil | ||||||
|  | 	case string(gtsmodel.ActivityStreamsService): | ||||||
|  | 		p, ok := t.(vocab.ActivityStreamsService) | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, errors.New("error resolving type as activitystreams service") | ||||||
|  | 		} | ||||||
|  | 		return p, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) | 	return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) | ||||||
|  | @ -243,3 +267,23 @@ func (f *federator) GetTransportForUser(username string) (transport.Transport, e | ||||||
| 	} | 	} | ||||||
| 	return transport, nil | 	return transport, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor vocab.ActivityStreamsActorProperty) bool { | ||||||
|  | 	if activityActor == nil || followActor == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	for aIter := activityActor.Begin(); aIter != activityActor.End(); aIter = aIter.Next() { | ||||||
|  | 		for fIter := followActor.Begin(); fIter != followActor.End(); fIter = fIter.Next() { | ||||||
|  | 			if aIter.GetIRI() == nil { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			if fIter.GetIRI() == nil { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			if aIter.GetIRI().String() == fIter.GetIRI().String() { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -83,6 +83,8 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr | ||||||
| 		return fmt.Errorf("error creating dbservice: %s", err) | 		return fmt.Errorf("error creating dbservice: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	federatingDB := federation.NewFederatingDB(dbService, c, log) | ||||||
|  | 
 | ||||||
| 	router, err := router.New(c, log) | 	router, err := router.New(c, log) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error creating router: %s", err) | 		return fmt.Errorf("error creating router: %s", err) | ||||||
|  | @ -100,7 +102,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr | ||||||
| 	mediaHandler := media.New(c, dbService, storageBackend, log) | 	mediaHandler := media.New(c, dbService, storageBackend, log) | ||||||
| 	oauthServer := oauth.New(dbService, log) | 	oauthServer := oauth.New(dbService, log) | ||||||
| 	transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log) | 	transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log) | ||||||
| 	federator := federation.NewFederator(dbService, transportController, c, log, typeConverter) | 	federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter) | ||||||
| 	processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log) | 	processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log) | ||||||
| 	if err := processor.Start(); err != nil { | 	if err := processor.Start(); err != nil { | ||||||
| 		return fmt.Errorf("error starting processor: %s", err) | 		return fmt.Errorf("error starting processor: %s", err) | ||||||
|  |  | ||||||
|  | @ -76,13 +76,13 @@ type Account struct { | ||||||
| 	*/ | 	*/ | ||||||
| 
 | 
 | ||||||
| 	// Does this account need an approval for new followers? | 	// Does this account need an approval for new followers? | ||||||
| 	Locked bool `pg:",default:'true'"` | 	Locked bool | ||||||
| 	// Should this account be shown in the instance's profile directory? | 	// Should this account be shown in the instance's profile directory? | ||||||
| 	Discoverable bool | 	Discoverable bool | ||||||
| 	// Default post privacy for this account | 	// Default post privacy for this account | ||||||
| 	Privacy Visibility | 	Privacy Visibility | ||||||
| 	// Set posts from this account to sensitive by default? | 	// Set posts from this account to sensitive by default? | ||||||
| 	Sensitive bool `pg:",default:'false'"` | 	Sensitive bool | ||||||
| 	// What language does this account post in? | 	// What language does this account post in? | ||||||
| 	Language string `pg:",default:'en'"` | 	Language string `pg:",default:'en'"` | ||||||
| 
 | 
 | ||||||
|  | @ -107,7 +107,7 @@ type Account struct { | ||||||
| 	// URL for getting the featured collection list of this account | 	// URL for getting the featured collection list of this account | ||||||
| 	FeaturedCollectionURI string `pg:",unique"` | 	FeaturedCollectionURI string `pg:",unique"` | ||||||
| 	// What type of activitypub actor is this account? | 	// What type of activitypub actor is this account? | ||||||
| 	ActorType ActivityStreamsActor | 	ActorType string | ||||||
| 	// This account is associated with x account id | 	// This account is associated with x account id | ||||||
| 	AlsoKnownAs string | 	AlsoKnownAs string | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,110 +18,101 @@ | ||||||
| 
 | 
 | ||||||
| package gtsmodel | package gtsmodel | ||||||
| 
 | 
 | ||||||
| // ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types |  | ||||||
| type ActivityStreamsObject string |  | ||||||
| 
 |  | ||||||
| const ( | const ( | ||||||
| 	// ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article | 	// ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article | ||||||
| 	ActivityStreamsArticle ActivityStreamsObject = "Article" | 	ActivityStreamsArticle = "Article" | ||||||
| 	// ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio | 	// ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio | ||||||
| 	ActivityStreamsAudio ActivityStreamsObject = "Audio" | 	ActivityStreamsAudio = "Audio" | ||||||
| 	// ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document | 	// ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document | ||||||
| 	ActivityStreamsDocument ActivityStreamsObject = "Event" | 	ActivityStreamsDocument = "Event" | ||||||
| 	// ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event | 	// ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event | ||||||
| 	ActivityStreamsEvent ActivityStreamsObject = "Event" | 	ActivityStreamsEvent = "Event" | ||||||
| 	// ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image | 	// ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image | ||||||
| 	ActivityStreamsImage ActivityStreamsObject = "Image" | 	ActivityStreamsImage = "Image" | ||||||
| 	// ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note | 	// ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note | ||||||
| 	ActivityStreamsNote ActivityStreamsObject = "Note" | 	ActivityStreamsNote = "Note" | ||||||
| 	// ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page | 	// ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page | ||||||
| 	ActivityStreamsPage ActivityStreamsObject = "Page" | 	ActivityStreamsPage = "Page" | ||||||
| 	// ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place | 	// ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place | ||||||
| 	ActivityStreamsPlace ActivityStreamsObject = "Place" | 	ActivityStreamsPlace = "Place" | ||||||
| 	// ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile | 	// ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile | ||||||
| 	ActivityStreamsProfile ActivityStreamsObject = "Profile" | 	ActivityStreamsProfile = "Profile" | ||||||
| 	// ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship | 	// ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship | ||||||
| 	ActivityStreamsRelationship ActivityStreamsObject = "Relationship" | 	ActivityStreamsRelationship = "Relationship" | ||||||
| 	// ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone | 	// ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone | ||||||
| 	ActivityStreamsTombstone ActivityStreamsObject = "Tombstone" | 	ActivityStreamsTombstone = "Tombstone" | ||||||
| 	// ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video | 	// ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video | ||||||
| 	ActivityStreamsVideo ActivityStreamsObject = "Video" | 	ActivityStreamsVideo = "Video" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ActivityStreamsActor refers to https://www.w3.org/TR/activitystreams-vocabulary/#actor-types |  | ||||||
| type ActivityStreamsActor string |  | ||||||
| 
 |  | ||||||
| const ( | const ( | ||||||
| 	// ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application | 	// ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application | ||||||
| 	ActivityStreamsApplication ActivityStreamsActor = "Application" | 	ActivityStreamsApplication = "Application" | ||||||
| 	// ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group | 	// ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group | ||||||
| 	ActivityStreamsGroup ActivityStreamsActor = "Group" | 	ActivityStreamsGroup = "Group" | ||||||
| 	// ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization | 	// ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization | ||||||
| 	ActivityStreamsOrganization ActivityStreamsActor = "Organization" | 	ActivityStreamsOrganization = "Organization" | ||||||
| 	// ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person | 	// ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person | ||||||
| 	ActivityStreamsPerson ActivityStreamsActor = "Person" | 	ActivityStreamsPerson = "Person" | ||||||
| 	// ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service | 	// ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service | ||||||
| 	ActivityStreamsService ActivityStreamsActor = "Service" | 	ActivityStreamsService = "Service" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ActivityStreamsActivity refers to https://www.w3.org/TR/activitystreams-vocabulary/#activity-types |  | ||||||
| type ActivityStreamsActivity string |  | ||||||
| 
 |  | ||||||
| const ( | const ( | ||||||
| 	// ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept | 	// ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept | ||||||
| 	ActivityStreamsAccept ActivityStreamsActivity = "Accept" | 	ActivityStreamsAccept = "Accept" | ||||||
| 	// ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add | 	// ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add | ||||||
| 	ActivityStreamsAdd ActivityStreamsActivity = "Add" | 	ActivityStreamsAdd = "Add" | ||||||
| 	// ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce | 	// ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce | ||||||
| 	ActivityStreamsAnnounce ActivityStreamsActivity = "Announce" | 	ActivityStreamsAnnounce = "Announce" | ||||||
| 	// ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive | 	// ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive | ||||||
| 	ActivityStreamsArrive ActivityStreamsActivity = "Arrive" | 	ActivityStreamsArrive = "Arrive" | ||||||
| 	// ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block | 	// ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block | ||||||
| 	ActivityStreamsBlock ActivityStreamsActivity = "Block" | 	ActivityStreamsBlock = "Block" | ||||||
| 	// ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create | 	// ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create | ||||||
| 	ActivityStreamsCreate ActivityStreamsActivity = "Create" | 	ActivityStreamsCreate = "Create" | ||||||
| 	// ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete | 	// ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete | ||||||
| 	ActivityStreamsDelete ActivityStreamsActivity = "Delete" | 	ActivityStreamsDelete = "Delete" | ||||||
| 	// ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike | 	// ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike | ||||||
| 	ActivityStreamsDislike ActivityStreamsActivity = "Dislike" | 	ActivityStreamsDislike = "Dislike" | ||||||
| 	// ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag | 	// ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag | ||||||
| 	ActivityStreamsFlag ActivityStreamsActivity = "Flag" | 	ActivityStreamsFlag = "Flag" | ||||||
| 	// ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow | 	// ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow | ||||||
| 	ActivityStreamsFollow ActivityStreamsActivity = "Follow" | 	ActivityStreamsFollow = "Follow" | ||||||
| 	// ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore | 	// ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore | ||||||
| 	ActivityStreamsIgnore ActivityStreamsActivity = "Ignore" | 	ActivityStreamsIgnore = "Ignore" | ||||||
| 	// ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite | 	// ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite | ||||||
| 	ActivityStreamsInvite ActivityStreamsActivity = "Invite" | 	ActivityStreamsInvite = "Invite" | ||||||
| 	// ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join | 	// ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join | ||||||
| 	ActivityStreamsJoin ActivityStreamsActivity = "Join" | 	ActivityStreamsJoin = "Join" | ||||||
| 	// ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave | 	// ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave | ||||||
| 	ActivityStreamsLeave ActivityStreamsActivity = "Leave" | 	ActivityStreamsLeave = "Leave" | ||||||
| 	// ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like | 	// ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like | ||||||
| 	ActivityStreamsLike ActivityStreamsActivity = "Like" | 	ActivityStreamsLike = "Like" | ||||||
| 	// ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen | 	// ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen | ||||||
| 	ActivityStreamsListen ActivityStreamsActivity = "Listen" | 	ActivityStreamsListen = "Listen" | ||||||
| 	// ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move | 	// ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move | ||||||
| 	ActivityStreamsMove ActivityStreamsActivity = "Move" | 	ActivityStreamsMove = "Move" | ||||||
| 	// ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer | 	// ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer | ||||||
| 	ActivityStreamsOffer ActivityStreamsActivity = "Offer" | 	ActivityStreamsOffer = "Offer" | ||||||
| 	// ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question | 	// ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question | ||||||
| 	ActivityStreamsQuestion ActivityStreamsActivity = "Question" | 	ActivityStreamsQuestion = "Question" | ||||||
| 	// ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject | 	// ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject | ||||||
| 	ActivityStreamsReject ActivityStreamsActivity = "Reject" | 	ActivityStreamsReject = "Reject" | ||||||
| 	// ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read | 	// ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read | ||||||
| 	ActivityStreamsRead ActivityStreamsActivity = "Read" | 	ActivityStreamsRead = "Read" | ||||||
| 	// ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove | 	// ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove | ||||||
| 	ActivityStreamsRemove ActivityStreamsActivity = "Remove" | 	ActivityStreamsRemove = "Remove" | ||||||
| 	// ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject | 	// ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject | ||||||
| 	ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject" | 	ActivityStreamsTentativeReject = "TentativeReject" | ||||||
| 	// ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept | 	// ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept | ||||||
| 	ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept" | 	ActivityStreamsTentativeAccept = "TentativeAccept" | ||||||
| 	// ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel | 	// ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel | ||||||
| 	ActivityStreamsTravel ActivityStreamsActivity = "Travel" | 	ActivityStreamsTravel = "Travel" | ||||||
| 	// ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo | 	// ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo | ||||||
| 	ActivityStreamsUndo ActivityStreamsActivity = "Undo" | 	ActivityStreamsUndo = "Undo" | ||||||
| 	// ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update | 	// ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update | ||||||
| 	ActivityStreamsUpdate ActivityStreamsActivity = "Update" | 	ActivityStreamsUpdate = "Update" | ||||||
| 	// ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view | 	// ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view | ||||||
| 	ActivityStreamsView ActivityStreamsActivity = "View" | 	ActivityStreamsView = "View" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -38,6 +38,14 @@ type Mention struct { | ||||||
| 	TargetAccountID string `pg:",notnull"` | 	TargetAccountID string `pg:",notnull"` | ||||||
| 	// Prevent this mention from generating a notification? | 	// Prevent this mention from generating a notification? | ||||||
| 	Silent bool | 	Silent bool | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		NON-DATABASE CONVENIENCE FIELDS | ||||||
|  | 		These fields are just for convenience while passing the mention | ||||||
|  | 		around internally, to make fewer database calls and whatnot. They're | ||||||
|  | 		not meant to be put in the database! | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
| 	// NameString is for putting in the namestring of the mentioned user | 	// NameString is for putting in the namestring of the mentioned user | ||||||
| 	// before the mention is dereferenced. Should be in a form along the lines of: | 	// before the mention is dereferenced. Should be in a form along the lines of: | ||||||
| 	// @whatever_username@example.org | 	// @whatever_username@example.org | ||||||
|  | @ -48,4 +56,6 @@ type Mention struct { | ||||||
| 	// | 	// | ||||||
| 	// This will not be put in the database, it's just for convenience. | 	// This will not be put in the database, it's just for convenience. | ||||||
| 	MentionedAccountURI string `pg:"-"` | 	MentionedAccountURI string `pg:"-"` | ||||||
|  | 	// A pointer to the gtsmodel account of the mentioned account. | ||||||
|  | 	GTSAccount *Account `pg:"-"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,9 +9,11 @@ package gtsmodel | ||||||
| 
 | 
 | ||||||
| // FromClientAPI wraps a message that travels from client API into the processor | // FromClientAPI wraps a message that travels from client API into the processor | ||||||
| type FromClientAPI struct { | type FromClientAPI struct { | ||||||
| 	APObjectType   ActivityStreamsObject | 	APObjectType   string | ||||||
| 	APActivityType ActivityStreamsActivity | 	APActivityType string | ||||||
| 	GTSModel       interface{} | 	GTSModel       interface{} | ||||||
|  | 	OriginAccount  *Account | ||||||
|  | 	TargetAccount  *Account | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // // ToFederator wraps a message that travels from the processor into the federator | // // ToFederator wraps a message that travels from the processor into the federator | ||||||
|  | @ -23,7 +25,8 @@ type FromClientAPI struct { | ||||||
| 
 | 
 | ||||||
| // FromFederator wraps a message that travels from the federator into the processor | // FromFederator wraps a message that travels from the federator into the processor | ||||||
| type FromFederator struct { | type FromFederator struct { | ||||||
| 	APObjectType   ActivityStreamsObject | 	APObjectType     string | ||||||
| 	APActivityType ActivityStreamsActivity | 	APActivityType   string | ||||||
| 	GTSModel       interface{} | 	GTSModel         interface{} | ||||||
|  | 	ReceivingAccount *Account | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										49
									
								
								internal/gtsmodel/relationship.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/gtsmodel/relationship.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 gtsmodel | ||||||
|  | 
 | ||||||
|  | // Relationship describes a requester's relationship with another account. | ||||||
|  | type Relationship struct { | ||||||
|  | 	// The account id. | ||||||
|  | 	ID string | ||||||
|  | 	// Are you following this user? | ||||||
|  | 	Following bool | ||||||
|  | 	// Are you receiving this user's boosts in your home timeline? | ||||||
|  | 	ShowingReblogs bool | ||||||
|  | 	// Have you enabled notifications for this user? | ||||||
|  | 	Notifying bool | ||||||
|  | 	// Are you followed by this user? | ||||||
|  | 	FollowedBy bool | ||||||
|  | 	// Are you blocking this user? | ||||||
|  | 	Blocking bool | ||||||
|  | 	// Is this user blocking you? | ||||||
|  | 	BlockedBy bool | ||||||
|  | 	// Are you muting this user? | ||||||
|  | 	Muting bool | ||||||
|  | 	// Are you muting notifications from this user? | ||||||
|  | 	MutingNotifications bool | ||||||
|  | 	// Do you have a pending follow request for this user? | ||||||
|  | 	Requested bool | ||||||
|  | 	// Are you blocking this user's domain? | ||||||
|  | 	DomainBlocking bool | ||||||
|  | 	// Are you featuring this user on your profile? | ||||||
|  | 	Endorsed bool | ||||||
|  | 	// Your note on this account. | ||||||
|  | 	Note string | ||||||
|  | } | ||||||
|  | @ -66,7 +66,7 @@ type Status struct { | ||||||
| 	VisibilityAdvanced *VisibilityAdvanced | 	VisibilityAdvanced *VisibilityAdvanced | ||||||
| 	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types | 	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types | ||||||
| 	// Will probably almost always be Note but who knows!. | 	// Will probably almost always be Note but who knows!. | ||||||
| 	ActivityStreamsType ActivityStreamsObject | 	ActivityStreamsType string | ||||||
| 	// Original text of the status without formatting | 	// Original text of the status without formatting | ||||||
| 	Text string | 	Text string | ||||||
| 	// Has this status been pinned by its owner? | 	// Has this status been pinned by its owner? | ||||||
|  |  | ||||||
|  | @ -67,7 +67,7 @@ type Handler interface { | ||||||
| 	// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | 	// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | ||||||
| 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | ||||||
| 	// and then returns information to the caller about the new header. | 	// and then returns information to the caller about the new header. | ||||||
| 	ProcessHeaderOrAvatar(img []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error) | 	ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) | ||||||
| 
 | 
 | ||||||
| 	// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, | 	// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, | ||||||
| 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, | 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, | ||||||
|  | @ -86,6 +86,8 @@ type Handler interface { | ||||||
| 	// information to the caller about the new attachment. It's the caller's responsibility to put the returned struct | 	// information to the caller about the new attachment. It's the caller's responsibility to put the returned struct | ||||||
| 	// in the database. | 	// in the database. | ||||||
| 	ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) | 	ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) | ||||||
|  | 
 | ||||||
|  | 	ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type mediaHandler struct { | type mediaHandler struct { | ||||||
|  | @ -112,7 +114,7 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo | ||||||
| // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | ||||||
| // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | ||||||
| // and then returns information to the caller about the new header. | // and then returns information to the caller about the new header. | ||||||
| func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error) { | func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||||
| 	l := mh.log.WithField("func", "SetHeaderForAccountID") | 	l := mh.log.WithField("func", "SetHeaderForAccountID") | ||||||
| 
 | 
 | ||||||
| 	if mediaType != Header && mediaType != Avatar { | 	if mediaType != Header && mediaType != Avatar { | ||||||
|  | @ -134,7 +136,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin | ||||||
| 	l.Tracef("read %d bytes of file", len(attachment)) | 	l.Tracef("read %d bytes of file", len(attachment)) | ||||||
| 
 | 
 | ||||||
| 	// process it | 	// process it | ||||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID) | 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing %s: %s", mediaType, err) | 		return nil, fmt.Errorf("error processing %s: %s", mediaType, err) | ||||||
| 	} | 	} | ||||||
|  | @ -315,3 +317,43 @@ func (mh *mediaHandler) ProcessRemoteAttachment(t transport.Transport, currentAt | ||||||
| 
 | 
 | ||||||
| 	return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL) | 	return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||||
|  | 
 | ||||||
|  | 	if !currentAttachment.Header && !currentAttachment.Avatar { | ||||||
|  | 		return nil, errors.New("provided attachment was set to neither header nor avatar") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if currentAttachment.Header && currentAttachment.Avatar { | ||||||
|  | 		return nil, errors.New("provided attachment was set to both header and avatar") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var headerOrAvi Type | ||||||
|  | 	if currentAttachment.Header { | ||||||
|  | 		headerOrAvi = Header | ||||||
|  | 	} else if currentAttachment.Avatar { | ||||||
|  | 		headerOrAvi = Avatar | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if currentAttachment.RemoteURL == "" { | ||||||
|  | 		return nil, errors.New("no remote URL on media attachment to dereference") | ||||||
|  | 	} | ||||||
|  | 	remoteIRI, err := url.Parse(currentAttachment.RemoteURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error parsing attachment url %s: %s", currentAttachment.RemoteURL, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// for content type, we assume we don't know what to expect... | ||||||
|  | 	expectedContentType := "*/*" | ||||||
|  | 	if currentAttachment.File.ContentType != "" { | ||||||
|  | 		// ... and then narrow it down if we do | ||||||
|  | 		expectedContentType = currentAttachment.File.ContentType | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	attachmentBytes, err := t.DereferenceMedia(context.Background(), remoteIRI, expectedContentType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return mh.ProcessHeaderOrAvatar(attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -147,7 +147,7 @@ func (suite *MediaTestSuite) TestSetHeaderOrAvatarForAccountID() { | ||||||
| 	f, err := ioutil.ReadFile("./test/test-jpeg.jpg") | 	f, err := ioutil.ReadFile("./test/test-jpeg.jpg") | ||||||
| 	assert.Nil(suite.T(), err) | 	assert.Nil(suite.T(), err) | ||||||
| 
 | 
 | ||||||
| 	ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header") | 	ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header", "") | ||||||
| 	assert.Nil(suite.T(), err) | 	assert.Nil(suite.T(), err) | ||||||
| 	suite.log.Debugf("%+v", ma) | 	suite.log.Debugf("%+v", ma) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string) (*gtsmodel.MediaAttachment, error) { | func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||||
| 	var isHeader bool | 	var isHeader bool | ||||||
| 	var isAvatar bool | 	var isAvatar bool | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +96,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string | ||||||
| 		ID:        newMediaID, | 		ID:        newMediaID, | ||||||
| 		StatusID:  "", | 		StatusID:  "", | ||||||
| 		URL:       originalURL, | 		URL:       originalURL, | ||||||
| 		RemoteURL: "", | 		RemoteURL: remoteURL, | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		UpdatedAt: time.Now(), | 		UpdatedAt: time.Now(), | ||||||
| 		Type:      gtsmodel.FileTypeImage, | 		Type:      gtsmodel.FileTypeImage, | ||||||
|  |  | ||||||
|  | @ -78,6 +78,15 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api | ||||||
| 		return nil, fmt.Errorf("db error: %s", err) | 		return nil, fmt.Errorf("db error: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// lazily dereference things on the account if it hasn't been done yet | ||||||
|  | 	var requestingUsername string | ||||||
|  | 	if authed.Account != nil { | ||||||
|  | 		requestingUsername = authed.Account.Username | ||||||
|  | 	} | ||||||
|  | 	if err := p.dereferenceAccountFields(targetAccount, requestingUsername); err != nil { | ||||||
|  | 		p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	var mastoAccount *apimodel.Account | 	var mastoAccount *apimodel.Account | ||||||
| 	var err error | 	var err error | ||||||
| 	if authed.Account != nil && targetAccount.ID == authed.Account.ID { | 	if authed.Account != nil && targetAccount.ID == authed.Account.ID { | ||||||
|  | @ -285,6 +294,12 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri | ||||||
| 			return nil, NewErrorInternalError(err) | 			return nil, NewErrorInternalError(err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		// derefence account fields in case we haven't done it already | ||||||
|  | 		if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil { | ||||||
|  | 			// don't bail if we can't fetch them, we'll try another time | ||||||
|  | 			p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		account, err := p.tc.AccountToMastoPublic(a) | 		account, err := p.tc.AccountToMastoPublic(a) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, NewErrorInternalError(err) | 			return nil, NewErrorInternalError(err) | ||||||
|  | @ -293,3 +308,238 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri | ||||||
| 	} | 	} | ||||||
| 	return accounts, nil | 	return accounts, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) { | ||||||
|  | 	blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if blocked { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	following := []gtsmodel.Follow{} | ||||||
|  | 	accounts := []apimodel.Account{} | ||||||
|  | 	if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil { | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 			return accounts, nil | ||||||
|  | 		} | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, f := range following { | ||||||
|  | 		blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 		if blocked { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		a := >smodel.Account{} | ||||||
|  | 		if err := p.db.GetByID(f.TargetAccountID, a); err != nil { | ||||||
|  | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// derefence account fields in case we haven't done it already | ||||||
|  | 		if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil { | ||||||
|  | 			// don't bail if we can't fetch them, we'll try another time | ||||||
|  | 			p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		account, err := p.tc.AccountToMastoPublic(a) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 		accounts = append(accounts, *account) | ||||||
|  | 	} | ||||||
|  | 	return accounts, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { | ||||||
|  | 	if authed == nil || authed.Account == nil { | ||||||
|  | 		return nil, NewErrorForbidden(errors.New("not authed")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r, err := p.tc.RelationshipToMasto(gtsR) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return r, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) { | ||||||
|  | 	// if there's a block between the accounts we shouldn't create the request ofc | ||||||
|  | 	blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 	if blocked { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// make sure the target account actually exists in our db | ||||||
|  | 	targetAcct := >smodel.Account{} | ||||||
|  | 	if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil { | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if a follow exists already | ||||||
|  | 	follows, err := p.db.Follows(authed.Account, targetAcct) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) | ||||||
|  | 	} | ||||||
|  | 	if follows { | ||||||
|  | 		// already follows so just return the relationship | ||||||
|  | 		return p.AccountRelationshipGet(authed, form.TargetAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if a follow exists already | ||||||
|  | 	followRequested, err := p.db.FollowRequested(authed.Account, targetAcct) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) | ||||||
|  | 	} | ||||||
|  | 	if followRequested { | ||||||
|  | 		// already follow requested so just return the relationship | ||||||
|  | 		return p.AccountRelationshipGet(authed, form.TargetAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// make the follow request | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		AccountID:       authed.Account.ID, | ||||||
|  | 		TargetAccountID: form.TargetAccountID, | ||||||
|  | 		ShowReblogs:     true, | ||||||
|  | 		URI:             util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host), | ||||||
|  | 		Notify:          false, | ||||||
|  | 	} | ||||||
|  | 	if form.Reblogs != nil { | ||||||
|  | 		fr.ShowReblogs = *form.Reblogs | ||||||
|  | 	} | ||||||
|  | 	if form.Notify != nil { | ||||||
|  | 		fr.Notify = *form.Notify | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// whack it in the database | ||||||
|  | 	if err := p.db.Put(fr); err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// if it's a local account that's not locked we can just straight up accept the follow request | ||||||
|  | 	if !targetAcct.Locked && targetAcct.Domain == "" { | ||||||
|  | 		if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err)) | ||||||
|  | 		} | ||||||
|  | 		// return the new relationship | ||||||
|  | 		return p.AccountRelationshipGet(authed, form.TargetAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously | ||||||
|  | 	p.fromClientAPI <- gtsmodel.FromClientAPI{ | ||||||
|  | 		APObjectType:   gtsmodel.ActivityStreamsFollow, | ||||||
|  | 		APActivityType: gtsmodel.ActivityStreamsCreate, | ||||||
|  | 		GTSModel: >smodel.Follow{ | ||||||
|  | 			AccountID:       authed.Account.ID, | ||||||
|  | 			TargetAccountID: form.TargetAccountID, | ||||||
|  | 			URI:             fr.URI, | ||||||
|  | 		}, | ||||||
|  | 		OriginAccount: authed.Account, | ||||||
|  | 		TargetAccount: targetAcct, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// return whatever relationship results from this | ||||||
|  | 	return p.AccountRelationshipGet(authed, form.TargetAccountID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { | ||||||
|  | 	// if there's a block between the accounts we shouldn't do anything | ||||||
|  | 	blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 	if blocked { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 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, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if a follow request exists, 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: authed.Account.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, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: 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: authed.Account.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, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: 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:       authed.Account.ID, | ||||||
|  | 				TargetAccountID: targetAccountID, | ||||||
|  | 				URI:             frURI, | ||||||
|  | 			}, | ||||||
|  | 			OriginAccount: authed.Account, | ||||||
|  | 			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:       authed.Account.ID, | ||||||
|  | 				TargetAccountID: targetAccountID, | ||||||
|  | 				URI:             fURI, | ||||||
|  | 			}, | ||||||
|  | 			OriginAccount: authed.Account, | ||||||
|  | 			TargetAccount: targetAcct, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// return whatever relationship results from all this | ||||||
|  | 	return p.AccountRelationshipGet(authed, targetAccountID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/streams" | 	"github.com/go-fed/activity/streams" | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | @ -46,7 +47,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht | ||||||
| 	// we might already have an entry for this account so check that first | 	// we might already have an entry for this account so check that first | ||||||
| 	requestingAccount := >smodel.Account{} | 	requestingAccount := >smodel.Account{} | ||||||
| 
 | 
 | ||||||
| 	err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount) | 	err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		// we do have it yay, return it | 		// we do have it yay, return it | ||||||
| 		return requestingAccount, nil | 		return requestingAccount, nil | ||||||
|  | @ -122,6 +123,89 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request) | ||||||
| 	return data, nil | 	return data, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { | ||||||
|  | 	// get the account the request is referring to | ||||||
|  | 	requestedAccount := >smodel.Account{} | ||||||
|  | 	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// authenticate the request | ||||||
|  | 	requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorNotAuthorized(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if blocked { | ||||||
|  | 		return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestedAccountURI, err := url.Parse(requestedAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	data, err := streams.Serialize(requestedFollowers) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return data, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) { | ||||||
|  | 		// get the account the request is referring to | ||||||
|  | 		requestedAccount := >smodel.Account{} | ||||||
|  | 		if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// authenticate the request | ||||||
|  | 		requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorNotAuthorized(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if blocked { | ||||||
|  | 			return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		s := >smodel.Status{} | ||||||
|  | 		if err := p.db.GetWhere([]db.Where{ | ||||||
|  | 			{Key: "id", Value: requestedStatusID}, | ||||||
|  | 			{Key: "account_id", Value: requestedAccount.ID}, | ||||||
|  | 		}, s); err != nil { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		asStatus, err := p.tc.StatusToAS(s) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		data, err := streams.Serialize(asStatus) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return data, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) { | func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) { | ||||||
| 	// get the account the request is referring to | 	// get the account the request is referring to | ||||||
| 	requestedAccount := >smodel.Account{} | 	requestedAccount := >smodel.Account{} | ||||||
|  |  | ||||||
|  | @ -19,55 +19,198 @@ | ||||||
| package message | package message | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { | func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { | ||||||
| 	switch clientMsg.APObjectType { | 	switch clientMsg.APActivityType { | ||||||
| 	case gtsmodel.ActivityStreamsNote: | 	case gtsmodel.ActivityStreamsCreate: | ||||||
| 		status, ok := clientMsg.GTSModel.(*gtsmodel.Status) | 		// CREATE | ||||||
| 		if !ok { | 		switch clientMsg.APObjectType { | ||||||
| 			return errors.New("note was not parseable as *gtsmodel.Status") | 		case gtsmodel.ActivityStreamsNote: | ||||||
| 		} | 			// CREATE NOTE | ||||||
|  | 			status, ok := clientMsg.GTSModel.(*gtsmodel.Status) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("note was not parseable as *gtsmodel.Status") | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 		if err := p.notifyStatus(status); err != nil { | 			if err := p.notifyStatus(status); err != nil { | ||||||
| 			return err | 				return err | ||||||
| 		} | 			} | ||||||
| 
 | 
 | ||||||
| 		if status.VisibilityAdvanced.Federated { | 			if status.VisibilityAdvanced.Federated { | ||||||
| 			return p.federateStatus(status) | 				return p.federateStatus(status) | ||||||
|  | 			} | ||||||
|  | 			return nil | ||||||
|  | 		case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 			// CREATE FOLLOW (request) | ||||||
|  | 			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("follow was not parseable as *gtsmodel.Follow") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := p.notifyFollow(follow); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
|  | 		// UPDATE | ||||||
|  | 	case gtsmodel.ActivityStreamsAccept: | ||||||
|  | 		// ACCEPT | ||||||
|  | 		switch clientMsg.APObjectType { | ||||||
|  | 		case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 			// ACCEPT FOLLOW | ||||||
|  | 			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("accept was not parseable as *gtsmodel.Follow") | ||||||
|  | 			} | ||||||
|  | 			return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsUndo: | ||||||
|  | 		// UNDO | ||||||
|  | 		switch clientMsg.APObjectType { | ||||||
|  | 		case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 			// UNDO FOLLOW | ||||||
|  | 			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("undo was not parseable as *gtsmodel.Follow") | ||||||
|  | 			} | ||||||
|  | 			return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
| 		} | 		} | ||||||
| 		return nil |  | ||||||
| 	} | 	} | ||||||
| 	return fmt.Errorf("message type unprocessable: %+v", clientMsg) | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) federateStatus(status *gtsmodel.Status) error { | func (p *processor) federateStatus(status *gtsmodel.Status) error { | ||||||
| 	// // derive the sending account -- it might be attached to the status already | 	asStatus, err := p.tc.StatusToAS(status) | ||||||
| 	// sendingAcct := >smodel.Account{} | 	if err != nil { | ||||||
| 	// if status.GTSAccount != nil { | 		return fmt.Errorf("federateStatus: error converting status to as format: %s", err) | ||||||
| 	// 	sendingAcct = status.GTSAccount | 	} | ||||||
| 	// } else { |  | ||||||
| 	// 	// it wasn't attached so get it from the db instead |  | ||||||
| 	// 	if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { |  | ||||||
| 	// 		return err |  | ||||||
| 	// 	} |  | ||||||
| 	// } |  | ||||||
| 
 | 
 | ||||||
| 	// outboxURI, err := url.Parse(sendingAcct.OutboxURI) | 	outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI) | ||||||
| 	// if err != nil { | 	if err != nil { | ||||||
| 	// 	return err | 		return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err) | ||||||
| 	// } | 	} | ||||||
| 
 | 
 | ||||||
| 	// // convert the status to AS format Note | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) | ||||||
| 	// note, err := p.tc.StatusToAS(status) | 	return err | ||||||
| 	// if err != nil { | } | ||||||
| 	// 	return err | 
 | ||||||
| 	// } | func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | ||||||
| 
 | 	// if both accounts are local there's nothing to do here | ||||||
| 	// _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
| 	return nil | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateFollow: error converting follow to as format: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(originAccount.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | ||||||
|  | 	// if both accounts are local there's nothing to do here | ||||||
|  | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// recreate the follow | ||||||
|  | 	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	targetAccountURI, err := url.Parse(targetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create an Undo and set the appropriate actor on it | ||||||
|  | 	undo := streams.NewActivityStreamsUndo() | ||||||
|  | 	undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor()) | ||||||
|  | 
 | ||||||
|  | 	// Set the recreated follow as the 'object' property. | ||||||
|  | 	undoObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	undoObject.AppendActivityStreamsFollow(asFollow) | ||||||
|  | 	undo.SetActivityStreamsObject(undoObject) | ||||||
|  | 
 | ||||||
|  | 	// Set the To of the undo as the target of the recreated follow | ||||||
|  | 	undoTo := streams.NewActivityStreamsToProperty() | ||||||
|  | 	undoTo.AppendIRI(targetAccountURI) | ||||||
|  | 	undo.SetActivityStreamsTo(undoTo) | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(originAccount.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// send off the Undo | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | ||||||
|  | 	// if both accounts are local there's nothing to do here | ||||||
|  | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// recreate the AS follow | ||||||
|  | 	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	acceptingAccountURI, err := url.Parse(targetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestingAccountURI, err := url.Parse(originAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create an Accept | ||||||
|  | 	accept := streams.NewActivityStreamsAccept() | ||||||
|  | 
 | ||||||
|  | 	// set the accepting actor on it | ||||||
|  | 	acceptActorProp := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	acceptActorProp.AppendIRI(acceptingAccountURI) | ||||||
|  | 	accept.SetActivityStreamsActor(acceptActorProp) | ||||||
|  | 
 | ||||||
|  | 	// Set the recreated follow as the 'object' property. | ||||||
|  | 	acceptObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	acceptObject.AppendActivityStreamsFollow(asFollow) | ||||||
|  | 	accept.SetActivityStreamsObject(acceptObject) | ||||||
|  | 
 | ||||||
|  | 	// Set the To of the accept as the originator of the follow | ||||||
|  | 	acceptTo := streams.NewActivityStreamsToProperty() | ||||||
|  | 	acceptTo.AppendIRI(requestingAccountURI) | ||||||
|  | 	accept.SetActivityStreamsTo(acceptTo) | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(targetAccount.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// send off the accept using the accepter's outbox | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept) | ||||||
|  | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,3 +23,7 @@ import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| func (p *processor) notifyStatus(status *gtsmodel.Status) error { | func (p *processor) notifyStatus(status *gtsmodel.Status) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) notifyFollow(follow *gtsmodel.Follow) error { | ||||||
|  |    return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -38,24 +38,60 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 
 | 
 | ||||||
| 	l.Debug("entering function PROCESS FROM FEDERATOR") | 	l.Debug("entering function PROCESS FROM FEDERATOR") | ||||||
| 
 | 
 | ||||||
| 	switch federatorMsg.APObjectType { | 	switch federatorMsg.APActivityType { | ||||||
| 	case gtsmodel.ActivityStreamsNote: | 	case gtsmodel.ActivityStreamsCreate: | ||||||
|  | 		// CREATE | ||||||
|  | 		switch federatorMsg.APObjectType { | ||||||
|  | 		case gtsmodel.ActivityStreamsNote: | ||||||
|  | 			// CREATE A STATUS | ||||||
|  | 			incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("note was not parseable as *gtsmodel.Status") | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 		incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) | 			l.Debug("will now derefence incoming status") | ||||||
| 		if !ok { | 			if err := p.dereferenceStatusFields(incomingStatus); err != nil { | ||||||
| 			return errors.New("note was not parseable as *gtsmodel.Status") | 				return fmt.Errorf("error dereferencing status from federator: %s", err) | ||||||
| 		} | 			} | ||||||
|  | 			if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { | ||||||
|  | 				return fmt.Errorf("error updating dereferenced status in the db: %s", err) | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 		l.Debug("will now derefence incoming status") | 			if err := p.notifyStatus(incomingStatus); err != nil { | ||||||
| 		if err := p.dereferenceStatusFields(incomingStatus); err != nil { | 				return err | ||||||
| 			return fmt.Errorf("error dereferencing status from federator: %s", err) | 			} | ||||||
| 		} | 		case gtsmodel.ActivityStreamsProfile: | ||||||
| 		if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { | 			// CREATE AN ACCOUNT | ||||||
| 			return fmt.Errorf("error updating dereferenced status in the db: %s", err) | 			incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) | ||||||
| 		} | 			if !ok { | ||||||
|  | 				return errors.New("profile was not parseable as *gtsmodel.Account") | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 		if err := p.notifyStatus(incomingStatus); err != nil { | 			l.Debug("will now derefence incoming account") | ||||||
| 			return err | 			if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { | ||||||
|  | 				return fmt.Errorf("error dereferencing account from federator: %s", err) | ||||||
|  | 			} | ||||||
|  | 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | ||||||
|  | 				return fmt.Errorf("error updating dereferenced account in the db: %s", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
|  | 		// UPDATE | ||||||
|  | 		switch federatorMsg.APObjectType { | ||||||
|  | 		case gtsmodel.ActivityStreamsProfile: | ||||||
|  | 			// UPDATE AN ACCOUNT | ||||||
|  | 			incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("profile was not parseable as *gtsmodel.Account") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			l.Debug("will now derefence incoming account") | ||||||
|  | 			if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { | ||||||
|  | 				return fmt.Errorf("error dereferencing account from federator: %s", err) | ||||||
|  | 			} | ||||||
|  | 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | ||||||
|  | 				return fmt.Errorf("error updating dereferenced account in the db: %s", err) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -121,7 +157,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { | ||||||
| 
 | 
 | ||||||
| 		// it might have been processed elsewhere so check first if it's already in the database or not | 		// it might have been processed elsewhere so check first if it's already in the database or not | ||||||
| 		maybeAttachment := >smodel.MediaAttachment{} | 		maybeAttachment := >smodel.MediaAttachment{} | ||||||
| 		err := p.db.GetWhere("remote_url", a.RemoteURL, maybeAttachment) | 		err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			// we already have it in the db, dereferenced, no need to do it again | 			// we already have it in the db, dereferenced, no need to do it again | ||||||
| 			l.Debugf("attachment already exists with id %s", maybeAttachment.ID) | 			l.Debugf("attachment already exists with id %s", maybeAttachment.ID) | ||||||
|  | @ -170,7 +206,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { | ||||||
| 		m.OriginAccountURI = status.GTSAccount.URI | 		m.OriginAccountURI = status.GTSAccount.URI | ||||||
| 
 | 
 | ||||||
| 		targetAccount := >smodel.Account{} | 		targetAccount := >smodel.Account{} | ||||||
| 		if err := p.db.GetWhere("uri", uri.String(), targetAccount); err != nil { | 		if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil { | ||||||
| 			// proper error | 			// proper error | ||||||
| 			if _, ok := err.(db.ErrNoEntries); !ok { | 			if _, ok := err.(db.ErrNoEntries); !ok { | ||||||
| 				return fmt.Errorf("db error checking for account with uri %s", uri.String()) | 				return fmt.Errorf("db error checking for account with uri %s", uri.String()) | ||||||
|  | @ -206,3 +242,27 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string) error { | ||||||
|  | 	l := p.log.WithFields(logrus.Fields{ | ||||||
|  | 		"func":               "dereferenceAccountFields", | ||||||
|  | 		"requestingUsername": requestingUsername, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t, err := p.federator.GetTransportForUser(requestingUsername) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error getting transport for user: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// fetch the header and avatar | ||||||
|  | 	if err := p.fetchHeaderAndAviForAccount(account, t); err != nil { | ||||||
|  | 		// if this doesn't work, just skip it -- we can do it later | ||||||
|  | 		l.Debugf("error fetching header/avi for account: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.db.UpdateByID(account.ID, account); err != nil { | ||||||
|  | 		return fmt.Errorf("error updating account in database: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -48,11 +48,28 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err | ||||||
| 	return accts, nil | 	return accts, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode { | func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) { | ||||||
| 	if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil { | 	follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID) | ||||||
| 		return NewErrorNotFound(err) | 	if err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(err) | ||||||
| 	} | 	} | ||||||
| 	return nil | 
 | ||||||
|  | 	p.fromClientAPI <- gtsmodel.FromClientAPI{ | ||||||
|  | 		APActivityType: gtsmodel.ActivityStreamsAccept, | ||||||
|  | 		GTSModel: follow, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r, err := p.tc.RelationshipToMasto(gtsR) | ||||||
|  | 	if  err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return r, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { | func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { | ||||||
|  |  | ||||||
|  | @ -22,12 +22,13 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) { | func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) { | ||||||
| 	i := >smodel.Instance{} | 	i := >smodel.Instance{} | ||||||
| 	if err := p.db.GetWhere("domain", domain, i); err != nil { | 	if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil { | ||||||
| 		return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) | 		return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -71,8 +71,16 @@ type Processor interface { | ||||||
| 	// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | 	// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | ||||||
| 	// the account given in authed. | 	// the account given in authed. | ||||||
| 	AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) | 	AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) | ||||||
| 	// AccountFollowersGet | 	// AccountFollowersGet fetches a list of the target account's followers. | ||||||
| 	AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) | 	AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) | ||||||
|  | 	// AccountFollowingGet fetches a list of the accounts that target account is following. | ||||||
|  | 	AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) | ||||||
|  | 	// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. | ||||||
|  | 	AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) | ||||||
|  | 	// AccountFollowCreate handles a follow request to an account, either remote or local. | ||||||
|  | 	AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) | ||||||
|  | 	// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local. | ||||||
|  | 	AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) | ||||||
| 
 | 
 | ||||||
| 	// 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) | ||||||
|  | @ -86,7 +94,7 @@ type Processor interface { | ||||||
| 	// FollowRequestsGet handles the getting of the authed account's incoming follow requests | 	// FollowRequestsGet handles the getting of the authed account's incoming follow requests | ||||||
| 	FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) | 	FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) | ||||||
| 	// FollowRequestAccept handles the acceptance of a follow request from the given account ID | 	// FollowRequestAccept handles the acceptance of a follow request from the given account ID | ||||||
| 	FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode | 	FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) | ||||||
| 
 | 
 | ||||||
| 	// InstanceGet retrieves instance information for serving at api/v1/instance | 	// InstanceGet retrieves instance information for serving at api/v1/instance | ||||||
| 	InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) | 	InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) | ||||||
|  | @ -125,6 +133,14 @@ type Processor interface { | ||||||
| 	// before returning a JSON serializable interface to the caller. | 	// before returning a JSON serializable interface to the caller. | ||||||
| 	GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) | 	GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) | ||||||
| 
 | 
 | ||||||
|  | 	// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate | ||||||
|  | 	// authentication before returning a JSON serializable interface to the caller. | ||||||
|  | 	GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) | ||||||
|  | 
 | ||||||
|  | 	// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate | ||||||
|  | 	// authentication before returning a JSON serializable interface to the caller. | ||||||
|  | 	GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) | ||||||
|  | 
 | ||||||
| 	// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. | 	// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. | ||||||
| 	GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) | 	GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) | ||||||
| 
 | 
 | ||||||
|  | @ -200,15 +216,11 @@ func (p *processor) Start() error { | ||||||
| 	DistLoop: | 	DistLoop: | ||||||
| 		for { | 		for { | ||||||
| 			select { | 			select { | ||||||
| 			// case clientMsg := <-p.toClientAPI: |  | ||||||
| 			// 	p.log.Infof("received message TO client API: %+v", clientMsg) |  | ||||||
| 			case clientMsg := <-p.fromClientAPI: | 			case clientMsg := <-p.fromClientAPI: | ||||||
| 				p.log.Infof("received message FROM client API: %+v", clientMsg) | 				p.log.Infof("received message FROM client API: %+v", clientMsg) | ||||||
| 				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.toFederator: |  | ||||||
| 			// 	p.log.Infof("received message TO federator: %+v", federatorMsg) |  | ||||||
| 			case federatorMsg := <-p.fromFederator: | 			case federatorMsg := <-p.fromFederator: | ||||||
| 				p.log.Infof("received message FROM federator: %+v", federatorMsg) | 				p.log.Infof("received message FROM federator: %+v", federatorMsg) | ||||||
| 				if err := p.processFromFederator(federatorMsg); err != nil { | 				if err := p.processFromFederator(federatorMsg); err != nil { | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -205,7 +206,7 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc | ||||||
| 		if err := p.db.Put(menchie); err != nil { | 		if err := p.db.Put(menchie); err != nil { | ||||||
| 			return fmt.Errorf("error putting mentions in db: %s", err) | 			return fmt.Errorf("error putting mentions in db: %s", err) | ||||||
| 		} | 		} | ||||||
| 		menchies = append(menchies, menchie.TargetAccountID) | 		menchies = append(menchies, menchie.ID) | ||||||
| 	} | 	} | ||||||
| 	// add full populated gts menchies to the status for passing them around conveniently | 	// add full populated gts menchies to the status for passing them around conveniently | ||||||
| 	status.GTSMentions = gtsMenchies | 	status.GTSMentions = gtsMenchies | ||||||
|  | @ -280,7 +281,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// do the setting | 	// do the setting | ||||||
| 	avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar) | 	avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing avatar: %s", err) | 		return nil, fmt.Errorf("error processing avatar: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -313,10 +314,42 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// do the setting | 	// do the setting | ||||||
| 	headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header) | 	headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | 		return nil, fmt.Errorf("error processing header: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return headerInfo, f.Close() | 	return headerInfo, f.Close() | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport | ||||||
|  | // on behalf of requestingUsername. | ||||||
|  | // | ||||||
|  | // targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary. | ||||||
|  | // | ||||||
|  | // SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated | ||||||
|  | // to reflect the creation of these new attachments. | ||||||
|  | func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error { | ||||||
|  | 	if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" { | ||||||
|  | 		a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ | ||||||
|  | 			RemoteURL: targetAccount.AvatarRemoteURL, | ||||||
|  | 			Avatar:    true, | ||||||
|  | 		}, targetAccount.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("error processing avatar for user: %s", err) | ||||||
|  | 		} | ||||||
|  | 		targetAccount.AvatarMediaAttachmentID = a.ID | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" { | ||||||
|  | 		a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ | ||||||
|  | 			RemoteURL: targetAccount.HeaderRemoteURL, | ||||||
|  | 			Header:    true, | ||||||
|  | 		}, targetAccount.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("error processing header for user: %s", err) | ||||||
|  | 		} | ||||||
|  | 		targetAccount.HeaderMediaAttachmentID = a.ID | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -106,17 +106,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error | ||||||
| 
 | 
 | ||||||
| // RemoveByCode deletes a token from the DB based on the Code field | // RemoveByCode deletes a token from the DB based on the Code field | ||||||
| func (pts *tokenStore) RemoveByCode(ctx context.Context, code string) error { | func (pts *tokenStore) RemoveByCode(ctx context.Context, code string) error { | ||||||
| 	return pts.db.DeleteWhere("code", code, &Token{}) | 	return pts.db.DeleteWhere([]db.Where{{Key: "code", Value: code}}, &Token{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RemoveByAccess deletes a token from the DB based on the Access field | // RemoveByAccess deletes a token from the DB based on the Access field | ||||||
| func (pts *tokenStore) RemoveByAccess(ctx context.Context, access string) error { | func (pts *tokenStore) RemoveByAccess(ctx context.Context, access string) error { | ||||||
| 	return pts.db.DeleteWhere("access", access, &Token{}) | 	return pts.db.DeleteWhere([]db.Where{{Key: "access", Value: access}}, &Token{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RemoveByRefresh deletes a token from the DB based on the Refresh field | // RemoveByRefresh deletes a token from the DB based on the Refresh field | ||||||
| func (pts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { | func (pts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { | ||||||
| 	return pts.db.DeleteWhere("refresh", refresh, &Token{}) | 	return pts.db.DeleteWhere([]db.Where{{Key: "refresh", Value: refresh}}, &Token{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetByCode selects a token from the DB based on the Code field | // GetByCode selects a token from the DB based on the Code field | ||||||
|  | @ -127,7 +127,7 @@ func (pts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.Token | ||||||
| 	pgt := &Token{ | 	pgt := &Token{ | ||||||
| 		Code: code, | 		Code: code, | ||||||
| 	} | 	} | ||||||
| 	if err := pts.db.GetWhere("code", code, pgt); err != nil { | 	if err := pts.db.GetWhere([]db.Where{{Key: "code", Value: code}}, pgt); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return TokenToOauthToken(pgt), nil | 	return TokenToOauthToken(pgt), nil | ||||||
|  | @ -141,7 +141,7 @@ func (pts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.T | ||||||
| 	pgt := &Token{ | 	pgt := &Token{ | ||||||
| 		Access: access, | 		Access: access, | ||||||
| 	} | 	} | ||||||
| 	if err := pts.db.GetWhere("access", access, pgt); err != nil { | 	if err := pts.db.GetWhere([]db.Where{{Key: "access", Value: access}}, pgt); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return TokenToOauthToken(pgt), nil | 	return TokenToOauthToken(pgt), nil | ||||||
|  | @ -155,7 +155,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2 | ||||||
| 	pgt := &Token{ | 	pgt := &Token{ | ||||||
| 		Refresh: refresh, | 		Refresh: refresh, | ||||||
| 	} | 	} | ||||||
| 	if err := pts.db.GetWhere("refresh", refresh, pgt); err != nil { | 	if err := pts.db.GetWhere([]db.Where{{Key: "refresh", Value: refresh}}, pgt); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return TokenToOauthToken(pgt), nil | 	return TokenToOauthToken(pgt), nil | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ type controller struct { | ||||||
| 	clock    pub.Clock | 	clock    pub.Clock | ||||||
| 	client   pub.HttpClient | 	client   pub.HttpClient | ||||||
| 	appAgent string | 	appAgent string | ||||||
|  | 	log      *logrus.Logger | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewController returns an implementation of the Controller interface for creating new transports | // NewController returns an implementation of the Controller interface for creating new transports | ||||||
|  | @ -48,6 +49,7 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient | ||||||
| 		clock:    clock, | 		clock:    clock, | ||||||
| 		client:   client, | 		client:   client, | ||||||
| 		appAgent: fmt.Sprintf("%s %s", config.ApplicationName, config.Host), | 		appAgent: fmt.Sprintf("%s %s", config.ApplicationName, config.Host), | ||||||
|  | 		log:      log, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -80,5 +82,6 @@ func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (T | ||||||
| 		sigTransport: sigTransport, | 		sigTransport: sigTransport, | ||||||
| 		getSigner:    getSigner, | 		getSigner:    getSigner, | ||||||
| 		getSignerMu:  &sync.Mutex{}, | 		getSignerMu:  &sync.Mutex{}, | ||||||
|  | 		log:          c.log, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/pub" | 	"github.com/go-fed/activity/pub" | ||||||
| 	"github.com/go-fed/httpsig" | 	"github.com/go-fed/httpsig" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Transport wraps the pub.Transport interface with some additional | // Transport wraps the pub.Transport interface with some additional | ||||||
|  | @ -31,6 +32,7 @@ type transport struct { | ||||||
| 	sigTransport *pub.HttpSigTransport | 	sigTransport *pub.HttpSigTransport | ||||||
| 	getSigner    httpsig.Signer | 	getSigner    httpsig.Signer | ||||||
| 	getSignerMu  *sync.Mutex | 	getSignerMu  *sync.Mutex | ||||||
|  | 	log          *logrus.Logger | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error { | func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error { | ||||||
|  | @ -38,14 +40,20 @@ func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url. | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error { | func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error { | ||||||
|  | 	l := t.log.WithField("func", "Deliver") | ||||||
|  | 	l.Debugf("performing POST to %s", to.String()) | ||||||
| 	return t.sigTransport.Deliver(c, b, to) | 	return t.sigTransport.Deliver(c, b, to) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) { | func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) { | ||||||
|  | 	l := t.log.WithField("func", "Dereference") | ||||||
|  | 	l.Debugf("performing GET to %s", iri.String()) | ||||||
| 	return t.sigTransport.Dereference(c, iri) | 	return t.sigTransport.Dereference(c, iri) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) { | func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) { | ||||||
|  | 	l := t.log.WithField("func", "DereferenceMedia") | ||||||
|  | 	l.Debugf("performing GET to %s", iri.String()) | ||||||
| 	req, err := http.NewRequest("GET", iri.String(), nil) | 	req, err := http.NewRequest("GET", iri.String(), nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode | ||||||
| 	uri := uriProp.GetIRI() | 	uri := uriProp.GetIRI() | ||||||
| 
 | 
 | ||||||
| 	acct := >smodel.Account{} | 	acct := >smodel.Account{} | ||||||
| 	err := c.db.GetWhere("uri", uri.String(), acct) | 	err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		// we already know this account so we can skip generating it | 		// we already know this account so we can skip generating it | ||||||
| 		return acct, nil | 		return acct, nil | ||||||
|  | @ -90,7 +90,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check for bot and actor type | 	// check for bot and actor type | ||||||
| 	switch gtsmodel.ActivityStreamsActor(accountable.GetTypeName()) { | 	switch accountable.GetTypeName() { | ||||||
| 	case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization: | 	case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization: | ||||||
| 		// people, groups, and organizations aren't bots | 		// people, groups, and organizations aren't bots | ||||||
| 		acct.Bot = false | 		acct.Bot = false | ||||||
|  | @ -101,7 +101,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode | ||||||
| 		// we don't know what this is! | 		// we don't know what this is! | ||||||
| 		return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName()) | 		return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName()) | ||||||
| 	} | 	} | ||||||
| 	acct.ActorType = gtsmodel.ActivityStreamsActor(accountable.GetTypeName()) | 	acct.ActorType = accountable.GetTypeName() | ||||||
| 
 | 
 | ||||||
| 	// TODO: locked aka manuallyApprovesFollowers | 	// TODO: locked aka manuallyApprovesFollowers | ||||||
| 
 | 
 | ||||||
|  | @ -220,7 +220,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e | ||||||
| 	status.APStatusOwnerURI = attributedTo.String() | 	status.APStatusOwnerURI = attributedTo.String() | ||||||
| 
 | 
 | ||||||
| 	statusOwner := >smodel.Account{} | 	statusOwner := >smodel.Account{} | ||||||
| 	if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil { | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String()}}, statusOwner); err != nil { | ||||||
| 		return nil, fmt.Errorf("couldn't get status owner from db: %s", err) | 		return nil, fmt.Errorf("couldn't get status owner from db: %s", err) | ||||||
| 	} | 	} | ||||||
| 	status.AccountID = statusOwner.ID | 	status.AccountID = statusOwner.ID | ||||||
|  | @ -235,7 +235,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e | ||||||
| 
 | 
 | ||||||
| 		// now we can check if we have the replied-to status in our db already | 		// now we can check if we have the replied-to status in our db already | ||||||
| 		inReplyToStatus := >smodel.Status{} | 		inReplyToStatus := >smodel.Status{} | ||||||
| 		if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil { | 		if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil { | ||||||
| 			// we have the status in our database already | 			// we have the status in our database already | ||||||
| 			// so we can set these fields here and then... | 			// so we can set these fields here and then... | ||||||
| 			status.InReplyToID = inReplyToStatus.ID | 			status.InReplyToID = inReplyToStatus.ID | ||||||
|  | @ -281,7 +281,10 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e | ||||||
| 
 | 
 | ||||||
| 	// if it's CC'ed to public, it's public or unlocked | 	// if it's CC'ed to public, it's public or unlocked | ||||||
| 	// mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message | 	// mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message | ||||||
| 	if isPublic(cc) || isPublic(to) { | 	if isPublic(cc) { | ||||||
|  | 		visibility = gtsmodel.VisibilityUnlocked | ||||||
|  | 	} | ||||||
|  | 	if isPublic(to) { | ||||||
| 		visibility = gtsmodel.VisibilityPublic | 		visibility = gtsmodel.VisibilityPublic | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -301,7 +304,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e | ||||||
| 	// we might be able to extract this from the contentMap field | 	// we might be able to extract this from the contentMap field | ||||||
| 
 | 
 | ||||||
| 	// ActivityStreamsType | 	// ActivityStreamsType | ||||||
| 	status.ActivityStreamsType = gtsmodel.ActivityStreamsObject(statusable.GetTypeName()) | 	status.ActivityStreamsType = statusable.GetTypeName() | ||||||
| 
 | 
 | ||||||
| 	return status, nil | 	return status, nil | ||||||
| } | } | ||||||
|  | @ -319,7 +322,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo | ||||||
| 		return nil, errors.New("error extracting actor property from follow") | 		return nil, errors.New("error extracting actor property from follow") | ||||||
| 	} | 	} | ||||||
| 	originAccount := >smodel.Account{} | 	originAccount := >smodel.Account{} | ||||||
| 	if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { | ||||||
| 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -328,7 +331,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo | ||||||
| 		return nil, errors.New("error extracting object property from follow") | 		return nil, errors.New("error extracting object property from follow") | ||||||
| 	} | 	} | ||||||
| 	targetAccount := >smodel.Account{} | 	targetAccount := >smodel.Account{} | ||||||
| 	if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { | ||||||
| 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -341,6 +344,40 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo | ||||||
| 	return followRequest, nil | 	return followRequest, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) { | ||||||
|  | 	idProp := followable.GetJSONLDId() | ||||||
|  | 	if idProp == nil || !idProp.IsIRI() { | ||||||
|  | 		return nil, errors.New("no id property set on follow, or was not an iri") | ||||||
|  | 	} | ||||||
|  | 	uri := idProp.GetIRI().String() | ||||||
|  | 
 | ||||||
|  | 	origin, err := extractActor(followable) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("error extracting actor property from follow") | ||||||
|  | 	} | ||||||
|  | 	originAccount := >smodel.Account{} | ||||||
|  | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	target, err := extractObject(followable) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("error extracting object property from follow") | ||||||
|  | 	} | ||||||
|  | 	targetAccount := >smodel.Account{} | ||||||
|  | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	follow := >smodel.Follow{ | ||||||
|  | 		URI:             uri, | ||||||
|  | 		AccountID:       originAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return follow, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func isPublic(tos []*url.URL) bool { | func isPublic(tos []*url.URL) bool { | ||||||
| 	for _, entry := range tos { | 	for _, entry := range tos { | ||||||
| 		if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { | 		if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { | ||||||
|  |  | ||||||
|  | @ -26,6 +26,10 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	asPublicURI = "https://www.w3.org/ns/activitystreams#Public" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // TypeConverter is an interface for the common action of converting between apimodule (frontend, serializable) models, | // TypeConverter is an interface for the common action of converting between apimodule (frontend, serializable) models, | ||||||
| // internal gts models used in the database, and activitypub models used in federation. | // internal gts models used in the database, and activitypub models used in federation. | ||||||
| // | // | ||||||
|  | @ -77,6 +81,9 @@ type TypeConverter interface { | ||||||
| 	// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance | 	// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance | ||||||
| 	InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) | 	InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) | ||||||
| 
 | 
 | ||||||
|  | 	// RelationshipToMasto converts a gts relationship into its mastodon equivalent for serving in various places | ||||||
|  | 	RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) | ||||||
|  | 
 | ||||||
| 	/* | 	/* | ||||||
| 		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL | 		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL | ||||||
| 	*/ | 	*/ | ||||||
|  | @ -94,6 +101,8 @@ type TypeConverter interface { | ||||||
| 	ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) | 	ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) | ||||||
| 	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. | 	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. | ||||||
| 	ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) | 	ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) | ||||||
|  | 	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow. | ||||||
|  | 	ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL | 		INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL | ||||||
|  | @ -104,6 +113,15 @@ type TypeConverter interface { | ||||||
| 
 | 
 | ||||||
| 	// 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) | ||||||
|  | 
 | ||||||
|  | 	// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation | ||||||
|  | 	FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) | ||||||
|  | 
 | ||||||
|  | 	// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation | ||||||
|  | 	MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) | ||||||
|  | 
 | ||||||
|  | 	// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation | ||||||
|  | 	AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type converter struct { | type converter struct { | ||||||
|  |  | ||||||
|  | @ -21,10 +21,12 @@ package typeutils | ||||||
| import ( | import ( | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
|  | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-fed/activity/streams" | 	"github.com/go-fed/activity/streams" | ||||||
| 	"github.com/go-fed/activity/streams/vocab" | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -256,5 +258,304 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { | func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { | ||||||
| 	return nil, nil | 	// ensure prerequisites here before we get stuck in | ||||||
|  | 
 | ||||||
|  | 	// check if author account is already attached to status and attach it if not | ||||||
|  | 	// if we can't retrieve this, bail here already because we can't attribute the status to anyone | ||||||
|  | 	if s.GTSAccount == nil { | ||||||
|  | 		a := >smodel.Account{} | ||||||
|  | 		if err := c.db.GetByID(s.AccountID, a); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) | ||||||
|  | 		} | ||||||
|  | 		s.GTSAccount = a | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create the Note! | ||||||
|  | 	status := streams.NewActivityStreamsNote() | ||||||
|  | 
 | ||||||
|  | 	// id | ||||||
|  | 	statusURI, err := url.Parse(s.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err) | ||||||
|  | 	} | ||||||
|  | 	statusIDProp := streams.NewJSONLDIdProperty() | ||||||
|  | 	statusIDProp.SetIRI(statusURI) | ||||||
|  | 	status.SetJSONLDId(statusIDProp) | ||||||
|  | 
 | ||||||
|  | 	// type | ||||||
|  | 	// will be set automatically by go-fed | ||||||
|  | 
 | ||||||
|  | 	// summary aka cw | ||||||
|  | 	statusSummaryProp := streams.NewActivityStreamsSummaryProperty() | ||||||
|  | 	statusSummaryProp.AppendXMLSchemaString(s.ContentWarning) | ||||||
|  | 	status.SetActivityStreamsSummary(statusSummaryProp) | ||||||
|  | 
 | ||||||
|  | 	// inReplyTo | ||||||
|  | 	if s.InReplyToID != "" { | ||||||
|  | 		// fetch the replied status if we don't have it on hand already | ||||||
|  | 		if s.GTSReplyToStatus == nil { | ||||||
|  | 			rs := >smodel.Status{} | ||||||
|  | 			if err := c.db.GetByID(s.InReplyToID, rs); err != nil { | ||||||
|  | 				return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err) | ||||||
|  | 			} | ||||||
|  | 			s.GTSReplyToStatus = rs | ||||||
|  | 		} | ||||||
|  | 		rURI, err := url.Parse(s.GTSReplyToStatus.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSReplyToStatus.URI, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		inReplyToProp := streams.NewActivityStreamsInReplyToProperty() | ||||||
|  | 		inReplyToProp.AppendIRI(rURI) | ||||||
|  | 		status.SetActivityStreamsInReplyTo(inReplyToProp) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// published | ||||||
|  | 	publishedProp := streams.NewActivityStreamsPublishedProperty() | ||||||
|  | 	publishedProp.Set(s.CreatedAt) | ||||||
|  | 	status.SetActivityStreamsPublished(publishedProp) | ||||||
|  | 
 | ||||||
|  | 	// url | ||||||
|  | 	if s.URL != "" { | ||||||
|  | 		sURL, err := url.Parse(s.URL) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		urlProp := streams.NewActivityStreamsUrlProperty() | ||||||
|  | 		urlProp.AppendIRI(sURL) | ||||||
|  | 		status.SetActivityStreamsUrl(urlProp) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// attributedTo | ||||||
|  | 	authorAccountURI, err := url.Parse(s.GTSAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 	attributedToProp := streams.NewActivityStreamsAttributedToProperty() | ||||||
|  | 	attributedToProp.AppendIRI(authorAccountURI) | ||||||
|  | 	status.SetActivityStreamsAttributedTo(attributedToProp) | ||||||
|  | 
 | ||||||
|  | 	// tags | ||||||
|  | 	tagProp := streams.NewActivityStreamsTagProperty() | ||||||
|  | 
 | ||||||
|  | 	// tag -- mentions | ||||||
|  | 	for _, m := range s.GTSMentions { | ||||||
|  | 		asMention, err := c.MentionToAS(m) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err) | ||||||
|  | 		} | ||||||
|  | 		tagProp.AppendActivityStreamsMention(asMention) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// tag -- emojis | ||||||
|  | 	// TODO | ||||||
|  | 
 | ||||||
|  | 	// tag -- hashtags | ||||||
|  | 	// TODO | ||||||
|  | 
 | ||||||
|  | 	status.SetActivityStreamsTag(tagProp) | ||||||
|  | 
 | ||||||
|  | 	// parse out some URIs we need here | ||||||
|  | 	authorFollowersURI, err := url.Parse(s.GTSAccount.FollowersURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.FollowersURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	publicURI, err := url.Parse(asPublicURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// to and cc | ||||||
|  | 	toProp := streams.NewActivityStreamsToProperty() | ||||||
|  | 	ccProp := streams.NewActivityStreamsCcProperty() | ||||||
|  | 	switch s.Visibility { | ||||||
|  | 	case gtsmodel.VisibilityDirect: | ||||||
|  | 		// if DIRECT, then only mentioned users should be added to TO, and nothing to CC | ||||||
|  | 		for _, m := range s.GTSMentions { | ||||||
|  | 			iri, err := url.Parse(m.GTSAccount.URI) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) | ||||||
|  | 			} | ||||||
|  | 			toProp.AppendIRI(iri) | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.VisibilityMutualsOnly: | ||||||
|  | 		// TODO | ||||||
|  | 	case gtsmodel.VisibilityFollowersOnly: | ||||||
|  | 		// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC | ||||||
|  | 		toProp.AppendIRI(authorFollowersURI) | ||||||
|  | 		for _, m := range s.GTSMentions { | ||||||
|  | 			iri, err := url.Parse(m.GTSAccount.URI) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) | ||||||
|  | 			} | ||||||
|  | 			ccProp.AppendIRI(iri) | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.VisibilityUnlocked: | ||||||
|  | 		// if UNLOCKED, we want to add followers to TO, and public and mentions to CC | ||||||
|  | 		toProp.AppendIRI(authorFollowersURI) | ||||||
|  | 		ccProp.AppendIRI(publicURI) | ||||||
|  | 		for _, m := range s.GTSMentions { | ||||||
|  | 			iri, err := url.Parse(m.GTSAccount.URI) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) | ||||||
|  | 			} | ||||||
|  | 			ccProp.AppendIRI(iri) | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.VisibilityPublic: | ||||||
|  | 		// if PUBLIC, we want to add public to TO, and followers and mentions to CC | ||||||
|  | 		toProp.AppendIRI(publicURI) | ||||||
|  | 		ccProp.AppendIRI(authorFollowersURI) | ||||||
|  | 		for _, m := range s.GTSMentions { | ||||||
|  | 			iri, err := url.Parse(m.GTSAccount.URI) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) | ||||||
|  | 			} | ||||||
|  | 			ccProp.AppendIRI(iri) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	status.SetActivityStreamsTo(toProp) | ||||||
|  | 	status.SetActivityStreamsCc(ccProp) | ||||||
|  | 
 | ||||||
|  | 	// conversation | ||||||
|  | 	// TODO | ||||||
|  | 
 | ||||||
|  | 	// content -- the actual post itself | ||||||
|  | 	contentProp := streams.NewActivityStreamsContentProperty() | ||||||
|  | 	contentProp.AppendXMLSchemaString(s.Content) | ||||||
|  | 	status.SetActivityStreamsContent(contentProp) | ||||||
|  | 
 | ||||||
|  | 	// attachment | ||||||
|  | 	attachmentProp := streams.NewActivityStreamsAttachmentProperty() | ||||||
|  | 	for _, a := range s.GTSMediaAttachments { | ||||||
|  | 		doc, err := c.AttachmentToAS(a) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err) | ||||||
|  | 		} | ||||||
|  | 		attachmentProp.AppendActivityStreamsDocument(doc) | ||||||
|  | 	} | ||||||
|  | 	status.SetActivityStreamsAttachment(attachmentProp) | ||||||
|  | 
 | ||||||
|  | 	// replies | ||||||
|  | 	// TODO | ||||||
|  | 	 | ||||||
|  | 	return status, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) { | ||||||
|  | 	// parse out the various URIs we need for this | ||||||
|  | 	// origin account (who's doing the follow) | ||||||
|  | 	originAccountURI, err := url.Parse(originAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err) | ||||||
|  | 	} | ||||||
|  | 	originActor := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	originActor.AppendIRI(originAccountURI) | ||||||
|  | 
 | ||||||
|  | 	// target account (who's being followed) | ||||||
|  | 	targetAccountURI, err := url.Parse(targetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// uri of the follow activity itself | ||||||
|  | 	followURI, err := url.Parse(f.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// start preparing the follow activity | ||||||
|  | 	follow := streams.NewActivityStreamsFollow() | ||||||
|  | 
 | ||||||
|  | 	// set the actor | ||||||
|  | 	follow.SetActivityStreamsActor(originActor) | ||||||
|  | 
 | ||||||
|  | 	// set the id | ||||||
|  | 	followIDProp := streams.NewJSONLDIdProperty() | ||||||
|  | 	followIDProp.SetIRI(followURI) | ||||||
|  | 	follow.SetJSONLDId(followIDProp) | ||||||
|  | 
 | ||||||
|  | 	// set the object | ||||||
|  | 	followObjectProp := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	followObjectProp.AppendIRI(targetAccountURI) | ||||||
|  | 	follow.SetActivityStreamsObject(followObjectProp) | ||||||
|  | 
 | ||||||
|  | 	// set the To property | ||||||
|  | 	followToProp := streams.NewActivityStreamsToProperty() | ||||||
|  | 	followToProp.AppendIRI(targetAccountURI) | ||||||
|  | 	follow.SetActivityStreamsTo(followToProp) | ||||||
|  | 
 | ||||||
|  | 	return follow, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) { | ||||||
|  | 	if m.GTSAccount == nil { | ||||||
|  | 		a := >smodel.Account{} | ||||||
|  | 		if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err) | ||||||
|  | 		} | ||||||
|  | 		m.GTSAccount = a | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create the mention | ||||||
|  | 	mention := streams.NewActivityStreamsMention() | ||||||
|  | 
 | ||||||
|  | 	// href -- this should be the URI of the mentioned user | ||||||
|  | 	hrefProp := streams.NewActivityStreamsHrefProperty() | ||||||
|  | 	hrefURI, err := url.Parse(m.GTSAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 	hrefProp.SetIRI(hrefURI) | ||||||
|  | 	mention.SetActivityStreamsHref(hrefProp) | ||||||
|  | 
 | ||||||
|  | 	// name -- this should be the namestring of the mentioned user, something like @whatever@example.org | ||||||
|  | 	var domain string | ||||||
|  | 	if m.GTSAccount.Domain == "" { | ||||||
|  | 		domain = c.config.Host | ||||||
|  | 	} else { | ||||||
|  | 		domain = m.GTSAccount.Domain | ||||||
|  | 	} | ||||||
|  | 	username := m.GTSAccount.Username | ||||||
|  | 	nameString := fmt.Sprintf("@%s@%s", username, domain) | ||||||
|  | 	nameProp := streams.NewActivityStreamsNameProperty() | ||||||
|  | 	nameProp.AppendXMLSchemaString(nameString) | ||||||
|  | 	mention.SetActivityStreamsName(nameProp) | ||||||
|  | 
 | ||||||
|  | 	return mention, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) { | ||||||
|  | 	// type -- Document | ||||||
|  | 	doc := streams.NewActivityStreamsDocument() | ||||||
|  | 
 | ||||||
|  | 	// mediaType aka mime content type | ||||||
|  | 	mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty() | ||||||
|  | 	mediaTypeProp.Set(a.File.ContentType) | ||||||
|  | 	doc.SetActivityStreamsMediaType(mediaTypeProp) | ||||||
|  | 
 | ||||||
|  | 	// url -- for the original image not the thumbnail | ||||||
|  | 	urlProp := streams.NewActivityStreamsUrlProperty() | ||||||
|  | 	imageURL, err := url.Parse(a.URL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err) | ||||||
|  | 	} | ||||||
|  | 	urlProp.AppendIRI(imageURL) | ||||||
|  | 	doc.SetActivityStreamsUrl(urlProp) | ||||||
|  | 
 | ||||||
|  | 	// name -- aka image description | ||||||
|  | 	nameProp := streams.NewActivityStreamsNameProperty() | ||||||
|  | 	nameProp.AppendXMLSchemaString(a.Description) | ||||||
|  | 	doc.SetActivityStreamsName(nameProp) | ||||||
|  | 
 | ||||||
|  | 	// blurhash | ||||||
|  | 	blurProp := streams.NewTootBlurhashProperty() | ||||||
|  | 	blurProp.Set(a.Blurhash) | ||||||
|  | 	doc.SetTootBlurhash(blurProp) | ||||||
|  | 
 | ||||||
|  | 	// focalpoint | ||||||
|  | 	// TODO | ||||||
|  | 
 | ||||||
|  | 	return doc, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -572,3 +572,21 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro | ||||||
| 
 | 
 | ||||||
| 	return mi, nil | 	return mi, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) { | ||||||
|  | 	return &model.Relationship{ | ||||||
|  | 		ID: r.ID, | ||||||
|  | 		Following: r.Following, | ||||||
|  | 		ShowingReblogs: r.ShowingReblogs, | ||||||
|  | 		Notifying: r.Notifying, | ||||||
|  | 		FollowedBy: r.FollowedBy, | ||||||
|  | 		Blocking: r.Blocking, | ||||||
|  | 		BlockedBy: r.BlockedBy, | ||||||
|  | 		Muting: r.Muting, | ||||||
|  | 		MutingNotifications: r.MutingNotifications, | ||||||
|  | 		Requested: r.Requested, | ||||||
|  | 		DomainBlocking: r.DomainBlocking, | ||||||
|  | 		Endorsed: r.Endorsed, | ||||||
|  | 		Note: r.Note, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -22,6 +22,8 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -47,6 +49,8 @@ const ( | ||||||
| 	FeaturedPath = "featured" | 	FeaturedPath = "featured" | ||||||
| 	// PublicKeyPath is for serving an account's public key | 	// PublicKeyPath is for serving an account's public key | ||||||
| 	PublicKeyPath = "main-key" | 	PublicKeyPath = "main-key" | ||||||
|  | 	// FollowPath used to generate the URI for an individual follow or follow request | ||||||
|  | 	FollowPath = "follow" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // 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 | ||||||
|  | @ -103,6 +107,12 @@ type UserURIs struct { | ||||||
| 	PublicKeyURI string | 	PublicKeyURI string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GenerateURIForFollow returns the AP URI for a new follow -- something like: | ||||||
|  | // https://example.org/users/whatever_user/follow/41c7f33f-1060-48d9-84df-38dcb13cf0d8 | ||||||
|  | func GenerateURIForFollow(username string, protocol string, host string) string { | ||||||
|  | 	return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, FollowPath, uuid.NewString()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // 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 | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ import ( | ||||||
| var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error { | var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error { | ||||||
| 	c := NewTestConfig() | 	c := NewTestConfig() | ||||||
| 	dbService := NewTestDB() | 	dbService := NewTestDB() | ||||||
|  | 	federatingDB := NewTestFederatingDB(dbService) | ||||||
| 	router := NewTestRouter() | 	router := NewTestRouter() | ||||||
| 	storageBackend := NewTestStorage() | 	storageBackend := NewTestStorage() | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +60,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr | ||||||
| 			Body:       r, | 			Body:       r, | ||||||
| 		}, nil | 		}, nil | ||||||
| 	})) | 	})) | ||||||
| 	federator := federation.NewFederator(dbService, transportController, c, log, typeConverter) | 	federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter) | ||||||
| 	processor := NewTestProcessor(dbService, storageBackend, federator) | 	processor := NewTestProcessor(dbService, storageBackend, federator) | ||||||
| 	if err := processor.Start(); err != nil { | 	if err := processor.Start(); err != nil { | ||||||
| 		return fmt.Errorf("error starting processor: %s", err) | 		return fmt.Errorf("error starting processor: %s", err) | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								testrig/federatingdb.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								testrig/federatingdb.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | package testrig | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewTestFederatingDB returns a federating DB with the underlying db | ||||||
|  | func NewTestFederatingDB(db db.DB) federation.FederatingDB { | ||||||
|  | 	return federation.NewFederatingDB(db, NewTestConfig(), NewTestLog()) | ||||||
|  | } | ||||||
|  | @ -26,5 +26,5 @@ import ( | ||||||
| 
 | 
 | ||||||
| // NewTestFederator returns a federator with the given database and (mock!!) transport controller. | // NewTestFederator returns a federator with the given database and (mock!!) transport controller. | ||||||
| func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator { | func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator { | ||||||
| 	return federation.NewFederator(db, tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db)) | 	return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db)) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue