diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/db/db.go | 2 | ||||
| -rw-r--r-- | internal/db/pg/pg.go | 52 | ||||
| -rw-r--r-- | internal/federation/federating_db.go | 17 | ||||
| -rw-r--r-- | internal/gtsmodel/statusfave.go | 10 | ||||
| -rw-r--r-- | internal/message/accountprocess.go | 7 | ||||
| -rw-r--r-- | internal/message/fromclientapiprocess.go | 34 | ||||
| -rw-r--r-- | internal/message/fromcommonprocess.go | 4 | ||||
| -rw-r--r-- | internal/message/statusprocess.go | 51 | ||||
| -rw-r--r-- | internal/typeutils/converter.go | 3 | ||||
| -rw-r--r-- | internal/typeutils/internaltoas.go | 81 | ||||
| -rw-r--r-- | internal/util/regexes.go | 13 | ||||
| -rw-r--r-- | internal/util/uri.go | 29 | 
12 files changed, 253 insertions, 50 deletions
diff --git a/internal/db/db.go b/internal/db/db.go index e43318c58..9ad811580 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -270,7 +270,7 @@ type DB interface {  	// FaveStatus faves the given status, using accountID as the faver.  	// The returned fave will be nil if the status was already faved. -	FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) +	// FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error)  	// UnfaveStatus unfaves the given status, using accountID as the unfaver (sure, that's a word).  	// The returned fave will be nil if the status was already not faved. diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 01dc71434..7f65055d6 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -1037,32 +1037,32 @@ func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID  	return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()  } -func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { -	// first check if a fave already exists, we can just return if so -	existingFave := >smodel.StatusFave{} -	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() -	if err == nil { -		// fave already exists so just return nothing at all -		return nil, nil -	} - -	// an error occurred so it might exist or not, we don't know -	if err != pg.ErrNoRows { -		return nil, err -	} - -	// it doesn't exist so create it -	newFave := >smodel.StatusFave{ -		AccountID:       accountID, -		TargetAccountID: status.AccountID, -		StatusID:        status.ID, -	} -	if _, err = ps.conn.Model(newFave).Insert(); err != nil { -		return nil, err -	} - -	return newFave, nil -} +// func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { +// 	// first check if a fave already exists, we can just return if so +// 	existingFave := >smodel.StatusFave{} +// 	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() +// 	if err == nil { +// 		// fave already exists so just return nothing at all +// 		return nil, nil +// 	} + +// 	// an error occurred so it might exist or not, we don't know +// 	if err != pg.ErrNoRows { +// 		return nil, err +// 	} + +// 	// it doesn't exist so create it +// 	newFave := >smodel.StatusFave{ +// 		AccountID:       accountID, +// 		TargetAccountID: status.AccountID, +// 		StatusID:        status.ID, +// 	} +// 	if _, err = ps.conn.Model(newFave).Insert(); err != nil { +// 		return nil, err +// 	} + +// 	return newFave, nil +// }  func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {  	// if a fave doesn't exist, we don't need to do anything diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index af685904a..6ae4dc083 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -777,7 +777,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err  				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)) +						return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host, uuid.NewString()))  					}  				}  			} @@ -787,7 +787,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err  		// ID might already be set on a note we've created, so check it here and return it if it is  		note, ok := t.(vocab.ActivityStreamsNote)  		if !ok { -			return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote") +			return nil, errors.New("newid: note couldn't be parsed into vocab.ActivityStreamsNote")  		}  		idProp := note.GetJSONLDId()  		if idProp != nil { @@ -795,6 +795,19 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err  				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 diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 9fb92b931..efbc37e17 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -32,7 +32,13 @@ type StatusFave struct {  	TargetAccountID string `pg:",notnull"`  	// database id of the status that has been 'faved'  	StatusID string `pg:",notnull"` +	// ActivityPub URI of this fave +	URI string `pg:",notnull"` -	// FavedStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. -	FavedStatus *Status `pg:"-"` +	// GTSStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. +	GTSStatus *Status `pg:"-"` +	// GTSTargetAccount is the account being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. +	GTSTargetAccount *Account `pg:"-"` +	// GTSFavingAccount is the account doing the faving. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. +	GTSFavingAccount *Account `pg:"-"`  } diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index 424081c34..22542f0c3 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -22,6 +22,7 @@ import (  	"errors"  	"fmt" +	"github.com/google/uuid"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -417,11 +418,15 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou  	}  	// make the follow request + +	newFollowID := uuid.NewString() +  	fr := >smodel.FollowRequest{ +		ID:              newFollowID,  		AccountID:       authed.Account.ID,  		TargetAccountID: form.TargetAccountID,  		ShowReblogs:     true, -		URI:             util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host), +		URI:             util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),  		Notify:          false,  	}  	if form.Reblogs != nil { diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go index e91bd6ce4..b0112152b 100644 --- a/internal/message/fromclientapiprocess.go +++ b/internal/message/fromclientapiprocess.go @@ -44,7 +44,7 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  				return err  			} -			if status.VisibilityAdvanced.Federated { +			if status.VisibilityAdvanced != nil && status.VisibilityAdvanced.Federated {  				return p.federateStatus(status)  			}  			return nil @@ -60,6 +60,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  			}  			return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) +		case gtsmodel.ActivityStreamsLike: +			// CREATE LIKE/FAVE +			fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) +			if !ok { +				return errors.New("fave was not parseable as *gtsmodel.StatusFave") +			} + +			if err := p.notifyFave(fave); err != nil { +				return err +			} + +			return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)  		}  	case gtsmodel.ActivityStreamsUpdate:  		// UPDATE @@ -214,3 +226,23 @@ func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originA  	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept)  	return err  } + +func (p *processor) federateFave(fave *gtsmodel.StatusFave, 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 +	} + +	// create the AS fave +	asFave, err := p.tc.FaveToAS(fave) +	if err != nil { +		return fmt.Errorf("federateFave: error converting fave to as format: %s", err) +	} + +	outboxIRI, err := url.Parse(originAccount.OutboxURI) +	if err != nil { +		return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) +	} +	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFave) +	return err +} diff --git a/internal/message/fromcommonprocess.go b/internal/message/fromcommonprocess.go index 486da39af..2403a8b72 100644 --- a/internal/message/fromcommonprocess.go +++ b/internal/message/fromcommonprocess.go @@ -27,3 +27,7 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {  func (p *processor) notifyFollow(follow *gtsmodel.Follow) error {  	return nil  } + +func (p *processor) notifyFave(fave *gtsmodel.StatusFave) error { +   return nil +} diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go index 86a07eb4f..6786b2dab 100644 --- a/internal/message/statusprocess.go +++ b/internal/message/statusprocess.go @@ -25,6 +25,7 @@ import (  	"github.com/google/uuid"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/oauth"  	"github.com/superseriousbusiness/gotosocial/internal/util" @@ -168,6 +169,14 @@ func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apim  		return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)  	} +	var boostOfStatus *gtsmodel.Status +	if targetStatus.BoostOfID != "" { +		boostOfStatus = >smodel.Status{} +		if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { +			return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) +		} +	} +  	l.Trace("going to see if status is visible")  	visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that  	if err != nil { @@ -185,20 +194,44 @@ func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apim  		}  	} -	// it's visible! it's faveable! so let's fave the FUCK out of it -	_, err = p.db.FaveStatus(targetStatus, authed.Account.ID) -	if err != nil { -		return nil, fmt.Errorf("error faveing status: %s", err) +	// first check if the status is already faved, if so we don't need to do anything +	newFave := true +	gtsFave := >smodel.Status{} +	if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: authed.Account.ID}}, gtsFave); err == nil { +		// we already have a fave for this status +		newFave = false  	} -	var boostOfStatus *gtsmodel.Status -	if targetStatus.BoostOfID != "" { -		boostOfStatus = >smodel.Status{} -		if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { -			return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) +	if newFave { +		thisFaveID := uuid.NewString() + +		// we need to create a new fave in the database +		gtsFave := >smodel.StatusFave{ +			ID:               thisFaveID, +			AccountID:        authed.Account.ID, +			TargetAccountID:  targetAccount.ID, +			StatusID:         targetStatus.ID, +			URI:              util.GenerateURIForLike(authed.Account.Username, p.config.Protocol, p.config.Host, thisFaveID), +			GTSStatus:        targetStatus, +			GTSTargetAccount: targetAccount, +			GTSFavingAccount: authed.Account, +		} + +		if err := p.db.Put(gtsFave); err != nil { +			return nil, err +		} + +		// send the new fave through the processor channel for federation etc +		p.fromClientAPI <- gtsmodel.FromClientAPI{ +			APObjectType:   gtsmodel.ActivityStreamsLike, +			APActivityType: gtsmodel.ActivityStreamsCreate, +			GTSModel:       gtsFave, +			OriginAccount:  authed.Account, +			TargetAccount:  targetAccount,  		}  	} +	// return the mastodon representation of the target status  	mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)  	if err != nil {  		return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 63e201ded..3ced20926 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -126,6 +126,9 @@ type TypeConverter interface {  	// 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)  }  type converter struct { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index b7056ccbe..821720e0d 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -559,3 +559,84 @@ func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityS  	return doc, nil  } + +/* +	We want to end up with something like this: + +	{ +	"@context": "https://www.w3.org/ns/activitystreams", +	"actor": "https://ondergrond.org/users/dumpsterqueer", +	"id": "https://ondergrond.org/users/dumpsterqueer#likes/44584", +	"object": "https://testingtesting123.xyz/users/gotosocial_test_account/statuses/771aea80-a33d-4d6d-8dfd-57d4d2bfcbd4", +	"type": "Like" +	} +*/ +func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) { +	// check if targetStatus is already pinned to this fave, and fetch it if not +	if f.GTSStatus == nil { +		s := >smodel.Status{} +		if err := c.db.GetByID(f.StatusID, s); err != nil { +			return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err) +		} +		f.GTSStatus = s +	} + +	// check if the targetAccount is already pinned to this fave, and fetch it if not +	if f.GTSTargetAccount == nil { +		a := >smodel.Account{} +		if err := c.db.GetByID(f.TargetAccountID, a); err != nil { +			return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err) +		} +		f.GTSTargetAccount = a +	} + +	// check if the faving account is already pinned to this fave, and fetch it if not +	if f.GTSFavingAccount == nil { +		a := >smodel.Account{} +		if err := c.db.GetByID(f.AccountID, a); err != nil { +			return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err) +		} +		f.GTSFavingAccount = a +	} + +	// create the like +	like := streams.NewActivityStreamsLike() + +	// set the actor property to the fave-ing account's URI +	actorProp := streams.NewActivityStreamsActorProperty() +	actorIRI, err := url.Parse(f.GTSFavingAccount.URI) +	if err != nil { +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSFavingAccount.URI, err) +	} +	actorProp.AppendIRI(actorIRI) +	like.SetActivityStreamsActor(actorProp) + +	// set the ID property to the fave's URI +	idProp := streams.NewJSONLDIdProperty() +	idIRI, err := url.Parse(f.URI) +	if err != nil { +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.URI, err) +	} +	idProp.Set(idIRI) +	like.SetJSONLDId(idProp) + +	// set the object property to the target status's URI +	objectProp := streams.NewActivityStreamsObjectProperty() +	statusIRI, err := url.Parse(f.GTSStatus.URI) +	if err != nil { +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSStatus.URI, err) +	} +	objectProp.AppendIRI(statusIRI) +	like.SetActivityStreamsObject(objectProp) + +	// set the TO property to the target account's IRI +	toProp := streams.NewActivityStreamsToProperty() +	toIRI, err := url.Parse(f.GTSTargetAccount.URI) +	if err != nil { +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSTargetAccount.URI, err) +	} +	toProp.AppendIRI(toIRI) +	like.SetActivityStreamsTo(toProp) + +	return like, nil +} diff --git a/internal/util/regexes.go b/internal/util/regexes.go index adab8c87f..55773c370 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -85,13 +85,18 @@ var (  	// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following  	followingPathRegex = regexp.MustCompile(followingPathRegexString) -	likedPathRegexString = fmt.Sprintf(`^/?%s/%s/%s$`, UsersPath, usernameRegexString, LikedPath) -	// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked -	likedPathRegex = regexp.MustCompile(likedPathRegexString) -  	// see https://ihateregex.io/expr/uuid/  	uuidRegexString = `[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}` +	likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath) +	// likedPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked +	likedPathRegex = regexp.MustCompile(likedPathRegexString) + +	likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, uuidRegexString) +	// likePathRegex parses a path that validates and captures the username part and the uuid part +	// from eg /users/example_username/liked/123e4567-e89b-12d3-a456-426655440000. +	likePathRegex = regexp.MustCompile(likePathRegexString) +  	statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, uuidRegexString)  	// statusesPathRegex parses a path that validates and captures the username part and the uuid part  	// from eg /users/example_username/statuses/123e4567-e89b-12d3-a456-426655440000. diff --git a/internal/util/uri.go b/internal/util/uri.go index 0ee4a5120..8d64bdd8e 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -22,8 +22,6 @@ import (  	"fmt"  	"net/url"  	"strings" - -	"github.com/google/uuid"  )  const ( @@ -109,8 +107,14 @@ type UserURIs struct {  // GenerateURIForFollow returns the AP URI for a new follow -- something like:  // https://example.org/users/whatever_user/follow/41c7f33f-1060-48d9-84df-38dcb13cf0d8 -func GenerateURIForFollow(username string, protocol string, host string) string { -	return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, FollowPath, uuid.NewString()) +func GenerateURIForFollow(username string, protocol string, host string, thisFollowID string) string { +	return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, FollowPath, thisFollowID) +} + +// GenerateURIForFollow returns the AP URI for a new like/fave -- something like: +// https://example.org/users/whatever_user/liked/41c7f33f-1060-48d9-84df-38dcb13cf0d8 +func GenerateURIForLike(username string, protocol string, host string, thisFavedID string) string { +	return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, LikedPath, thisFavedID)  }  // GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host. @@ -183,6 +187,11 @@ func IsLikedPath(id *url.URL) bool {  	return likedPathRegex.MatchString(strings.ToLower(id.Path))  } +// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_UUID_OF_A_STATUS +func IsLikePath(id *url.URL) bool { +	return likePathRegex.MatchString(strings.ToLower(id.Path)) +} +  // IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS  func IsStatusesPath(id *url.URL) bool {  	return statusesPathRegex.MatchString(strings.ToLower(id.Path)) @@ -254,3 +263,15 @@ func ParseFollowingPath(id *url.URL) (username string, err error) {  	username = matches[1]  	return  } + +// ParseLikedPath returns the username and uuid from a path such as /users/example_username/liked/SOME_UUID_OF_A_STATUS +func ParseLikedPath(id *url.URL) (username string, uuid string, err error) { +	matches := likePathRegex.FindStringSubmatch(id.Path) +	if len(matches) != 3 { +		err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches)) +		return +	} +	username = matches[1] +	uuid = matches[2] +	return +}  | 
