mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 01:02:25 -05:00 
			
		
		
		
	lots of stufffffffffffffffff
This commit is contained in:
		
					parent
					
						
							
								dc06e71b76
							
						
					
				
			
			
				commit
				
					
						8832784778
					
				
			
		
					 26 changed files with 721 additions and 88 deletions
				
			
		|  | @ -57,6 +57,8 @@ const ( | |||
| 	GetStatusesPath = BasePathWithID + "/statuses" | ||||
| 	// GetFollowersPath is for showing an account's followers | ||||
| 	GetFollowersPath = BasePathWithID + "/followers" | ||||
| 	// GetRelationshipsPath is for showing an account's relationship with other accounts | ||||
| 	GetRelationshipsPath = BasePath + "/relationships" | ||||
| ) | ||||
| 
 | ||||
| // Module implements the ClientAPIModule interface for account-related actions | ||||
|  | @ -82,6 +84,7 @@ func (m *Module) Route(r router.Router) error { | |||
| 	r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) | ||||
| 	r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) | ||||
| 	r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) | ||||
| 	r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										41
									
								
								internal/api/client/account/relationships.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								internal/api/client/account/relationships.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| 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 { | ||||
| 		l.Debug("no account id specified in query") | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| } | ||||
|  | @ -48,10 +48,11 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil { | ||||
| 	r, errWithCode := m.processor.FollowRequestAccept(authed, originAccountID) | ||||
| 	if errWithCode != nil { | ||||
| 		l.Debug(errWithCode.Error()) | ||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.Status(http.StatusOK) | ||||
| 	c.JSON(http.StatusOK, r) | ||||
| } | ||||
|  |  | |||
|  | @ -77,18 +77,18 @@ type Account struct { | |||
| // See https://docs.joinmastodon.org/methods/accounts/ | ||||
| type AccountCreateRequest struct { | ||||
| 	// 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 | ||||
| 	Username string `form:"username" binding:"required"` | ||||
| 	Username string `form:"username" json:"username" xml:"username" binding:"required"` | ||||
| 	// 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 | ||||
| 	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. | ||||
| 	// 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 | ||||
| 	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 | ||||
| 	IP net.IP `form:"-"` | ||||
| } | ||||
|  | @ -97,33 +97,33 @@ type AccountCreateRequest struct { | |||
| // See https://docs.joinmastodon.org/methods/accounts/ | ||||
| type UpdateCredentialsRequest struct { | ||||
| 	// 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. | ||||
| 	Bot *bool `form:"bot"` | ||||
| 	Bot *bool `form:"bot" json:"bot" xml:"bot"` | ||||
| 	// 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. | ||||
| 	Note *string `form:"note"` | ||||
| 	Note *string `form:"note" json:"note" xml:"note"` | ||||
| 	// 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 *multipart.FileHeader `form:"header"` | ||||
| 	Header *multipart.FileHeader `form:"header" json:"header" xml:"header"` | ||||
| 	// 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 | ||||
| 	Source *UpdateSource `form:"source"` | ||||
| 	Source *UpdateSource `form:"source" json:"source" xml:"source"` | ||||
| 	// 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. | ||||
| type UpdateSource struct { | ||||
| 	// 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. | ||||
| 	Sensitive *bool `form:"sensitive"` | ||||
| 	Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"` | ||||
| 	// 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. | ||||
|  |  | |||
|  | @ -43,5 +43,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule { | |||
| func (m *Module) Route(s router.Router) error { | ||||
| 	s.AttachMiddleware(m.FlocBlock) | ||||
| 	s.AttachMiddleware(m.ExtraHeaders) | ||||
| 	s.AttachMiddleware(m.UserAgentBlock) | ||||
| 	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 | ||||
| 	} | ||||
| } | ||||
|  | @ -117,7 +117,9 @@ type DB interface { | |||
| 
 | ||||
| 	// 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. | ||||
| 	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. | ||||
| 	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. | ||||
|  | @ -204,6 +206,9 @@ type DB interface { | |||
| 	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. | ||||
| 	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 | ||||
| 	// privacy settings of the status, and any blocks/mutes that might exist between the two accounts | ||||
| 	// or account domains. | ||||
|  |  | |||
|  | @ -307,30 +307,34 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac | |||
| 	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{} | ||||
| 	if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { | ||||
| 		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{ | ||||
| 		AccountID:       originAccountID, | ||||
| 		TargetAccountID: targetAccountID, | ||||
| 		URI:             fr.URI, | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := ps.conn.Model(follow).Insert(); err != nil { | ||||
| 		return err | ||||
| 	// if the follow already exists, just update the URI -- we don't need to do anything else | ||||
| 	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 { | ||||
| 		return err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| 	return follow, nil | ||||
| } | ||||
| 
 | ||||
| func (ps *postgresService) CreateInstanceAccount() error { | ||||
|  | @ -681,6 +685,60 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro | |||
| 	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) { | ||||
| 	l := ps.log.WithField("func", "StatusVisible") | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ package federation | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
|  | @ -371,24 +372,37 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | |||
| 			"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) | ||||
| 	if targetAcctI == nil { | ||||
| 		l.Error("target account wasn't set on context") | ||||
| 		return nil | ||||
| 	} | ||||
| 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | ||||
| 	if !ok { | ||||
| 		l.Error("target account was set on context but couldn't be parsed") | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | ||||
| 	if fromFederatorChanI == nil { | ||||
| 		l.Error("from federator channel wasn't set on context") | ||||
| 		return nil | ||||
| 	} | ||||
| 	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | ||||
| 	if !ok { | ||||
| 		l.Error("from federator channel was set on context but couldn't be parsed") | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) { | ||||
|  | @ -433,7 +447,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | |||
| 		} | ||||
| 
 | ||||
| 		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) | ||||
| 			} | ||||
| 		} | ||||
|  | @ -450,14 +464,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | |||
| // the entire value. | ||||
| // | ||||
| // 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( | ||||
| 		logrus.Fields{ | ||||
| 			"func":   "Update", | ||||
| 			"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 gtsmodel.ActivityStreamsActivity(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 | ||||
| } | ||||
| 
 | ||||
|  | @ -490,7 +577,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v | |||
| 	) | ||||
| 	l.Debug("entering GETOUTBOX function") | ||||
| 
 | ||||
| 	return nil, nil | ||||
| 	return streams.NewActivityStreamsOrderedCollectionPage(), nil | ||||
| } | ||||
| 
 | ||||
| // SetOutbox saves the outbox value given from GetOutbox, with new items | ||||
|  | @ -522,9 +609,18 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err | |||
| 			"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 | ||||
| 	} | ||||
| 
 | ||||
| 	return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString())) | ||||
| 	l.Debugf("received NEWID request for asType %s", string(b)) | ||||
| 
 | ||||
| 	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 | ||||
|  |  | |||
|  | @ -20,12 +20,14 @@ package federation | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/go-fed/activity/pub" | ||||
| 	"github.com/go-fed/activity/streams" | ||||
| 	"github.com/go-fed/activity/streams/vocab" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
|  | @ -146,6 +148,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr | |||
| 		} | ||||
| 
 | ||||
| 		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) | ||||
|  | @ -228,17 +246,19 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa | |||
| 		"func": "FederatingCallbacks", | ||||
| 	}) | ||||
| 
 | ||||
