From 4b1d9d3780134098ff06877abc20c970c32d4aac Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Sun, 24 Oct 2021 11:57:39 +0200
Subject: [PATCH] Serve `outbox` for Actor (#289)
* add statusesvisible convenience function
* add minID + onlyPublic to account statuses get
* move swagger collection stuff to common
* start working on Outbox GETting
* move functions into federationProcessor
* outboxToASCollection
* add statusesvisible convenience function
* add minID + onlyPublic to account statuses get
* move swagger collection stuff to common
* start working on Outbox GETting
* move functions into federationProcessor
* outboxToASCollection
* bit more work on outbox paging
* wrapNoteInCreate function
* test + hook up the processor functions
* don't do prev + next links on empty reply
* test get outbox through api
* don't fail on no status entries
* add outbox implementation doc
* typo
---
docs/federation/behaviors/outbox.md | 47 +++
internal/api/client/account/account.go | 8 +-
internal/api/client/account/statuses.go | 36 +-
internal/api/s2s/user/common.go | 38 ++
internal/api/s2s/user/inboxpost_test.go | 2 +-
internal/api/s2s/user/outboxget.go | 142 +++++++
internal/api/s2s/user/outboxget_test.go | 206 +++++++++
internal/api/s2s/user/repliesget.go | 38 +-
internal/api/s2s/user/user.go | 5 +
internal/db/account.go | 2 +-
internal/db/bundb/account.go | 18 +-
internal/federation/federatingdb/util.go | 13 +
internal/processing/account.go | 4 +-
internal/processing/account/account.go | 2 +-
internal/processing/account/delete.go | 2 +-
internal/processing/account/getstatuses.go | 4 +-
internal/processing/federation.go | 393 +-----------------
internal/processing/federation/federation.go | 103 +++++
.../processing/federation/getfollowers.go | 74 ++++
.../processing/federation/getfollowing.go | 74 ++++
internal/processing/federation/getnodeinfo.go | 69 +++
internal/processing/federation/getoutbox.go | 107 +++++
internal/processing/federation/getstatus.go | 91 ++++
.../processing/federation/getstatusreplies.go | 164 ++++++++
internal/processing/federation/getuser.go | 85 ++++
.../processing/federation/getwebfinger.go | 64 +++
internal/processing/federation/postinbox.go | 32 ++
internal/processing/fromclientapi.go | 7 +-
internal/processing/fromfederator_test.go | 2 +-
internal/processing/processor.go | 40 +-
internal/typeutils/converter.go | 19 +
internal/typeutils/internaltoas.go | 144 +++++++
internal/typeutils/internaltoas_test.go | 86 +++-
internal/typeutils/wrap.go | 64 +++
internal/typeutils/wrap_test.go | 74 ++++
internal/visibility/filter.go | 4 +
internal/visibility/statusvisible.go | 14 +
testrig/testmodels.go | 44 +-
38 files changed, 1851 insertions(+), 470 deletions(-)
create mode 100644 docs/federation/behaviors/outbox.md
create mode 100644 internal/api/s2s/user/outboxget.go
create mode 100644 internal/api/s2s/user/outboxget_test.go
create mode 100644 internal/processing/federation/federation.go
create mode 100644 internal/processing/federation/getfollowers.go
create mode 100644 internal/processing/federation/getfollowing.go
create mode 100644 internal/processing/federation/getnodeinfo.go
create mode 100644 internal/processing/federation/getoutbox.go
create mode 100644 internal/processing/federation/getstatus.go
create mode 100644 internal/processing/federation/getstatusreplies.go
create mode 100644 internal/processing/federation/getuser.go
create mode 100644 internal/processing/federation/getwebfinger.go
create mode 100644 internal/processing/federation/postinbox.go
create mode 100644 internal/typeutils/wrap_test.go
diff --git a/docs/federation/behaviors/outbox.md b/docs/federation/behaviors/outbox.md
new file mode 100644
index 000000000..d16ca9d62
--- /dev/null
+++ b/docs/federation/behaviors/outbox.md
@@ -0,0 +1,47 @@
+# ActivityPub Outbox
+
+GoToSocial implements Outboxes for Actors (ie., instance accounts) following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#outbox).
+
+To get an [OrderedCollection](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection) of Activities that an Actor has published recently, remote servers can do a `GET` request to a user's outbox. The address of this will be something like `https://example.org/users/whatever/outbox`.
+
+The server will return an OrderedCollection of the following structure:
+
+```json
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.org/users/whatever/outbox",
+ "type": "OrderedCollection",
+ "first": "https://example.org/users/whatever/outbox?page=true"
+}
+```
+
+Note that the `OrderedCollection` itself contains no items. Callers must dereference the `first` page to start getting items. For example, a `GET` to `https://example.org/users/whatever/outbox?page=true` will produce something like the following:
+
+```json
+{
+ "id": "https://example.org/users/whatever/outbox?page=true",
+ "type": "OrderedCollectionPage",
+ "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
+ "prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
+ "partOf": "https://example.org/users/whatever/outbox",
+ "orderedItems": [
+ "id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity",
+ "type": "Create",
+ "actor": "https://example.org/users/whatever",
+ "published": "2021-10-18T20:06:18Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://example.org/users/whatever/followers"
+ ],
+ "object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7"
+ ]
+}
+```
+
+The `orderedItems` array will contain up to 30 entries. To get more entries beyond that, the caller can use the `next` link provided in the response.
+
+Note that in the returned `orderedItems`, all activity types will be `Create`. On each activity, the `object` field will be the AP URI of an original public status created by the Actor who owns the Outbox (ie., a `Note` with `https://www.w3.org/ns/activitystreams#Public` in the `to` field, which is not a reply to another status). Callers can use the returned AP URIs to dereference the content of the notes.
+
+Contrary to the ActivityPub spec, GoToSocial will deny requests that are not HTTP signed--that is, unauthenticated requests. This is consistent with GoToSocial's authentication policies for other federation API endpoints. This is to ensure that GoToSocial can deny requests from domains or users that have been blocked either by the GoToSocial instance itself (domain block), or by the individual owner of the Outbox.
diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go
index bb11888d3..6e8b1e242 100644
--- a/internal/api/client/account/account.go
+++ b/internal/api/client/account/account.go
@@ -39,8 +39,12 @@ const (
PinnedKey = "pinned"
// MaxIDKey is for specifying the maximum ID of the status to retrieve.
MaxIDKey = "max_id"
- // MediaOnlyKey is for specifying that only statuses with media should be returned in a list of returned statuses by an account.
- MediaOnlyKey = "only_media"
+ // MinIDKey is for specifying the minimum ID of the status to retrieve.
+ MinIDKey = "min_id"
+ // OnlyMediaKey is for specifying that only statuses with media should be returned in a list of returned statuses by an account.
+ OnlyMediaKey = "only_media"
+ // OnlyPublicKey is for specifying that only statuses with visibility public should be returned in a list of returned statuses by account.
+ OnlyPublicKey = "only_public"
// IDKey is the key to use for retrieving account ID in requests
IDKey = "id"
diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go
index ab1d4e2e6..9a60e80ee 100644
--- a/internal/api/client/account/statuses.go
+++ b/internal/api/client/account/statuses.go
@@ -64,6 +64,12 @@ import (
// Return only statuses *OLDER* than the given max status ID.
// The status with the specified ID will not be included in the response.
// in: query
+// - name: min_id
+// type: string
+// description: |-
+// Return only statuses *NEWER* than the given min status ID.
+// The status with the specified ID will not be included in the response.
+// in: query
// required: false
// - name: pinned_only
// type: boolean
@@ -71,12 +77,18 @@ import (
// default: false
// in: query
// required: false
-// - name: media_only
+// - name: only_media
// type: boolean
// description: Show only statuses with media attachments.
// default: false
// in: query
// required: false
+// - name: only_public
+// type: boolean
+// description: Show only statuses with a privacy setting of 'public'.
+// default: false
+// in: query
+// required: false
//
// security:
// - OAuth2 Bearer:
@@ -143,6 +155,12 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
maxID = maxIDString
}
+ minID := ""
+ minIDString := c.Query(MinIDKey)
+ if minIDString != "" {
+ minID = minIDString
+ }
+
pinnedOnly := false
pinnedString := c.Query(PinnedKey)
if pinnedString != "" {
@@ -156,7 +174,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
}
mediaOnly := false
- mediaOnlyString := c.Query(MediaOnlyKey)
+ mediaOnlyString := c.Query(OnlyMediaKey)
if mediaOnlyString != "" {
i, err := strconv.ParseBool(mediaOnlyString)
if err != nil {
@@ -167,7 +185,19 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
mediaOnly = i
}
- statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
+ publicOnly := false
+ publicOnlyString := c.Query(OnlyPublicKey)
+ if mediaOnlyString != "" {
+ i, err := strconv.ParseBool(publicOnlyString)
+ if err != nil {
+ l.Debugf("error parsing public only string: %s", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"})
+ return
+ }
+ mediaOnly = i
+ }
+
+ statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if errWithCode != nil {
l.Debugf("error from processor account statuses get: %s", errWithCode)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
diff --git a/internal/api/s2s/user/common.go b/internal/api/s2s/user/common.go
index e96082c3e..9f426274d 100644
--- a/internal/api/s2s/user/common.go
+++ b/internal/api/s2s/user/common.go
@@ -57,3 +57,41 @@ func negotiateFormat(c *gin.Context) (string, error) {
}
return format, nil
}
+
+// SwaggerCollection represents an activitypub collection.
+// swagger:model swaggerCollection
+type SwaggerCollection struct {
+ // ActivityStreams context.
+ // example: https://www.w3.org/ns/activitystreams
+ Context string `json:"@context"`
+ // ActivityStreams ID.
+ // example: https://example.org/users/some_user/statuses/106717595988259568/replies
+ ID string `json:"id"`
+ // ActivityStreams type.
+ // example: Collection
+ Type string `json:"type"`
+ // ActivityStreams first property.
+ First SwaggerCollectionPage `json:"first"`
+ // ActivityStreams last property.
+ Last SwaggerCollectionPage `json:"last,omitempty"`
+}
+
+// SwaggerCollectionPage represents one page of a collection.
+// swagger:model swaggerCollectionPage
+type SwaggerCollectionPage struct {
+ // ActivityStreams ID.
+ // example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true
+ ID string `json:"id"`
+ // ActivityStreams type.
+ // example: CollectionPage
+ Type string `json:"type"`
+ // Link to the next page.
+ // example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true
+ Next string `json:"next"`
+ // Collection this page belongs to.
+ // example: https://example.org/users/some_user/statuses/106717595988259568/replies
+ PartOf string `json:"partOf"`
+ // Items on this page.
+ // example: ["https://example.org/users/some_other_user/statuses/086417595981111564", "https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R"]
+ Items []string `json:"items"`
+}
diff --git a/internal/api/s2s/user/inboxpost_test.go b/internal/api/s2s/user/inboxpost_test.go
index 79c44116d..554f3d729 100644
--- a/internal/api/s2s/user/inboxpost_test.go
+++ b/internal/api/s2s/user/inboxpost_test.go
@@ -436,7 +436,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
suite.ErrorIs(err, db.ErrNoEntries)
// no statuses from foss satan should be left in the database
- dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", false, false)
+ dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false)
suite.ErrorIs(err, db.ErrNoEntries)
suite.Empty(dbStatuses)
diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/s2s/user/outboxget.go
new file mode 100644
index 000000000..46f9d2ded
--- /dev/null
+++ b/internal/api/s2s/user/outboxget.go
@@ -0,0 +1,142 @@
+/*
+ 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 user
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+)
+
+// OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet
+//
+// Get the public outbox collection for an actor.
+//
+// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`.
+//
+// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`.
+//
+// HTTP signature is required on the request.
+//
+// ---
+// tags:
+// - s2s/federation
+//
+// produces:
+// - application/activity+json
+//
+// parameters:
+// - name: username
+// type: string
+// description: Username of the account.
+// in: path
+// required: true
+// - name: page
+// type: boolean
+// description: Return response as a CollectionPage.
+// in: query
+// default: false
+// - name: min_id
+// type: string
+// description: Minimum ID of the next status, used for paging.
+// in: query
+// - name: max_id
+// type: string
+// description: Maximum ID of the next status, used for paging.
+// in: query
+//
+// responses:
+// '200':
+// in: body
+// schema:
+// "$ref": "#/definitions/swaggerCollection"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+func (m *Module) OutboxGETHandler(c *gin.Context) {
+ l := logrus.WithFields(logrus.Fields{
+ "func": "OutboxGETHandler",
+ "url": c.Request.RequestURI,
+ })
+
+ requestedUsername := c.Param(UsernameKey)
+ if requestedUsername == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ return
+ }
+
+ page := false
+ pageString := c.Query(PageKey)
+ if pageString != "" {
+ i, err := strconv.ParseBool(pageString)
+ if err != nil {
+ l.Debugf("error parsing page string: %s", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"})
+ return
+ }
+ page = i
+ }
+
+ minID := ""
+ minIDString := c.Query(MinIDKey)
+ if minIDString != "" {
+ minID = minIDString
+ }
+
+ maxID := ""
+ maxIDString := c.Query(MaxIDKey)
+ if maxIDString != "" {
+ maxID = maxIDString
+ }
+
+ format, err := negotiateFormat(c)
+ if err != nil {
+ c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)})
+ return
+ }
+ l.Tracef("negotiated format: %s", format)
+
+ ctx := transferContext(c)
+
+ outbox, errWithCode := m.processor.GetFediOutbox(ctx, requestedUsername, page, maxID, minID, c.Request.URL)
+ if errWithCode != nil {
+ l.Info(errWithCode.Error())
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ return
+ }
+
+ b, mErr := json.Marshal(outbox)
+ if mErr != nil {
+ err := fmt.Errorf("could not marshal json: %s", mErr)
+ l.Error(err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.Data(http.StatusOK, format, b)
+}
diff --git a/internal/api/s2s/user/outboxget_test.go b/internal/api/s2s/user/outboxget_test.go
new file mode 100644
index 000000000..f1818683e
--- /dev/null
+++ b/internal/api/s2s/user/outboxget_test.go
@@ -0,0 +1,206 @@
+/*
+ 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 user_test
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-fed/activity/streams"
+ "github.com/go-fed/activity/streams/vocab"
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type OutboxGetTestSuite struct {
+ UserStandardTestSuite
+}
+
+func (suite *OutboxGetTestSuite) TestGetOutbox() {
+ // the dereference we're gonna use
+ derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
+ signedRequest := derefRequests["foss_satan_dereference_zork_outbox"]
+ targetAccount := suite.testAccounts["local_account_1"]
+
+ tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db)
+ federator := testrig.NewTestFederator(suite.db, tc, suite.storage)
+ processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
+ userModule := user.New(suite.config, processor).(*user.Module)
+
+ // setup request
+ recorder := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(recorder)
+ ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI, nil) // the endpoint we're hitting
+ ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
+ ctx.Request.Header.Set("Date", signedRequest.DateHeader)
+
+ // we need to pass the context through signature check first to set appropriate values on it
+ suite.securityModule.SignatureCheck(ctx)
+
+ // normally the router would populate these params from the path values,
+ // but because we're calling the function directly, we need to set them manually.
+ ctx.Params = gin.Params{
+ gin.Param{
+ Key: user.UsernameKey,
+ Value: targetAccount.Username,
+ },
+ }
+
+ // trigger the function being tested
+ userModule.OutboxGETHandler(ctx)
+
+ // check response
+ suite.EqualValues(http.StatusOK, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+ b, err := ioutil.ReadAll(result.Body)
+ suite.NoError(err)
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","first":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","id":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollection"}`, string(b))
+
+ m := make(map[string]interface{})
+ err = json.Unmarshal(b, &m)
+ suite.NoError(err)
+
+ t, err := streams.ToType(context.Background(), m)
+ suite.NoError(err)
+
+ _, ok := t.(vocab.ActivityStreamsOrderedCollection)
+ suite.True(ok)
+}
+
+func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
+ // the dereference we're gonna use
+ derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
+ signedRequest := derefRequests["foss_satan_dereference_zork_outbox_first"]
+ targetAccount := suite.testAccounts["local_account_1"]
+
+ tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db)
+ federator := testrig.NewTestFederator(suite.db, tc, suite.storage)
+ processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
+ userModule := user.New(suite.config, processor).(*user.Module)
+
+ // setup request
+ recorder := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(recorder)
+ ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true", nil) // the endpoint we're hitting
+ ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
+ ctx.Request.Header.Set("Date", signedRequest.DateHeader)
+
+ // we need to pass the context through signature check first to set appropriate values on it
+ suite.securityModule.SignatureCheck(ctx)
+
+ // normally the router would populate these params from the path values,
+ // but because we're calling the function directly, we need to set them manually.
+ ctx.Params = gin.Params{
+ gin.Param{
+ Key: user.UsernameKey,
+ Value: targetAccount.Username,
+ },
+ }
+
+ // trigger the function being tested
+ userModule.OutboxGETHandler(ctx)
+
+ // check response
+ suite.EqualValues(http.StatusOK, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+ b, err := ioutil.ReadAll(result.Body)
+ suite.NoError(err)
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","next":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":{"actor":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity","object":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY","published":"2021-10-20T10:40:37Z","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"},"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","prev":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026min_id=01F8MHAMCHF6Y650WCRSCP4WMY","type":"OrderedCollectionPage"}`, string(b))
+
+ m := make(map[string]interface{})
+ err = json.Unmarshal(b, &m)
+ suite.NoError(err)
+
+ t, err := streams.ToType(context.Background(), m)
+ suite.NoError(err)
+
+ _, ok := t.(vocab.ActivityStreamsOrderedCollectionPage)
+ suite.True(ok)
+}
+
+func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
+ // the dereference we're gonna use
+ derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
+ signedRequest := derefRequests["foss_satan_dereference_zork_outbox_next"]
+ targetAccount := suite.testAccounts["local_account_1"]
+
+ tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db)
+ federator := testrig.NewTestFederator(suite.db, tc, suite.storage)
+ processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
+ userModule := user.New(suite.config, processor).(*user.Module)
+
+ // setup request
+ recorder := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(recorder)
+ ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true&max_id=01F8MHAMCHF6Y650WCRSCP4WMY", nil) // the endpoint we're hitting
+ ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
+ ctx.Request.Header.Set("Date", signedRequest.DateHeader)
+
+ // we need to pass the context through signature check first to set appropriate values on it
+ suite.securityModule.SignatureCheck(ctx)
+
+ // normally the router would populate these params from the path values,
+ // but because we're calling the function directly, we need to set them manually.
+ ctx.Params = gin.Params{
+ gin.Param{
+ Key: user.UsernameKey,
+ Value: targetAccount.Username,
+ },
+ gin.Param{
+ Key: user.MaxIDKey,
+ Value: "01F8MHAMCHF6Y650WCRSCP4WMY",
+ },
+ }
+
+ // trigger the function being tested
+ userModule.OutboxGETHandler(ctx)
+
+ // check response
+ suite.EqualValues(http.StatusOK, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+ b, err := ioutil.ReadAll(result.Body)
+ suite.NoError(err)
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026maxID=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":[],"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollectionPage"}`, string(b))
+
+ m := make(map[string]interface{})
+ err = json.Unmarshal(b, &m)
+ suite.NoError(err)
+
+ t, err := streams.ToType(context.Background(), m)
+ suite.NoError(err)
+
+ _, ok := t.(vocab.ActivityStreamsOrderedCollectionPage)
+ suite.True(ok)
+}
+
+func TestOutboxGetTestSuite(t *testing.T) {
+ suite.Run(t, new(OutboxGetTestSuite))
+}
diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go
index b24dead31..e6328a26b 100644
--- a/internal/api/s2s/user/repliesget.go
+++ b/internal/api/s2s/user/repliesget.go
@@ -75,7 +75,7 @@ import (
// '200':
// in: body
// schema:
-// "$ref": "#/definitions/swaggerStatusRepliesCollection"
+// "$ref": "#/definitions/swaggerCollection"
// '400':
// description: bad request
// '401':
@@ -158,39 +158,3 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
c.Data(http.StatusOK, format, b)
}
-
-// SwaggerStatusRepliesCollection represents a response to GET /users/{username}/statuses/{status}/replies.
-// swagger:model swaggerStatusRepliesCollection
-type SwaggerStatusRepliesCollection struct {
- // ActivityStreams context.
- // example: https://www.w3.org/ns/activitystreams
- Context string `json:"@context"`
- // ActivityStreams ID.
- // example: https://example.org/users/some_user/statuses/106717595988259568/replies
- ID string `json:"id"`
- // ActivityStreams type.
- // example: Collection
- Type string `json:"type"`
- // ActivityStreams first property.
- First SwaggerStatusRepliesCollectionPage `json:"first"`
-}
-
-// SwaggerStatusRepliesCollectionPage represents one page of a collection.
-// swagger:model swaggerStatusRepliesCollectionPage
-type SwaggerStatusRepliesCollectionPage struct {
- // ActivityStreams ID.
- // example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true
- ID string `json:"id"`
- // ActivityStreams type.
- // example: CollectionPage
- Type string `json:"type"`
- // Link to the next page.
- // example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true
- Next string `json:"next"`
- // Collection this page belongs to.
- // example: https://example.org/users/some_user/statuses/106717595988259568/replies
- PartOf string `json:"partOf"`
- // Items on this page.
- // example: ["https://example.org/users/some_other_user/statuses/086417595981111564", "https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R"]
- Items []string `json:"items"`
-}
diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go
index ef0057f98..56b940f1d 100644
--- a/internal/api/s2s/user/user.go
+++ b/internal/api/s2s/user/user.go
@@ -37,6 +37,8 @@ const (
OnlyOtherAccountsKey = "only_other_accounts"
// MinIDKey is for filtering status responses.
MinIDKey = "min_id"
+ // MaxIDKey is for filtering status responses.
+ MaxIDKey = "max_id"
// PageKey is for filtering status responses.
PageKey = "page"
@@ -50,6 +52,8 @@ const (
UsersPublicKeyPath = UsersBasePathWithUsername + "/" + util.PublicKeyPath
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
+ // UsersOutboxPath is for serving GET requests to a user's outbox with the given username key.
+ UsersOutboxPath = UsersBasePathWithUsername + "/" + util.OutboxPath
// UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key.
UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath
// UsersFollowingPath is for serving GET request's to a user's following list, with the given username key.
@@ -83,5 +87,6 @@ func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler)
s.AttachHandler(http.MethodGet, UsersStatusRepliesPath, m.StatusRepliesGETHandler)
+ s.AttachHandler(http.MethodGet, UsersOutboxPath, m.OutboxGETHandler)
return nil
}
diff --git a/internal/db/account.go b/internal/db/account.go
index 058a89859..0d52ce39d 100644
--- a/internal/db/account.go
+++ b/internal/db/account.go
@@ -52,7 +52,7 @@ type Account interface {
// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can
// be very memory intensive so you probably shouldn't do this!
// In case of no entries, a 'no entries' error will be returned
- GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, Error)
+ GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error)
GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error)
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 745e41567..9def811fb 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -231,7 +231,7 @@ func (a *accountDB) CountAccountStatuses(ctx context.Context, accountID string)
Count(ctx)
}
-func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, db.Error) {
+func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) {
statuses := []*gtsmodel.Status{}
q := a.conn.
@@ -247,14 +247,22 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
q = q.Limit(limit)
}
- if pinnedOnly {
- q = q.Where("pinned = ?", true)
+ if excludeReplies {
+ q = q.WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id"))
}
if maxID != "" {
q = q.Where("id < ?", maxID)
}
+ if minID != "" {
+ q = q.Where("id > ?", minID)
+ }
+
+ if pinnedOnly {
+ q = q.Where("pinned = ?", true)
+ }
+
if mediaOnly {
q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.
@@ -263,8 +271,8 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
})
}
- if excludeReplies {
- q = q.WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id"))
+ if publicOnly {
+ q = q.Where("visibility = ?", gtsmodel.VisibilityPublic)
}
if err := q.Scan(ctx); err != nil {
diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go
index 3fd9653af..34a103a49 100644
--- a/internal/federation/federatingdb/util.go
+++ b/internal/federation/federatingdb/util.go
@@ -135,6 +135,19 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,
return idProp.GetIRI(), nil
}
}
+ case ap.ActivityCreate:
+ // CREATE
+ // ID might already be set on a Create, so check it here and return it if it is
+ create, ok := t.(vocab.ActivityStreamsCreate)
+ if !ok {
+ return nil, errors.New("newid: create couldn't be parsed into vocab.ActivityStreamsCreate")
+ }
+ idProp := create.GetJSONLDId()
+ if idProp != nil {
+ if idProp.IsIRI() {
+ return idProp.GetIRI(), nil
+ }
+ }
case ap.ActivityAnnounce:
// ANNOUNCE aka BOOST
// ID might already be set on an announce we've created, so check it here and return it if it is
diff --git a/internal/processing/account.go b/internal/processing/account.go
index 94ba596ac..f1c119083 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -38,8 +38,8 @@ func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form
return p.accountProcessor.Update(ctx, authed.Account, form)
}
-func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
- return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
+func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
+ return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
}
func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index e88cd3a94..4e807540c 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -50,7 +50,7 @@ type Processor interface {
Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
- StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
+ StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers.
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// FollowingGet fetches a list of the accounts that target account is following.
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
index 4be0c859e..20198e8ef 100644
--- a/internal/processing/account/delete.go
+++ b/internal/processing/account/delete.go
@@ -139,7 +139,7 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
var maxID string
selectStatusesLoop:
for {
- statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, false, false)
+ statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, "", false, false, false)
if err != nil {
if err == db.ErrNoEntries {
// no statuses left for this instance so we're done
diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go
index 56b5b0eae..bd327cae6 100644
--- a/internal/processing/account/getstatuses.go
+++ b/internal/processing/account/getstatuses.go
@@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
+func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
@@ -37,7 +37,7 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
apiStatuses := []apimodel.Status{}
- statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
+ statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if err != nil {
if err == db.ErrNoEntries {
return apiStatuses, nil
diff --git a/internal/processing/federation.go b/internal/processing/federation.go
index 1336a6e46..023930494 100644
--- a/internal/processing/federation.go
+++ b/internal/processing/federation.go
@@ -20,418 +20,49 @@ package processing
import (
"context"
- "errors"
- "fmt"
"net/http"
"net/url"
- "github.com/go-fed/activity/streams"
- "github.com/go-fed/activity/streams/vocab"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- var requestedPerson vocab.ActivityStreamsPerson
- if util.IsPublicKeyPath(requestURL) {
- // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
- requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- } else if util.IsUserPath(requestURL) {
- // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
- requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if err != nil || !authenticated {
- return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
- }
-
- // if we're not already handshaking/dereferencing a remote account, dereference it now
- if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) {
- requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
- }
-
- requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- } else {
- return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
- }
-
- data, err := streams.Serialize(requestedPerson)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
+ return p.federationProcessor.GetUser(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if err != nil || !authenticated {
- return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
- }
-
- requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedAccountURI, err := url.Parse(requestedAccount.URI)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
- }
-
- requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
- }
-
- data, err := streams.Serialize(requestedFollowers)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
+ return p.federationProcessor.GetFollowers(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if err != nil || !authenticated {
- return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
- }
-
- requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedAccountURI, err := url.Parse(requestedAccount.URI)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
- }
-
- requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
- }
-
- data, err := streams.Serialize(requestedFollowing)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
+ return p.federationProcessor.GetFollowing(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if err != nil || !authenticated {
- return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
- }
-
- requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- // authorize the request:
- // 1. check if a block exists between the requester and the requestee
- blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- // get the status out of the database here
- s := >smodel.Status{}
- if err := p.db.GetWhere(ctx, []db.Where{
- {Key: "id", Value: requestedStatusID},
- {Key: "account_id", Value: requestedAccount.ID},
- }, s); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
- }
-
- visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if !visible {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
- }
-
- // requester is authorized to view the status, so convert it to AP representation and serialize it
- asStatus, err := p.tc.StatusToAS(ctx, s)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- data, err := streams.Serialize(asStatus)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
+ return p.federationProcessor.GetStatus(ctx, requestedUsername, requestedStatusID, requestURL)
}
func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
+ return p.federationProcessor.GetStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, requestURL)
+}
- // authenticate the request
- requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if err != nil || !authenticated {
- return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
- }
-
- requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- // authorize the request:
- // 1. check if a block exists between the requester and the requestee
- blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- // get the status out of the database here
- s := >smodel.Status{}
- if err := p.db.GetWhere(ctx, []db.Where{
- {Key: "id", Value: requestedStatusID},
- {Key: "account_id", Value: requestedAccount.ID},
- }, s); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
- }
-
- visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if !visible {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
- }
-
- var data map[string]interface{}
-
- // now there are three scenarios:
- // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
- // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items.
- // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items!
-
- if !page {
- // scenario 1
-
- // get the collection
- collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- data, err = streams.Serialize(collection)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- } else if page && requestURL.Query().Get("only_other_accounts") == "" {
- // scenario 2
-
- // get the collection
- collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- // but only return the first page
- data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage())
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- } else {
- // scenario 3
- // get immediate children
- replies, err := p.db.GetStatusChildren(ctx, s, true, minID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // filter children and extract URIs
- replyURIs := map[string]*url.URL{}
- for _, r := range replies {
- // only show public or unlocked statuses as replies
- if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked {
- continue
- }
-
- // respect onlyOtherAccounts parameter
- if onlyOtherAccounts && r.AccountID == requestedAccount.ID {
- continue
- }
-
- // only show replies that the status owner can see
- visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount)
- if err != nil || !visibleToStatusOwner {
- continue
- }
-
- // only show replies that the requester can see
- visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount)
- if err != nil || !visibleToRequester {
- continue
- }
-
- rURI, err := url.Parse(r.URI)
- if err != nil {
- continue
- }
-
- replyURIs[r.ID] = rURI
- }
-
- repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- data, err = streams.Serialize(repliesPage)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- }
-
- return data, nil
+func (p *processor) GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL)
}
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
- // get the account the request is referring to
- requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // return the webfinger representation
- return &apimodel.WellKnownResponse{
- Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.AccountDomain),
- Aliases: []string{
- requestedAccount.URI,
- requestedAccount.URL,
- },
- Links: []apimodel.Link{
- {
- Rel: "http://webfinger.net/rel/profile-page",
- Type: "text/html",
- Href: requestedAccount.URL,
- },
- {
- Rel: "self",
- Type: "application/activity+json",
- Href: requestedAccount.URI,
- },
- },
- }, nil
+ return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername)
}
func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) {
- return &apimodel.WellKnownResponse{
- Links: []apimodel.Link{
- {
- Rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
- Href: fmt.Sprintf("%s://%s/nodeinfo/2.0", p.config.Protocol, p.config.Host),
- },
- },
- }, nil
+ return p.federationProcessor.GetNodeInfoRel(ctx, request)
}
func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) {
- return &apimodel.Nodeinfo{
- Version: "2.0",
- Software: apimodel.NodeInfoSoftware{
- Name: "gotosocial",
- Version: p.config.SoftwareVersion,
- },
- Protocols: []string{"activitypub"},
- Services: apimodel.NodeInfoServices{
- Inbound: []string{},
- Outbound: []string{},
- },
- OpenRegistrations: p.config.AccountsConfig.OpenRegistration,
- Usage: apimodel.NodeInfoUsage{
- Users: apimodel.NodeInfoUsers{},
- },
- Metadata: make(map[string]interface{}),
- }, nil
+ return p.federationProcessor.GetNodeInfo(ctx, request)
}
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
- contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
- return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
+ return p.federationProcessor.PostInbox(ctx, w, r)
}
diff --git a/internal/processing/federation/federation.go b/internal/processing/federation/federation.go
new file mode 100644
index 000000000..b050406c5
--- /dev/null
+++ b/internal/processing/federation/federation.go
@@ -0,0 +1,103 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+)
+
+// Processor wraps functions for processing federation API requests.
+type Processor interface {
+ // GetUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
+ // before returning a JSON serializable interface to the caller.
+ GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // GetFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // GetFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // GetStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
+ GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
+
+ // GetNodeInfoRel returns a well known response giving the path to node info.
+ GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
+
+ // GetNodeInfo returns a node info struct in response to a node info request.
+ GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode)
+
+ // GetOutbox returns the activitypub representation of a local user's outbox.
+ // This contains links to PUBLIC posts made by this user.
+ GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
+
+ // PostInbox handles POST requests to a user's inbox for new activitypub messages.
+ //
+ // PostInbox returns true if the request was handled as an ActivityPub POST to an actor's inbox.
+ // If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
+ //
+ // If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
+ //
+ // If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
+ //
+ // If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
+ PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
+}
+
+type processor struct {
+ db db.DB
+ config *config.Config
+ federator federation.Federator
+ tc typeutils.TypeConverter
+ filter visibility.Filter
+ fromFederator chan messages.FromFederator
+}
+
+// New returns a new federation processor.
+func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, federator federation.Federator, fromFederator chan messages.FromFederator) Processor {
+ return &processor{
+ db: db,
+ config: config,
+ federator: federator,
+ tc: tc,
+ filter: visibility.NewFilter(db),
+ fromFederator: fromFederator,
+ }
+}
diff --git a/internal/processing/federation/getfollowers.go b/internal/processing/federation/getfollowers.go
new file mode 100644
index 000000000..b17c90e07
--- /dev/null
+++ b/internal/processing/federation/getfollowers.go
@@ -0,0 +1,74 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedAccountURI, err := url.Parse(requestedAccount.URI)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
+ }
+
+ requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
+ }
+
+ data, err := streams.Serialize(requestedFollowers)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getfollowing.go b/internal/processing/federation/getfollowing.go
new file mode 100644
index 000000000..e2d50d238
--- /dev/null
+++ b/internal/processing/federation/getfollowing.go
@@ -0,0 +1,74 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedAccountURI, err := url.Parse(requestedAccount.URI)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
+ }
+
+ requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
+ }
+
+ data, err := streams.Serialize(requestedFollowing)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getnodeinfo.go b/internal/processing/federation/getnodeinfo.go
new file mode 100644
index 000000000..4eb8ad3b7
--- /dev/null
+++ b/internal/processing/federation/getnodeinfo.go
@@ -0,0 +1,69 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+const (
+ nodeInfoVersion = "2.0"
+ nodeInfoSoftwareName = "gotosocial"
+)
+
+var (
+ nodeInfoRel = fmt.Sprintf("http://nodeinfo.diaspora.software/ns/schema/%s", nodeInfoVersion)
+ nodeInfoProtocols = []string{"activitypub"}
+)
+
+func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) {
+ return &apimodel.WellKnownResponse{
+ Links: []apimodel.Link{
+ {
+ Rel: nodeInfoRel,
+ Href: fmt.Sprintf("%s://%s/nodeinfo/%s", p.config.Protocol, p.config.Host, nodeInfoVersion),
+ },
+ },
+ }, nil
+}
+
+func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) {
+ return &apimodel.Nodeinfo{
+ Version: nodeInfoVersion,
+ Software: apimodel.NodeInfoSoftware{
+ Name: nodeInfoSoftwareName,
+ Version: p.config.SoftwareVersion,
+ },
+ Protocols: nodeInfoProtocols,
+ Services: apimodel.NodeInfoServices{
+ Inbound: []string{},
+ Outbound: []string{},
+ },
+ OpenRegistrations: p.config.AccountsConfig.OpenRegistration,
+ Usage: apimodel.NodeInfoUsage{
+ Users: apimodel.NodeInfoUsers{},
+ },
+ Metadata: make(map[string]interface{}),
+ }, nil
+}
diff --git a/internal/processing/federation/getoutbox.go b/internal/processing/federation/getoutbox.go
new file mode 100644
index 000000000..a3b2cff3c
--- /dev/null
+++ b/internal/processing/federation/getoutbox.go
@@ -0,0 +1,107 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ // authorize the request:
+ // 1. check if a block exists between the requester and the requestee
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ var data map[string]interface{}
+ // now there are two scenarios:
+ // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
+ // 2. we're asked for a specific page; this can be either the first page or any other page
+
+ if !page {
+ /*
+ scenario 1: return the collection with no items
+ we want something that looks like this:
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.org/users/whatever/outbox",
+ "type": "OrderedCollection",
+ "first": "https://example.org/users/whatever/outbox?page=true",
+ "last": "https://example.org/users/whatever/outbox?min_id=0&page=true"
+ }
+ */
+ collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err = streams.Serialize(collection)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+ }
+
+ // scenario 2 -- get the requested page
+ // limit pages to 30 entries per page
+ publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, maxID, minID, false, false, true)
+ if err != nil && err != db.ErrNoEntries {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ data, err = streams.Serialize(outboxPage)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getstatus.go b/internal/processing/federation/getstatus.go
new file mode 100644
index 000000000..a4f251023
--- /dev/null
+++ b/internal/processing/federation/getstatus.go
@@ -0,0 +1,91 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ // authorize the request:
+ // 1. check if a block exists between the requester and the requestee
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ // get the status out of the database here
+ s := >smodel.Status{}
+ if err := p.db.GetWhere(ctx, []db.Where{
+ {Key: "id", Value: requestedStatusID},
+ {Key: "account_id", Value: requestedAccount.ID},
+ }, s); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
+ }
+
+ // requester is authorized to view the status, so convert it to AP representation and serialize it
+ asStatus, err := p.tc.StatusToAS(ctx, s)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err := streams.Serialize(asStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getstatusreplies.go b/internal/processing/federation/getstatusreplies.go
new file mode 100644
index 000000000..0fa0cc386
--- /dev/null
+++ b/internal/processing/federation/getstatusreplies.go
@@ -0,0 +1,164 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ // authorize the request:
+ // 1. check if a block exists between the requester and the requestee
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ // get the status out of the database here
+ s := >smodel.Status{}
+ if err := p.db.GetWhere(ctx, []db.Where{
+ {Key: "id", Value: requestedStatusID},
+ {Key: "account_id", Value: requestedAccount.ID},
+ }, s); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
+ }
+
+ var data map[string]interface{}
+
+ // now there are three scenarios:
+ // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
+ // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items.
+ // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items!
+
+ if !page {
+ // scenario 1
+
+ // get the collection
+ collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err = streams.Serialize(collection)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else if page && requestURL.Query().Get("only_other_accounts") == "" {
+ // scenario 2
+
+ // get the collection
+ collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ // but only return the first page
+ data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage())
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else {
+ // scenario 3
+ // get immediate children
+ replies, err := p.db.GetStatusChildren(ctx, s, true, minID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // filter children and extract URIs
+ replyURIs := map[string]*url.URL{}
+ for _, r := range replies {
+ // only show public or unlocked statuses as replies
+ if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked {
+ continue
+ }
+
+ // respect onlyOtherAccounts parameter
+ if onlyOtherAccounts && r.AccountID == requestedAccount.ID {
+ continue
+ }
+
+ // only show replies that the status owner can see
+ visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount)
+ if err != nil || !visibleToStatusOwner {
+ continue
+ }
+
+ // only show replies that the requester can see
+ visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount)
+ if err != nil || !visibleToRequester {
+ continue
+ }
+
+ rURI, err := url.Parse(r.URI)
+ if err != nil {
+ continue
+ }
+
+ replyURIs[r.ID] = rURI
+ }
+
+ repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ data, err = streams.Serialize(repliesPage)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getuser.go b/internal/processing/federation/getuser.go
new file mode 100644
index 000000000..0d80e528e
--- /dev/null
+++ b/internal/processing/federation/getuser.go
@@ -0,0 +1,85 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/go-fed/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ var requestedPerson vocab.ActivityStreamsPerson
+ if util.IsPublicKeyPath(requestURL) {
+ // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
+ requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else if util.IsUserPath(requestURL) {
+ // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
+ }
+
+ // if we're not already handshaking/dereferencing a remote account, dereference it now
+ if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) {
+ requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+ }
+
+ requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else {
+ return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
+ }
+
+ data, err := streams.Serialize(requestedPerson)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/processing/federation/getwebfinger.go b/internal/processing/federation/getwebfinger.go
new file mode 100644
index 000000000..aaa7687be
--- /dev/null
+++ b/internal/processing/federation/getwebfinger.go
@@ -0,0 +1,64 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+const (
+ webfingerProfilePage = "http://webfinger.net/rel/profile-page"
+ webFingerProfilePageContentType = "text/html"
+ webfingerSelf = "self"
+ webFingerSelfContentType = "application/activity+json"
+ webfingerAccount = "acct"
+)
+
+func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
+ // get the account the request is referring to
+ requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // return the webfinger representation
+ return &apimodel.WellKnownResponse{
+ Subject: fmt.Sprintf("%s:%s@%s", webfingerAccount, requestedAccount.Username, p.config.AccountDomain),
+ Aliases: []string{
+ requestedAccount.URI,
+ requestedAccount.URL,
+ },
+ Links: []apimodel.Link{
+ {
+ Rel: webfingerProfilePage,
+ Type: webFingerProfilePageContentType,
+ Href: requestedAccount.URL,
+ },
+ {
+ Rel: webfingerSelf,
+ Type: webFingerSelfContentType,
+ Href: requestedAccount.URI,
+ },
+ },
+ }, nil
+}
diff --git a/internal/processing/federation/postinbox.go b/internal/processing/federation/postinbox.go
new file mode 100644
index 000000000..df9da0a51
--- /dev/null
+++ b/internal/processing/federation/postinbox.go
@@ -0,0 +1,32 @@
+/*
+ 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 federation
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ // pass the fromFederator channel through to postInbox, since it'll be needed later
+ contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
+ return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
+}
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
index d4e8f5fa5..3054fbf57 100644
--- a/internal/processing/fromclientapi.go
+++ b/internal/processing/fromclientapi.go
@@ -269,12 +269,17 @@ func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status)
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
}
+ create, err := p.tc.WrapNoteInCreate(asStatus, false)
+ if err != nil {
+ return fmt.Errorf("federateStatus: error wrapping status in create: %s", err)
+ }
+
outboxIRI, err := url.Parse(status.Account.OutboxURI)
if err != nil {
return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.Account.OutboxURI, err)
}
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, asStatus)
+ _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, create)
return err
}
diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go
index 09519d1d3..6846357d1 100644
--- a/internal/processing/fromfederator_test.go
+++ b/internal/processing/fromfederator_test.go
@@ -339,7 +339,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
suite.False(zorkFollowsSatan)
// no statuses from foss satan should be left in the database
- dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", false, false)
+ dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false)
suite.ErrorIs(err, db.ErrNoEntries)
suite.Empty(dbStatuses)
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 8fd372735..d0a636b27 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
+ federationProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/federation"
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
@@ -78,7 +79,7 @@ type Processor interface {
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
- AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
+ AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
// AccountFollowersGet fetches a list of the target account's followers.
AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// AccountFollowingGet fetches a list of the accounts that target account is following.
@@ -190,32 +191,26 @@ type Processor interface {
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
// before returning a JSON serializable interface to the caller.
GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
-
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
-
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
-
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
-
// GetFediStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
-
+ // GetFediOutbox returns the public outbox of the requested user, with the given parameters.
+ GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
-
// GetNodeInfoRel returns a well known response giving the path to node info.
GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
-
// GetNodeInfo returns a node info struct in response to a node info request.
GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode)
-
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
//
// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox.
@@ -248,12 +243,13 @@ type processor struct {
SUB-PROCESSORS
*/
- accountProcessor account.Processor
- adminProcessor admin.Processor
- statusProcessor status.Processor
- streamingProcessor streaming.Processor
- mediaProcessor mediaProcessor.Processor
- userProcessor user.Processor
+ accountProcessor account.Processor
+ adminProcessor admin.Processor
+ statusProcessor status.Processor
+ streamingProcessor streaming.Processor
+ mediaProcessor mediaProcessor.Processor
+ userProcessor user.Processor
+ federationProcessor federationProcessor.Processor
}
// NewProcessor returns a new Processor that uses the given federator
@@ -267,6 +263,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config)
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config)
userProcessor := user.New(db, config)
+ federationProcessor := federationProcessor.New(db, tc, config, federator, fromFederator)
return &processor{
fromClientAPI: fromClientAPI,
@@ -282,12 +279,13 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
db: db,
filter: visibility.NewFilter(db),
- accountProcessor: accountProcessor,
- adminProcessor: adminProcessor,
- statusProcessor: statusProcessor,
- streamingProcessor: streamingProcessor,
- mediaProcessor: mediaProcessor,
- userProcessor: userProcessor,
+ accountProcessor: accountProcessor,
+ adminProcessor: adminProcessor,
+ statusProcessor: statusProcessor,
+ streamingProcessor: streamingProcessor,
+ mediaProcessor: mediaProcessor,
+ userProcessor: userProcessor,
+ federationProcessor: federationProcessor,
}
}
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 812c37935..eeb5bead1 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -155,6 +155,19 @@ type TypeConverter interface {
StatusToASRepliesCollection(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool) (vocab.ActivityStreamsCollection, error)
// StatusURIsToASRepliesPage returns a collection page with appropriate next/part of pagination.
StatusURIsToASRepliesPage(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool, minID string, replies map[string]*url.URL) (vocab.ActivityStreamsCollectionPage, error)
+ // OutboxToASCollection returns an ordered collection with appropriate id, next, and last fields.
+ // The returned collection won't have any actual entries; just links to where entries can be obtained.
+ OutboxToASCollection(ctx context.Context, outboxID string) (vocab.ActivityStreamsOrderedCollection, error)
+ // StatusesToASOutboxPage returns an ordered collection page using the given statuses and parameters as contents.
+ //
+ // The maxID and minID should be the parameters that were passed to the database to obtain the given statuses.
+ // These will be used to create the 'id' field of the collection.
+ //
+ // OutboxID is used to create the 'partOf' field in the collection.
+ //
+ // Appropriate 'next' and 'prev' fields will be created based on the highest and lowest IDs present in the statuses slice.
+ StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error)
+
/*
INTERNAL (gts) MODEL TO INTERNAL MODEL
*/
@@ -170,6 +183,12 @@ type TypeConverter interface {
// WrapPersonInUpdate
WrapPersonInUpdate(person vocab.ActivityStreamsPerson, originAccount *gtsmodel.Account) (vocab.ActivityStreamsUpdate, error)
+ // WrapNoteInCreate wraps a Note with a Create activity.
+ //
+ // If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create,
+ // but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference,
+ // and still have control over whether or not they're allowed to actually see the contents.
+ WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error)
}
type converter struct {
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 73d274908..047d61b80 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -32,6 +32,13 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
+// const (
+// // highestID is the highest possible ULID
+// highestID = "ZZZZZZZZZZZZZZZZZZZZZZZZZZ"
+// // lowestID is the lowest possible ULID
+// lowestID = "00000000000000000000000000"
+// )
+
// Converts a gts model account into an Activity Streams person type.
func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
person := streams.NewActivityStreamsPerson()
@@ -1013,3 +1020,140 @@ func (c *converter) StatusURIsToASRepliesPage(ctx context.Context, status *gtsmo
return page, nil
}
+
+/*
+ the goal is to end up with something like this:
+ {
+ "id": "https://example.org/users/whatever/outbox?page=true",
+ "type": "OrderedCollectionPage",
+ "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
+ "prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
+ "partOf": "https://example.org/users/whatever/outbox",
+ "orderedItems": [
+ "id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity",
+ "type": "Create",
+ "actor": "https://example.org/users/whatever",
+ "published": "2021-10-18T20:06:18Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://example.org/users/whatever/followers"
+ ],
+ "object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7"
+ ]
+ }
+*/
+func (c *converter) StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error) {
+ page := streams.NewActivityStreamsOrderedCollectionPage()
+
+ // .id
+ pageIDProp := streams.NewJSONLDIdProperty()
+ pageID := fmt.Sprintf("%s?page=true", outboxID)
+ if minID != "" {
+ pageID = fmt.Sprintf("%s&minID=%s", pageID, minID)
+ }
+ if maxID != "" {
+ pageID = fmt.Sprintf("%s&maxID=%s", pageID, maxID)
+ }
+ pageIDURI, err := url.Parse(pageID)
+ if err != nil {
+ return nil, err
+ }
+ pageIDProp.SetIRI(pageIDURI)
+ page.SetJSONLDId(pageIDProp)
+
+ // .partOf
+ collectionIDURI, err := url.Parse(outboxID)
+ if err != nil {
+ return nil, err
+ }
+ partOfProp := streams.NewActivityStreamsPartOfProperty()
+ partOfProp.SetIRI(collectionIDURI)
+ page.SetActivityStreamsPartOf(partOfProp)
+
+ // .orderedItems
+ itemsProp := streams.NewActivityStreamsOrderedItemsProperty()
+ var highest string
+ var lowest string
+ for _, s := range statuses {
+ note, err := c.StatusToAS(ctx, s)
+ if err != nil {
+ return nil, err
+ }
+
+ create, err := c.WrapNoteInCreate(note, true)
+ if err != nil {
+ return nil, err
+ }
+
+ itemsProp.AppendActivityStreamsCreate(create)
+
+ if highest == "" || s.ID > highest {
+ highest = s.ID
+ }
+ if lowest == "" || s.ID < lowest {
+ lowest = s.ID
+ }
+ }
+ page.SetActivityStreamsOrderedItems(itemsProp)
+
+ // .next
+ if lowest != "" {
+ nextProp := streams.NewActivityStreamsNextProperty()
+ nextPropIDString := fmt.Sprintf("%s?page=true&max_id=%s", outboxID, lowest)
+ nextPropIDURI, err := url.Parse(nextPropIDString)
+ if err != nil {
+ return nil, err
+ }
+ nextProp.SetIRI(nextPropIDURI)
+ page.SetActivityStreamsNext(nextProp)
+ }
+
+ // .prev
+ if highest != "" {
+ prevProp := streams.NewActivityStreamsPrevProperty()
+ prevPropIDString := fmt.Sprintf("%s?page=true&min_id=%s", outboxID, highest)
+ prevPropIDURI, err := url.Parse(prevPropIDString)
+ if err != nil {
+ return nil, err
+ }
+ prevProp.SetIRI(prevPropIDURI)
+ page.SetActivityStreamsPrev(prevProp)
+ }
+
+ return page, nil
+}
+
+/*
+ we want something that looks like this:
+
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.org/users/whatever/outbox",
+ "type": "OrderedCollection",
+ "first": "https://example.org/users/whatever/outbox?page=true"
+ }
+*/
+func (c *converter) OutboxToASCollection(ctx context.Context, outboxID string) (vocab.ActivityStreamsOrderedCollection, error) {
+ collection := streams.NewActivityStreamsOrderedCollection()
+
+ collectionIDProp := streams.NewJSONLDIdProperty()
+ outboxIDURI, err := url.Parse(outboxID)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing url %s", outboxID)
+ }
+ collectionIDProp.SetIRI(outboxIDURI)
+ collection.SetJSONLDId(collectionIDProp)
+
+ collectionFirstProp := streams.NewActivityStreamsFirstProperty()
+ collectionFirstPropID := fmt.Sprintf("%s?page=true", outboxID)
+ collectionFirstPropIDURI, err := url.Parse(collectionFirstPropID)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing url %s", collectionFirstPropID)
+ }
+ collectionFirstProp.SetIRI(collectionFirstPropIDURI)
+ collection.SetActivityStreamsFirst(collectionFirstProp)
+
+ return collection, nil
+}
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 50f92e043..d8098098f 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -37,18 +37,98 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
testAccount := suite.testAccounts["local_account_1"] // take zork for this test
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
ser, err := streams.Serialize(asPerson)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
bytes, err := json.Marshal(ser)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
fmt.Println(string(bytes))
// TODO: write assertions here, rn we're just eyeballing the output
}
+func (suite *InternalToASTestSuite) TestOutboxToASCollection() {
+ testAccount := suite.testAccounts["admin_account"]
+ ctx := context.Background()
+
+ collection, err := suite.typeconverter.OutboxToASCollection(ctx, testAccount.OutboxURI)
+ suite.NoError(err)
+
+ ser, err := streams.Serialize(collection)
+ assert.NoError(suite.T(), err)
+
+ bytes, err := json.Marshal(ser)
+ suite.NoError(err)
+
+ /*
+ we want this:
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "first": "http://localhost:8080/users/admin/outbox?page=true",
+ "id": "http://localhost:8080/users/admin/outbox",
+ "type": "OrderedCollection"
+ }
+ */
+
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","first":"http://localhost:8080/users/admin/outbox?page=true","id":"http://localhost:8080/users/admin/outbox","type":"OrderedCollection"}`, string(bytes))
+}
+
+func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() {
+ testAccount := suite.testAccounts["admin_account"]
+ ctx := context.Background()
+
+ // get public statuses from testaccount
+ statuses, err := suite.db.GetAccountStatuses(ctx, testAccount.ID, 30, true, "", "", false, false, true)
+ suite.NoError(err)
+
+ page, err := suite.typeconverter.StatusesToASOutboxPage(ctx, testAccount.OutboxURI, "", "", statuses)
+ suite.NoError(err)
+
+ ser, err := streams.Serialize(page)
+ assert.NoError(suite.T(), err)
+
+ bytes, err := json.Marshal(ser)
+ suite.NoError(err)
+
+ /*
+
+ we want this:
+
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:8080/users/admin/outbox?page=true",
+ "next": "http://localhost:8080/users/admin/outbox?page=true&max_id=01F8MH75CBF9JFX4ZAD54N0W0R",
+ "orderedItems": [
+ {
+ "actor": "http://localhost:8080/users/admin",
+ "cc": "http://localhost:8080/users/admin/followers",
+ "id": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity",
+ "object": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
+ "published": "2021-10-20T12:36:45Z",
+ "to": "https://www.w3.org/ns/activitystreams#Public",
+ "type": "Create"
+ },
+ {
+ "actor": "http://localhost:8080/users/admin",
+ "cc": "http://localhost:8080/users/admin/followers",
+ "id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity",
+ "object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
+ "published": "2021-10-20T11:36:45Z",
+ "to": "https://www.w3.org/ns/activitystreams#Public",
+ "type": "Create"
+ }
+ ],
+ "partOf": "http://localhost:8080/users/admin/outbox",
+ "prev": "http://localhost:8080/users/admin/outbox?page=true&min_id=01F8MHAAY43M6RJ473VQFCVH37",
+ "type": "OrderedCollectionPage"
+ }
+ */
+
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/admin/outbox?page=true","next":"http://localhost:8080/users/admin/outbox?page=true\u0026max_id=01F8MH75CBF9JFX4ZAD54N0W0R","orderedItems":[{"actor":"http://localhost:8080/users/admin","cc":"http://localhost:8080/users/admin/followers","id":"http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity","object":"http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37","published":"2021-10-20T12:36:45Z","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"},{"actor":"http://localhost:8080/users/admin","cc":"http://localhost:8080/users/admin/followers","id":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity","object":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","published":"2021-10-20T11:36:45Z","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"}],"partOf":"http://localhost:8080/users/admin/outbox","prev":"http://localhost:8080/users/admin/outbox?page=true\u0026min_id=01F8MHAAY43M6RJ473VQFCVH37","type":"OrderedCollectionPage"}`, string(bytes))
+}
+
func TestInternalToASTestSuite(t *testing.T) {
suite.Run(t, new(InternalToASTestSuite))
}
diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go
index d6aa11cb4..e7c422eba 100644
--- a/internal/typeutils/wrap.go
+++ b/internal/typeutils/wrap.go
@@ -7,6 +7,7 @@ import (
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
@@ -66,3 +67,66 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
return update, nil
}
+
+func (c *converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error) {
+ create := streams.NewActivityStreamsCreate()
+
+ // Object property
+ objectProp := streams.NewActivityStreamsObjectProperty()
+ if objectIRIOnly {
+ objectProp.AppendIRI(note.GetJSONLDId().GetIRI())
+ } else {
+ objectProp.AppendActivityStreamsNote(note)
+ }
+ create.SetActivityStreamsObject(objectProp)
+
+ // ID property
+ idProp := streams.NewJSONLDIdProperty()
+ createID := fmt.Sprintf("%s/activity", note.GetJSONLDId().GetIRI().String())
+ createIDIRI, err := url.Parse(createID)
+ if err != nil {
+ return nil, err
+ }
+ idProp.SetIRI(createIDIRI)
+ create.SetJSONLDId(idProp)
+
+ // Actor Property
+ actorProp := streams.NewActivityStreamsActorProperty()
+ actorIRI, err := ap.ExtractAttributedTo(note)
+ if err != nil {
+ return nil, fmt.Errorf("WrapNoteInCreate: couldn't extract AttributedTo: %s", err)
+ }
+ actorProp.AppendIRI(actorIRI)
+ create.SetActivityStreamsActor(actorProp)
+
+ // Published Property
+ publishedProp := streams.NewActivityStreamsPublishedProperty()
+ published, err := ap.ExtractPublished(note)
+ if err != nil {
+ return nil, fmt.Errorf("WrapNoteInCreate: couldn't extract Published: %s", err)
+ }
+ publishedProp.Set(published)
+ create.SetActivityStreamsPublished(publishedProp)
+
+ // To Property
+ toProp := streams.NewActivityStreamsToProperty()
+ tos, err := ap.ExtractTos(note)
+ if err == nil {
+ for _, to := range tos {
+ toProp.AppendIRI(to)
+ }
+ create.SetActivityStreamsTo(toProp)
+ }
+
+ // Cc Property
+ ccProp := streams.NewActivityStreamsCcProperty()
+ ccs, err := ap.ExtractCCs(note)
+ if err == nil {
+ for _, cc := range ccs {
+ ccProp.AppendIRI(cc)
+ }
+ create.SetActivityStreamsCc(ccProp)
+ }
+
+ return create, nil
+}
diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go
new file mode 100644
index 000000000..14309c00c
--- /dev/null
+++ b/internal/typeutils/wrap_test.go
@@ -0,0 +1,74 @@
+/*
+ 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 typeutils_test
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/stretchr/testify/suite"
+)
+
+type WrapTestSuite struct {
+ TypeUtilsTestSuite
+}
+
+func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() {
+ testStatus := suite.testStatuses["local_account_1_status_1"]
+
+ note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
+ suite.NoError(err)
+
+ create, err := suite.typeconverter.WrapNoteInCreate(note, true)
+ suite.NoError(err)
+ suite.NotNil(create)
+
+ createI, err := streams.Serialize(create)
+ suite.NoError(err)
+
+ bytes, err := json.Marshal(createI)
+ suite.NoError(err)
+
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","actor":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity","object":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY","published":"2021-10-20T12:40:37+02:00","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"}`, string(bytes))
+}
+
+func (suite *WrapTestSuite) TestWrapNoteInCreate() {
+ testStatus := suite.testStatuses["local_account_1_status_1"]
+
+ note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
+ suite.NoError(err)
+
+ create, err := suite.typeconverter.WrapNoteInCreate(note, false)
+ suite.NoError(err)
+ suite.NotNil(create)
+
+ createI, err := streams.Serialize(create)
+ suite.NoError(err)
+
+ bytes, err := json.Marshal(createI)
+ suite.NoError(err)
+
+ suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","actor":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity","object":{"attachment":[],"attributedTo":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","content":"hello everyone!","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY","published":"2021-10-20T12:40:37+02:00","replies":{"first":{"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"},"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"Collection"},"summary":"introduction post","tag":[],"to":"https://www.w3.org/ns/activitystreams#Public","type":"Note","url":"http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY"},"published":"2021-10-20T12:40:37+02:00","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"}`, string(bytes))
+}
+
+func TestWrapTestSuite(t *testing.T) {
+ suite.Run(t, new(WrapTestSuite))
+}
diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go
index e3435842e..9f4bb4aad 100644
--- a/internal/visibility/filter.go
+++ b/internal/visibility/filter.go
@@ -32,6 +32,10 @@ type Filter interface {
// or account domains, and other relevant accounts mentioned in or replied to by the status.
StatusVisible(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error)
+ // StatusesVisible calls StatusVisible for each status in the statuses slice, and returns a slice of only
+ // statuses which are visible to the requestingAccount.
+ StatusesVisible(ctx context.Context, statuses []*gtsmodel.Status, requestingAccount *gtsmodel.Account) ([]*gtsmodel.Status, error)
+
// StatusHometimelineable returns true if targetStatus should be in the home timeline of the requesting account.
//
// This function will call StatusVisible internally, so it's not necessary to call it beforehand.
diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go
index 1e48abcda..a99494836 100644
--- a/internal/visibility/statusvisible.go
+++ b/internal/visibility/statusvisible.go
@@ -239,3 +239,17 @@ func (f *filter) StatusVisible(ctx context.Context, targetStatus *gtsmodel.Statu
// If we reached here, all is okay
return true, nil
}
+
+func (f *filter) StatusesVisible(ctx context.Context, statuses []*gtsmodel.Status, requestingAccount *gtsmodel.Account) ([]*gtsmodel.Status, error) {
+ filtered := []*gtsmodel.Status{}
+ for _, s := range statuses {
+ visible, err := f.StatusVisible(ctx, s, requestingAccount)
+ if err != nil {
+ return nil, err
+ }
+ if visible {
+ filtered = append(filtered, s)
+ }
+ }
+ return filtered, nil
+}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 854cd1795..c879d9e38 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -484,7 +484,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
},
}
- preserializedKeys := []string {
+ preserializedKeys := []string{
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGj2wLnDIHnP6wjJ+WmIhp7NGAaKWwfxBWfdMFR+Y0ilkK5ld5igT45UHAmzN3v4HcwHGGpPITD9caDYj5YaGOX+dSdGLgXWwItR0j+ivrHEJmvz8hG6z9wKEZKUUrRw7Ob72S0LOsreq98bjdiWJKHNka27slqQjGyhLQtcg6pe1CLJtnuJH4GEMLj7jJB3/Mqv3vl5CQZ+Js0bXfgw5TF/x/Bzq/8qsxQ1vnmYHJsR0eLPEuDJOvoFPiJZytI09S7qBEJL5PDeVSfjQi3o71sqOzZlEL0b0Ny48rfo/mwJAdkmfcnydRDxeGUEqpAWICCOdUL0+W3/fCffaRZsk1AgMBAAECggEAUuyO6QJgeoF8dGsmMxSc0/ANRp1tpRpLznNZ77ipUYP9z+mG2sFjdjb4kOHASuB18aWFRAAbAQ76fGzuqYe2muk+iFcG/EDH35MUCnRuZxA0QwjX6pHOW2NZZFKyCnLwohJUj74Na65ufMk4tXysydrmaKsfq4i+m5bE6NkiOCtbXsjUGVdJKzkT6X1gEyEPEHgrgVZz9OpRY5nwjZBMcFI6EibFnWdehcuCQLESIX9ll/QzGvTJ1p8xeVJs2ktLWKQ38RewwucNYVLVJmxS1LCPP8x+yHVkOxD66eIncY26sjX+VbyICkaG/ZjKBuoOekOq/T+b6q5ESxWUNfcu+QKBgQDmt3WVBrW6EXKtN1MrVyBoSfn9WHyf8Rfb84t5iNtaWGSyPZK/arUw1DRbI0TdPjct//wMWoUU2/uqcPSzudTaPena3oxjKReXso1hcynHqboCaXJMxWSqDQLumbrVY05C1WFSyhRY0iQS5fIrNzD4+6rmeC2Aj5DKNW5Atda8dwKBgQDcUdhQfjL9SmzzIeAqJUBIfSSI2pSTsZrnrvMtSMkYJbzwYrUdhIVxaS4hXuQYmGgwonLctyvJxVxEMnf+U0nqPgJHE9nGQb5BbK6/LqxBWRJQlc+W6EYodIwvtE5B4JNkPE5757u+xlDdHe2zGUGXSIf4IjBNbSpCu6RcFsGOswKBgEnr4gqbmcJCMOH65fTu930yppxbq6J7Vs+sWrXX+aAazjilrc0S3XcFprjEth3E/10HtbQnlJg4W4wioOSs19wNFk6AG67xzZNXLCFbCrnkUarQKkUawcQSYywbqVcReFPFlmc2RAqpWdGMR2k9R72etQUe4EVeul9veyHUoTbFAoGBAKj3J9NLhaVVb8ri3vzThsJRHzTJlYrTeb5XIO5I1NhtEMK2oLobiQ+aH6O+F2Z5c+Zgn4CABdf/QSyYHAhzLcu0dKC4K5rtjpC0XiwHClovimk9C3BrgGrEP0LSn/XL2p3T1kkWRpkflKKPsl1ZcEEqggSdi7fFkdSN/ZYWaakbAoGBALWVGpA/vXmaZEV/hTDdtDnIHj6RXfKHCsfnyI7AdjUX4gokzdcEvFsEIoI+nnXR/PIAvwqvQw4wiUqQnp2VB8r73YZvW/0npnsidQw3ZjqnyvZ9X8y80nYs7DjSlaG0A8huy2TUdFnJyCMWby30g82kf0b/lhotJg4d3fIDou51",
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6q61hiC7OhlMz7JNnLiL/RwOaFC8955GDvwSMH9Zw3oguWH9nLqkmlJ98cnqRG9ZC0qVo6Gagl7gv6yOHDwD4xZI8JoV2ZfNdDzq4QzoBIzMtRsbSS4IvrF3JP+kDH1tim+CbRMBxiFJgLgS6yeeQlLNvBW+CIYzmeCimZ6CWCr91rZPIprUIdjvhxrM9EQU072Pmzn2gpGM6K5gAReN+LtP+VSBC61x7GQJxBaJNtk11PXkgG99EdFi9vvgEBbM9bdcawvf8jxvjgsgdaDx/1cypDdnaL8eistmyv1YI67bKvrSPCEh55b90hl3o3vW4W5G4gcABoyORON96Y+i9AgMBAAECggEBAKp+tyNH0QiMo13fjFpHR2vFnsKSAPwXj063nx2kzqXUeqlp5yOE+LXmNSzjGpOCy1XJM474BRRUvsP1jkODLq4JNiF+RZP4Vij/CfDWZho33jxSUrIsiUGluxtfJiHV+A++s4zdZK/NhP+XyHYah0gEqUaTvl8q6Zhu0yH5sDCZHDLxDBpgiT5qD3lli8/o2xzzBdaibZdjQyHi9v5Yi3+ysly1tmfmqnkXSsevAubwJu504WxvDUSo7hPpG4a8Xb8ODqL738GIF2UY/olCcGkWqTQEr2pOqG9XbMmlUWnxG62GCfK6KtGfIzCyBBkGO2PZa9aPhVnv2bkYxI4PkLkCgYEAzAp7xH88UbSX31suDRa4jZwgtzhJLeyc3YxO5C4XyWZ89oWrA30V1KvfVwFRavYRJW07a+r0moba+0E1Nj5yZVXPOVu0bWd9ZyMbdH2L6MRZoJWU5bUOwyruulRCkqASZbWo4G05NOVesOyY1bhZGE7RyUW0vOo8tSyyRQ8nUGMCgYEA6jTQbDry4QkUP9tDhvc8+LsobIF1mPLEJui+mT98+9IGar6oeVDKekmNDO0Dx2+miLfjMNhCb5qUc8g036ZsekHt2WuQKunADua0coB00CebMdr6AQFf7QOQ/RuA+/gPJ5G0GzWB3YOQ5gE88tTCO/jBfmikVOZvLtgXUGjo3F8CgYEAl2poMoehQZjc41mMsRXdWukztgPE+pmORzKqENbLvB+cOG01XV9j5fCtyqklvFRioP2QjSNM5aeRtcbMMDbjOaQWJaCSImYcP39kDmxkeRXM1UhruJNGIzsm8Ys55Al53ZSTgAhN3Z0hSfYp7N/i7hD/yXc7Cr5g0qoamPkH2bUCgYApf0oeoyM9tDoeRl9knpHzEFZNQ3LusrUGn96FkLY4eDIi371CIYp+uGGBlM1CnQnI16wtj2PWGnGLQkH8DqTR1LSr/V8B+4DIIyB92TzZVOsunjoFy5SPjj42WpU0D/O/cxWSbJyh/xnBZx7Bd+kibyT5nNjhIiM5DZiz6qK3yQKBgAOO/MFKHKpKOXrtafbqCyculG/ope2u4eBveHKO6ByWcUSbuD9ebtr7Lu5AC5tKUJLkSyRx4EHk71bqP1yOITj8z9wQWdVyLxtVtyj9SUkUNvGwIj+F7NJ5VgHzWVZtvYWDCzrfxkEhKk3DRIIVjqmEohJcaOZoZ2Q/f8sjlId6",
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1NzommDoutE+FVAbgovPb5ioRRS1k93hhH5Mpe4KfAQb1k0aGA/TjrFr2HcRbOtldo6Fe+RRCAm5Go+sBx829zyMEdXbGtR4Pym78xYoRpsCwD+2AK/edQLjsdDf9zXZod9ig/Pe59awYaeuSFyK/w9q94ncuiE7m+MKfXJnTS/qiwwkxWIRm9lBprPIT0DwXCtoh7FdpsOmjLu2QdGADV+9KSDgV5IbVcxwjPY03vHJS4UAIP5eS46TtSrNF3hyM9Q8vGIPAixOVyAY53cRQUxZWU/FIaNjaEgpreUQfK1pgr0gxh1K7IKwmyF3f/JgL0urFljYp2UonzRU5XKHJAgMBAAECggEBAKVT2HLDqTlY+b/LNGcXY+H4b+LHuS2HdUUuuGU9MKN+HWpIziuQSoi4g1hNOgp9ezgqBByQpAHBE/jQraQ3NKZ55xm3TQDm1qFTb8SfOGL4Po2iSm0IL+VA2jWnpjmgjOmshXACusPmtfakE+55uxM3TUa16UQDyfCBfZZEtnaFLTYzJ7KmD2GPot8SCxJBqNmW7AL8pMSIxMC3cRxUbK4R3+KIisXUuB50jZH3zGHxi34e2jA6gDeFmzgHCDJRidHMsCTHTaATzlvVz9YwwNqPQaYY7OFouZXwFxVAxIg/1zVvLc3zx1gWt+UDFeI7h6Eq0h5DZPdUiR4mrhAKd70CgYEAw6WKbPgjzhJI9XVmnu0aMHHH4MK8pbIq4kddChw24yZv1e9qnNTHw3YK17X9Fqog9CU1OX3M/vddfQbc34SorBmtmGYgOfDSuXTct52Ppyl4CRwndYQc0A88Hw+klluTEPY3+NRV6YSzv8vkNMasVuOh0YI1xzbpc+Bb5LL3kwMCgYEA7R4PLYYmtzKAY2YTQOXGBh3xd6UEHgks30W+QzDxvOv75svZt6yDgiwJzXtyrQzbNaH6yca5nfjkqyhnHwpguJ6DK7+S/RnZfVib5MqRwiU7g8l3neKhIXs6xZxfORunDU9T5ntbyNaGv/TJ2cXNw+9VskhBaHfEN/kmaBNNuEMCgYARLuzlfTXH15tI07Lbqn9uWc/wUao381oI3bOyO6Amey2/YHPAqn+RD0EMiRNddjvGta3jCsWCbz9qx7uGdiRKWUcB55ZVAG3BlB3+knwXdnDwe+SLUbsmGvBw2fLesdRM3RM1a5DQHbOb2NCGQhzI1N1VhVYr1QrT/pSTlZRg+QKBgCE05nc/pEhfoC9LakLaauMManaQ+4ShUFFsWPrb7d7BRaPKxJC+biRauny2XxbxB/n410BOvkvrQUre+6ITN/xi5ofH6nPbnOO69woRfFwuDqmkG0ZXKK2hrldiUMuUnc51X5CVkgMMWA6l32bKFsjryZqQF+jjbO1RzRkiKu41AoGAHQer1NyajHEpEfempx8YTsAnOn+Hi33cXAaQoTkS41lX2YK0cBkD18yhubczZcKnMW+GRKZRYXMm0NfwiuIo5oIYWeO6K+rXF+SKptC5mnw/3FhDVnghDAmEqOcRSWnFXARk1WEbFtwG5phDeFrWXsqPzGAjoZ8bhLvKRsrG4OM=",
@@ -495,7 +495,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
if diff := len(accounts) - len(preserializedKeys); diff > 0 {
var keyStrings = make([]string, diff)
- for i := 0; i < diff; i ++ {
+ for i := 0; i < diff; i++ {
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
key, _ := x509.MarshalPKCS8PrivateKey(priv)
keyStrings = append(keyStrings, base64.StdEncoding.EncodeToString(key))
@@ -524,6 +524,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
}
return accounts
}
+
// NewTestAttachments returns a map of attachments keyed according to which account
// and status they belong to, and which attachment number of that status they are.
func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
@@ -863,8 +864,8 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
TagIDs: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"},
MentionIDs: []string{},
EmojiIDs: []string{"01F8MH9H8E4VG3KDYJR9EGPXCQ"},
- CreatedAt: time.Now().Add(-71 * time.Hour),
- UpdatedAt: time.Now().Add(-71 * time.Hour),
+ CreatedAt: TimeMustParse("2021-10-20T11:36:45Z"),
+ UpdatedAt: TimeMustParse("2021-10-20T11:36:45Z"),
Local: true,
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
@@ -886,8 +887,8 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
URI: "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
URL: "http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
Content: "🐕🐕🐕🐕🐕",
- CreatedAt: time.Now().Add(-70 * time.Hour),
- UpdatedAt: time.Now().Add(-70 * time.Hour),
+ CreatedAt: TimeMustParse("2021-10-20T12:36:45Z"),
+ UpdatedAt: TimeMustParse("2021-10-20T12:36:45Z"),
Local: true,
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
@@ -934,8 +935,8 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
URL: "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
Content: "hello everyone!",
- CreatedAt: time.Now().Add(-47 * time.Hour),
- UpdatedAt: time.Now().Add(-47 * time.Hour),
+ CreatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
+ UpdatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
Local: true,
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
@@ -1536,11 +1537,38 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
DateHeader: date,
}
+ target = URLMustParse(accounts["local_account_1"].OutboxURI)
+ sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
+ fossSatanDereferenceZorkOutbox := ActivityWithSignature{
+ SignatureHeader: sig,
+ DigestHeader: digest,
+ DateHeader: date,
+ }
+
+ target = URLMustParse(accounts["local_account_1"].OutboxURI + "?page=true")
+ sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
+ fossSatanDereferenceZorkOutboxFirst := ActivityWithSignature{
+ SignatureHeader: sig,
+ DigestHeader: digest,
+ DateHeader: date,
+ }
+
+ target = URLMustParse(accounts["local_account_1"].OutboxURI + "?page=true&max_id=01F8MHAMCHF6Y650WCRSCP4WMY")
+ sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
+ fossSatanDereferenceZorkOutboxNext := ActivityWithSignature{
+ SignatureHeader: sig,
+ DigestHeader: digest,
+ DateHeader: date,
+ }
+
return map[string]ActivityWithSignature{
"foss_satan_dereference_zork": fossSatanDereferenceZork,
"foss_satan_dereference_local_account_1_status_1_replies": fossSatanDereferenceLocalAccount1Status1Replies,
"foss_satan_dereference_local_account_1_status_1_replies_next": fossSatanDereferenceLocalAccount1Status1RepliesNext,
"foss_satan_dereference_local_account_1_status_1_replies_last": fossSatanDereferenceLocalAccount1Status1RepliesLast,
+ "foss_satan_dereference_zork_outbox": fossSatanDereferenceZorkOutbox,
+ "foss_satan_dereference_zork_outbox_first": fossSatanDereferenceZorkOutboxFirst,
+ "foss_satan_dereference_zork_outbox_next": fossSatanDereferenceZorkOutboxNext,
}
}