diff options
| author | 2021-05-27 16:06:24 +0200 | |
|---|---|---|
| committer | 2021-05-27 16:06:24 +0200 | |
| commit | 40add686913b7eb6edd5a780e37e7513b43a337f (patch) | |
| tree | 75549dff97e5a15f732a505d4d00aa7a686bdad8 | |
| parent | Faves (#31) (diff) | |
| download | gotosocial-40add686913b7eb6edd5a780e37e7513b43a337f.tar.xz | |
Notifications (#34)
Notifications working for:
* Mentions
* Faves
* New follow requests
* New followers
42 files changed, 2096 insertions, 1194 deletions
diff --git a/internal/api/client/notification/notification.go b/internal/api/client/notification/notification.go new file mode 100644 index 000000000..bc06b31e4 --- /dev/null +++ b/internal/api/client/notification/notification.go @@ -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 +} diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go new file mode 100644 index 000000000..3e4970800 --- /dev/null +++ b/internal/api/client/notification/notificationsget.go @@ -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) +} diff --git a/internal/api/model/notification.go b/internal/api/model/notification.go index c8d080e2a..2163251b4 100644 --- a/internal/api/model/notification.go +++ b/internal/api/model/notification.go @@ -41,5 +41,5 @@ type Notification struct {  	// OPTIONAL  	// 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"`  } diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 2456d1a8f..963ef4f86 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -31,7 +31,7 @@ type Status struct {  	// Is this status marked as sensitive content?  	Sensitive bool `json:"sensitive"`  	// 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 Visibility `json:"visibility"`  	// Primary language of this status. (ISO 639 Part 1 two-letter language code) diff --git a/internal/db/db.go b/internal/db/db.go index 9ad811580..e71484a6d 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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.  	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  	*/ diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 7f65055d6..9b6c7a114 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -1138,6 +1138,35 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str  	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  */ diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go deleted file mode 100644 index 6ae4dc083..000000000 --- a/internal/federation/federating_db.go +++ /dev/null @@ -1,1052 +0,0 @@ -/* -   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 federation - -import ( -	"context" -	"encoding/json" -	"errors" -	"fmt" -	"net/url" -	"sync" - -	"github.com/go-fed/activity/pub" -	"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/config" -	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/typeutils" -	"github.com/superseriousbusiness/gotosocial/internal/util" -) - -type FederatingDB interface { -	pub.Database -	Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error -	Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error -} - -// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface. -// 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 -} - -// NewFederatingDB returns a FederatingDB interface using the given database, config, and logger. -func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) FederatingDB { -	return &federatingDB{ -		locks:         new(sync.Map), -		db:            db, -		config:        config, -		log:           log, -		typeConverter: typeutils.NewConverter(config, db), -	} -} - -/* -   GO-FED DB INTERFACE-IMPLEMENTING FUNCTIONS -*/ - -// 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 -} - -// 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 - -	// if err := f.db.GetByID(statusID, >smodel.Status{}); err != nil { -	// 	if _, ok := err.(db.ErrNoEntries); ok { -	// 		// we don't have it -	// 		return false, nil -	// 	} -	// 	// actual error -	// 	return false, fmt.Errorf("error getting status from db: %s", err) -	// } - -	// // we must have it -	// return true, 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 -} - -// 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()) -} - -// 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) -} - -// 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) -} - -// 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 -} - -// 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) -	} - -	return nil, nil -} - -// 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) -		} - -		if !targetAcct.Locked { -			if _, err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { -				return fmt.Errorf("database error accepting follow request: %s", err) -			} -		} -	} -	return nil -} - -// 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 -} - -// 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 -} - -// 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 -} - -// 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())) -} - -// 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 -} - -// 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 -} - -// 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 -} - -/* -	CUSTOM FUNCTIONALITY FOR GTS -*/ - -func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error { -	l := f.log.WithFields( -		logrus.Fields{ -			"func":   "Undo", -			"asType": undo.GetTypeName(), -		}, -	) -	m, err := streams.Serialize(undo) -	if err != nil { -		return err -	} -	b, err := json.Marshal(m) -	if err != nil { -		return err -	} -	l.Debugf("received UNDO asType %s", string(b)) - -	targetAcctI := ctx.Value(util.APAccount) -	if targetAcctI == nil { -		l.Error("UNDO: target account wasn't set on context") -		return nil -	} -	targetAcct, ok := targetAcctI.(*gtsmodel.Account) -	if !ok { -		l.Error("UNDO: target account was set on context but couldn't be parsed") -		return nil -	} - -	undoObject := undo.GetActivityStreamsObject() -	if undoObject == nil { -		return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo") -	} - -	for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() { -		switch iter.GetType().GetTypeName() { -		case string(gtsmodel.ActivityStreamsFollow): -			// UNDO FOLLOW -			ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) -			if !ok { -				return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow") -			} -			// make sure the actor owns the follow -			if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) { -				return errors.New("UNDO: follow actor and activity actor not the same") -			} -			// convert the follow to something we can understand -			gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow) -			if err != nil { -				return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err) -			} -			// make sure the addressee of the original follow is the same as whatever inbox this landed in -			if gtsFollow.TargetAccountID != targetAcct.ID { -				return errors.New("UNDO: follow object account and inbox account were not the same") -			} -			// delete any existing FOLLOW -			if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.Follow{}); err != nil { -				return fmt.Errorf("UNDO: db error removing follow: %s", err) -			} -			// delete any existing FOLLOW REQUEST -			if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.FollowRequest{}); err != nil { -				return fmt.Errorf("UNDO: db error removing follow request: %s", err) -			} -			l.Debug("follow undone") -			return nil -		case string(gtsmodel.ActivityStreamsLike): -			// UNDO LIKE -		case string(gtsmodel.ActivityStreamsAnnounce): -			// UNDO BOOST/REBLOG/ANNOUNCE -		} -	} - -	return nil -} - -func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error { -	l := f.log.WithFields( -		logrus.Fields{ -			"func":   "Accept", -			"asType": accept.GetTypeName(), -		}, -	) -	m, err := streams.Serialize(accept) -	if err != nil { -		return err -	} -	b, err := json.Marshal(m) -	if err != nil { -		return err -	} -	l.Debugf("received ACCEPT asType %s", string(b)) - -	inboxAcctI := ctx.Value(util.APAccount) -	if inboxAcctI == nil { -		l.Error("ACCEPT: inbox account wasn't set on context") -		return nil -	} -	inboxAcct, ok := inboxAcctI.(*gtsmodel.Account) -	if !ok { -		l.Error("ACCEPT: inbox account was set on context but couldn't be parsed") -		return nil -	} - -	acceptObject := accept.GetActivityStreamsObject() -	if acceptObject == nil { -		return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo") -	} - -	for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() { -		switch iter.GetType().GetTypeName() { -		case string(gtsmodel.ActivityStreamsFollow): -			// ACCEPT FOLLOW -			asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) -			if !ok { -				return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow") -			} -			// convert the follow to something we can understand -			gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow) -			if err != nil { -				return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err) -			} -			// make sure the addressee of the original follow is the same as whatever inbox this landed in -			if gtsFollow.AccountID != inboxAcct.ID { -				return errors.New("ACCEPT: follow object account and inbox account were not the same") -			} -			_, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID) -			return err -		} -	} - -	return nil -} diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go new file mode 100644 index 000000000..d08544c99 --- /dev/null +++ b/internal/federation/federatingdb/accept.go @@ -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 +} diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go new file mode 100644 index 000000000..026674e43 --- /dev/null +++ b/internal/federation/federatingdb/create.go @@ -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 +} diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go new file mode 100644 index 000000000..b5207fc34 --- /dev/null +++ b/internal/federation/federatingdb/db.go @@ -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), +	} +} diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go new file mode 100644 index 000000000..fe32434c4 --- /dev/null +++ b/internal/federation/federatingdb/delete.go @@ -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 +} diff --git a/internal/federation/federatingdb/exists.go b/internal/federation/federatingdb/exists.go new file mode 100644 index 000000000..b5c10b895 --- /dev/null +++ b/internal/federation/federatingdb/exists.go @@ -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 +} diff --git a/internal/federation/federatingdb/followers.go b/internal/federation/federatingdb/followers.go new file mode 100644 index 000000000..28f3bb6d1 --- /dev/null +++ b/internal/federation/federatingdb/followers.go @@ -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 +} diff --git a/internal/federation/federatingdb/following.go b/internal/federation/federatingdb/following.go new file mode 100644 index 000000000..342250880 --- /dev/null +++ b/internal/federation/federatingdb/following.go @@ -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 +} diff --git a/internal/federation/federatingdb/get.go b/internal/federation/federatingdb/get.go new file mode 100644 index 000000000..77a24bf43 --- /dev/null +++ b/internal/federation/federatingdb/get.go @@ -0,0 +1,83 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package 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") +} diff --git a/internal/federation/federatingdb/inbox.go b/internal/federation/federatingdb/inbox.go new file mode 100644 index 000000000..25ef2cda5 --- /dev/null +++ b/internal/federation/federatingdb/inbox.go @@ -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 +} diff --git a/internal/federation/federatingdb/liked.go b/internal/federation/federatingdb/liked.go new file mode 100644 index 000000000..5645d6f1e --- /dev/null +++ b/internal/federation/federatingdb/liked.go @@ -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 +} diff --git a/internal/federation/federatingdb/lock.go b/internal/federation/federatingdb/lock.go new file mode 100644 index 000000000..417fd79b2 --- /dev/null +++ b/internal/federation/federatingdb/lock.go @@ -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 +} diff --git a/internal/federation/federatingdb/outbox.go b/internal/federation/federatingdb/outbox.go new file mode 100644 index 000000000..1568e0017 --- /dev/null +++ b/internal/federation/federatingdb/outbox.go @@ -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) +} diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go new file mode 100644 index 000000000..fe7160021 --- /dev/null +++ b/internal/federation/federatingdb/owns.go @@ -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()) +} diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go new file mode 100644 index 000000000..8ca681bb2 --- /dev/null +++ b/internal/federation/federatingdb/undo.go @@ -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 +} diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go new file mode 100644 index 000000000..3ac5f265a --- /dev/null +++ b/internal/federation/federatingdb/update.go @@ -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 +} diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go new file mode 100644 index 000000000..be3cff944 --- /dev/null +++ b/internal/federation/federatingdb/util.go @@ -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) +} diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index ab4b5ccbc..39ed49cfb 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -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  // type and extension. The unhandled ones are passed to DefaultCallback.  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{  		// OnFollow determines what action to take for this particular callback  		// 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{}{ -		// override default undo behavior +		// override default undo behavior and trigger our own side effects  		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {  			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 {  			return f.FederatingDB().Accept(ctx, accept)  		}, diff --git a/internal/federation/federator.go b/internal/federation/federator.go index f09a77279..f2f223a65 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -26,6 +26,7 @@ import (  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"  	"github.com/superseriousbusiness/gotosocial/internal/transport"  	"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() pub.FederatingActor  	// 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.  	// 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) @@ -54,7 +55,7 @@ type Federator interface {  type federator struct {  	config              *config.Config  	db                  db.DB -	federatingDB        FederatingDB +	federatingDB        federatingdb.DB  	clock               pub.Clock  	typeConverter       typeutils.TypeConverter  	transportController transport.Controller @@ -63,7 +64,7 @@ type federator struct {  }  // 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{}  	f := &federator{ @@ -84,6 +85,6 @@ func (f *federator) FederatingActor() pub.FederatingActor {  	return f.actor  } -func (f *federator) FederatingDB() FederatingDB { +func (f *federator) FederatingDB() federatingdb.DB {  	return f.federatingDB  } diff --git a/internal/federation/util.go b/internal/federation/util.go index 6ae0152df..837d92759 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -280,22 +280,4 @@ func (f *federator) GetTransportForUser(username string) (transport.Transport, e  	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 -} + diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 8e6c50a61..824754049 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -37,6 +37,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"  	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/timeline"  	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" @@ -45,6 +46,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db/pg"  	"github.com/superseriousbusiness/gotosocial/internal/federation" +	"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/media"  	"github.com/superseriousbusiness/gotosocial/internal/message" @@ -73,6 +75,7 @@ var models []interface{} = []interface{}{  	>smodel.User{},  	>smodel.Emoji{},  	>smodel.Instance{}, +	>smodel.Notification{},  	&oauth.Token{},  	&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)  	} -	federatingDB := federation.NewFederatingDB(dbService, c, log) +	federatingDB := federatingdb.New(dbService, c, log)  	router, err := router.New(c, log)  	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)  	usersModule := user.New(c, processor, log)  	timelineModule := timeline.New(c, processor, log) +	notificationModule := notification.New(c, processor, log)  	mm := mediaModule.New(c, processor, log)  	fileServerModule := fileserver.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,  		usersModule,  		timelineModule, +		notificationModule,  	}  	for _, m := range apis { diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go new file mode 100644 index 000000000..35e0ca173 --- /dev/null +++ b/internal/gtsmodel/notification.go @@ -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" +) diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index b5ac8def1..16c00ca73 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -80,7 +80,7 @@ type Status struct {  	*/  	// Account that created this status -	GTSAccount *Account `pg:"-"` +	GTSAuthorAccount *Account `pg:"-"`  	// Mentions created in this status  	GTSMentions []*Mention `pg:"-"`  	// Hashtags used in this status diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go index b0112152b..12e4bd3c0 100644 --- a/internal/message/fromclientapiprocess.go +++ b/internal/message/fromclientapiprocess.go @@ -49,17 +49,17 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  			}  			return nil  		case gtsmodel.ActivityStreamsFollow: -			// CREATE FOLLOW (request) -			follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) +			// CREATE FOLLOW REQUEST +			followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)  			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 p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) +			return p.federateFollow(followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount)  		case gtsmodel.ActivityStreamsLike:  			// CREATE LIKE/FAVE  			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")  			} -			if err := p.notifyFave(fave); err != nil { +			if err := p.notifyFave(fave, clientMsg.TargetAccount); err != nil {  				return err  			} @@ -84,6 +84,11 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  			if !ok {  				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)  		}  	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)  	} -	outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI) +	outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)  	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)  	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 originAccount.Domain == "" && targetAccount.Domain == "" {  		return nil  	} +	follow := p.tc.FollowRequestToFollow(followRequest) +  	asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)  	if err != nil {  		return fmt.Errorf("federateFollow: error converting follow to as format: %s", err) diff --git a/internal/message/fromcommonprocess.go b/internal/message/fromcommonprocess.go index 2403a8b72..73d58f1d1 100644 --- a/internal/message/fromcommonprocess.go +++ b/internal/message/fromcommonprocess.go @@ -18,16 +18,143 @@  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 { +	// 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 +} + +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  } -func (p *processor) notifyFollow(follow *gtsmodel.Follow) 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) error { -   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  } diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go index d3ebce400..10dbfcf6e 100644 --- a/internal/message/fromfederatorprocess.go +++ b/internal/message/fromfederatorprocess.go @@ -74,6 +74,26 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  			if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {  				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:  		// UPDATE @@ -106,6 +126,20 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  			// DELETE A PROFILE/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 @@ -215,8 +249,8 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {  		}  		m.StatusID = status.ID -		m.OriginAccountID = status.GTSAccount.ID -		m.OriginAccountURI = status.GTSAccount.URI +		m.OriginAccountID = status.GTSAuthorAccount.ID +		m.OriginAccountURI = status.GTSAuthorAccount.URI  		targetAccount := >smodel.Account{}  		if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil { diff --git a/internal/message/notificationsprocess.go b/internal/message/notificationsprocess.go new file mode 100644 index 000000000..64726b75f --- /dev/null +++ b/internal/message/notificationsprocess.go @@ -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 +} diff --git a/internal/message/processor.go b/internal/message/processor.go index bcd64d47a..49a4f6f05 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -106,6 +106,9 @@ type Processor interface {  	// 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) +	// 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(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. diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go index 6786b2dab..f64c35948 100644 --- a/internal/message/statusprocess.go +++ b/internal/message/statusprocess.go @@ -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 -	// first we create a new status and add some basic info to it -- this will be the wrapper for the boosted status - -	// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs -	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, err := p.tc.StatusToBoost(targetStatus, authed.Account) +	if err != nil { +		return nil, NewErrorInternalError(err)  	} +	boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID +	boostWrapperStatus.GTSBoostedAccount = targetAccount +  	// put the boost in the database  	if err := p.db.Put(boostWrapperStatus); err != nil {  		return nil, NewErrorInternalError(err) diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go index c31a37a25..eea7fd7d9 100644 --- a/internal/typeutils/asinterfaces.go +++ b/internal/typeutils/asinterfaces.go @@ -102,6 +102,15 @@ type Followable interface {  	withObject  } +// Likeable represents the minimum interface for an activitystreams 'like' activity. +type Likeable interface { +	withJSONLDId +	withTypeName + +	withActor +	withObject +} +  type withJSONLDId interface {  	GetJSONLDId() vocab.JSONLDIdProperty  } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index dcc2674cd..0fee13b13 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -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)  	}  	status.AccountID = statusOwner.ID -	status.GTSAccount = statusOwner +	status.GTSAuthorAccount = statusOwner  	// check if there's a post that this is a reply to  	inReplyToURI, err := extractInReplyToURI(statusable) @@ -380,6 +380,48 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e  	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 {  	for _, entry := range tos {  		if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 3ced20926..cf94faf2e 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -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,  	// so serve it only to an authorized user who should have permission to see it.  	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  	// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.  	// In other words, this is the public record that the server has of an account.  	AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) -  	// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error  	// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields  	// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.  	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  	// 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.  	AppToMastoPublic(application *gtsmodel.Application) (*model.Application, error) -  	// AttachmentToMasto converts a gts model media attacahment into its mastodon representation for serialization on the API.  	AttachmentToMasto(attachment *gtsmodel.MediaAttachment) (model.Attachment, error) -  	// MentionToMasto converts a gts model mention into its mastodon (frontend) representation for serialization on the API.  	MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) -  	// EmojiToMasto converts a gts model emoji into its mastodon (frontend) representation for serialization on the API.  	EmojiToMasto(e *gtsmodel.Emoji) (model.Emoji, error) -  	// TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API.  	TagToMasto(t *gtsmodel.Tag) (model.Tag, error) -  	// 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(m gtsmodel.Visibility) model.Visibility -  	// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance  	InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) -  	// RelationshipToMasto converts a gts relationship into its mastodon equivalent for serving in various places  	RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) +	// NotificationToMasto converts a gts notification into a mastodon notification +	NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error)  	/*  		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL @@ -107,6 +98,8 @@ type TypeConverter interface {  	ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)  	// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.  	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 @@ -114,21 +107,25 @@ type TypeConverter interface {  	// AccountToAS converts a gts model account into an activity streams person, suitable for federation  	AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) -  	// StatusToAS converts a gts model status into an activity streams note, suitable for federation  	StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) -  	// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation  	FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) -  	// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation  	MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) -  	// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation  	AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) -  	// FaveToAS converts a gts model status fave into an activityStreams LIKE, suitable for federation.  	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 { diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go new file mode 100644 index 000000000..3110b382c --- /dev/null +++ b/internal/typeutils/internal.go @@ -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 +} diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 821720e0d..805f69afb 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -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  	// 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{}  		if err := c.db.GetByID(s.AccountID, a); err != nil {  			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)  		} -		s.GTSAccount = a +		s.GTSAuthorAccount = a  	}  	// create the Note! @@ -328,9 +328,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	}  	// attributedTo -	authorAccountURI, err := url.Parse(s.GTSAccount.URI) +	authorAccountURI, err := url.Parse(s.GTSAuthorAccount.URI)  	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.AppendIRI(authorAccountURI) @@ -357,9 +357,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	status.SetActivityStreamsTag(tagProp)  	// parse out some URIs we need here -	authorFollowersURI, err := url.Parse(s.GTSAccount.FollowersURI) +	authorFollowersURI, err := url.Parse(s.GTSAuthorAccount.FollowersURI)  	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) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 7fbe9eb3f..de3b94e01 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -138,6 +138,9 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		fields = append(fields, mField)  	} +	emojis := []model.Emoji{} +	// TODO: account emojis +  	var acct string  	if a.Domain != "" {  		// this is a remote user @@ -165,7 +168,7 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		FollowingCount: followingCount,  		StatusesCount:  statusesCount,  		LastStatusAt:   lastStatusAt, -		Emojis:         nil, // TODO: implement this +		Emojis:         emojis, // TODO: implement this  		Fields:         fields,  	}, nil  } @@ -267,7 +270,7 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) {  func (c *converter) StatusToMasto(  	s *gtsmodel.Status, -	targetAccount *gtsmodel.Account, +	statusAuthor *gtsmodel.Account,  	requestingAccount *gtsmodel.Account,  	boostOfAccount *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 {  		return nil, fmt.Errorf("error parsing account of status author: %s", err)  	} @@ -517,7 +520,7 @@ func (c *converter) StatusToMasto(  		Content:            s.Content,  		Reblog:             mastoRebloggedStatus,  		Application:        mastoApplication, -		Account:            mastoTargetAccount, +		Account:            mastoAuthorAccount,  		MediaAttachments:   mastoAttachments,  		Mentions:           mastoMentions,  		Tags:               mastoTags, @@ -594,3 +597,68 @@ func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relati  		Note:                r.Note,  	}, 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 +} diff --git a/testrig/federatingdb.go b/testrig/federatingdb.go index 5cce24752..6e099fac9 100644 --- a/testrig/federatingdb.go +++ b/testrig/federatingdb.go @@ -2,10 +2,10 @@ package testrig  import (  	"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 -func NewTestFederatingDB(db db.DB) federation.FederatingDB { -	return federation.NewFederatingDB(db, NewTestConfig(), NewTestLog()) +func NewTestFederatingDB(db db.DB) federatingdb.DB { +	return federatingdb.New(db, NewTestConfig(), NewTestLog())  }  | 