| 	targetAcctI := ctx.Value(util.APAccount) | ||||
| 	if targetAcctI == nil { | ||||
| 		l.Error("target account wasn't set on context") | ||||
| 	receivingAcctI := ctx.Value(util.APAccount) | ||||
| 	if receivingAcctI == nil { | ||||
| 		l.Error("receiving account wasn't set on context") | ||||
| 		return | ||||
| 	} | ||||
| 	targetAcct, ok := targetAcctI.(*gtsmodel.Account) | ||||
| 	receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) | ||||
| 	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 | ||||
| 	if targetAcct.Locked { | ||||
| 	if receivingAcct.Locked { | ||||
| 		onFollow = pub.OnFollowDoNothing | ||||
| 	} | ||||
| 
 | ||||
|  | @ -248,6 +268,13 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa | |||
| 		OnFollow: onFollow, | ||||
| 	} | ||||
| 
 | ||||
| 	// override default undo behavior | ||||
| 	other = []interface{}{ | ||||
| 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | ||||
| 			return f.typeConverter. | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -76,13 +76,13 @@ type Account struct { | |||
| 	*/ | ||||
| 
 | ||||
| 	// 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? | ||||
| 	Discoverable bool | ||||
| 	// Default post privacy for this account | ||||
| 	Privacy Visibility | ||||
| 	// Set posts from this account to sensitive by default? | ||||
| 	Sensitive bool `pg:",default:'false'"` | ||||
| 	Sensitive bool | ||||
| 	// What language does this account post in? | ||||
| 	Language string `pg:",default:'en'"` | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,4 +26,5 @@ type FromFederator struct { | |||
| 	APObjectType     ActivityStreamsObject | ||||
| 	APActivityType   ActivityStreamsActivity | ||||
| 	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 | ||||
| } | ||||
|  | @ -67,7 +67,7 @@ type Handler interface { | |||
| 	// 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, | ||||
| 	// 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, | ||||
| 	// 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 | ||||
| 	// in the database. | ||||
| 	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 { | ||||
|  | @ -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, | ||||
| // 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. | ||||
| 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") | ||||
| 
 | ||||
| 	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)) | ||||
| 
 | ||||
| 	// process it | ||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID) | ||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL) | ||||
| 	if err != nil { | ||||
| 		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) | ||||
| } | ||||
| 
 | ||||
| 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") | ||||
| 	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) | ||||
| 	suite.log.Debugf("%+v", ma) | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ import ( | |||
| 	"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 isAvatar bool | ||||
| 
 | ||||
|  | @ -96,7 +96,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string | |||
| 		ID:        newMediaID, | ||||
| 		StatusID:  "", | ||||
| 		URL:       originalURL, | ||||
| 		RemoteURL: "", | ||||
| 		RemoteURL: remoteURL, | ||||
| 		CreatedAt: time.Now(), | ||||
| 		UpdatedAt: time.Now(), | ||||
| 		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) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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 err error | ||||
| 	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) | ||||
| 		} | ||||
| 
 | ||||
