diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go index dce810202..cab9245d7 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/account/account.go @@ -32,6 +32,8 @@ import ( ) const ( + // LimitKey is for setting the return amount limit for eg., requesting an account's statuses + LimitKey = "limit" // IDKey is the key to use for retrieving account ID in requests IDKey = "id" // BasePath is the base API path for this module @@ -42,6 +44,8 @@ const ( VerifyPath = BasePath + "/verify_credentials" // UpdateCredentialsPath is for updating account credentials UpdateCredentialsPath = BasePath + "/update_credentials" + // GetStatusesPath is for showing an account's statuses + GetStatusesPath = BasePathWithID + "/statuses" ) // Module implements the ClientAPIModule interface for account-related actions @@ -65,6 +69,7 @@ func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) + r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) return nil } diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go new file mode 100644 index 000000000..11ccd29dc --- /dev/null +++ b/internal/api/client/account/statuses.go @@ -0,0 +1,61 @@ +/* + 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 account + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountStatusesGETHandler serves the statuses of the requested account, if they're visible to the requester. +func (m *Module) AccountStatusesGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, false, false, false, false) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + targetAcctID := c.Param(IDKey) + if targetAcctID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + return + } + + limit := 30 + limitString := c.Query(LimitKey) + if limitString != "" { + l, err := strconv.ParseInt(limitString, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + return + } + limit = int(l) + } + + statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, statuses) +} diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 2cb22aa0d..d90c51c93 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -86,23 +86,23 @@ type Status struct { // It should be used at the path https://mastodon.example/api/v1/statuses type StatusCreateRequest struct { // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. - Status string `form:"status"` + Status string `form:"status" json:"status" xml:"status"` // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. MediaIDs []string `form:"media_ids" json:"media_ids" xml:"media_ids"` // Poll to include with this status. - Poll *PollRequest `form:"poll"` + Poll *PollRequest `form:"poll" json:"poll" xml:"poll"` // ID of the status being replied to, if status is a reply - InReplyToID string `form:"in_reply_to_id"` + InReplyToID string `form:"in_reply_to_id" json:"in_reply_to_id" xml:"in_reply_to_id"` // Mark status and attached media as sensitive? - Sensitive bool `form:"sensitive"` + Sensitive bool `form:"sensitive" json:"sensitive" xml:"sensitive"` // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. - SpoilerText string `form:"spoiler_text"` + SpoilerText string `form:"spoiler_text" json:"spoiler_text" xml:"spoiler_text"` // Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. - Visibility Visibility `form:"visibility"` + Visibility Visibility `form:"visibility" json:"visibility" xml:"visibility"` // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. - ScheduledAt string `form:"scheduled_at"` + ScheduledAt string `form:"scheduled_at" json:"scheduled_at" xml:"scheduled_at"` // ISO 639 language code for this status. - Language string `form:"language"` + Language string `form:"language" json:"language" xml:"language"` } // Visibility denotes the visibility of this status to other users @@ -130,13 +130,13 @@ type AdvancedStatusCreateForm struct { // to the standard mastodon-compatible ones. type AdvancedVisibilityFlagsForm struct { // The gotosocial visibility model - VisibilityAdvanced *string `form:"visibility_advanced"` + VisibilityAdvanced *string `form:"visibility_advanced" json:"visibility_advanced" xml:"visibility_advanced"` // This status will be federated beyond the local timeline(s) - Federated *bool `form:"federated"` + Federated *bool `form:"federated" json:"federated" xml:"federated"` // This status can be boosted/reblogged - Boostable *bool `form:"boostable"` + Boostable *bool `form:"boostable" json:"boostable" xml:"boostable"` // This status can be replied to - Replyable *bool `form:"replyable"` + Replyable *bool `form:"replyable" json:"replyable" xml:"replyable"` // This status can be liked/faved - Likeable *bool `form:"likeable"` + Likeable *bool `form:"likeable" json:"likeable" xml:"likeable"` } diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index 4ea0412e7..c9c783f1f 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -364,7 +364,7 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er // // Under certain conditions and network activities, Create may be called // multiple times for the same ActivityStreams object. -func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { +func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { l := f.log.WithFields( logrus.Fields{ "func": "Create", @@ -373,6 +373,24 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { ) l.Debugf("received CREATE asType %+v", asType) + 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") + } + + 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.ActivityStreamsCreate: create, ok := asType.(vocab.ActivityStreamsCreate) @@ -391,6 +409,12 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { if err := f.db.Put(status); err != nil { return fmt.Errorf("database error inserting status: %s", err) } + + fromFederatorChan <- gtsmodel.FromFederator{ + APObjectType: gtsmodel.ActivityStreamsNote, + APActivityType: gtsmodel.ActivityStreamsCreate, + Activity: status, + } } } case gtsmodel.ActivityStreamsFollow: @@ -407,6 +431,12 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { if err := f.db.Put(followRequest); err != nil { return fmt.Errorf("database error inserting follow request: %s", err) } + + if !targetAcct.Locked { + if err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil { + return fmt.Errorf("database error accepting follow request: %s", err) + } + } } return nil } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 0d2a8d9dd..d8f6eb839 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -71,49 +71,7 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques l.Debug(err) return nil, err } - - // derefence the actor of the activity already - // var requestingActorIRI *url.URL - // actorProp := activity.GetActivityStreamsActor() - // if actorProp != nil { - // for i := actorProp.Begin(); i != actorProp.End(); i = i.Next() { - // if i.IsIRI() { - // requestingActorIRI = i.GetIRI() - // break - // } - // } - // } - // if requestingActorIRI != nil { - - // requestedAccountI := ctx.Value(util.APAccount) - // requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) - // if !ok { - // return nil, errors.New("requested account was not set on request context") - // } - - // requestingActor := >smodel.Account{} - // if err := f.db.GetWhere("uri", requestingActorIRI.String(), requestingActor); err != nil { - // // there's been a proper error so return it - // if _, ok := err.(db.ErrNoEntries); !ok { - // return nil, fmt.Errorf("error getting requesting actor with id %s: %s", requestingActorIRI.String(), err) - // } - - // // we don't know this account (yet) so let's dereference it right now - // person, err := f.DereferenceRemoteAccount(requestedAccount.Username, publicKeyOwnerURI) - // if err != nil { - // return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err) - // } - - // a, err := f.typeConverter.ASRepresentationToAccount(person) - // if err != nil { - // return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err) - // } - // requestingAccount = a - // } - // } - // set the activity on the context for use later on - return context.WithValue(ctx, util.APActivity, activity), nil } @@ -285,14 +243,6 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa } 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, diff --git a/internal/gtsmodel/messages.go b/internal/gtsmodel/messages.go new file mode 100644 index 000000000..e19b5cac1 --- /dev/null +++ b/internal/gtsmodel/messages.go @@ -0,0 +1,29 @@ +package gtsmodel + +// ToClientAPI wraps a message that travels from the processor into the client API +type ToClientAPI struct { + APObjectType ActivityStreamsObject + APActivityType ActivityStreamsActivity + Activity interface{} +} + +// FromClientAPI wraps a message that travels from client API into the processor +type FromClientAPI struct { + APObjectType ActivityStreamsObject + APActivityType ActivityStreamsActivity + Activity interface{} +} + +// ToFederator wraps a message that travels from the processor into the federator +type ToFederator struct { + APObjectType ActivityStreamsObject + APActivityType ActivityStreamsActivity + Activity interface{} +} + +// FromFederator wraps a message that travels from the federator into the processor +type FromFederator struct { + APObjectType ActivityStreamsObject + APActivityType ActivityStreamsActivity + Activity interface{} +} diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index 9433140d7..cf9b8b6c4 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -166,3 +166,67 @@ func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCrede } return acctSensitive, nil } + +func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int) ([]apimodel.Status, ErrorWithCode) { + targetAccount := >smodel.Account{} + if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return nil, NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID)) + } + return nil, NewErrorInternalError(err) + } + + statuses := []gtsmodel.Status{} + apiStatuses := []apimodel.Status{} + if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return apiStatuses, nil + } + return nil, NewErrorInternalError(err) + } + + for _, s := range statuses { + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s) + if err != nil { + return nil, NewErrorInternalError(err) + } + + visible, err := p.db.StatusVisible(&s, targetAccount, authed.Account, relevantAccounts) + if err != nil { + return nil, NewErrorInternalError(err) + } + if !visible { + continue + } + + var boostedStatus *gtsmodel.Status + if s.BoostOfID != "" { + bs := >smodel.Status{} + if err := p.db.GetByID(s.BoostOfID, bs); err != nil { + return nil, NewErrorInternalError(err) + } + boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs) + if err != nil { + return nil, NewErrorInternalError(err) + } + + boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts) + if err != nil { + return nil, NewErrorInternalError(err) + } + + if boostedVisible { + boostedStatus = bs + } + } + + apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus) + if err != nil { + return nil, NewErrorInternalError(err) + } + + apiStatuses = append(apiStatuses, *apiStatus) + } + + return apiStatuses, nil +} diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index 133e7dbaa..63bda46a2 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -60,7 +60,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht } // put it in our channel to queue it for async processing - p.FromFederator() <- FromFederator{ + p.FromFederator() <- gtsmodel.FromFederator{ APObjectType: gtsmodel.ActivityStreamsProfile, APActivityType: gtsmodel.ActivityStreamsCreate, Activity: requestingAccount, diff --git a/internal/message/processor.go b/internal/message/processor.go index 7fc850e37..ae55a1502 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -23,7 +23,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -45,13 +44,13 @@ import ( // for clean distribution of messages without slowing down the client API and harming the user experience. type Processor interface { // ToClientAPI returns a channel for putting in messages that need to go to the gts client API. - ToClientAPI() chan ToClientAPI + ToClientAPI() chan gtsmodel.ToClientAPI // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor - FromClientAPI() chan FromClientAPI + FromClientAPI() chan gtsmodel.FromClientAPI // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). - ToFederator() chan ToFederator + ToFederator() chan gtsmodel.ToFederator // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor - FromFederator() chan FromFederator + FromFederator() chan gtsmodel.FromFederator // Start starts the Processor, reading from its channels and passing messages back and forth. Start() error // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. @@ -71,6 +70,7 @@ type Processor interface { AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) // AccountUpdate processes the update of an account with the given form AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) + AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int) ([]apimodel.Status, 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) @@ -142,10 +142,10 @@ type Processor interface { // processor just implements the Processor interface type processor struct { // federator pub.FederatingActor - toClientAPI chan ToClientAPI - fromClientAPI chan FromClientAPI - toFederator chan ToFederator - fromFederator chan FromFederator + toClientAPI chan gtsmodel.ToClientAPI + fromClientAPI chan gtsmodel.FromClientAPI + toFederator chan gtsmodel.ToFederator + fromFederator chan gtsmodel.FromFederator federator federation.Federator stop chan interface{} log *logrus.Logger @@ -160,10 +160,10 @@ type processor struct { // NewProcessor returns a new Processor that uses the given federator and logger func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor { return &processor{ - toClientAPI: make(chan ToClientAPI, 100), - fromClientAPI: make(chan FromClientAPI, 100), - toFederator: make(chan ToFederator, 100), - fromFederator: make(chan FromFederator, 100), + toClientAPI: make(chan gtsmodel.ToClientAPI, 100), + fromClientAPI: make(chan gtsmodel.FromClientAPI, 100), + toFederator: make(chan gtsmodel.ToFederator, 100), + fromFederator: make(chan gtsmodel.FromFederator, 100), federator: federator, stop: make(chan interface{}), log: log, @@ -176,19 +176,19 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f } } -func (p *processor) ToClientAPI() chan ToClientAPI { +func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI { return p.toClientAPI } -func (p *processor) FromClientAPI() chan FromClientAPI { +func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI { return p.fromClientAPI } -func (p *processor) ToFederator() chan ToFederator { +func (p *processor) ToFederator() chan gtsmodel.ToFederator { return p.toFederator } -func (p *processor) FromFederator() chan FromFederator { +func (p *processor) FromFederator() chan gtsmodel.FromFederator { return p.fromFederator } @@ -209,6 +209,9 @@ func (p *processor) Start() error { p.log.Infof("received message TO federator: %+v", federatorMsg) case federatorMsg := <-p.fromFederator: p.log.Infof("received message FROM federator: %+v", federatorMsg) + if err := p.processFromFederator(federatorMsg); err != nil { + p.log.Error(err) + } case <-p.stop: break DistLoop } @@ -224,35 +227,11 @@ func (p *processor) Stop() error { return nil } -// ToClientAPI wraps a message that travels from the processor into the client API -type ToClientAPI struct { - APObjectType gtsmodel.ActivityStreamsObject - APActivityType gtsmodel.ActivityStreamsActivity - Activity interface{} +func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error { + return nil } -// FromClientAPI wraps a message that travels from client API into the processor -type FromClientAPI struct { - APObjectType gtsmodel.ActivityStreamsObject - APActivityType gtsmodel.ActivityStreamsActivity - Activity interface{} -} - -// ToFederator wraps a message that travels from the processor into the federator -type ToFederator struct { - APObjectType gtsmodel.ActivityStreamsObject - APActivityType gtsmodel.ActivityStreamsActivity - Activity interface{} -} - -// FromFederator wraps a message that travels from the federator into the processor -type FromFederator struct { - APObjectType gtsmodel.ActivityStreamsObject - APActivityType gtsmodel.ActivityStreamsActivity - Activity interface{} -} - -func (p *processor) processFromClientAPI(clientMsg FromClientAPI) error { +func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error { switch clientMsg.APObjectType { case gtsmodel.ActivityStreamsNote: status, ok := clientMsg.Activity.(*gtsmodel.Status) @@ -273,30 +252,30 @@ func (p *processor) processFromClientAPI(clientMsg FromClientAPI) error { } func (p *processor) federateStatus(status *gtsmodel.Status) error { - // derive the sending account -- it might be attached to the status already - sendingAcct := >smodel.Account{} - if status.GTSAccount != nil { - sendingAcct = status.GTSAccount - } else { - // it wasn't attached so get it from the db instead - if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { - return err - } - } + // // derive the sending account -- it might be attached to the status already + // sendingAcct := >smodel.Account{} + // if status.GTSAccount != nil { + // sendingAcct = status.GTSAccount + // } else { + // // it wasn't attached so get it from the db instead + // if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { + // return err + // } + // } - outboxURI, err := url.Parse(sendingAcct.OutboxURI) - if err != nil { - return err - } + // outboxURI, err := url.Parse(sendingAcct.OutboxURI) + // if err != nil { + // return err + // } - // convert the status to AS format Note - note, err := p.tc.StatusToAS(status) - if err != nil { - return err - } + // // convert the status to AS format Note + // note, err := p.tc.StatusToAS(status) + // if err != nil { + // return err + // } - _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) - return err + // _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) + return nil } func (p *processor) notifyStatus(status *gtsmodel.Status) error { diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go index d9d115aec..f7ca16847 100644 --- a/internal/message/statusprocess.go +++ b/internal/message/statusprocess.go @@ -82,7 +82,7 @@ func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatus } // put the new status in the appropriate channel for async processing - p.fromClientAPI <- FromClientAPI{ + p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: newStatus.ActivityStreamsType, APActivityType: gtsmodel.ActivityStreamsCreate, Activity: newStatus, diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 861350b44..c5c634d07 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -20,6 +20,7 @@ package typeutils import ( "fmt" + "strings" "time" "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -195,7 +196,7 @@ func (c *converter) AppToMastoPublic(a *gtsmodel.Application) (*model.Applicatio func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (model.Attachment, error) { return model.Attachment{ ID: a.ID, - Type: string(a.Type), + Type: strings.ToLower(string(a.Type)), URL: a.URL, PreviewURL: a.Thumbnail.URL, RemoteURL: a.RemoteURL,