From 883278477847025c4cffd0dba41d0b374a60da8a Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Tue, 18 May 2021 19:56:11 +0200 Subject: [PATCH] lots of stufffffffffffffffff --- internal/api/client/account/account.go | 3 + internal/api/client/account/relationships.go | 41 +++++++ internal/api/client/followrequest/accept.go | 5 +- internal/api/model/account.go | 36 +++--- internal/api/security/security.go | 1 + internal/api/security/useragentblock.go | 43 +++++++ internal/db/db.go | 7 +- internal/db/pg/pg.go | 72 ++++++++++-- internal/federation/federating_db.go | 110 +++++++++++++++-- internal/federation/federatingprotocol.go | 39 ++++++- internal/gtsmodel/account.go | 4 +- internal/gtsmodel/messages.go | 7 +- internal/gtsmodel/relationship.go | 49 ++++++++ internal/media/handler.go | 48 +++++++- internal/media/handler_test.go | 2 +- internal/media/processicon.go | 4 +- internal/message/accountprocess.go | 33 ++++++ internal/message/fromclientapiprocess.go | 117 ++++++++++++++++--- internal/message/fromfederatorprocess.go | 90 +++++++++++--- internal/message/frprocess.go | 25 +++- internal/message/processor.go | 4 +- internal/message/processorutil.go | 37 +++++- internal/transport/controller.go | 3 + internal/transport/transport.go | 8 ++ internal/typeutils/converter.go | 3 + internal/typeutils/internaltofrontend.go | 18 +++ 26 files changed, 721 insertions(+), 88 deletions(-) create mode 100644 internal/api/client/account/relationships.go create mode 100644 internal/api/security/useragentblock.go create mode 100644 internal/gtsmodel/relationship.go diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go index 1e4b716f5..583eb3e42 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/account/account.go @@ -57,6 +57,8 @@ const ( GetStatusesPath = BasePathWithID + "/statuses" // GetFollowersPath is for showing an account's followers GetFollowersPath = BasePathWithID + "/followers" + // GetRelationshipsPath is for showing an account's relationship with other accounts + GetRelationshipsPath = BasePath + "/relationships" ) // Module implements the ClientAPIModule interface for account-related actions @@ -82,6 +84,7 @@ func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) + r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) return nil } diff --git a/internal/api/client/account/relationships.go b/internal/api/client/account/relationships.go new file mode 100644 index 000000000..b5d0ca837 --- /dev/null +++ b/internal/api/client/account/relationships.go @@ -0,0 +1,41 @@ +package account + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountRelationshipsGETHandler serves the relationship of the requesting account with one or more requested account IDs. +func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { + l := m.log.WithField("func", "AccountRelationshipsGETHandler") + + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("error authing: %s", err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + targetAccountIDs := c.QueryArray("id[]") + if len(targetAccountIDs) == 0 { + l.Debug("no account id specified in query") + c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + return + } + + relationships := []model.Relationship{} + + for _, targetAccountID := range targetAccountIDs { + r, errWithCode := m.processor.AccountRelationshipGet(authed, targetAccountID) + if err != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + relationships = append(relationships, *r) + } + + c.JSON(http.StatusOK, relationships) +} diff --git a/internal/api/client/followrequest/accept.go b/internal/api/client/followrequest/accept.go index 45dc1a2af..bb2910c8f 100644 --- a/internal/api/client/followrequest/accept.go +++ b/internal/api/client/followrequest/accept.go @@ -48,10 +48,11 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { return } - if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil { + r, errWithCode := m.processor.FollowRequestAccept(authed, originAccountID) + if errWithCode != nil { l.Debug(errWithCode.Error()) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) return } - c.Status(http.StatusOK) + c.JSON(http.StatusOK, r) } diff --git a/internal/api/model/account.go b/internal/api/model/account.go index efb69d6fd..c8c3c3837 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -77,18 +77,18 @@ type Account struct { // See https://docs.joinmastodon.org/methods/accounts/ type AccountCreateRequest struct { // Text that will be reviewed by moderators if registrations require manual approval. - Reason string `form:"reason"` + Reason string `form:"reason" json:"reason" xml:"reason"` // The desired username for the account - Username string `form:"username" binding:"required"` + Username string `form:"username" json:"username" xml:"username" binding:"required"` // The email address to be used for login - Email string `form:"email" binding:"required"` + Email string `form:"email" json:"email" xml:"email" binding:"required"` // The password to be used for login - Password string `form:"password" binding:"required"` + Password string `form:"password" json:"password" xml:"password" binding:"required"` // Whether the user agrees to the local rules, terms, and policies. // These should be presented to the user in order to allow them to consent before setting this parameter to TRUE. - Agreement bool `form:"agreement" binding:"required"` + Agreement bool `form:"agreement" json:"agreement" xml:"agreement" binding:"required"` // The language of the confirmation email that will be sent - Locale string `form:"locale" binding:"required"` + Locale string `form:"locale" json:"locale" xml:"locale" binding:"required"` // The IP of the sign up request, will not be parsed from the form but must be added manually IP net.IP `form:"-"` } @@ -97,33 +97,33 @@ type AccountCreateRequest struct { // See https://docs.joinmastodon.org/methods/accounts/ type UpdateCredentialsRequest struct { // Whether the account should be shown in the profile directory. - Discoverable *bool `form:"discoverable"` + Discoverable *bool `form:"discoverable" json:"discoverable" xml:"discoverable"` // Whether the account has a bot flag. - Bot *bool `form:"bot"` + Bot *bool `form:"bot" json:"bot" xml:"bot"` // The display name to use for the profile. - DisplayName *string `form:"display_name"` + DisplayName *string `form:"display_name" json:"display_name" xml:"display_name"` // The account bio. - Note *string `form:"note"` + Note *string `form:"note" json:"note" xml:"note"` // Avatar image encoded using multipart/form-data - Avatar *multipart.FileHeader `form:"avatar"` + Avatar *multipart.FileHeader `form:"avatar" json:"avatar" xml:"avatar"` // Header image encoded using multipart/form-data - Header *multipart.FileHeader `form:"header"` + Header *multipart.FileHeader `form:"header" json:"header" xml:"header"` // Whether manual approval of follow requests is required. - Locked *bool `form:"locked"` + Locked *bool `form:"locked" json:"locked" xml:"locked"` // New Source values for this account - Source *UpdateSource `form:"source"` + Source *UpdateSource `form:"source" json:"source" xml:"source"` // Profile metadata name and value - FieldsAttributes *[]UpdateField `form:"fields_attributes"` + FieldsAttributes *[]UpdateField `form:"fields_attributes" json:"fields_attributes" xml:"fields_attributes"` } // UpdateSource is to be used specifically in an UpdateCredentialsRequest. type UpdateSource struct { // Default post privacy for authored statuses. - Privacy *string `form:"privacy"` + Privacy *string `form:"privacy" json:"privacy" xml:"privacy"` // Whether to mark authored statuses as sensitive by default. - Sensitive *bool `form:"sensitive"` + Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"` // Default language to use for authored statuses. (ISO 6391) - Language *string `form:"language"` + Language *string `form:"language" json:"language" xml:"language"` } // UpdateField is to be used specifically in an UpdateCredentialsRequest. diff --git a/internal/api/security/security.go b/internal/api/security/security.go index eaae8471e..523b5dd55 100644 --- a/internal/api/security/security.go +++ b/internal/api/security/security.go @@ -43,5 +43,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule { func (m *Module) Route(s router.Router) error { s.AttachMiddleware(m.FlocBlock) s.AttachMiddleware(m.ExtraHeaders) + s.AttachMiddleware(m.UserAgentBlock) return nil } diff --git a/internal/api/security/useragentblock.go b/internal/api/security/useragentblock.go new file mode 100644 index 000000000..f7d3a4ffc --- /dev/null +++ b/internal/api/security/useragentblock.go @@ -0,0 +1,43 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package security + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// UserAgentBlock is a middleware that prevents google chrome cohort tracking by +// writing the Permissions-Policy header after all other parts of the request have been completed. +// See: https://plausible.io/blog/google-floc +func (m *Module) UserAgentBlock(c *gin.Context) { + + ua := c.Request.UserAgent() + if ua == "" { + c.AbortWithStatus(http.StatusTeapot) + return + } + + if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) { + c.AbortWithStatus(http.StatusTeapot) + return + } +} diff --git a/internal/db/db.go b/internal/db/db.go index cbcd698c9..375641502 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -117,7 +117,9 @@ type DB interface { // AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. // In other words, it should create the follow, and delete the existing follow request. - AcceptFollowRequest(originAccountID string, targetAccountID string) error + // + // It will return the newly created follow for further processing. + AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) // CreateInstanceAccount creates an account in the database with the same username as the instance host value. // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. @@ -204,6 +206,9 @@ type DB interface { // That is, it returns true if account1 blocks account2, OR if account2 blocks account1. Blocked(account1 string, account2 string) (bool, error) + // GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. + GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) + // StatusVisible returns true if targetStatus is visible to requestingAccount, based on the // privacy settings of the status, and any blocks/mutes that might exist between the two accounts // or account domains. diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index d3590a027..cd629a7eb 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -307,30 +307,34 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac HANDY SHORTCUTS */ -func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error { +func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) { + // make sure the original follow request exists fr := >smodel.FollowRequest{} if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { if err == pg.ErrMultiRows { - return db.ErrNoEntries{} + return nil, db.ErrNoEntries{} } - return err + return nil, err } + // create a new follow to 'replace' the request with follow := >smodel.Follow{ AccountID: originAccountID, TargetAccountID: targetAccountID, URI: fr.URI, } - if _, err := ps.conn.Model(follow).Insert(); err != nil { - return err + // if the follow already exists, just update the URI -- we don't need to do anything else + if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { + return nil, err } + // now remove the follow request if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { - return err + return nil, err } - return nil + return follow, nil } func (ps *postgresService) CreateInstanceAccount() error { @@ -681,6 +685,60 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro return blocked, nil } +func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) { + r := >smodel.Relationship{ + ID: targetAccount, + } + + // check if the requesting account follows the target account + follow := >smodel.Follow{} + if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { + if err != pg.ErrNoRows { + // a proper error + return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) + } + // no follow exists so these are all false + r.Following = false + r.ShowingReblogs = false + r.Notifying = false + } else { + // follow exists so we can fill these fields out... + r.Following = true + r.ShowingReblogs = follow.ShowReblogs + r.Notifying = follow.Notify + } + + // check if the target account follows the requesting account + followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) + } + r.FollowedBy = followedBy + + // check if the requesting account blocks the target account + blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) + } + r.Blocking = blocking + + // check if the target account blocks the requesting account + blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) + } + r.BlockedBy = blockedBy + + // check if there's a pending following request from requesting account to target account + requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) + } + r.Requested = requested + + return r, nil +} + func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { l := ps.log.WithField("func", "StatusVisible") diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index f72c5e636..b8c7ec9db 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -20,6 +20,7 @@ package federation import ( "context" + "encoding/json" "errors" "fmt" "net/url" @@ -371,24 +372,37 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { "asType": asType.GetTypeName(), }, ) - l.Debugf("received CREATE asType %+v", asType) + m, err := streams.Serialize(asType) + if err != nil { + return err + } + b, err := json.Marshal(m) + if err != nil { + return err + } + + l.Debugf("received CREATE asType %s", string(b)) targetAcctI := ctx.Value(util.APAccount) if targetAcctI == nil { l.Error("target account wasn't set on context") + return nil } targetAcct, ok := targetAcctI.(*gtsmodel.Account) if !ok { l.Error("target account was set on context but couldn't be parsed") + return nil } fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) if fromFederatorChanI == nil { l.Error("from federator channel wasn't set on context") + return nil } fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) if !ok { l.Error("from federator channel was set on context but couldn't be parsed") + return nil } switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) { @@ -433,7 +447,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { } if !targetAcct.Locked { - if err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { + if _, err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { return fmt.Errorf("database error accepting follow request: %s", err) } } @@ -450,14 +464,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { // the entire value. // // The library makes this call only after acquiring a lock first. -func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { +func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { l := f.log.WithFields( logrus.Fields{ "func": "Update", "asType": asType.GetTypeName(), }, ) - l.Debugf("received UPDATE asType %+v", asType) + m, err := streams.Serialize(asType) + if err != nil { + return err + } + b, err := json.Marshal(m) + if err != nil { + return err + } + + l.Debugf("received UPDATE asType %s", string(b)) + + receivingAcctI := ctx.Value(util.APAccount) + if receivingAcctI == nil { + l.Error("receiving account wasn't set on context") + } + receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) + if !ok { + l.Error("receiving account was set on context but couldn't be parsed") + } + + fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) + if fromFederatorChanI == nil { + l.Error("from federator channel wasn't set on context") + } + fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) + if !ok { + l.Error("from federator channel was set on context but couldn't be parsed") + } + + switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) { + case gtsmodel.ActivityStreamsUpdate: + update, ok := asType.(vocab.ActivityStreamsCreate) + if !ok { + return errors.New("could not convert type to create") + } + object := update.GetActivityStreamsObject() + for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { + switch objectIter.GetType().GetTypeName() { + case string(gtsmodel.ActivityStreamsPerson): + person := objectIter.GetActivityStreamsPerson() + updatedAcct, err := f.typeConverter.ASRepresentationToAccount(person) + if err != nil { + return fmt.Errorf("error converting person to account: %s", err) + } + if err := f.db.Put(updatedAcct); err != nil { + return fmt.Errorf("database error inserting updated account: %s", err) + } + + fromFederatorChan <- gtsmodel.FromFederator{ + APObjectType: gtsmodel.ActivityStreamsProfile, + APActivityType: gtsmodel.ActivityStreamsUpdate, + GTSModel: updatedAcct, + ReceivingAccount: receivingAcct, + } + + case string(gtsmodel.ActivityStreamsApplication): + application := objectIter.GetActivityStreamsApplication() + updatedAcct, err := f.typeConverter.ASRepresentationToAccount(application) + if err != nil { + return fmt.Errorf("error converting person to account: %s", err) + } + if err := f.db.Put(updatedAcct); err != nil { + return fmt.Errorf("database error inserting updated account: %s", err) + } + + fromFederatorChan <- gtsmodel.FromFederator{ + APObjectType: gtsmodel.ActivityStreamsProfile, + APActivityType: gtsmodel.ActivityStreamsUpdate, + GTSModel: updatedAcct, + ReceivingAccount: receivingAcct, + } + } + } + } return nil } @@ -490,7 +577,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v ) l.Debug("entering GETOUTBOX function") - return nil, nil + return streams.NewActivityStreamsOrderedCollectionPage(), nil } // SetOutbox saves the outbox value given from GetOutbox, with new items @@ -522,9 +609,18 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err "asType": t.GetTypeName(), }, ) - l.Debugf("received NEWID request for asType %+v", t) + m, err := streams.Serialize(t) + if err != nil { + return nil, err + } + b, err := json.Marshal(m) + if err != nil { + return nil, err + } - return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString())) + l.Debugf("received NEWID request for asType %s", string(b)) + + return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString())) } // Followers obtains the Followers Collection for an actor with the diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index d8f6eb839..9941ecc0c 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -20,12 +20,14 @@ package federation import ( "context" + "encoding/json" "errors" "fmt" "net/http" "net/url" "github.com/go-fed/activity/pub" + "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -146,6 +148,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr } requestingAccount = a + + // send the newly dereferenced account into the processor channel for further async processing + fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) + if fromFederatorChanI == nil { + l.Error("from federator channel wasn't set on context") + } + fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator) + if !ok { + l.Error("from federator channel was set on context but couldn't be parsed") + } + + fromFederatorChan <- gtsmodel.FromFederator{ + APObjectType: gtsmodel.ActivityStreamsProfile, + APActivityType: gtsmodel.ActivityStreamsCreate, + GTSModel: requestingAccount, + } } withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) @@ -228,17 +246,19 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa "func": "FederatingCallbacks", }) - targetAcctI := ctx.Value(util.APAccount) - if targetAcctI == nil { - l.Error("target account wasn't set on context") + receivingAcctI := ctx.Value(util.APAccount) + if receivingAcctI == nil { + l.Error("receiving account wasn't set on context") + return } - targetAcct, ok := targetAcctI.(*gtsmodel.Account) + receivingAcct, ok := receivingAcctI.(*gtsmodel.Account) if !ok { - l.Error("target account was set on context but couldn't be parsed") + l.Error("receiving account was set on context but couldn't be parsed") + return } var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept - if targetAcct.Locked { + if receivingAcct.Locked { onFollow = pub.OnFollowDoNothing } @@ -248,6 +268,13 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa OnFollow: onFollow, } + // override default undo behavior + other = []interface{}{ + func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { + return f.typeConverter. + }, + } + return } diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 56c401e62..dce0795fe 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -76,13 +76,13 @@ type Account struct { */ // Does this account need an approval for new followers? - Locked bool `pg:",default:'true'"` + Locked bool // Should this account be shown in the instance's profile directory? Discoverable bool // Default post privacy for this account Privacy Visibility // Set posts from this account to sensitive by default? - Sensitive bool `pg:",default:'false'"` + Sensitive bool // What language does this account post in? Language string `pg:",default:'en'"` diff --git a/internal/gtsmodel/messages.go b/internal/gtsmodel/messages.go index 43f30634a..5c2275461 100644 --- a/internal/gtsmodel/messages.go +++ b/internal/gtsmodel/messages.go @@ -23,7 +23,8 @@ type FromClientAPI struct { // FromFederator wraps a message that travels from the federator into the processor type FromFederator struct { - APObjectType ActivityStreamsObject - APActivityType ActivityStreamsActivity - GTSModel interface{} + APObjectType ActivityStreamsObject + APActivityType ActivityStreamsActivity + GTSModel interface{} + ReceivingAccount *Account } diff --git a/internal/gtsmodel/relationship.go b/internal/gtsmodel/relationship.go new file mode 100644 index 000000000..4e6cc03f6 --- /dev/null +++ b/internal/gtsmodel/relationship.go @@ -0,0 +1,49 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel + +// Relationship describes a requester's relationship with another account. +type Relationship struct { + // The account id. + ID string + // Are you following this user? + Following bool + // Are you receiving this user's boosts in your home timeline? + ShowingReblogs bool + // Have you enabled notifications for this user? + Notifying bool + // Are you followed by this user? + FollowedBy bool + // Are you blocking this user? + Blocking bool + // Is this user blocking you? + BlockedBy bool + // Are you muting this user? + Muting bool + // Are you muting notifications from this user? + MutingNotifications bool + // Do you have a pending follow request for this user? + Requested bool + // Are you blocking this user's domain? + DomainBlocking bool + // Are you featuring this user on your profile? + Endorsed bool + // Your note on this account. + Note string +} diff --git a/internal/media/handler.go b/internal/media/handler.go index 8bbff9c46..b59e836ed 100644 --- a/internal/media/handler.go +++ b/internal/media/handler.go @@ -67,7 +67,7 @@ type Handler interface { // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, // and then returns information to the caller about the new header. - ProcessHeaderOrAvatar(img []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error) + ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) // ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, @@ -86,6 +86,8 @@ type Handler interface { // information to the caller about the new attachment. It's the caller's responsibility to put the returned struct // in the database. ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) + + ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) } type mediaHandler struct { @@ -112,7 +114,7 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, // and then returns information to the caller about the new header. -func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error) { +func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) { l := mh.log.WithField("func", "SetHeaderForAccountID") if mediaType != Header && mediaType != Avatar { @@ -134,7 +136,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin l.Tracef("read %d bytes of file", len(attachment)) // process it - ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID) + ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL) if err != nil { return nil, fmt.Errorf("error processing %s: %s", mediaType, err) } @@ -315,3 +317,43 @@ func (mh *mediaHandler) ProcessRemoteAttachment(t transport.Transport, currentAt return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL) } + +func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) { + + if !currentAttachment.Header && !currentAttachment.Avatar { + return nil, errors.New("provided attachment was set to neither header nor avatar") + } + + if currentAttachment.Header && currentAttachment.Avatar { + return nil, errors.New("provided attachment was set to both header and avatar") + } + + var headerOrAvi Type + if currentAttachment.Header { + headerOrAvi = Header + } else if currentAttachment.Avatar { + headerOrAvi = Avatar + } + + if currentAttachment.RemoteURL == "" { + return nil, errors.New("no remote URL on media attachment to dereference") + } + remoteIRI, err := url.Parse(currentAttachment.RemoteURL) + if err != nil { + return nil, fmt.Errorf("error parsing attachment url %s: %s", currentAttachment.RemoteURL, err) + } + + // for content type, we assume we don't know what to expect... + expectedContentType := "*/*" + if currentAttachment.File.ContentType != "" { + // ... and then narrow it down if we do + expectedContentType = currentAttachment.File.ContentType + } + + attachmentBytes, err := t.DereferenceMedia(context.Background(), remoteIRI, expectedContentType) + if err != nil { + return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err) + } + + return mh.ProcessHeaderOrAvatar(attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL) +} diff --git a/internal/media/handler_test.go b/internal/media/handler_test.go index 03dcdc21d..02bf334c5 100644 --- a/internal/media/handler_test.go +++ b/internal/media/handler_test.go @@ -147,7 +147,7 @@ func (suite *MediaTestSuite) TestSetHeaderOrAvatarForAccountID() { f, err := ioutil.ReadFile("./test/test-jpeg.jpg") assert.Nil(suite.T(), err) - ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header") + ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header", "") assert.Nil(suite.T(), err) suite.log.Debugf("%+v", ma) diff --git a/internal/media/processicon.go b/internal/media/processicon.go index 962d1c6d8..bc2c55809 100644 --- a/internal/media/processicon.go +++ b/internal/media/processicon.go @@ -28,7 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string) (*gtsmodel.MediaAttachment, error) { +func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) { var isHeader bool var isAvatar bool @@ -96,7 +96,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string ID: newMediaID, StatusID: "", URL: originalURL, - RemoteURL: "", + RemoteURL: remoteURL, CreatedAt: time.Now(), UpdatedAt: time.Now(), Type: gtsmodel.FileTypeImage, diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index a10f6d016..9581c707c 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -78,6 +78,15 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api return nil, fmt.Errorf("db error: %s", err) } + // lazily dereference things on the account if it hasn't been done yet + var requestingUsername string + if authed.Account != nil { + requestingUsername = authed.Account.Username + } + if err := p.dereferenceAccountFields(targetAccount, requestingUsername); err != nil { + p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) + } + var mastoAccount *apimodel.Account var err error if authed.Account != nil && targetAccount.ID == authed.Account.ID { @@ -285,6 +294,12 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri return nil, NewErrorInternalError(err) } + // derefence account fields in case we haven't done it already + if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil { + // don't bail if we can't fetch them, we'll try another time + p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err) + } + account, err := p.tc.AccountToMastoPublic(a) if err != nil { return nil, NewErrorInternalError(err) @@ -293,3 +308,21 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri } return accounts, nil } + +func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { + if authed == nil || authed.Account == nil { + return nil, NewErrorForbidden(errors.New("not authed")) + } + + gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID) + if err != nil { + return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) + } + + r, err := p.tc.RelationshipToMasto(gtsR) + if err != nil { + return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) + } + + return r, nil +} diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go index 1a12216e7..f78b5ec8b 100644 --- a/internal/message/fromclientapiprocess.go +++ b/internal/message/fromclientapiprocess.go @@ -19,30 +19,48 @@ package message import ( + "context" "errors" "fmt" + "net/url" + "github.com/go-fed/activity/pub" + "github.com/go-fed/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { - switch clientMsg.APObjectType { - case gtsmodel.ActivityStreamsNote: - status, ok := clientMsg.GTSModel.(*gtsmodel.Status) + switch clientMsg.APActivityType { + case gtsmodel.ActivityStreamsCreate: + // CREATE + switch clientMsg.APObjectType { + case gtsmodel.ActivityStreamsNote: + // CREATE NOTE + status, ok := clientMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + if err := p.notifyStatus(status); err != nil { + return err + } + + if status.VisibilityAdvanced.Federated { + return p.federateStatus(status) + } + return nil + } + case gtsmodel.ActivityStreamsUpdate: + // UPDATE + case gtsmodel.ActivityStreamsAccept: + // ACCEPT + follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") + return errors.New("accept was not parseable as *gtsmodel.Follow") } - - if err := p.notifyStatus(status); err != nil { - return err - } - - if status.VisibilityAdvanced.Federated { - return p.federateStatus(status) - } - return nil + return p.federateAcceptFollowRequest(follow) } - return fmt.Errorf("message type unprocessable: %+v", clientMsg) + return nil } func (p *processor) federateStatus(status *gtsmodel.Status) error { @@ -71,3 +89,74 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error { // _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) return nil } + +func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow) error { + + followAccepter := >smodel.Account{} + if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil { + return fmt.Errorf("error federating follow accept: %s", err) + } + followAccepterIRI, err := url.Parse(followAccepter.URI) + if err != nil { + return fmt.Errorf("error parsing URL: %s", err) + } + followAccepterOutboxIRI, err := url.Parse(followAccepter.OutboxURI) + if err != nil { + return fmt.Errorf("error parsing URL: %s", err) + } + me := streams.NewActivityStreamsActorProperty() + me.AppendIRI(followAccepterIRI) + + followRequester := >smodel.Account{} + if err := p.db.GetByID(follow.AccountID, followRequester); err != nil { + return fmt.Errorf("error federating follow accept: %s", err) + } + requesterIRI, err := url.Parse(followRequester.URI) + if err != nil { + return fmt.Errorf("error parsing URL: %s", err) + } + them := streams.NewActivityStreamsActorProperty() + them.AppendIRI(requesterIRI) + + // prepare the follow + ASFollow := streams.NewActivityStreamsFollow() + // set the follow requester as the actor + ASFollow.SetActivityStreamsActor(them) + // set the ID from the follow + ASFollowURI, err := url.Parse(follow.URI) + if err != nil { + return fmt.Errorf("error parsing URL: %s", err) + } + ASFollowIDProp := streams.NewJSONLDIdProperty() + ASFollowIDProp.SetIRI(ASFollowURI) + ASFollow.SetJSONLDId(ASFollowIDProp) + + // set the object as the accepter URI + ASFollowObjectProp := streams.NewActivityStreamsObjectProperty() + ASFollowObjectProp.AppendIRI(followAccepterIRI) + + // Prepare the response. + ASAccept := streams.NewActivityStreamsAccept() + // Set us as the 'actor'. + ASAccept.SetActivityStreamsActor(me) + + // Set the Follow as the 'object' property. + ASAcceptObject := streams.NewActivityStreamsObjectProperty() + ASAcceptObject.AppendActivityStreamsFollow(ASFollow) + ASAccept.SetActivityStreamsObject(ASAcceptObject) + + // Add all actors on the original Follow to the 'to' property. + ASAcceptTo := streams.NewActivityStreamsToProperty() + followActors := ASFollow.GetActivityStreamsActor() + for iter := followActors.Begin(); iter != followActors.End(); iter = iter.Next() { + id, err := pub.ToId(iter) + if err != nil { + return err + } + ASAcceptTo.AppendIRI(id) + } + ASAccept.SetActivityStreamsTo(ASAcceptTo) + + _, err = p.federator.FederatingActor().Send(context.Background(), followAccepterOutboxIRI, ASAccept) + return err +} diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go index 2dd8e9e3b..96f9ab0e5 100644 --- a/internal/message/fromfederatorprocess.go +++ b/internal/message/fromfederatorprocess.go @@ -38,24 +38,60 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er l.Debug("entering function PROCESS FROM FEDERATOR") - switch federatorMsg.APObjectType { - case gtsmodel.ActivityStreamsNote: + switch federatorMsg.APActivityType { + case gtsmodel.ActivityStreamsCreate: + // CREATE + switch federatorMsg.APObjectType { + case gtsmodel.ActivityStreamsNote: + // CREATE A STATUS + incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } - incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } + l.Debug("will now derefence incoming status") + if err := p.dereferenceStatusFields(incomingStatus); err != nil { + return fmt.Errorf("error dereferencing status from federator: %s", err) + } + if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { + return fmt.Errorf("error updating dereferenced status in the db: %s", err) + } - l.Debug("will now derefence incoming status") - if err := p.dereferenceStatusFields(incomingStatus); err != nil { - return fmt.Errorf("error dereferencing status from federator: %s", err) - } - if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil { - return fmt.Errorf("error updating dereferenced status in the db: %s", err) - } + if err := p.notifyStatus(incomingStatus); err != nil { + return err + } + case gtsmodel.ActivityStreamsProfile: + // CREATE AN ACCOUNT + incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("profile was not parseable as *gtsmodel.Account") + } - if err := p.notifyStatus(incomingStatus); err != nil { - return err + l.Debug("will now derefence incoming account") + if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { + return fmt.Errorf("error dereferencing account from federator: %s", err) + } + if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { + return fmt.Errorf("error updating dereferenced account in the db: %s", err) + } + } + case gtsmodel.ActivityStreamsUpdate: + // UPDATE + switch federatorMsg.APObjectType { + case gtsmodel.ActivityStreamsProfile: + // UPDATE AN ACCOUNT + incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("profile was not parseable as *gtsmodel.Account") + } + + l.Debug("will now derefence incoming account") + if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { + return fmt.Errorf("error dereferencing account from federator: %s", err) + } + if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { + return fmt.Errorf("error updating dereferenced account in the db: %s", err) + } } } @@ -206,3 +242,27 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { return nil } + +func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string) error { + l := p.log.WithFields(logrus.Fields{ + "func": "dereferenceAccountFields", + "requestingUsername": requestingUsername, + }) + + t, err := p.federator.GetTransportForUser(requestingUsername) + if err != nil { + return fmt.Errorf("error getting transport for user: %s", err) + } + + // fetch the header and avatar + if err := p.fetchHeaderAndAviForAccount(account, t); err != nil { + // if this doesn't work, just skip it -- we can do it later + l.Debugf("error fetching header/avi for account: %s", err) + } + + if err := p.db.UpdateByID(account.ID, account); err != nil { + return fmt.Errorf("error updating account in database: %s", err) + } + + return nil +} diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go index cc3838598..fd64b4c50 100644 --- a/internal/message/frprocess.go +++ b/internal/message/frprocess.go @@ -48,11 +48,28 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err return accts, nil } -func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode { - if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil { - return NewErrorNotFound(err) +func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) { + follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID) + if err != nil { + return nil, NewErrorNotFound(err) } - return nil + + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APActivityType: gtsmodel.ActivityStreamsAccept, + GTSModel: follow, + } + + gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + + r, err := p.tc.RelationshipToMasto(gtsR) + if err != nil { + return nil, NewErrorInternalError(err) + } + + return r, nil } func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { diff --git a/internal/message/processor.go b/internal/message/processor.go index c9ba5f858..a49707694 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -73,6 +73,8 @@ type Processor interface { AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) // AccountFollowersGet AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) + // AccountRelationshipGet + AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) @@ -86,7 +88,7 @@ type Processor interface { // FollowRequestsGet handles the getting of the authed account's incoming follow requests FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) // FollowRequestAccept handles the acceptance of a follow request from the given account ID - FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode + FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) // InstanceGet retrieves instance information for serving at api/v1/instance InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go index 676635a51..db0d63875 100644 --- a/internal/message/processorutil.go +++ b/internal/message/processorutil.go @@ -29,6 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -280,7 +281,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID } // do the setting - avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar) + avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "") if err != nil { return nil, fmt.Errorf("error processing avatar: %s", err) } @@ -313,10 +314,42 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID } // do the setting - headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header) + headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "") if err != nil { return nil, fmt.Errorf("error processing header: %s", err) } return headerInfo, f.Close() } + +// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport +// on behalf of requestingUsername. +// +// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary. +// +// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated +// to reflect the creation of these new attachments. +func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error { + if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" { + a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ + RemoteURL: targetAccount.AvatarRemoteURL, + Avatar: true, + }, targetAccount.ID) + if err != nil { + return fmt.Errorf("error processing avatar for user: %s", err) + } + targetAccount.AvatarMediaAttachmentID = a.ID + } + + if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" { + a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ + RemoteURL: targetAccount.HeaderRemoteURL, + Header: true, + }, targetAccount.ID) + if err != nil { + return fmt.Errorf("error processing header for user: %s", err) + } + targetAccount.HeaderMediaAttachmentID = a.ID + } + return nil +} diff --git a/internal/transport/controller.go b/internal/transport/controller.go index ad754080a..c01af0900 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -39,6 +39,7 @@ type controller struct { clock pub.Clock client pub.HttpClient appAgent string + log *logrus.Logger } // NewController returns an implementation of the Controller interface for creating new transports @@ -48,6 +49,7 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient clock: clock, client: client, appAgent: fmt.Sprintf("%s %s", config.ApplicationName, config.Host), + log: log, } } @@ -80,5 +82,6 @@ func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (T sigTransport: sigTransport, getSigner: getSigner, getSignerMu: &sync.Mutex{}, + log: c.log, }, nil } diff --git a/internal/transport/transport.go b/internal/transport/transport.go index afd408519..4fba484cd 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -11,6 +11,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/go-fed/httpsig" + "github.com/sirupsen/logrus" ) // Transport wraps the pub.Transport interface with some additional @@ -31,6 +32,7 @@ type transport struct { sigTransport *pub.HttpSigTransport getSigner httpsig.Signer getSignerMu *sync.Mutex + log *logrus.Logger } func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error { @@ -38,14 +40,20 @@ func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url. } func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error { + l := t.log.WithField("func", "Deliver") + l.Debugf("performing POST to %s", to.String()) return t.sigTransport.Deliver(c, b, to) } func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) { + l := t.log.WithField("func", "Dereference") + l.Debugf("performing GET to %s", iri.String()) return t.sigTransport.Dereference(c, iri) } func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) { + l := t.log.WithField("func", "DereferenceMedia") + l.Debugf("performing GET to %s", iri.String()) req, err := http.NewRequest("GET", iri.String(), nil) if err != nil { return nil, err diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 8f310c921..415b6f4a6 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -77,6 +77,9 @@ type TypeConverter interface { // InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) + // RelationshipToMasto converts a gts relationship into its mastodon equivalent for serving in various places + RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) + /* FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL */ diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index e4ccab988..70f8a8d3c 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -572,3 +572,21 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro return mi, nil } + +func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) { + return &model.Relationship{ + ID: r.ID, + Following: r.Following, + ShowingReblogs: r.ShowingReblogs, + Notifying: r.Notifying, + FollowedBy: r.FollowedBy, + Blocking: r.Blocking, + BlockedBy: r.BlockedBy, + Muting: r.Muting, + MutingNotifications: r.MutingNotifications, + Requested: r.Requested, + DomainBlocking: r.DomainBlocking, + Endorsed: r.Endorsed, + Note: r.Note, + }, nil +}