| 		// 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) | ||||
| 		if err != nil { | ||||
| 			return nil, NewErrorInternalError(err) | ||||
|  | @ -293,3 +308,21 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri | |||
| 	} | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -19,15 +19,23 @@ | |||
| package message | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/go-fed/activity/pub" | ||||
| 	"github.com/go-fed/activity/streams" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| ) | ||||
| 
 | ||||
| func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { | ||||
| 	switch clientMsg.APActivityType { | ||||
| 	case gtsmodel.ActivityStreamsCreate: | ||||
| 		// CREATE | ||||
| 		switch clientMsg.APObjectType { | ||||
| 		case gtsmodel.ActivityStreamsNote: | ||||
| 			// CREATE NOTE | ||||
| 			status, ok := clientMsg.GTSModel.(*gtsmodel.Status) | ||||
| 			if !ok { | ||||
| 				return errors.New("note was not parseable as *gtsmodel.Status") | ||||
|  | @ -42,7 +50,17 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | |||
| 			} | ||||
| 			return nil | ||||
| 		} | ||||
| 	return fmt.Errorf("message type unprocessable: %+v", clientMsg) | ||||
| 	case gtsmodel.ActivityStreamsUpdate: | ||||
| 		// UPDATE | ||||
| 	case gtsmodel.ActivityStreamsAccept: | ||||
| 		// ACCEPT | ||||
| 		follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) | ||||
| 		if !ok { | ||||
| 			return errors.New("accept was not parseable as *gtsmodel.Follow") | ||||
| 		} | ||||
| 		return p.federateAcceptFollowRequest(follow) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (p *processor) federateStatus(status *gtsmodel.Status) error { | ||||
|  | @ -71,3 +89,74 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error { | |||
| 	// _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow) error { | ||||
| 
 | ||||
| 	followAccepter := >smodel.Account{} | ||||
| 	if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil { | ||||
| 		return fmt.Errorf("error federating follow accept: %s", err) | ||||
| 	} | ||||
| 	followAccepterIRI, err := url.Parse(followAccepter.URI) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error parsing URL: %s", err) | ||||
| 	} | ||||
| 	followAccepterOutboxIRI, err := url.Parse(followAccepter.OutboxURI) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error parsing URL: %s", err) | ||||
| 	} | ||||
| 	me := streams.NewActivityStreamsActorProperty() | ||||
| 	me.AppendIRI(followAccepterIRI) | ||||
| 
 | ||||
| 	followRequester := >smodel.Account{} | ||||
| 	if err := p.db.GetByID(follow.AccountID, followRequester); err != nil { | ||||
| 		return fmt.Errorf("error federating follow accept: %s", err) | ||||
| 	} | ||||
| 	requesterIRI, err := url.Parse(followRequester.URI) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error parsing URL: %s", err) | ||||
| 	} | ||||
| 	them := streams.NewActivityStreamsActorProperty() | ||||
| 	them.AppendIRI(requesterIRI) | ||||
| 
 | ||||
| 	// prepare the follow | ||||
| 	ASFollow := streams.NewActivityStreamsFollow() | ||||
| 	// set the follow requester as the actor | ||||
| 	ASFollow.SetActivityStreamsActor(them) | ||||
| 	// set the ID from the follow | ||||
| 	ASFollowURI, err := url.Parse(follow.URI) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error parsing URL: %s", err) | ||||
| 	} | ||||
| 	ASFollowIDProp := streams.NewJSONLDIdProperty() | ||||
| 	ASFollowIDProp.SetIRI(ASFollowURI) | ||||
| 	ASFollow.SetJSONLDId(ASFollowIDProp) | ||||
| 
 | ||||
