diff --git a/internal/api/client/followrequest/accept.go b/internal/api/client/followrequest/accept.go new file mode 100644 index 000000000..ecfc8c700 --- /dev/null +++ b/internal/api/client/followrequest/accept.go @@ -0,0 +1,55 @@ +/* + 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 followrequest + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { + l := m.log.WithField("func", "statusCreatePOSTHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + return + } + + originAccountID := c.Param(IDKey) + if originAccountID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) + return + } + + if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil { + l.Debug(errWithCode.Error()) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + c.Status(http.StatusOK) +} diff --git a/internal/api/client/followrequest/deny.go b/internal/api/client/followrequest/deny.go new file mode 100644 index 000000000..59136cb7d --- /dev/null +++ b/internal/api/client/followrequest/deny.go @@ -0,0 +1,25 @@ +/* + 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 followrequest + +import "github.com/gin-gonic/gin" + +func (m *Module) FollowRequestDenyPOSTHandler(c *gin.Context) { + +} diff --git a/internal/api/client/followrequest/followrequest.go b/internal/api/client/followrequest/followrequest.go new file mode 100644 index 000000000..8be957009 --- /dev/null +++ b/internal/api/client/followrequest/followrequest.go @@ -0,0 +1,68 @@ +/* + 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 followrequest + +import ( + "net/http" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( + // IDKey is for status UUIDs + IDKey = "id" + // BasePath is the base path for serving the follow request API + BasePath = "/api/v1/follow_requests" + // BasePathWithID is just the base path with the ID key in it. + // Use this anywhere you need to know the ID of the follow request being queried. + BasePathWithID = BasePath + "/:" + IDKey + + // AcceptPath is used for accepting follow requests + AcceptPath = BasePathWithID + "/authorize" + // DenyPath is used for denying follow requests + DenyPath = BasePathWithID + "/reject" +) + +// Module implements the ClientAPIModule interface for every related to interacting with follow requests +type Module struct { + config *config.Config + processor message.Processor + log *logrus.Logger +} + +// New returns a new follow request module +func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { + return &Module{ + config: config, + processor: processor, + log: log, + } +} + +// Route attaches all routes from this module to the given router +func (m *Module) Route(r router.Router) error { + r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) + r.AttachHandler(http.MethodPost, AcceptPath, m.FollowRequestAcceptPOSTHandler) + r.AttachHandler(http.MethodPost, DenyPath, m.FollowRequestDenyPOSTHandler) + return nil +} diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequest/get.go new file mode 100644 index 000000000..d117095c2 --- /dev/null +++ b/internal/api/client/followrequest/get.go @@ -0,0 +1,50 @@ +/* + 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 followrequest + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (m *Module) FollowRequestGETHandler(c *gin.Context) { + l := m.log.WithField("func", "statusCreatePOSTHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + return + } + + accts, errWithCode := m.processor.FollowRequestsGet(authed) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, accts) +} diff --git a/internal/db/db.go b/internal/db/db.go index b281dd8d7..f425f0ead 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -112,6 +112,8 @@ type DB interface { HANDY SHORTCUTS */ + AcceptFollowRequest(originAccountID string, targetAccountID string) 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'. // This is needed for things like serving files that belong to the instance and not an individual user/account. @@ -148,6 +150,8 @@ type DB interface { // In case of no entries, a 'no entries' error will be returned GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error + GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error + // GetStatusesByAccountID is a shortcut for the common action of fetching a list of statuses produced by accountID. // The given slice 'statuses' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index a50c4867f..82db68559 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -307,6 +307,32 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac HANDY SHORTCUTS */ +func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error { + 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 err + } + + follow := >smodel.Follow{ + AccountID: originAccountID, + TargetAccountID: targetAccountID, + URI: fr.URI, + } + + if _, err := ps.conn.Model(follow).Insert(); err != nil { + return err + } + + if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { + return err + } + + return nil +} + func (ps *postgresService) CreateInstanceAccount() error { username := ps.config.Host key, err := rsa.GenerateKey(rand.Reader, 2048) @@ -393,7 +419,7 @@ func (ps *postgresService) GetLocalAccountByUsername(username string, account *g func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return db.ErrNoEntries{} + return nil } return err } @@ -403,7 +429,7 @@ func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, follo func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return db.ErrNoEntries{} + return nil } return err } @@ -413,7 +439,17 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following * func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return db.ErrNoEntries{} + return nil + } + return err + } + return nil +} + +func (ps *postgresService) GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error { + if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return nil } return err } diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index 40c12ecb3..f3d0cbe1b 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -28,6 +28,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" + "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -115,7 +116,7 @@ func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (con "id": id.String(), }, ) - l.Debug("entering INBOXCONTAINS function") + l.Debugf("entering INBOXCONTAINS function with for inbox %s and id %s", inbox.String(), id.String()) if !util.IsInboxPath(inbox) { return false, fmt.Errorf("%s is not an inbox URI", inbox.String()) @@ -152,6 +153,12 @@ func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (con // // The library makes this call only after acquiring a lock first. func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "GetInbox", + }, + ) + l.Debugf("entering GETINBOX function with inboxIRI %s", inboxIRI.String()) return streams.NewActivityStreamsOrderedCollectionPage(), nil } @@ -161,6 +168,12 @@ func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox voc // // The library makes this call only after acquiring a lock first. func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "SetInbox", + }, + ) + l.Debug("entering SETINBOX function") return nil } @@ -174,7 +187,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { "id": id.String(), }, ) - l.Debug("entering OWNS function") + l.Debugf("entering OWNS function with id %s", id.String()) // if the id host isn't this instance host, we don't own this IRI if id.Host != f.config.Host { @@ -233,7 +246,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac "inboxIRI": outboxIRI.String(), }, ) - l.Debug("entering ACTORFOROUTBOX function") + l.Debugf("entering ACTORFOROUTBOX function with outboxIRI %s", outboxIRI.String()) if !util.IsOutboxPath(outboxIRI) { return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) @@ -258,7 +271,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto "inboxIRI": inboxIRI.String(), }, ) - l.Debug("entering ACTORFORINBOX function") + l.Debugf("entering ACTORFORINBOX function with inboxIRI %s", inboxIRI.String()) if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) @@ -284,7 +297,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out "inboxIRI": inboxIRI.String(), }, ) - l.Debug("entering OUTBOXFORINBOX function") + l.Debugf("entering OUTBOXFORINBOX function with inboxIRI %s", inboxIRI.String()) if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) @@ -310,7 +323,7 @@ func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err "id": id.String(), }, ) - l.Debug("entering EXISTS function") + l.Debugf("entering EXISTS function with id %s", id.String()) return false, nil } @@ -384,7 +397,15 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { if !ok { return errors.New("could not convert type to follow") } - + + followRequest, err := f.typeConverter.ASFollowToFollowRequest(follow) + if err != nil { + return fmt.Errorf("could not convert Follow to follow request: %s", err) + } + + if err := f.db.Put(followRequest); err != nil { + return fmt.Errorf("database error inserting follow request: %s", err) + } } return nil } @@ -431,6 +452,13 @@ func (f *federatingDB) Delete(c context.Context, id *url.URL) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "GetOutbox", + }, + ) + l.Debug("entering GETOUTBOX function") + return nil, nil } @@ -440,6 +468,13 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v // // The library makes this call only after acquiring a lock first. func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "SetOutbox", + }, + ) + l.Debug("entering SETOUTBOX function") + return nil } @@ -450,7 +485,6 @@ func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreams // The go-fed library will handle setting the 'id' property on the // activity or object provided with the value returned. func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { - l := f.log.WithFields( logrus.Fields{ "func": "NewID", @@ -458,7 +492,8 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err }, ) l.Debugf("received NEWID request for asType %+v", t) - return nil, nil + + return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString())) } // Followers obtains the Followers Collection for an actor with the @@ -468,7 +503,39 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { - return nil, nil + l := f.log.WithFields( + logrus.Fields{ + "func": "Followers", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) + + acct := >smodel.Account{} + if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) + } + + acctFollowers := []gtsmodel.Follow{} + if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers); err != nil { + return nil, fmt.Errorf("db error getting followers for account id %s: %s", acct.ID, err) + } + + followers = streams.NewActivityStreamsCollection() + items := streams.NewActivityStreamsItemsProperty() + for _, follow := range acctFollowers { + gtsFollower := >smodel.Account{} + if err := f.db.GetByID(follow.AccountID, gtsFollower); err != nil { + return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) + } + uri, err := url.Parse(gtsFollower.URI) + if err != nil { + return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollower.URI, err) + } + items.AppendIRI(uri) + } + followers.SetActivityStreamsItems(items) + return } // Following obtains the Following Collection for an actor with the @@ -477,8 +544,40 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower // If modified, the library will then call Update. // // The library makes this call only after acquiring a lock first. -func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { - return nil, nil +func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Following", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) + + acct := >smodel.Account{} + if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) + } + + acctFollowing := []gtsmodel.Follow{} + if err := f.db.GetFollowingByAccountID(acct.ID, &acctFollowing); err != nil { + return nil, fmt.Errorf("db error getting following for account id %s: %s", acct.ID, err) + } + + following = streams.NewActivityStreamsCollection() + items := streams.NewActivityStreamsItemsProperty() + for _, follow := range acctFollowing { + gtsFollowing := >smodel.Account{} + if err := f.db.GetByID(follow.AccountID, gtsFollowing); err != nil { + return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) + } + uri, err := url.Parse(gtsFollowing.URI) + if err != nil { + return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollowing.URI, err) + } + items.AppendIRI(uri) + } + following.SetActivityStreamsItems(items) + return } // Liked obtains the Liked Collection for an actor with the @@ -487,6 +586,13 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (follower // If modified, the library will then call Update. // // The library makes this call only after acquiring a lock first. -func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { +func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Liked", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String()) return nil, nil } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 45fca5f01..0d2a8d9dd 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -266,6 +266,37 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er // Applications are not expected to handle every single ActivityStreams // type and extension. The unhandled ones are passed to DefaultCallback. func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { + l := f.log.WithFields(logrus.Fields{ + "func": "FederatingCallbacks", + }) + + targetAcctI := ctx.Value(util.APAccount) + if targetAcctI == nil { + l.Error("target account wasn't set on context") + } + targetAcct, ok := targetAcctI.(*gtsmodel.Account) + if !ok { + l.Error("target account was set on context but couldn't be parsed") + } + + var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept + if targetAcct.Locked { + onFollow = pub.OnFollowDoNothing + } + + wrapped = pub.FederatingWrappedCallbacks{ + // Follow handles additional side effects for the Follow ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function can have one of several default behaviors, + // depending on the value of the OnFollow setting. + Follow: func(context.Context, vocab.ActivityStreamsFollow) error { + return nil + }, + // OnFollow determines what action to take for this particular callback + // if a Follow Activity is handled. + OnFollow: onFollow, + } return } diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index e83c55f5d..fb83a4231 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -34,6 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/app" "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/client/status" @@ -111,6 +112,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr accountModule := account.New(c, processor, log) instanceModule := instance.New(c, processor, log) appsModule := app.New(c, processor, log) + followRequestsModule := followrequest.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log) usersModule := user.New(c, processor, log) mm := mediaModule.New(c, processor, log) @@ -128,6 +130,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr accountModule, instanceModule, appsModule, + followRequestsModule, mm, fileServerModule, adminModule, diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go new file mode 100644 index 000000000..c96b83dec --- /dev/null +++ b/internal/message/frprocess.go @@ -0,0 +1,42 @@ +package message + +import ( + 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" +) + +func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) { + frs := []gtsmodel.FollowRequest{} + if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, NewErrorInternalError(err) + } + } + + accts := []apimodel.Account{} + for _, fr := range frs { + acct := >smodel.Account{} + if err := p.db.GetByID(fr.AccountID, acct); err != nil { + return nil, NewErrorInternalError(err) + } + mastoAcct, err := p.tc.AccountToMastoPublic(acct) + if err != nil { + return nil, NewErrorInternalError(err) + } + accts = append(accts, *mastoAcct) + } + 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) + } + return nil +} + +func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { + return nil +} diff --git a/internal/message/processor.go b/internal/message/processor.go index 44e30e562..7fc850e37 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -81,6 +81,11 @@ type Processor interface { // FileGet handles the fetching of a media attachment file via the fileserver. FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) + // 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 + // InstanceGet retrieves instance information for serving at api/v1/instance InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go index b5ce2d02a..4ee3347bd 100644 --- a/internal/typeutils/asextractionutil.go +++ b/internal/typeutils/asextractionutil.go @@ -519,3 +519,29 @@ func extractMention(i Mentionable) (*gtsmodel.Mention, error) { mention.MentionedAccountURI = hrefProp.GetIRI().String() return mention, nil } + +func extractActor(i withActor) (*url.URL, error) { + actorProp := i.GetActivityStreamsActor() + if actorProp == nil { + return nil, errors.New("actor property was nil") + } + for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + return nil, errors.New("no iri found for actor prop") +} + +func extractObject(i withObject) (*url.URL, error) { + objectProp := i.GetActivityStreamsObject() + if objectProp == nil { + return nil, errors.New("object property was nil") + } + for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + return nil, errors.New("no iri found for object prop") +} diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go index fe2306c5b..4a12f9b61 100644 --- a/internal/typeutils/asinterfaces.go +++ b/internal/typeutils/asinterfaces.go @@ -95,6 +95,14 @@ type Mentionable interface { withHref } +type Followable interface { + withJSONLDId + withTypeName + + withActor + withObject +} + type withJSONLDId interface { GetJSONLDId() vocab.JSONLDIdProperty } @@ -218,3 +226,11 @@ type withHref interface { type withUpdated interface { GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty } + +type withActor interface { + GetActivityStreamsActor() vocab.ActivityStreamsActorProperty +} + +type withObject interface { + GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty +} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index f8b76ffe5..c68e50d3c 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -306,6 +306,41 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e return status, nil } +func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) { + + idProp := followable.GetJSONLDId() + if idProp == nil || !idProp.IsIRI() { + return nil, errors.New("no id property set on follow, or was not an iri") + } + uri := idProp.GetIRI().String() + + origin, err := extractActor(followable) + if err != nil { + return nil, errors.New("error extracting actor property from follow") + } + originAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + target, err := extractObject(followable) + if err != nil { + return nil, errors.New("error extracting object property from follow") + } + targetAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + followRequest := >smodel.FollowRequest{ + URI: uri, + AccountID: originAccount.ID, + TargetAccountID: targetAccount.ID, + } + + return followRequest, nil +} + func isPublic(tos []*url.URL) bool { for _, entry := range tos { if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index ffd176c5d..8f310c921 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -92,6 +92,8 @@ type TypeConverter interface { ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) // ASStatus converts a remote activitystreams 'status' representation into a gts model status. ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) + // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. + ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) /* INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL