commit e670c32a9147f632d06ee10c170201677ec1e12d
parent 2dbd132e50098dd5d4893ee76b87003bcad91bcb
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Mon, 24 May 2021 18:49:48 +0200
Faves (#31)
* start on federating faves
* outbound federation of likes working
Diffstat:
12 files changed, 253 insertions(+), 50 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+}