| 	// set the object as the accepter URI | ||||
| 	ASFollowObjectProp := streams.NewActivityStreamsObjectProperty() | ||||
| 	ASFollowObjectProp.AppendIRI(followAccepterIRI) | ||||
| 
 | ||||
| 	// Prepare the response. | ||||
| 	ASAccept := streams.NewActivityStreamsAccept() | ||||
| 	// Set us as the 'actor'. | ||||
| 	ASAccept.SetActivityStreamsActor(me) | ||||
| 
 | ||||
| 	// Set the Follow as the 'object' property. | ||||
| 	ASAcceptObject := streams.NewActivityStreamsObjectProperty() | ||||
| 	ASAcceptObject.AppendActivityStreamsFollow(ASFollow) | ||||
| 	ASAccept.SetActivityStreamsObject(ASAcceptObject) | ||||
| 
 | ||||
| 	// Add all actors on the original Follow to the 'to' property. | ||||
| 	ASAcceptTo := streams.NewActivityStreamsToProperty() | ||||
| 	followActors := ASFollow.GetActivityStreamsActor() | ||||
| 	for iter := followActors.Begin(); iter != followActors.End(); iter = iter.Next() { | ||||
| 		id, err := pub.ToId(iter) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		ASAcceptTo.AppendIRI(id) | ||||
| 	} | ||||
| 	ASAccept.SetActivityStreamsTo(ASAcceptTo) | ||||
| 
 | ||||
| 	_, err = p.federator.FederatingActor().Send(context.Background(), followAccepterOutboxIRI, ASAccept) | ||||
| 	return err | ||||
| } | ||||
|  |  | |||
|  | @ -38,9 +38,12 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | |||
| 
 | ||||
| 	l.Debug("entering function PROCESS FROM FEDERATOR") | ||||
| 
 | ||||
| 	switch federatorMsg.APActivityType { | ||||
| 	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") | ||||
|  | @ -57,6 +60,39 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | |||
| 			if err := p.notifyStatus(incomingStatus); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		case gtsmodel.ActivityStreamsProfile: | ||||
| 			// CREATE 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) | ||||
| 			} | ||||
| 		} | ||||
| 	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) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
|  | @ -206,3 +242,27 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { | |||
| 
 | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode { | ||||
| 	if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil { | ||||
| 		return NewErrorNotFound(err) | ||||
| func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) { | ||||
| 	follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID) | ||||
| 	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 { | ||||
|  |  | |||
|  | @ -73,6 +73,8 @@ type Processor interface { | |||
| 	AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) | ||||
| 	// AccountFollowersGet | ||||
| 	AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) | ||||
| 	// AccountRelationshipGet | ||||
| 	AccountRelationshipGet(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(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) | ||||
|  | @ -86,7 +88,7 @@ type Processor interface { | |||
| 	// FollowRequestsGet handles the getting of the authed account's incoming follow requests | ||||
| 	FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) | ||||
| 	// 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(domain string) (*apimodel.Instance, ErrorWithCode) | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ import ( | |||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||
| ) | ||||
| 
 | ||||
|  | @ -280,7 +281,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID | |||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("error processing avatar: %s", err) | ||||
| 	} | ||||
|  | @ -313,10 +314,42 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID | |||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ type controller struct { | |||
| 	clock    pub.Clock | ||||
| 	client   pub.HttpClient | ||||
| 	appAgent string | ||||
| 	log      *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| // 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, | ||||
| 		client:   client, | ||||
| 		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, | ||||
| 		getSigner:    getSigner, | ||||
| 		getSignerMu:  &sync.Mutex{}, | ||||
| 		log:          c.log, | ||||
| 	}, nil | ||||
| } | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/go-fed/activity/pub" | ||||
| 	"github.com/go-fed/httpsig" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| // Transport wraps the pub.Transport interface with some additional | ||||
|  | @ -31,6 +32,7 @@ type transport struct { | |||
| 	sigTransport *pub.HttpSigTransport | ||||
| 	getSigner    httpsig.Signer | ||||
| 	getSignerMu  *sync.Mutex | ||||
| 	log          *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	l := t.log.WithField("func", "Deliver") | ||||
| 	l.Debugf("performing POST to %s", to.String()) | ||||
| 	return t.sigTransport.Deliver(c, b, to) | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  |  | |||
|  | @ -77,6 +77,9 @@ type TypeConverter interface { | |||
| 	// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance | ||||
| 	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 | ||||
| 	*/ | ||||
|  |  | |||
|  | @ -572,3 +572,21 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro | |||
| 
 | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue