mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 01:02:25 -05:00 
			
		
		
		
	Notifications (#34)
Notifications working for: * Mentions * Faves * New follow requests * New followers
This commit is contained in:
		
					parent
					
						
							
								e670c32a91
							
						
					
				
			
			
				commit
				
					
						40add68691
					
				
			
		
					 42 changed files with 2096 additions and 1194 deletions
				
			
		
							
								
								
									
										66
									
								
								internal/api/client/notification/notification.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/api/client/notification/notification.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | /* | ||||||
|  |    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 notification | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// IDKey is for notification UUIDs | ||||||
|  | 	IDKey = "id" | ||||||
|  | 	// BasePath is the base path for serving the notification API | ||||||
|  | 	BasePath = "/api/v1/notifications" | ||||||
|  | 	// BasePathWithID is just the base path with the ID key in it. | ||||||
|  | 	// Use this anywhere you need to know the ID of the notification being queried. | ||||||
|  | 	BasePathWithID = BasePath + "/:" + IDKey | ||||||
|  | 
 | ||||||
|  | 	// MaxIDKey is the url query for setting a max notification ID to return | ||||||
|  | 	MaxIDKey = "max_id" | ||||||
|  | 	// Limit key is for specifying maximum number of notifications to return. | ||||||
|  | 	LimitKey = "limit" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with notifications | ||||||
|  | type Module struct { | ||||||
|  | 	config    *config.Config | ||||||
|  | 	processor message.Processor | ||||||
|  | 	log       *logrus.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns a new notification module | ||||||
|  | func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { | ||||||
|  | 	return &Module{ | ||||||
|  | 		config:    config, | ||||||
|  | 		processor: processor, | ||||||
|  | 		log:       log, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Route attaches all routes from this module to the given router | ||||||
|  | func (m *Module) Route(r router.Router) error { | ||||||
|  | 	r.AttachHandler(http.MethodGet, BasePath, m.NotificationsGETHandler) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								internal/api/client/notification/notificationsget.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								internal/api/client/notification/notificationsget.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | /* | ||||||
|  |    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 notification | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (m *Module) NotificationsGETHandler(c *gin.Context) { | ||||||
|  | 	l := m.log.WithFields(logrus.Fields{ | ||||||
|  | 		"func":        "NotificationsGETHandler", | ||||||
|  | 		"request_uri": c.Request.RequestURI, | ||||||
|  | 		"user_agent":  c.Request.UserAgent(), | ||||||
|  | 		"origin_ip":   c.ClientIP(), | ||||||
|  | 	}) | ||||||
|  | 	l.Debugf("entering function") | ||||||
|  | 
 | ||||||
|  | 	authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Errorf("error authing status faved by request: %s", err) | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	limit := 20 | ||||||
|  | 	limitString := c.Query(LimitKey) | ||||||
|  | 	if limitString != "" { | ||||||
|  | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			l.Debugf("error parsing limit string: %s", err) | ||||||
|  | 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		limit = int(i) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	maxID := "" | ||||||
|  | 	maxIDString := c.Query(MaxIDKey) | ||||||
|  | 	if maxIDString != "" { | ||||||
|  | 		maxID = maxIDString | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notifs, errWithCode := m.processor.NotificationsGet(authed, limit, maxID) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		l.Debugf("error processing notifications get: %s", errWithCode.Error()) | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, notifs) | ||||||
|  | } | ||||||
|  | @ -41,5 +41,5 @@ type Notification struct { | ||||||
| 	// OPTIONAL | 	// OPTIONAL | ||||||
| 
 | 
 | ||||||
| 	// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. | 	// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. | ||||||
| 	Status *Status `json:"status"` | 	Status *Status `json:"status,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ type Status struct { | ||||||
| 	// Is this status marked as sensitive content? | 	// Is this status marked as sensitive content? | ||||||
| 	Sensitive bool `json:"sensitive"` | 	Sensitive bool `json:"sensitive"` | ||||||
| 	// Subject or summary line, below which status content is collapsed until expanded. | 	// Subject or summary line, below which status content is collapsed until expanded. | ||||||
| 	SpoilerText string `json:"spoiler_text,omitempty"` | 	SpoilerText string `json:"spoiler_text"` | ||||||
| 	// Visibility of this status. | 	// Visibility of this status. | ||||||
| 	Visibility Visibility `json:"visibility"` | 	Visibility Visibility `json:"visibility"` | ||||||
| 	// Primary language of this status. (ISO 639 Part 1 two-letter language code) | 	// Primary language of this status. (ISO 639 Part 1 two-letter language code) | ||||||
|  |  | ||||||
|  | @ -284,6 +284,8 @@ type DB interface { | ||||||
| 	// It will use the given filters and try to return as many statuses up to the limit as possible. | 	// It will use the given filters and try to return as many statuses up to the limit as possible. | ||||||
| 	GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) | 	GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) | ||||||
| 
 | 
 | ||||||
|  | 	GetNotificationsForAccount(accountID string, limit int, maxID string) ([]*gtsmodel.Notification, error) | ||||||
|  | 
 | ||||||
| 	/* | 	/* | ||||||
| 		USEFUL CONVERSION FUNCTIONS | 		USEFUL CONVERSION FUNCTIONS | ||||||
| 	*/ | 	*/ | ||||||
|  |  | ||||||
|  | @ -1138,6 +1138,35 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str | ||||||
| 	return statuses, nil | 	return statuses, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string) ([]*gtsmodel.Notification, error) { | ||||||
|  | 	notifications := []*gtsmodel.Notification{} | ||||||
|  | 
 | ||||||
|  | 	q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	if maxID != "" { | ||||||
|  | 		n := >smodel.Notification{} | ||||||
|  | 		if err := ps.conn.Model(n).Where("id = ?", maxID).Select(); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		q = q.Where("created_at < ?", n.CreatedAt) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if limit != 0 { | ||||||
|  | 		q = q.Limit(limit) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	q = q.Order("created_at DESC") | ||||||
|  | 
 | ||||||
|  | 	if err := q.Select(); err != nil { | ||||||
|  | 		if err != pg.ErrNoRows { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 	return notifications, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /* | /* | ||||||
| 	CONVERSION FUNCTIONS | 	CONVERSION FUNCTIONS | ||||||
| */ | */ | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										94
									
								
								internal/federation/federatingdb/accept.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								internal/federation/federatingdb/accept.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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)) | ||||||
|  | 
 | ||||||
|  | 	fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) | ||||||
|  | 	if fromFederatorChanI == nil { | ||||||
|  | 		l.Error("ACCEPT: from federator channel wasn't set on context") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("ACCEPT: from federator channel was set on context but couldn't be parsed") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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.ActivityStreamsAccept") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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") | ||||||
|  | 			} | ||||||
|  | 			follow, err := f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 				APObjectType:     gtsmodel.ActivityStreamsFollow, | ||||||
|  | 				APActivityType:   gtsmodel.ActivityStreamsAccept, | ||||||
|  | 				GTSModel:         follow, | ||||||
|  | 				ReceivingAccount: inboxAcct, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										161
									
								
								internal/federation/federatingdb/create.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								internal/federation/federatingdb/create.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,161 @@ | ||||||
|  | /* | ||||||
|  |    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 federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Create adds a new entry to the database which must be able to be | ||||||
|  | // keyed by its id. | ||||||
|  | // | ||||||
|  | // Note that Activity values received from federated peers may also be | ||||||
|  | // created in the database this way if the Federating Protocol is | ||||||
|  | // enabled. The client may freely decide to store only the id instead of | ||||||
|  | // the entire value. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | // | ||||||
|  | // Under certain conditions and network activities, Create may be called | ||||||
|  | // multiple times for the same ActivityStreams object. | ||||||
|  | func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":   "Create", | ||||||
|  | 			"asType": asType.GetTypeName(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	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 asType.GetTypeName() { | ||||||
|  | 	case gtsmodel.ActivityStreamsCreate: | ||||||
|  | 		create, ok := asType.(vocab.ActivityStreamsCreate) | ||||||
|  | 		if !ok { | ||||||
|  | 			return errors.New("could not convert type to create") | ||||||
|  | 		} | ||||||
|  | 		object := create.GetActivityStreamsObject() | ||||||
|  | 		for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { | ||||||
|  | 			switch objectIter.GetType().GetTypeName() { | ||||||
|  | 			case gtsmodel.ActivityStreamsNote: | ||||||
|  | 				note := objectIter.GetActivityStreamsNote() | ||||||
|  | 				status, err := f.typeConverter.ASStatusToStatus(note) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return fmt.Errorf("error converting note to status: %s", err) | ||||||
|  | 				} | ||||||
|  | 				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) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 					APObjectType:     gtsmodel.ActivityStreamsNote, | ||||||
|  | 					APActivityType:   gtsmodel.ActivityStreamsCreate, | ||||||
|  | 					GTSModel:         status, | ||||||
|  | 					ReceivingAccount: targetAcct, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 		follow, ok := asType.(vocab.ActivityStreamsFollow) | ||||||
|  | 		if !ok { | ||||||
|  | 			return errors.New("could not convert type to follow") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		followRequest, err := f.typeConverter.ASFollowToFollowRequest(follow) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("could not convert Follow to follow request: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err := f.db.Put(followRequest); err != nil { | ||||||
|  | 			return fmt.Errorf("database error inserting follow request: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 			APObjectType:     gtsmodel.ActivityStreamsFollow, | ||||||
|  | 			APActivityType:   gtsmodel.ActivityStreamsCreate, | ||||||
|  | 			GTSModel:         followRequest, | ||||||
|  | 			ReceivingAccount: targetAcct, | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsLike: | ||||||
|  | 		like, ok := asType.(vocab.ActivityStreamsLike) | ||||||
|  | 		if !ok { | ||||||
|  | 			return errors.New("could not convert type to like") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fave, err := f.typeConverter.ASLikeToFave(like) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("could not convert Like to fave: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err := f.db.Put(fave); err != nil { | ||||||
|  | 			return fmt.Errorf("database error inserting fave: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 			APObjectType:     gtsmodel.ActivityStreamsLike, | ||||||
|  | 			APActivityType:   gtsmodel.ActivityStreamsCreate, | ||||||
|  | 			GTSModel:         fave, | ||||||
|  | 			ReceivingAccount: targetAcct, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								internal/federation/federatingdb/db.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/federation/federatingdb/db.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | ||||||
|  | /* | ||||||
|  |    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 federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/pub" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // DB wraps the pub.Database interface with a couple of custom functions for GoToSocial. | ||||||
|  | type DB 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. | ||||||
|  | // It doesn't care what the underlying implementation of the DB interface is, as long as it works. | ||||||
|  | type federatingDB struct { | ||||||
|  | 	locks         *sync.Map | ||||||
|  | 	db            db.DB | ||||||
|  | 	config        *config.Config | ||||||
|  | 	log           *logrus.Logger | ||||||
|  | 	typeConverter typeutils.TypeConverter | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns a DB interface using the given database, config, and logger. | ||||||
|  | func New(db db.DB, config *config.Config, log *logrus.Logger) DB { | ||||||
|  | 	return &federatingDB{ | ||||||
|  | 		locks:         new(sync.Map), | ||||||
|  | 		db:            db, | ||||||
|  | 		config:        config, | ||||||
|  | 		log:           log, | ||||||
|  | 		typeConverter: typeutils.NewConverter(config, db), | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								internal/federation/federatingdb/delete.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								internal/federation/federatingdb/delete.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Delete removes the entry with the given id. | ||||||
|  | // | ||||||
|  | // Delete is only called for federated objects. Deletes from the Social | ||||||
|  | // Protocol instead call Update to create a Tombstone. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "Delete", | ||||||
|  | 			"id":   id.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("received DELETE id %s", id.String()) | ||||||
|  | 
 | ||||||
|  | 	inboxAcctI := ctx.Value(util.APAccount) | ||||||
|  | 	if inboxAcctI == nil { | ||||||
|  | 		l.Error("inbox account wasn't set on context") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	inboxAcct, ok := inboxAcctI.(*gtsmodel.Account) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("inbox 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 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// in a delete we only get the URI, we can't know if we have a status or a profile or something else, | ||||||
|  | 	// so we have to try a few different things... | ||||||
|  | 	where := []db.Where{{Key: "uri", Value: id.String()}} | ||||||
|  | 
 | ||||||
|  | 	s := >smodel.Status{} | ||||||
|  | 	if err := f.db.GetWhere(where, s); err == nil { | ||||||
|  | 		// it's a status | ||||||
|  | 		l.Debugf("uri is for status with id: %s", s.ID) | ||||||
|  | 		if err := f.db.DeleteByID(s.ID, >smodel.Status{}); err != nil { | ||||||
|  | 			return fmt.Errorf("Delete: err deleting status: %s", err) | ||||||
|  | 		} | ||||||
|  | 		fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 			APObjectType:     gtsmodel.ActivityStreamsNote, | ||||||
|  | 			APActivityType:   gtsmodel.ActivityStreamsDelete, | ||||||
|  | 			GTSModel:         s, | ||||||
|  | 			ReceivingAccount: inboxAcct, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	a := >smodel.Account{} | ||||||
|  | 	if err := f.db.GetWhere(where, a); err == nil { | ||||||
|  | 		// it's an account | ||||||
|  | 		l.Debugf("uri is for an account with id: %s", s.ID) | ||||||
|  | 		if err := f.db.DeleteByID(a.ID, >smodel.Account{}); err != nil { | ||||||
|  | 			return fmt.Errorf("Delete: err deleting account: %s", err) | ||||||
|  | 		} | ||||||
|  | 		fromFederatorChan <- gtsmodel.FromFederator{ | ||||||
|  | 			APObjectType:     gtsmodel.ActivityStreamsProfile, | ||||||
|  | 			APActivityType:   gtsmodel.ActivityStreamsDelete, | ||||||
|  | 			GTSModel:         a, | ||||||
|  | 			ReceivingAccount: inboxAcct, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								internal/federation/federatingdb/exists.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/federation/federatingdb/exists.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Exists returns true if the database has an entry for the specified | ||||||
|  | // id. It may not be owned by this application instance. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "Exists", | ||||||
|  | 			"id":   id.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering EXISTS function with id %s", id.String()) | ||||||
|  | 
 | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								internal/federation/federatingdb/followers.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/federation/federatingdb/followers.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Followers obtains the Followers Collection for an actor with the | ||||||
|  | // given id. | ||||||
|  | // | ||||||
|  | // If modified, the library will then call Update. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "Followers", | ||||||
|  | 			"actorIRI": actorIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) | ||||||
|  | 
 | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	acctFollowers := []gtsmodel.Follow{} | ||||||
|  | 	if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("db error getting followers for account id %s: %s", acct.ID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	followers = streams.NewActivityStreamsCollection() | ||||||
|  | 	items := streams.NewActivityStreamsItemsProperty() | ||||||
|  | 	for _, follow := range acctFollowers { | ||||||
|  | 		gtsFollower := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetByID(follow.AccountID, gtsFollower); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) | ||||||
|  | 		} | ||||||
|  | 		uri, err := url.Parse(gtsFollower.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollower.URI, err) | ||||||
|  | 		} | ||||||
|  | 		items.AppendIRI(uri) | ||||||
|  | 	} | ||||||
|  | 	followers.SetActivityStreamsItems(items) | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								internal/federation/federatingdb/following.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/federation/federatingdb/following.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Following obtains the Following Collection for an actor with the | ||||||
|  | // given id. | ||||||
|  | // | ||||||
|  | // If modified, the library will then call Update. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "Following", | ||||||
|  | 			"actorIRI": actorIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) | ||||||
|  | 
 | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	acctFollowing := []gtsmodel.Follow{} | ||||||
|  | 	if err := f.db.GetFollowingByAccountID(acct.ID, &acctFollowing); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("db error getting following for account id %s: %s", acct.ID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	following = streams.NewActivityStreamsCollection() | ||||||
|  | 	items := streams.NewActivityStreamsItemsProperty() | ||||||
|  | 	for _, follow := range acctFollowing { | ||||||
|  | 		gtsFollowing := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetByID(follow.AccountID, gtsFollowing); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) | ||||||
|  | 		} | ||||||
|  | 		uri, err := url.Parse(gtsFollowing.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollowing.URI, err) | ||||||
|  | 		} | ||||||
|  | 		items.AppendIRI(uri) | ||||||
|  | 	} | ||||||
|  | 	following.SetActivityStreamsItems(items) | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								internal/federation/federatingdb/get.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								internal/federation/federatingdb/get.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Get returns the database entry for the specified id. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "Get", | ||||||
|  | 			"id":   id.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debug("entering GET function") | ||||||
|  | 
 | ||||||
|  | 	if util.IsUserPath(id) { | ||||||
|  | 		acct := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		l.Debug("is user path! returning account") | ||||||
|  | 		return f.typeConverter.AccountToAS(acct) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if util.IsFollowersPath(id) { | ||||||
|  | 		acct := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetWhere([]db.Where{{Key: "followers_uri", Value: id.String()}}, acct); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		followersURI, err := url.Parse(acct.FollowersURI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return f.Followers(c, followersURI) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if util.IsFollowingPath(id) { | ||||||
|  | 		acct := >smodel.Account{} | ||||||
|  | 		if err := f.db.GetWhere([]db.Where{{Key: "following_uri", Value: id.String()}}, acct); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		followingURI, err := url.Parse(acct.FollowingURI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return f.Following(c, followingURI) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil, errors.New("could not get") | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								internal/federation/federatingdb/inbox.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/federation/federatingdb/inbox.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"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/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // InboxContains returns true if the OrderedCollection at 'inbox' | ||||||
|  | // contains the specified 'id'. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "InboxContains", | ||||||
|  | 			"id":   id.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering INBOXCONTAINS function with for inbox %s and id %s", inbox.String(), id.String()) | ||||||
|  | 
 | ||||||
|  | 	if !util.IsInboxPath(inbox) { | ||||||
|  | 		return false, fmt.Errorf("%s is not an inbox URI", inbox.String()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	activityI := c.Value(util.APActivity) | ||||||
|  | 	if activityI == nil { | ||||||
|  | 		return false, fmt.Errorf("no activity was set for id %s", id.String()) | ||||||
|  | 	} | ||||||
|  | 	activity, ok := activityI.(pub.Activity) | ||||||
|  | 	if !ok || activity == nil { | ||||||
|  | 		return false, fmt.Errorf("could not parse contextual activity for id %s", id.String()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	l.Debugf("activity type %s for id %s", activity.GetTypeName(), id.String()) | ||||||
|  | 
 | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetInbox returns the first ordered collection page of the outbox at | ||||||
|  | // the specified IRI, for prepending new items. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "GetInbox", | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering GETINBOX function with inboxIRI %s", inboxIRI.String()) | ||||||
|  | 	return streams.NewActivityStreamsOrderedCollectionPage(), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetInbox saves the inbox value given from GetInbox, with new items | ||||||
|  | // prepended. Note that the new items must not be added as independent | ||||||
|  | // database entries. Separate calls to Create will do that. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "SetInbox", | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debug("entering SETINBOX function") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								internal/federation/federatingdb/liked.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/federation/federatingdb/liked.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Liked obtains the Liked Collection for an actor with the | ||||||
|  | // given id. | ||||||
|  | // | ||||||
|  | // If modified, the library will then call Update. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "Liked", | ||||||
|  | 			"actorIRI": actorIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String()) | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								internal/federation/federatingdb/lock.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								internal/federation/federatingdb/lock.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | /* | ||||||
|  |    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 federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"net/url" | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Lock takes a lock for the object at the specified id. If an error | ||||||
|  | // is returned, the lock must not have been taken. | ||||||
|  | // | ||||||
|  | // The lock must be able to succeed for an id that does not exist in | ||||||
|  | // the database. This means acquiring the lock does not guarantee the | ||||||
|  | // entry exists in the database. | ||||||
|  | // | ||||||
|  | // Locks are encouraged to be lightweight and in the Go layer, as some | ||||||
|  | // processes require tight loops acquiring and releasing locks. | ||||||
|  | // | ||||||
|  | // Used to ensure race conditions in multiple requests do not occur. | ||||||
|  | func (f *federatingDB) Lock(c context.Context, id *url.URL) error { | ||||||
|  | 	// Before any other Database methods are called, the relevant `id` | ||||||
|  | 	// entries are locked to allow for fine-grained concurrency. | ||||||
|  | 
 | ||||||
|  | 	// Strategy: create a new lock, if stored, continue. Otherwise, lock the | ||||||
|  | 	// existing mutex. | ||||||
|  | 	mu := &sync.Mutex{} | ||||||
|  | 	mu.Lock() // Optimistically lock if we do store it. | ||||||
|  | 	i, loaded := f.locks.LoadOrStore(id.String(), mu) | ||||||
|  | 	if loaded { | ||||||
|  | 		mu = i.(*sync.Mutex) | ||||||
|  | 		mu.Lock() | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Unlock makes the lock for the object at the specified id available. | ||||||
|  | // If an error is returned, the lock must have still been freed. | ||||||
|  | // | ||||||
|  | // Used to ensure race conditions in multiple requests do not occur. | ||||||
|  | func (f *federatingDB) Unlock(c context.Context, id *url.URL) error { | ||||||
|  | 	// Once Go-Fed is done calling Database methods, the relevant `id` | ||||||
|  | 	// entries are unlocked. | ||||||
|  | 
 | ||||||
|  | 	i, ok := f.locks.Load(id.String()) | ||||||
|  | 	if !ok { | ||||||
|  | 		return errors.New("missing an id in unlock") | ||||||
|  | 	} | ||||||
|  | 	mu := i.(*sync.Mutex) | ||||||
|  | 	mu.Unlock() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								internal/federation/federatingdb/outbox.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								internal/federation/federatingdb/outbox.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // GetOutbox returns the first ordered collection page of the outbox | ||||||
|  | // at the specified IRI, for prepending new items. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "GetOutbox", | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debug("entering GETOUTBOX function") | ||||||
|  | 
 | ||||||
|  | 	return streams.NewActivityStreamsOrderedCollectionPage(), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetOutbox saves the outbox value given from GetOutbox, with new items | ||||||
|  | // prepended. Note that the new items must not be added as independent | ||||||
|  | // database entries. Separate calls to Create will do that. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "SetOutbox", | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debug("entering SETOUTBOX function") | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // OutboxForInbox fetches the corresponding actor's outbox IRI for the | ||||||
|  | // actor's inbox IRI. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "OutboxForInbox", | ||||||
|  | 			"inboxIRI": inboxIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering OUTBOXFORINBOX function with inboxIRI %s", inboxIRI.String()) | ||||||
|  | 
 | ||||||
|  | 	if !util.IsInboxPath(inboxIRI) { | ||||||
|  | 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | ||||||
|  | 		} | ||||||
|  | 		return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	return url.Parse(acct.OutboxURI) | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								internal/federation/federatingdb/owns.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								internal/federation/federatingdb/owns.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,123 @@ | ||||||
|  | /* | ||||||
|  |    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 federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Owns returns true if the IRI belongs to this instance, and if | ||||||
|  | // the database has an entry for the IRI. | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "Owns", | ||||||
|  | 			"id":   id.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering OWNS function with id %s", id.String()) | ||||||
|  | 
 | ||||||
|  | 	// if the id host isn't this instance host, we don't own this IRI | ||||||
|  | 	if id.Host != f.config.Host { | ||||||
|  | 		l.Debugf("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host) | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// apparently it belongs to this host, so what *is* it? | ||||||
|  | 
 | ||||||
|  | 	// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS | ||||||
|  | 	if util.IsStatusesPath(id) { | ||||||
|  | 		_, uid, err := util.ParseStatusesPath(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | ||||||
|  | 		} | ||||||
|  | 		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil { | ||||||
|  | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 				// there are no entries for this status | ||||||
|  | 				return false, nil | ||||||
|  | 			} | ||||||
|  | 			// an actual error happened | ||||||
|  | 			return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err) | ||||||
|  | 		} | ||||||
|  | 		l.Debug("we DO own this") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if it's a user, eg /users/example_username | ||||||
|  | 	if util.IsUserPath(id) { | ||||||
|  | 		username, err := util.ParseUserPath(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | ||||||
|  | 		} | ||||||
|  | 		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { | ||||||
|  | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 				// there are no entries for this username | ||||||
|  | 				return false, nil | ||||||
|  | 			} | ||||||
|  | 			// an actual error happened | ||||||
|  | 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | ||||||
|  | 		} | ||||||
|  | 		l.Debug("we DO own this") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if util.IsFollowersPath(id) { | ||||||
|  | 		username, err := util.ParseFollowersPath(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | ||||||
|  | 		} | ||||||
|  | 		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { | ||||||
|  | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 				// there are no entries for this username | ||||||
|  | 				return false, nil | ||||||
|  | 			} | ||||||
|  | 			// an actual error happened | ||||||
|  | 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | ||||||
|  | 		} | ||||||
|  | 		l.Debug("we DO own this") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if util.IsFollowingPath(id) { | ||||||
|  | 		username, err := util.ParseFollowingPath(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) | ||||||
|  | 		} | ||||||
|  | 		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { | ||||||
|  | 			if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 				// there are no entries for this username | ||||||
|  | 				return false, nil | ||||||
|  | 			} | ||||||
|  | 			// an actual error happened | ||||||
|  | 			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) | ||||||
|  | 		} | ||||||
|  | 		l.Debug("we DO own this") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false, fmt.Errorf("could not match activityID: %s", id.String()) | ||||||
|  | } | ||||||
							
								
								
									
										89
									
								
								internal/federation/federatingdb/undo.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								internal/federation/federatingdb/undo.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										142
									
								
								internal/federation/federatingdb/update.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/federation/federatingdb/update.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,142 @@ | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Update sets an existing entry to the database based on the value's | ||||||
|  | // id. | ||||||
|  | // | ||||||
|  | // Note that Activity values received from federated peers may also be | ||||||
|  | // updated in the database this way if the Federating Protocol is | ||||||
|  | // enabled. The client may freely decide to store only the id instead of | ||||||
|  | // the entire value. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":   "Update", | ||||||
|  | 			"asType": asType.GetTypeName(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	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") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestingAcctI := ctx.Value(util.APRequestingAccount) | ||||||
|  | 	if receivingAcctI == nil { | ||||||
|  | 		l.Error("requesting account wasn't set on context") | ||||||
|  | 	} | ||||||
|  | 	requestingAcct, ok := requestingAcctI.(*gtsmodel.Account) | ||||||
|  | 	if !ok { | ||||||
|  | 		l.Error("requesting 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") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	typeName := asType.GetTypeName() | ||||||
|  | 	if typeName == gtsmodel.ActivityStreamsApplication || | ||||||
|  | 		typeName == gtsmodel.ActivityStreamsGroup || | ||||||
|  | 		typeName == gtsmodel.ActivityStreamsOrganization || | ||||||
|  | 		typeName == gtsmodel.ActivityStreamsPerson || | ||||||
|  | 		typeName == gtsmodel.ActivityStreamsService { | ||||||
|  | 		// it's an UPDATE to some kind of account | ||||||
|  | 		var accountable typeutils.Accountable | ||||||
|  | 
 | ||||||
|  | 		switch asType.GetTypeName() { | ||||||
|  | 		case gtsmodel.ActivityStreamsApplication: | ||||||
|  | 			l.Debug("got update for APPLICATION") | ||||||
|  | 			i, ok := asType.(vocab.ActivityStreamsApplication) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("could not convert type to application") | ||||||
|  | 			} | ||||||
|  | 			accountable = i | ||||||
|  | 		case gtsmodel.ActivityStreamsGroup: | ||||||
|  | 			l.Debug("got update for GROUP") | ||||||
|  | 			i, ok := asType.(vocab.ActivityStreamsGroup) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("could not convert type to group") | ||||||
|  | 			} | ||||||
|  | 			accountable = i | ||||||
|  | 		case gtsmodel.ActivityStreamsOrganization: | ||||||
|  | 			l.Debug("got update for ORGANIZATION") | ||||||
|  | 			i, ok := asType.(vocab.ActivityStreamsOrganization) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("could not convert type to organization") | ||||||
|  | 			} | ||||||
|  | 			accountable = i | ||||||
|  | 		case gtsmodel.ActivityStreamsPerson: | ||||||
|  | 			l.Debug("got update for PERSON") | ||||||
|  | 			i, ok := asType.(vocab.ActivityStreamsPerson) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("could not convert type to person") | ||||||
|  | 			} | ||||||
|  | 			accountable = i | ||||||
|  | 		case gtsmodel.ActivityStreamsService: | ||||||
|  | 			l.Debug("got update for SERVICE") | ||||||
|  | 			i, ok := asType.(vocab.ActivityStreamsService) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("could not convert type to service") | ||||||
|  | 			} | ||||||
|  | 			accountable = i | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		updatedAcct, err := f.typeConverter.ASRepresentationToAccount(accountable, true) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("error converting to account: %s", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if requestingAcct.URI != updatedAcct.URI { | ||||||
|  | 			return fmt.Errorf("update for account %s was requested by account %s, this is not valid", updatedAcct.URI, requestingAcct.URI) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		updatedAcct.ID = requestingAcct.ID // set this here so the db will update properly instead of trying to PUT this and getting constraint issues | ||||||
|  | 		if err := f.db.UpdateByID(requestingAcct.ID, 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 | ||||||
|  | } | ||||||
							
								
								
									
										187
									
								
								internal/federation/federatingdb/util.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								internal/federation/federatingdb/util.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,187 @@ | ||||||
|  | /* | ||||||
|  |    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 federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewID creates a new IRI id for the provided activity or object. The | ||||||
|  | // implementation does not need to set the 'id' property and simply | ||||||
|  | // needs to determine the value. | ||||||
|  | // | ||||||
|  | // The go-fed library will handle setting the 'id' property on the | ||||||
|  | // activity or object provided with the value returned. | ||||||
|  | func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":   "NewID", | ||||||
|  | 			"asType": t.GetTypeName(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	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)) | ||||||
|  | 
 | ||||||
|  | 	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, uuid.NewString())) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	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: note couldn't be parsed into vocab.ActivityStreamsNote") | ||||||
|  | 		} | ||||||
|  | 		idProp := note.GetJSONLDId() | ||||||
|  | 		if idProp != nil { | ||||||
|  | 			if idProp.IsIRI() { | ||||||
|  | 				return idProp.GetIRI(), nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsLike: | ||||||
|  | 		// LIKE aka FAVE | ||||||
|  | 		// ID might already be set on a fave we've created, so check it here and return it if it is | ||||||
|  | 		fave, ok := t.(vocab.ActivityStreamsLike) | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsLike") | ||||||
|  | 		} | ||||||
|  | 		idProp := fave.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())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ActorForOutbox fetches the actor's IRI for the given outbox IRI. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "ActorForOutbox", | ||||||
|  | 			"inboxIRI": outboxIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering ACTORFOROUTBOX function with outboxIRI %s", outboxIRI.String()) | ||||||
|  | 
 | ||||||
|  | 	if !util.IsOutboxPath(outboxIRI) { | ||||||
|  | 		return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil { | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 			return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) | ||||||
|  | 		} | ||||||
|  | 		return nil, fmt.Errorf("db error searching for actor with outbox %s", outboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	return url.Parse(acct.URI) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ActorForInbox fetches the actor's IRI for the given outbox IRI. | ||||||
|  | // | ||||||
|  | // The library makes this call only after acquiring a lock first. | ||||||
|  | func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) { | ||||||
|  | 	l := f.log.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func":     "ActorForInbox", | ||||||
|  | 			"inboxIRI": inboxIRI.String(), | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	l.Debugf("entering ACTORFORINBOX function with inboxIRI %s", inboxIRI.String()) | ||||||
|  | 
 | ||||||
|  | 	if !util.IsInboxPath(inboxIRI) { | ||||||
|  | 		return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); ok { | ||||||
|  | 			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) | ||||||
|  | 		} | ||||||
|  | 		return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) | ||||||
|  | 	} | ||||||
|  | 	return url.Parse(acct.URI) | ||||||
|  | } | ||||||
|  | @ -240,38 +240,20 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er | ||||||
| // Applications are not expected to handle every single ActivityStreams | // Applications are not expected to handle every single ActivityStreams | ||||||
| // type and extension. The unhandled ones are passed to DefaultCallback. | // type and extension. The unhandled ones are passed to DefaultCallback. | ||||||
| func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { | func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { | ||||||
| 	l := f.log.WithFields(logrus.Fields{ |  | ||||||
| 		"func": "FederatingCallbacks", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	receivingAcctI := ctx.Value(util.APAccount) |  | ||||||
| 	if receivingAcctI == nil { |  | ||||||
| 		l.Error("receiving account wasn't set on context") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) |  | ||||||
| 	if !ok { |  | ||||||
| 		l.Error("receiving account was set on context but couldn't be parsed") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept |  | ||||||
| 	if receivingAcct.Locked { |  | ||||||
| 		onFollow = pub.OnFollowDoNothing |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	wrapped = pub.FederatingWrappedCallbacks{ | 	wrapped = pub.FederatingWrappedCallbacks{ | ||||||
| 		// OnFollow determines what action to take for this particular callback | 		// OnFollow determines what action to take for this particular callback | ||||||
| 		// if a Follow Activity is handled. | 		// if a Follow Activity is handled. | ||||||
| 		OnFollow: onFollow, | 		// | ||||||
|  | 		// For our implementation, we always want to do nothing because we have internal logic for handling follows. | ||||||
|  | 		OnFollow: pub.OnFollowDoNothing, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	other = []interface{}{ | 	other = []interface{}{ | ||||||
| 		// override default undo behavior | 		// override default undo behavior and trigger our own side effects | ||||||
| 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | ||||||
| 			return f.FederatingDB().Undo(ctx, undo) | 			return f.FederatingDB().Undo(ctx, undo) | ||||||
| 		}, | 		}, | ||||||
| 		// override default accept behavior | 		// override default accept behavior and trigger our own side effects | ||||||
| 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | ||||||
| 			return f.FederatingDB().Accept(ctx, accept) | 			return f.FederatingDB().Accept(ctx, accept) | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ 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/federatingdb" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
|  | @ -35,7 +36,7 @@ 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 returns the underlying FederatingDB interface. | ||||||
| 	FederatingDB() FederatingDB | 	FederatingDB() federatingdb.DB | ||||||
| 	// 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) | ||||||
|  | @ -54,7 +55,7 @@ type Federator interface { | ||||||
| type federator struct { | type federator struct { | ||||||
| 	config              *config.Config | 	config              *config.Config | ||||||
| 	db                  db.DB | 	db                  db.DB | ||||||
| 	federatingDB        FederatingDB | 	federatingDB        federatingdb.DB | ||||||
| 	clock               pub.Clock | 	clock               pub.Clock | ||||||
| 	typeConverter       typeutils.TypeConverter | 	typeConverter       typeutils.TypeConverter | ||||||
| 	transportController transport.Controller | 	transportController transport.Controller | ||||||
|  | @ -63,7 +64,7 @@ type federator struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewFederator returns a new federator | // NewFederator returns a new federator | ||||||
| func NewFederator(db db.DB, federatingDB FederatingDB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator { | func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator { | ||||||
| 
 | 
 | ||||||
| 	clock := &Clock{} | 	clock := &Clock{} | ||||||
| 	f := &federator{ | 	f := &federator{ | ||||||
|  | @ -84,6 +85,6 @@ func (f *federator) FederatingActor() pub.FederatingActor { | ||||||
| 	return f.actor | 	return f.actor | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *federator) FederatingDB() FederatingDB { | func (f *federator) FederatingDB() federatingdb.DB { | ||||||
| 	return f.federatingDB | 	return f.federatingDB | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -280,22 +280,4 @@ 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 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -37,6 +37,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance" | ||||||
| 	mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" | 	mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/notification" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" | 	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" | ||||||
|  | @ -45,6 +46,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/pg" | 	"github.com/superseriousbusiness/gotosocial/internal/db/pg" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" | 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" | ||||||
| 	"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/message" | 	"github.com/superseriousbusiness/gotosocial/internal/message" | ||||||
|  | @ -73,6 +75,7 @@ var models []interface{} = []interface{}{ | ||||||
| 	>smodel.User{}, | 	>smodel.User{}, | ||||||
| 	>smodel.Emoji{}, | 	>smodel.Emoji{}, | ||||||
| 	>smodel.Instance{}, | 	>smodel.Instance{}, | ||||||
|  | 	>smodel.Notification{}, | ||||||
| 	&oauth.Token{}, | 	&oauth.Token{}, | ||||||
| 	&oauth.Client{}, | 	&oauth.Client{}, | ||||||
| } | } | ||||||
|  | @ -84,7 +87,7 @@ 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) | 	federatingDB := federatingdb.New(dbService, c, log) | ||||||
| 
 | 
 | ||||||
| 	router, err := router.New(c, log) | 	router, err := router.New(c, log) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -118,6 +121,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr | ||||||
| 	webfingerModule := webfinger.New(c, processor, log) | 	webfingerModule := webfinger.New(c, processor, log) | ||||||
| 	usersModule := user.New(c, processor, log) | 	usersModule := user.New(c, processor, log) | ||||||
| 	timelineModule := timeline.New(c, processor, log) | 	timelineModule := timeline.New(c, processor, log) | ||||||
|  | 	notificationModule := notification.New(c, processor, log) | ||||||
| 	mm := mediaModule.New(c, processor, log) | 	mm := mediaModule.New(c, processor, log) | ||||||
| 	fileServerModule := fileserver.New(c, processor, log) | 	fileServerModule := fileserver.New(c, processor, log) | ||||||
| 	adminModule := admin.New(c, processor, log) | 	adminModule := admin.New(c, processor, log) | ||||||
|  | @ -141,6 +145,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr | ||||||
| 		webfingerModule, | 		webfingerModule, | ||||||
| 		usersModule, | 		usersModule, | ||||||
| 		timelineModule, | 		timelineModule, | ||||||
|  | 		notificationModule, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, m := range apis { | 	for _, m := range apis { | ||||||
|  |  | ||||||
							
								
								
									
										70
									
								
								internal/gtsmodel/notification.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								internal/gtsmodel/notification.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | /* | ||||||
|  |    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 | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | // Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc. | ||||||
|  | type Notification struct { | ||||||
|  | 	// ID of this notification in the database | ||||||
|  | 	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||||
|  | 	// Type of this notification | ||||||
|  | 	NotificationType NotificationType `pg:",notnull"` | ||||||
|  | 	// Creation time of this notification | ||||||
|  | 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
|  | 	// Which account does this notification target (ie., who will receive the notification?) | ||||||
|  | 	TargetAccountID string `pg:",notnull"` | ||||||
|  | 	// Which account performed the action that created this notification? | ||||||
|  | 	OriginAccountID string `pg:",notnull"` | ||||||
|  | 	// If the notification pertains to a status, what is the database ID of that status? | ||||||
|  | 	StatusID string | ||||||
|  | 	// Has this notification been read already? | ||||||
|  | 	Read bool | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		NON-DATABASE fields | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	// gts model of the target account, won't be put in the database, it's just for convenience when passing the notification around. | ||||||
|  | 	GTSTargetAccount *Account `pg:"-"` | ||||||
|  | 	// gts model of the origin account, won't be put in the database, it's just for convenience when passing the notification around. | ||||||
|  | 	GTSOriginAccount *Account `pg:"-"` | ||||||
|  | 	// gts model of the relevant status, won't be put in the database, it's just for convenience when passing the notification around. | ||||||
|  | 	GTSStatus *Status `pg:"-"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NotificationType describes the reason/type of this notification. | ||||||
|  | type NotificationType string | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// NotificationFollow -- someone followed you | ||||||
|  | 	NotificationFollow NotificationType = "follow" | ||||||
|  | 	// NotificationFollowRequest -- someone requested to follow you | ||||||
|  | 	NotificationFollowRequest NotificationType = "follow_request" | ||||||
|  | 	// NotificationMention -- someone mentioned you in their status | ||||||
|  | 	NotificationMention NotificationType = "mention" | ||||||
|  | 	// NotificationReblog -- someone boosted one of your statuses | ||||||
|  | 	NotificationReblog NotificationType = "reblog" | ||||||
|  | 	// NotifiationFave -- someone faved/liked one of your statuses | ||||||
|  | 	NotificationFave NotificationType = "favourite" | ||||||
|  | 	// NotificationPoll -- a poll you voted in or created has ended | ||||||
|  | 	NotificationPoll NotificationType = "poll" | ||||||
|  | 	// NotificationStatus -- someone you enabled notifications for has posted a status. | ||||||
|  | 	NotificationStatus NotificationType = "status" | ||||||
|  | ) | ||||||
|  | @ -80,7 +80,7 @@ type Status struct { | ||||||
| 	*/ | 	*/ | ||||||
| 
 | 
 | ||||||
| 	// Account that created this status | 	// Account that created this status | ||||||
| 	GTSAccount *Account `pg:"-"` | 	GTSAuthorAccount *Account `pg:"-"` | ||||||
| 	// Mentions created in this status | 	// Mentions created in this status | ||||||
| 	GTSMentions []*Mention `pg:"-"` | 	GTSMentions []*Mention `pg:"-"` | ||||||
| 	// Hashtags used in this status | 	// Hashtags used in this status | ||||||
|  |  | ||||||
|  | @ -49,17 +49,17 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | ||||||
| 			} | 			} | ||||||
| 			return nil | 			return nil | ||||||
| 		case gtsmodel.ActivityStreamsFollow: | 		case gtsmodel.ActivityStreamsFollow: | ||||||
| 			// CREATE FOLLOW (request) | 			// CREATE FOLLOW REQUEST | ||||||
| 			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) | 			followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				return errors.New("follow was not parseable as *gtsmodel.Follow") | 				return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if err := p.notifyFollow(follow); err != nil { | 			if err := p.notifyFollowRequest(followRequest, clientMsg.TargetAccount); err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | 			return p.federateFollow(followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
| 		case gtsmodel.ActivityStreamsLike: | 		case gtsmodel.ActivityStreamsLike: | ||||||
| 			// CREATE LIKE/FAVE | 			// CREATE LIKE/FAVE | ||||||
| 			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) | 			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) | ||||||
|  | @ -67,7 +67,7 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | ||||||
| 				return errors.New("fave was not parseable as *gtsmodel.StatusFave") | 				return errors.New("fave was not parseable as *gtsmodel.StatusFave") | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if err := p.notifyFave(fave); err != nil { | 			if err := p.notifyFave(fave, clientMsg.TargetAccount); err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -84,6 +84,11 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				return errors.New("accept was not parseable as *gtsmodel.Follow") | 				return errors.New("accept was not parseable as *gtsmodel.Follow") | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := p.notifyFollow(follow, clientMsg.TargetAccount); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | 			return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | ||||||
| 		} | 		} | ||||||
| 	case gtsmodel.ActivityStreamsUndo: | 	case gtsmodel.ActivityStreamsUndo: | ||||||
|  | @ -107,21 +112,23 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error { | ||||||
| 		return fmt.Errorf("federateStatus: error converting status to as format: %s", err) | 		return fmt.Errorf("federateStatus: error converting status to as format: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI) | 	outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err) | 		return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) | 	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | ||||||
| 	// if both accounts are local there's nothing to do here | 	// if both accounts are local there's nothing to do here | ||||||
| 	if originAccount.Domain == "" && targetAccount.Domain == "" { | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	follow := p.tc.FollowRequestToFollow(followRequest) | ||||||
|  | 
 | ||||||
| 	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) | 	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("federateFollow: error converting follow to as format: %s", err) | 		return fmt.Errorf("federateFollow: error converting follow to as format: %s", err) | ||||||
|  |  | ||||||
|  | @ -18,16 +18,143 @@ | ||||||
| 
 | 
 | ||||||
| package message | package message | ||||||
| 
 | 
 | ||||||
| import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| func (p *processor) notifyStatus(status *gtsmodel.Status) error { | func (p *processor) notifyStatus(status *gtsmodel.Status) error { | ||||||
|  | 	// if there are no mentions in this status then just bail | ||||||
|  | 	if len(status.Mentions) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if status.GTSMentions == nil { | ||||||
|  | 		// there are mentions but they're not fully populated on the status yet so do this | ||||||
|  | 		menchies := []*gtsmodel.Mention{} | ||||||
|  | 		for _, m := range status.Mentions { | ||||||
|  | 			gtsm := >smodel.Mention{} | ||||||
|  | 			if err := p.db.GetByID(m, gtsm); err != nil { | ||||||
|  | 				return fmt.Errorf("notifyStatus: error getting mention with id %s from the db: %s", m, err) | ||||||
|  | 			} | ||||||
|  | 			menchies = append(menchies, gtsm) | ||||||
|  | 		} | ||||||
|  | 		status.GTSMentions = menchies | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// now we have mentions as full gtsmodel.Mention structs on the status we can continue | ||||||
|  | 	for _, m := range status.GTSMentions { | ||||||
|  | 		// make sure this is a local account, otherwise we don't need to create a notification for it | ||||||
|  | 		if m.GTSAccount == nil { | ||||||
|  | 			a := >smodel.Account{} | ||||||
|  | 			if err := p.db.GetByID(m.TargetAccountID, a); err != nil { | ||||||
|  | 				// we don't have the account or there's been an error | ||||||
|  | 				return fmt.Errorf("notifyStatus: error getting account with id %s from the db: %s", m.TargetAccountID, err) | ||||||
|  | 			} | ||||||
|  | 			m.GTSAccount = a | ||||||
|  | 		} | ||||||
|  | 		if m.GTSAccount.Domain != "" { | ||||||
|  | 			// not a local account so skip it | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// make sure a notif doesn't already exist for this mention | ||||||
|  | 		err := p.db.GetWhere([]db.Where{ | ||||||
|  | 			{Key: "notification_type", Value: gtsmodel.NotificationMention}, | ||||||
|  | 			{Key: "target_account_id", Value: m.TargetAccountID}, | ||||||
|  | 			{Key: "origin_account_id", Value: status.AccountID}, | ||||||
|  | 			{Key: "status_id", Value: status.ID}, | ||||||
|  | 		}, >smodel.Notification{}) | ||||||
|  | 		if err == nil { | ||||||
|  | 			// notification exists already so just continue | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if _, ok := err.(db.ErrNoEntries); !ok { | ||||||
|  | 			// there's a real error in the db | ||||||
|  | 			return fmt.Errorf("notifyStatus: error checking existence of notification for mention with id %s : %s", m.ID, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it | ||||||
|  | 		notif := >smodel.Notification{ | ||||||
|  | 			NotificationType: gtsmodel.NotificationMention, | ||||||
|  | 			TargetAccountID:  m.TargetAccountID, | ||||||
|  | 			OriginAccountID:  status.AccountID, | ||||||
|  | 			StatusID:         status.ID, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err := p.db.Put(notif); err != nil { | ||||||
|  | 			return fmt.Errorf("notifyStatus: error putting notification in database: %s", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) notifyFollow(follow *gtsmodel.Follow) error { | func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, receivingAccount *gtsmodel.Account) error { | ||||||
|  | 	// return if this isn't a local account | ||||||
|  | 	if receivingAccount.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notif := >smodel.Notification{ | ||||||
|  | 		NotificationType: gtsmodel.NotificationFollowRequest, | ||||||
|  | 		TargetAccountID:  followRequest.TargetAccountID, | ||||||
|  | 		OriginAccountID:  followRequest.AccountID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.db.Put(notif); err != nil { | ||||||
|  | 		return fmt.Errorf("notifyFollowRequest: error putting notification in database: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) notifyFave(fave *gtsmodel.StatusFave) error { | func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsmodel.Account) error { | ||||||
|  | 	// return if this isn't a local account | ||||||
|  | 	if receivingAccount.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// first remove the follow request notification | ||||||
|  | 	if err := p.db.DeleteWhere([]db.Where{ | ||||||
|  | 		{Key: "notification_type", Value: gtsmodel.NotificationFollowRequest}, | ||||||
|  | 		{Key: "target_account_id", Value: follow.TargetAccountID}, | ||||||
|  | 		{Key: "origin_account_id", Value: follow.AccountID}, | ||||||
|  | 	}, >smodel.Notification{}); err != nil { | ||||||
|  | 		return fmt.Errorf("notifyFollow: error removing old follow request notification from database: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// now create the new follow notification | ||||||
|  | 	notif := >smodel.Notification{ | ||||||
|  | 		NotificationType: gtsmodel.NotificationFollow, | ||||||
|  | 		TargetAccountID:  follow.TargetAccountID, | ||||||
|  | 		OriginAccountID:  follow.AccountID, | ||||||
|  | 	} | ||||||
|  | 	if err := p.db.Put(notif); err != nil { | ||||||
|  | 		return fmt.Errorf("notifyFollow: error putting notification in database: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsmodel.Account) error { | ||||||
|  | 	// return if this isn't a local account | ||||||
|  | 	if receivingAccount.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notif := >smodel.Notification{ | ||||||
|  | 		NotificationType: gtsmodel.NotificationFave, | ||||||
|  | 		TargetAccountID:  fave.TargetAccountID, | ||||||
|  | 		OriginAccountID:  fave.AccountID, | ||||||
|  | 		StatusID:         fave.StatusID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.db.Put(notif); err != nil { | ||||||
|  | 		return fmt.Errorf("notifyFave: error putting notification in database: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -74,6 +74,26 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | 			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { | ||||||
| 				return fmt.Errorf("error updating dereferenced account in the db: %s", err) | 				return fmt.Errorf("error updating dereferenced account in the db: %s", err) | ||||||
| 			} | 			} | ||||||
|  | 		case gtsmodel.ActivityStreamsLike: | ||||||
|  | 			// CREATE A FAVE | ||||||
|  | 			incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("like was not parseable as *gtsmodel.StatusFave") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := p.notifyFave(incomingFave, federatorMsg.ReceivingAccount); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 			// CREATE A FOLLOW REQUEST | ||||||
|  | 			incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("like was not parseable as *gtsmodel.FollowRequest") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := p.notifyFollowRequest(incomingFollowRequest, federatorMsg.ReceivingAccount); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	case gtsmodel.ActivityStreamsUpdate: | 	case gtsmodel.ActivityStreamsUpdate: | ||||||
| 		// UPDATE | 		// UPDATE | ||||||
|  | @ -106,6 +126,20 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er | ||||||
| 			// DELETE A PROFILE/ACCOUNT | 			// DELETE A PROFILE/ACCOUNT | ||||||
| 			// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account | 			// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account | ||||||
| 		} | 		} | ||||||
|  | 	case gtsmodel.ActivityStreamsAccept: | ||||||
|  | 		// ACCEPT | ||||||
|  | 		switch federatorMsg.APObjectType { | ||||||
|  | 		case gtsmodel.ActivityStreamsFollow: | ||||||
|  | 			// ACCEPT A FOLLOW | ||||||
|  | 			follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("follow was not parseable as *gtsmodel.Follow") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := p.notifyFollow(follow, federatorMsg.ReceivingAccount); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -215,8 +249,8 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		m.StatusID = status.ID | 		m.StatusID = status.ID | ||||||
| 		m.OriginAccountID = status.GTSAccount.ID | 		m.OriginAccountID = status.GTSAuthorAccount.ID | ||||||
| 		m.OriginAccountURI = status.GTSAccount.URI | 		m.OriginAccountURI = status.GTSAuthorAccount.URI | ||||||
| 
 | 
 | ||||||
| 		targetAccount := >smodel.Account{} | 		targetAccount := >smodel.Account{} | ||||||
| 		if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil { | 		if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil { | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								internal/message/notificationsprocess.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/message/notificationsprocess.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | package message | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode) { | ||||||
|  | 	notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mastoNotifs := []*apimodel.Notification{} | ||||||
|  | 	for _, n := range notifs { | ||||||
|  | 		mastoNotif, err := p.tc.NotificationToMasto(n) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 		mastoNotifs = append(mastoNotifs, mastoNotif) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return mastoNotifs, nil | ||||||
|  | } | ||||||
|  | @ -106,6 +106,9 @@ type Processor interface { | ||||||
| 	// MediaUpdate handles the PUT of a media attachment with the given ID and form | 	// MediaUpdate handles the PUT of a media attachment with the given ID and form | ||||||
| 	MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) | 	MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) | ||||||
| 
 | 
 | ||||||
|  | 	// NotificationsGet | ||||||
|  | 	NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode) | ||||||
|  | 
 | ||||||
| 	// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. | 	// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. | ||||||
| 	StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) | 	StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) | ||||||
| 	// StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. | 	// StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. | ||||||
|  |  | ||||||
|  | @ -278,54 +278,14 @@ func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*api | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// it's visible! it's boostable! so let's boost the FUCK out of it | 	// it's visible! it's boostable! so let's boost the FUCK out of it | ||||||
| 	// first we create a new status and add some basic info to it -- this will be the wrapper for the boosted status | 	boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, authed.Account) | ||||||
| 
 | 	if err != nil { | ||||||
| 	// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs | 		return nil, NewErrorInternalError(err) | ||||||
| 	uris := util.GenerateURIsForAccount(authed.Account.Username, p.config.Protocol, p.config.Host) |  | ||||||
| 	boostWrapperStatusID := uuid.NewString() |  | ||||||
| 	boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID) |  | ||||||
| 	boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID) |  | ||||||
| 
 |  | ||||||
| 	boostWrapperStatus := >smodel.Status{ |  | ||||||
| 		ID:  boostWrapperStatusID, |  | ||||||
| 		URI: boostWrapperStatusURI, |  | ||||||
| 		URL: boostWrapperStatusURL, |  | ||||||
| 
 |  | ||||||
| 		// the boosted status is not created now, but the boost certainly is |  | ||||||
| 		CreatedAt:                time.Now(), |  | ||||||
| 		UpdatedAt:                time.Now(), |  | ||||||
| 		Local:                    true, // always local since this is being done through the client API |  | ||||||
| 		AccountID:                authed.Account.ID, |  | ||||||
| 		CreatedWithApplicationID: authed.Application.ID, |  | ||||||
| 
 |  | ||||||
| 		// replies can be boosted, but boosts are never replies |  | ||||||
| 		InReplyToID:        "", |  | ||||||
| 		InReplyToAccountID: "", |  | ||||||
| 
 |  | ||||||
| 		// these will all be wrapped in the boosted status so set them empty here |  | ||||||
| 		Attachments: []string{}, |  | ||||||
| 		Tags:        []string{}, |  | ||||||
| 		Mentions:    []string{}, |  | ||||||
| 		Emojis:      []string{}, |  | ||||||
| 
 |  | ||||||
| 		// the below fields will be taken from the target status |  | ||||||
| 		Content:             util.HTMLFormat(targetStatus.Content), |  | ||||||
| 		ContentWarning:      targetStatus.ContentWarning, |  | ||||||
| 		ActivityStreamsType: targetStatus.ActivityStreamsType, |  | ||||||
| 		Sensitive:           targetStatus.Sensitive, |  | ||||||
| 		Language:            targetStatus.Language, |  | ||||||
| 		Text:                targetStatus.Text, |  | ||||||
| 		BoostOfID:           targetStatus.ID, |  | ||||||
| 		Visibility:          targetStatus.Visibility, |  | ||||||
| 		VisibilityAdvanced:  targetStatus.VisibilityAdvanced, |  | ||||||
| 
 |  | ||||||
| 		// attach these here for convenience -- the boosted status/account won't go in the DB |  | ||||||
| 		// but they're needed in the processor and for the frontend. Since we have them, we can |  | ||||||
| 		// attach them so we don't need to fetch them again later (save some DB calls) |  | ||||||
| 		GTSBoostedStatus:  targetStatus, |  | ||||||
| 		GTSBoostedAccount: targetAccount, |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID | ||||||
|  | 	boostWrapperStatus.GTSBoostedAccount = targetAccount | ||||||
|  | 
 | ||||||
| 	// put the boost in the database | 	// put the boost in the database | ||||||
| 	if err := p.db.Put(boostWrapperStatus); err != nil { | 	if err := p.db.Put(boostWrapperStatus); err != nil { | ||||||
| 		return nil, NewErrorInternalError(err) | 		return nil, NewErrorInternalError(err) | ||||||
|  |  | ||||||
|  | @ -102,6 +102,15 @@ type Followable interface { | ||||||
| 	withObject | 	withObject | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Likeable represents the minimum interface for an activitystreams 'like' activity. | ||||||
|  | type Likeable interface { | ||||||
|  | 	withJSONLDId | ||||||
|  | 	withTypeName | ||||||
|  | 
 | ||||||
|  | 	withActor | ||||||
|  | 	withObject | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type withJSONLDId interface { | type withJSONLDId interface { | ||||||
| 	GetJSONLDId() vocab.JSONLDIdProperty | 	GetJSONLDId() vocab.JSONLDIdProperty | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -226,7 +226,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e | ||||||
| 		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 | ||||||
| 	status.GTSAccount = statusOwner | 	status.GTSAuthorAccount = statusOwner | ||||||
| 
 | 
 | ||||||
| 	// check if there's a post that this is a reply to | 	// check if there's a post that this is a reply to | ||||||
| 	inReplyToURI, err := extractInReplyToURI(statusable) | 	inReplyToURI, err := extractInReplyToURI(statusable) | ||||||
|  | @ -380,6 +380,48 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e | ||||||
| 	return follow, nil | 	return follow, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) { | ||||||
|  | 	idProp := likeable.GetJSONLDId() | ||||||
|  | 	if idProp == nil || !idProp.IsIRI() { | ||||||
|  | 		return nil, errors.New("no id property set on like, or was not an iri") | ||||||
|  | 	} | ||||||
|  | 	uri := idProp.GetIRI().String() | ||||||
|  | 
 | ||||||
|  | 	origin, err := extractActor(likeable) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("error extracting actor property from like") | ||||||
|  | 	} | ||||||
|  | 	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(likeable) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("error extracting object property from like") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	targetStatus := >smodel.Status{} | ||||||
|  | 	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetStatus); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error extracting status with uri %s from the database: %s", target.String(), err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	targetAccount := >smodel.Account{} | ||||||
|  | 	if err := c.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return >smodel.StatusFave{ | ||||||
|  | 		TargetAccountID:  targetAccount.ID, | ||||||
|  | 		StatusID:         targetStatus.ID, | ||||||
|  | 		AccountID:        originAccount.ID, | ||||||
|  | 		URI:              uri, | ||||||
|  | 		GTSStatus:        targetStatus, | ||||||
|  | 		GTSTargetAccount: targetAccount, | ||||||
|  | 		GTSFavingAccount: originAccount, | ||||||
|  | 	}, 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") { | ||||||
|  |  | ||||||
|  | @ -44,45 +44,36 @@ type TypeConverter interface { | ||||||
| 	// if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, | 	// if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, | ||||||
| 	// so serve it only to an authorized user who should have permission to see it. | 	// so serve it only to an authorized user who should have permission to see it. | ||||||
| 	AccountToMastoSensitive(account *gtsmodel.Account) (*model.Account, error) | 	AccountToMastoSensitive(account *gtsmodel.Account) (*model.Account, error) | ||||||
| 
 |  | ||||||
| 	// AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error | 	// AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error | ||||||
| 	// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. | 	// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. | ||||||
| 	// In other words, this is the public record that the server has of an account. | 	// In other words, this is the public record that the server has of an account. | ||||||
| 	AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) | 	AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) | ||||||
| 
 |  | ||||||
| 	// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error | 	// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error | ||||||
| 	// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields | 	// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields | ||||||
| 	// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. | 	// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. | ||||||
| 	AppToMastoSensitive(application *gtsmodel.Application) (*model.Application, error) | 	AppToMastoSensitive(application *gtsmodel.Application) (*model.Application, error) | ||||||
| 
 |  | ||||||
| 	// AppToMastoPublic takes a db model application as a param, and returns a populated mastotype application, or an error | 	// AppToMastoPublic takes a db model application as a param, and returns a populated mastotype application, or an error | ||||||
| 	// if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive | 	// if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive | ||||||
| 	// fields sanitized so that it can be served to non-authorized accounts without revealing any private information. | 	// fields sanitized so that it can be served to non-authorized accounts without revealing any private information. | ||||||
| 	AppToMastoPublic(application *gtsmodel.Application) (*model.Application, error) | 	AppToMastoPublic(application *gtsmodel.Application) (*model.Application, error) | ||||||
| 
 |  | ||||||
| 	// AttachmentToMasto converts a gts model media attacahment into its mastodon representation for serialization on the API. | 	// AttachmentToMasto converts a gts model media attacahment into its mastodon representation for serialization on the API. | ||||||
| 	AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (model.Attachment, error) | 	AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (model.Attachment, error) | ||||||
| 
 |  | ||||||
| 	// MentionToMasto converts a gts model mention into its mastodon (frontend) representation for serialization on the API. | 	// MentionToMasto converts a gts model mention into its mastodon (frontend) representation for serialization on the API. | ||||||
| 	MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) | 	MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) | ||||||
| 
 |  | ||||||
| 	// EmojiToMasto converts a gts model emoji into its mastodon (frontend) representation for serialization on the API. | 	// EmojiToMasto converts a gts model emoji into its mastodon (frontend) representation for serialization on the API. | ||||||
| 	EmojiToMasto(e *gtsmodel.Emoji) (model.Emoji, error) | 	EmojiToMasto(e *gtsmodel.Emoji) (model.Emoji, error) | ||||||
| 
 |  | ||||||
| 	// TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API. | 	// TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API. | ||||||
| 	TagToMasto(t *gtsmodel.Tag) (model.Tag, error) | 	TagToMasto(t *gtsmodel.Tag) (model.Tag, error) | ||||||
| 
 |  | ||||||
| 	// StatusToMasto converts a gts model status into its mastodon (frontend) representation for serialization on the API. | 	// StatusToMasto converts a gts model status into its mastodon (frontend) representation for serialization on the API. | ||||||
| 	StatusToMasto(s *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*model.Status, error) | 	StatusToMasto(s *gtsmodel.Status, statusAuthor *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*model.Status, error) | ||||||
| 
 |  | ||||||
| 	// VisToMasto converts a gts visibility into its mastodon equivalent | 	// VisToMasto converts a gts visibility into its mastodon equivalent | ||||||
| 	VisToMasto(m gtsmodel.Visibility) model.Visibility | 	VisToMasto(m gtsmodel.Visibility) model.Visibility | ||||||
| 
 |  | ||||||
| 	// 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 converts a gts relationship into its mastodon equivalent for serving in various places | ||||||
| 	RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) | 	RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) | ||||||
|  | 	// NotificationToMasto converts a gts notification into a mastodon notification | ||||||
|  | 	NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL | 		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL | ||||||
|  | @ -107,6 +98,8 @@ type TypeConverter interface { | ||||||
| 	ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) | 	ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) | ||||||
| 	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow. | 	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow. | ||||||
| 	ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) | 	ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) | ||||||
|  | 	// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave. | ||||||
|  | 	ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) | ||||||
| 
 | 
 | ||||||
| 	/* | 	/* | ||||||
| 		INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL | 		INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL | ||||||
|  | @ -114,21 +107,25 @@ type TypeConverter interface { | ||||||
| 
 | 
 | ||||||
| 	// AccountToAS converts a gts model account into an activity streams person, suitable for federation | 	// AccountToAS converts a gts model account into an activity streams person, suitable for federation | ||||||
| 	AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) | 	AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) | ||||||
| 
 |  | ||||||
| 	// 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 | 	// 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) | 	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 converts a gts model mention into an activity streams Mention, suitable for federation | ||||||
| 	MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) | 	MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) | ||||||
| 
 |  | ||||||
| 	// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation | 	// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation | ||||||
| 	AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) | 	AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) | ||||||
| 
 |  | ||||||
| 	// FaveToAS converts a gts model status fave into an activityStreams LIKE, suitable for federation. | 	// FaveToAS converts a gts model status fave into an activityStreams LIKE, suitable for federation. | ||||||
| 	FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) | 	FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		INTERNAL (gts) MODEL TO INTERNAL MODEL | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	// FollowRequestToFollow just converts a follow request into a follow, that's it! No bells and whistles. | ||||||
|  | 	FollowRequestToFollow(f *gtsmodel.FollowRequest) *gtsmodel.Follow | ||||||
|  | 	// StatusToBoost wraps the given status into a boosting status. | ||||||
|  | 	StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.Account) (*gtsmodel.Status, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type converter struct { | type converter struct { | ||||||
|  |  | ||||||
							
								
								
									
										76
									
								
								internal/typeutils/internal.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/typeutils/internal.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | ||||||
|  | package typeutils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (c *converter) FollowRequestToFollow(f *gtsmodel.FollowRequest) *gtsmodel.Follow { | ||||||
|  | 	return >smodel.Follow{ | ||||||
|  | 		ID:              f.ID, | ||||||
|  | 		CreatedAt:       f.CreatedAt, | ||||||
|  | 		UpdatedAt:       f.UpdatedAt, | ||||||
|  | 		AccountID:       f.AccountID, | ||||||
|  | 		TargetAccountID: f.TargetAccountID, | ||||||
|  | 		ShowReblogs:     f.ShowReblogs, | ||||||
|  | 		URI:             f.URI, | ||||||
|  | 		Notify:          f.Notify, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.Account) (*gtsmodel.Status, error) { | ||||||
|  | 	// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs | ||||||
|  | 	uris := util.GenerateURIsForAccount(boostingAccount.Username, c.config.Protocol, c.config.Host) | ||||||
|  | 	boostWrapperStatusID := uuid.NewString() | ||||||
|  | 	boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID) | ||||||
|  | 	boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID) | ||||||
|  | 
 | ||||||
|  | 	local := true | ||||||
|  | 	if boostingAccount.Domain != "" { | ||||||
|  | 		local = false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	boostWrapperStatus := >smodel.Status{ | ||||||
|  | 		ID:  boostWrapperStatusID, | ||||||
|  | 		URI: boostWrapperStatusURI, | ||||||
|  | 		URL: boostWrapperStatusURL, | ||||||
|  | 
 | ||||||
|  | 		// the boosted status is not created now, but the boost certainly is | ||||||
|  | 		CreatedAt:                time.Now(), | ||||||
|  | 		UpdatedAt:                time.Now(), | ||||||
|  | 		Local:                    local, | ||||||
|  | 		AccountID:                boostingAccount.ID, | ||||||
|  | 
 | ||||||
|  | 		// replies can be boosted, but boosts are never replies | ||||||
|  | 		InReplyToID:        "", | ||||||
|  | 		InReplyToAccountID: "", | ||||||
|  | 
 | ||||||
|  | 		// these will all be wrapped in the boosted status so set them empty here | ||||||
|  | 		Attachments: []string{}, | ||||||
|  | 		Tags:        []string{}, | ||||||
|  | 		Mentions:    []string{}, | ||||||
|  | 		Emojis:      []string{}, | ||||||
|  | 
 | ||||||
|  | 		// the below fields will be taken from the target status | ||||||
|  | 		Content:             util.HTMLFormat(s.Content), | ||||||
|  | 		ContentWarning:      s.ContentWarning, | ||||||
|  | 		ActivityStreamsType: s.ActivityStreamsType, | ||||||
|  | 		Sensitive:           s.Sensitive, | ||||||
|  | 		Language:            s.Language, | ||||||
|  | 		Text:                s.Text, | ||||||
|  | 		BoostOfID:           s.ID, | ||||||
|  | 		Visibility:          s.Visibility, | ||||||
|  | 		VisibilityAdvanced:  s.VisibilityAdvanced, | ||||||
|  | 
 | ||||||
|  | 		// attach these here for convenience -- the boosted status/account won't go in the DB | ||||||
|  | 		// but they're needed in the processor and for the frontend. Since we have them, we can | ||||||
|  | 		// attach them so we don't need to fetch them again later (save some DB calls) | ||||||
|  | 		GTSBoostedStatus: s, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return boostWrapperStatus, nil | ||||||
|  | } | ||||||
|  | @ -262,12 +262,12 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e | ||||||
| 
 | 
 | ||||||
| 	// check if author account is already attached to status and attach it if not | 	// 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 we can't retrieve this, bail here already because we can't attribute the status to anyone | ||||||
| 	if s.GTSAccount == nil { | 	if s.GTSAuthorAccount == nil { | ||||||
| 		a := >smodel.Account{} | 		a := >smodel.Account{} | ||||||
| 		if err := c.db.GetByID(s.AccountID, a); err != nil { | 		if err := c.db.GetByID(s.AccountID, a); err != nil { | ||||||
| 			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) | 			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) | ||||||
| 		} | 		} | ||||||
| 		s.GTSAccount = a | 		s.GTSAuthorAccount = a | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// create the Note! | 	// create the Note! | ||||||
|  | @ -328,9 +328,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// attributedTo | 	// attributedTo | ||||||
| 	authorAccountURI, err := url.Parse(s.GTSAccount.URI) | 	authorAccountURI, err := url.Parse(s.GTSAuthorAccount.URI) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.URI, err) | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.URI, err) | ||||||
| 	} | 	} | ||||||
| 	attributedToProp := streams.NewActivityStreamsAttributedToProperty() | 	attributedToProp := streams.NewActivityStreamsAttributedToProperty() | ||||||
| 	attributedToProp.AppendIRI(authorAccountURI) | 	attributedToProp.AppendIRI(authorAccountURI) | ||||||
|  | @ -357,9 +357,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e | ||||||
| 	status.SetActivityStreamsTag(tagProp) | 	status.SetActivityStreamsTag(tagProp) | ||||||
| 
 | 
 | ||||||
| 	// parse out some URIs we need here | 	// parse out some URIs we need here | ||||||
| 	authorFollowersURI, err := url.Parse(s.GTSAccount.FollowersURI) | 	authorFollowersURI, err := url.Parse(s.GTSAuthorAccount.FollowersURI) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.FollowersURI, err) | 		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.FollowersURI, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	publicURI, err := url.Parse(asPublicURI) | 	publicURI, err := url.Parse(asPublicURI) | ||||||
|  |  | ||||||
|  | @ -138,6 +138,9 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e | ||||||
| 		fields = append(fields, mField) | 		fields = append(fields, mField) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	emojis := []model.Emoji{} | ||||||
|  | 	// TODO: account emojis | ||||||
|  | 
 | ||||||
| 	var acct string | 	var acct string | ||||||
| 	if a.Domain != "" { | 	if a.Domain != "" { | ||||||
| 		// this is a remote user | 		// this is a remote user | ||||||
|  | @ -165,7 +168,7 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e | ||||||
| 		FollowingCount: followingCount, | 		FollowingCount: followingCount, | ||||||
| 		StatusesCount:  statusesCount, | 		StatusesCount:  statusesCount, | ||||||
| 		LastStatusAt:   lastStatusAt, | 		LastStatusAt:   lastStatusAt, | ||||||
| 		Emojis:         nil, // TODO: implement this | 		Emojis:         emojis, // TODO: implement this | ||||||
| 		Fields:         fields, | 		Fields:         fields, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  | @ -267,7 +270,7 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) { | ||||||
| 
 | 
 | ||||||
| func (c *converter) StatusToMasto( | func (c *converter) StatusToMasto( | ||||||
| 	s *gtsmodel.Status, | 	s *gtsmodel.Status, | ||||||
| 	targetAccount *gtsmodel.Account, | 	statusAuthor *gtsmodel.Account, | ||||||
| 	requestingAccount *gtsmodel.Account, | 	requestingAccount *gtsmodel.Account, | ||||||
| 	boostOfAccount *gtsmodel.Account, | 	boostOfAccount *gtsmodel.Account, | ||||||
| 	replyToAccount *gtsmodel.Account, | 	replyToAccount *gtsmodel.Account, | ||||||
|  | @ -379,7 +382,7 @@ func (c *converter) StatusToMasto( | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mastoTargetAccount, err := c.AccountToMastoPublic(targetAccount) | 	mastoAuthorAccount, err := c.AccountToMastoPublic(statusAuthor) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error parsing account of status author: %s", err) | 		return nil, fmt.Errorf("error parsing account of status author: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -517,7 +520,7 @@ func (c *converter) StatusToMasto( | ||||||
| 		Content:            s.Content, | 		Content:            s.Content, | ||||||
| 		Reblog:             mastoRebloggedStatus, | 		Reblog:             mastoRebloggedStatus, | ||||||
| 		Application:        mastoApplication, | 		Application:        mastoApplication, | ||||||
| 		Account:            mastoTargetAccount, | 		Account:            mastoAuthorAccount, | ||||||
| 		MediaAttachments:   mastoAttachments, | 		MediaAttachments:   mastoAttachments, | ||||||
| 		Mentions:           mastoMentions, | 		Mentions:           mastoMentions, | ||||||
| 		Tags:               mastoTags, | 		Tags:               mastoTags, | ||||||
|  | @ -594,3 +597,68 @@ func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relati | ||||||
| 		Note:                r.Note, | 		Note:                r.Note, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) { | ||||||
|  | 
 | ||||||
|  | 	if n.GTSTargetAccount == nil { | ||||||
|  | 		tAccount := >smodel.Account{} | ||||||
|  | 		if err := c.db.GetByID(n.TargetAccountID, tAccount); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("NotificationToMasto: error getting target account with id %s from the db: %s", n.TargetAccountID, err) | ||||||
|  | 		} | ||||||
|  | 		n.GTSTargetAccount = tAccount | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if n.GTSOriginAccount == nil { | ||||||
|  | 		ogAccount := >smodel.Account{} | ||||||
|  | 		if err := c.db.GetByID(n.OriginAccountID, ogAccount); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("NotificationToMasto: error getting origin account with id %s from the db: %s", n.OriginAccountID, err) | ||||||
|  | 		} | ||||||
|  | 		n.GTSOriginAccount = ogAccount | ||||||
|  | 	} | ||||||
|  | 	mastoAccount, err := c.AccountToMastoPublic(n.GTSOriginAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("NotificationToMasto: error converting account to masto: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var mastoStatus *model.Status | ||||||
|  | 	if n.StatusID != "" { | ||||||
|  | 		if n.GTSStatus == nil { | ||||||
|  | 			status := >smodel.Status{} | ||||||
|  | 			if err := c.db.GetByID(n.StatusID, status); err != nil { | ||||||
|  | 				return nil, fmt.Errorf("NotificationToMasto: error getting status with id %s from the db: %s", n.StatusID, err) | ||||||
|  | 			} | ||||||
|  | 			n.GTSStatus = status | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var replyToAccount *gtsmodel.Account | ||||||
|  | 		if n.GTSStatus.InReplyToAccountID != "" { | ||||||
|  | 			r := >smodel.Account{} | ||||||
|  | 			if err := c.db.GetByID(n.GTSStatus.InReplyToAccountID, r); err != nil { | ||||||
|  | 				return nil, fmt.Errorf("NotificationToMasto: error getting replied to account with id %s from the db: %s", n.GTSStatus.InReplyToAccountID, err) | ||||||
|  | 			} | ||||||
|  | 			replyToAccount = r | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if n.GTSStatus.GTSAuthorAccount == nil { | ||||||
|  | 			if n.GTSStatus.AccountID == n.GTSTargetAccount.ID { | ||||||
|  | 				n.GTSStatus.GTSAuthorAccount = n.GTSTargetAccount | ||||||
|  | 			} else if n.GTSStatus.AccountID == n.GTSOriginAccount.ID { | ||||||
|  | 				n.GTSStatus.GTSAuthorAccount = n.GTSOriginAccount | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var err error | ||||||
|  | 		mastoStatus, err = c.StatusToMasto(n.GTSStatus, n.GTSStatus.GTSAuthorAccount, n.GTSTargetAccount, nil, replyToAccount, nil) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("NotificationToMasto: error converting status to masto: %s", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &model.Notification{ | ||||||
|  | 		ID:        n.ID, | ||||||
|  | 		Type:      string(n.NotificationType), | ||||||
|  | 		CreatedAt: n.CreatedAt.Format(time.RFC3339), | ||||||
|  | 		Account:   mastoAccount, | ||||||
|  | 		Status:    mastoStatus, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ package testrig | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation" | 	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewTestFederatingDB returns a federating DB with the underlying db | // NewTestFederatingDB returns a federating DB with the underlying db | ||||||
| func NewTestFederatingDB(db db.DB) federation.FederatingDB { | func NewTestFederatingDB(db db.DB) federatingdb.DB { | ||||||
| 	return federation.NewFederatingDB(db, NewTestConfig(), NewTestLog()) | 	return federatingdb.New(db, NewTestConfig(), NewTestLog()) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue