summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/activitypub/emoji/emojiget_test.go10
-rw-r--r--internal/api/activitypub/users/inboxpost_test.go4
-rw-r--r--internal/api/activitypub/users/outboxget_test.go2
-rw-r--r--internal/api/activitypub/users/repliesget_test.go2
-rw-r--r--internal/api/activitypub/users/user_test.go10
-rw-r--r--internal/api/client/accounts/account_test.go9
-rw-r--r--internal/api/client/accounts/accounts.go5
-rw-r--r--internal/api/client/accounts/lists.go97
-rw-r--r--internal/api/client/accounts/lists_test.go103
-rw-r--r--internal/api/client/admin/admin_test.go7
-rw-r--r--internal/api/client/bookmarks/bookmarks_test.go10
-rw-r--r--internal/api/client/favourites/favourites_test.go10
-rw-r--r--internal/api/client/favourites/favouritesget.go2
-rw-r--r--internal/api/client/followrequests/followrequest_test.go9
-rw-r--r--internal/api/client/instance/instance_test.go7
-rw-r--r--internal/api/client/lists/list.go19
-rw-r--r--internal/api/client/lists/listaccounts.go156
-rw-r--r--internal/api/client/lists/listaccountsadd.go120
-rw-r--r--internal/api/client/lists/listaccountsremove.go120
-rw-r--r--internal/api/client/lists/listcreate.go106
-rw-r--r--internal/api/client/lists/listdelete.go91
-rw-r--r--internal/api/client/lists/listget.go95
-rw-r--r--internal/api/client/lists/listsget.go (renamed from internal/api/client/lists/listsgets.go)46
-rw-r--r--internal/api/client/lists/listupdate.go152
-rw-r--r--internal/api/client/media/mediacreate_test.go8
-rw-r--r--internal/api/client/media/mediaupdate_test.go8
-rw-r--r--internal/api/client/notifications/notificationget.go2
-rw-r--r--internal/api/client/notifications/notificationsclear.go2
-rw-r--r--internal/api/client/notifications/notificationsget.go2
-rw-r--r--internal/api/client/reports/reports_test.go9
-rw-r--r--internal/api/client/search/search_test.go9
-rw-r--r--internal/api/client/statuses/status_test.go9
-rw-r--r--internal/api/client/streaming/stream.go63
-rw-r--r--internal/api/client/streaming/streaming.go17
-rw-r--r--internal/api/client/streaming/streaming_test.go9
-rw-r--r--internal/api/client/timelines/home.go58
-rw-r--r--internal/api/client/timelines/list.go152
-rw-r--r--internal/api/client/timelines/public.go58
-rw-r--r--internal/api/client/timelines/timeline.go3
-rw-r--r--internal/api/client/user/user_test.go10
-rw-r--r--internal/api/fileserver/fileserver_test.go8
-rw-r--r--internal/api/model/list.go49
-rw-r--r--internal/api/util/parsequery.go58
-rw-r--r--internal/api/wellknown/webfinger/webfinger_test.go10
44 files changed, 1585 insertions, 151 deletions
diff --git a/internal/api/activitypub/emoji/emojiget_test.go b/internal/api/activitypub/emoji/emojiget_test.go
index 16b004299..2438e09f4 100644
--- a/internal/api/activitypub/emoji/emojiget_test.go
+++ b/internal/api/activitypub/emoji/emojiget_test.go
@@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -74,9 +75,14 @@ func (suite *EmojiGetTestSuite) SetupTest() {
suite.state.DB = suite.db
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
-
suite.tc = testrig.NewTestTypeConverter(suite.db)
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
@@ -86,8 +92,6 @@ func (suite *EmojiGetTestSuite) SetupTest() {
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked)
-
- suite.NoError(suite.processor.Start())
}
func (suite *EmojiGetTestSuite) TearDownTest() {
diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go
index 26c4029a2..4d494cf64 100644
--- a/internal/api/activitypub/users/inboxpost_test.go
+++ b/internal/api/activitypub/users/inboxpost_test.go
@@ -89,7 +89,6 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
@@ -190,7 +189,6 @@ func (suite *InboxPostTestSuite) TestPostUnblock() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
@@ -296,7 +294,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
@@ -425,7 +422,6 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
diff --git a/internal/api/activitypub/users/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go
index a75d850b6..1abe31ef6 100644
--- a/internal/api/activitypub/users/outboxget_test.go
+++ b/internal/api/activitypub/users/outboxget_test.go
@@ -106,7 +106,6 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
@@ -181,7 +180,6 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
diff --git a/internal/api/activitypub/users/repliesget_test.go b/internal/api/activitypub/users/repliesget_test.go
index a37868fd7..f81dddadd 100644
--- a/internal/api/activitypub/users/repliesget_test.go
+++ b/internal/api/activitypub/users/repliesget_test.go
@@ -106,7 +106,6 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
@@ -171,7 +170,6 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
- suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
diff --git a/internal/api/activitypub/users/user_test.go b/internal/api/activitypub/users/user_test.go
index aed687fef..8c33ce2f2 100644
--- a/internal/api/activitypub/users/user_test.go
+++ b/internal/api/activitypub/users/user_test.go
@@ -31,6 +31,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -83,6 +84,13 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
@@ -94,8 +102,6 @@ func (suite *UserStandardTestSuite) SetupTest() {
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked)
-
- suite.NoError(suite.processor.Start())
}
func (suite *UserStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/accounts/account_test.go b/internal/api/client/accounts/account_test.go
index b168f216c..678fc8a5d 100644
--- a/internal/api/client/accounts/account_test.go
+++ b/internal/api/client/accounts/account_test.go
@@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -86,6 +87,12 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
@@ -94,8 +101,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.accountsModule = accounts.New(suite.processor)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *AccountStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/accounts/accounts.go b/internal/api/client/accounts/accounts.go
index a6bedd6e1..298104a8d 100644
--- a/internal/api/client/accounts/accounts.go
+++ b/internal/api/client/accounts/accounts.go
@@ -70,6 +70,8 @@ const (
UnblockPath = BasePathWithID + "/unblock"
// DeleteAccountPath is for deleting one's account via the API
DeleteAccountPath = BasePath + "/delete"
+ // ListsPath is for seeing which lists an account is.
+ ListsPath = BasePathWithID + "/lists"
)
type Module struct {
@@ -115,4 +117,7 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
// block or unblock account
attachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler)
attachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler)
+
+ // account lists
+ attachHandler(http.MethodGet, ListsPath, m.AccountListsGETHandler)
}
diff --git a/internal/api/client/accounts/lists.go b/internal/api/client/accounts/lists.go
new file mode 100644
index 000000000..4ce1bf729
--- /dev/null
+++ b/internal/api/client/accounts/lists.go
@@ -0,0 +1,97 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountListsGETHandler swagger:operation GET /api/v1/accounts/{id}/lists accountLists
+//
+// See all lists of yours that contain requested account.
+//
+// ---
+// tags:
+// - accounts
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: Account ID.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// name: lists
+// description: Array of all lists containing this account.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/list"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) AccountListsGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, false, false, false, false)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetAcctID := c.Param(IDKey)
+ if targetAcctID == "" {
+ err := errors.New("no account id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ lists, errWithCode := m.processor.Account().ListsGet(c.Request.Context(), authed.Account, targetAcctID)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, lists)
+}
diff --git a/internal/api/client/accounts/lists_test.go b/internal/api/client/accounts/lists_test.go
new file mode 100644
index 000000000..6984d6ef8
--- /dev/null
+++ b/internal/api/client/accounts/lists_test.go
@@ -0,0 +1,103 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package accounts_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type ListsTestSuite struct {
+ AccountStandardTestSuite
+}
+
+func (suite *ListsTestSuite) getLists(targetAccountID string, expectedHTTPStatus int, expectedBody string) []*apimodel.List {
+ var (
+ recorder = httptest.NewRecorder()
+ ctx, _ = testrig.CreateGinTestContext(recorder, nil)
+ request = httptest.NewRequest(http.MethodGet, "http://localhost:8080/api/v1/accounts/"+targetAccountID+"/lists", nil)
+ )
+
+ // Set up the test context.
+ ctx.Request = request
+ ctx.AddParam("id", targetAccountID)
+ ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
+
+ // Trigger the handler.
+ suite.accountsModule.AccountListsGETHandler(ctx)
+
+ // Read the result.
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := io.ReadAll(result.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ errs := gtserror.MultiError{}
+
+ // Check expected code + body.
+ if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
+ errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
+ }
+
+ // If we got an expected body, return early.
+ if expectedBody != "" && string(b) != expectedBody {
+ errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
+ }
+
+ if err := errs.Combine(); err != nil {
+ suite.FailNow("", "%v (body %s)", err, string(b))
+ }
+
+ // Return list response.
+ resp := new([]*apimodel.List)
+ if err := json.Unmarshal(b, resp); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ return *resp
+}
+
+func (suite *ListsTestSuite) TestGetListsHit() {
+ targetAccount := suite.testAccounts["admin_account"]
+ suite.getLists(targetAccount.ID, http.StatusOK, `[{"id":"01H0G8E4Q2J3FE3JDWJVWEDCD1","title":"Cool Ass Posters From This Instance","replies_policy":"followed"}]`)
+}
+
+func (suite *ListsTestSuite) TestGetListsNoHit() {
+ targetAccount := suite.testAccounts["remote_account_1"]
+ suite.getLists(targetAccount.ID, http.StatusOK, `[]`)
+}
+
+func TestListsTestSuite(t *testing.T) {
+ suite.Run(t, new(ListsTestSuite))
+}
diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go
index c6de665fa..261e9ff4e 100644
--- a/internal/api/client/admin/admin_test.go
+++ b/internal/api/client/admin/admin_test.go
@@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -92,6 +93,12 @@ func (suite *AdminStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
diff --git a/internal/api/client/bookmarks/bookmarks_test.go b/internal/api/client/bookmarks/bookmarks_test.go
index 6f20c4762..b41964584 100644
--- a/internal/api/client/bookmarks/bookmarks_test.go
+++ b/internal/api/client/bookmarks/bookmarks_test.go
@@ -42,6 +42,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -98,6 +99,13 @@ func (suite *BookmarkTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
@@ -107,8 +115,6 @@ func (suite *BookmarkTestSuite) SetupTest() {
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.statusModule = statuses.New(suite.processor)
suite.bookmarkModule = bookmarks.New(suite.processor)
-
- suite.NoError(suite.processor.Start())
}
func (suite *BookmarkTestSuite) TearDownTest() {
diff --git a/internal/api/client/favourites/favourites_test.go b/internal/api/client/favourites/favourites_test.go
index c6e42e113..1a3a324a8 100644
--- a/internal/api/client/favourites/favourites_test.go
+++ b/internal/api/client/favourites/favourites_test.go
@@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -82,6 +83,13 @@ func (suite *FavouritesStandardTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
@@ -90,8 +98,6 @@ func (suite *FavouritesStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.favModule = favourites.New(suite.processor)
-
- suite.NoError(suite.processor.Start())
}
func (suite *FavouritesStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go
index 198dd1b12..112bbd856 100644
--- a/internal/api/client/favourites/favouritesget.go
+++ b/internal/api/client/favourites/favouritesget.go
@@ -128,7 +128,7 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
limit = int(i)
}
- resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
+ resp, errWithCode := m.processor.Timeline().FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/followrequests/followrequest_test.go b/internal/api/client/followrequests/followrequest_test.go
index a1aca89ff..58d191fa7 100644
--- a/internal/api/client/followrequests/followrequest_test.go
+++ b/internal/api/client/followrequests/followrequest_test.go
@@ -35,6 +35,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -83,6 +84,12 @@ func (suite *FollowRequestStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
@@ -90,8 +97,6 @@ func (suite *FollowRequestStandardTestSuite) SetupTest() {
suite.followRequestModule = followrequests.New(suite.processor)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *FollowRequestStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/instance/instance_test.go b/internal/api/client/instance/instance_test.go
index 2fe29f75e..745d76a24 100644
--- a/internal/api/client/instance/instance_test.go
+++ b/internal/api/client/instance/instance_test.go
@@ -35,6 +35,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -85,6 +86,12 @@ func (suite *InstanceStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
diff --git a/internal/api/client/lists/list.go b/internal/api/client/lists/list.go
index b1c193397..515075271 100644
--- a/internal/api/client/lists/list.go
+++ b/internal/api/client/lists/list.go
@@ -25,8 +25,15 @@ import (
)
const (
+ IDKey = "id"
// BasePath is the base path for serving the lists API, minus the 'api' prefix
- BasePath = "/v1/lists"
+ BasePath = "/v1/lists"
+ BasePathWithID = BasePath + "/:" + IDKey
+ AccountsPath = BasePathWithID + "/accounts"
+ MaxIDKey = "max_id"
+ LimitKey = "limit"
+ SinceIDKey = "since_id"
+ MinIDKey = "min_id"
)
type Module struct {
@@ -40,5 +47,15 @@ func New(processor *processing.Processor) *Module {
}
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
+ // create / get / update / delete lists
+ attachHandler(http.MethodPost, BasePath, m.ListCreatePOSTHandler)
attachHandler(http.MethodGet, BasePath, m.ListsGETHandler)
+ attachHandler(http.MethodGet, BasePathWithID, m.ListGETHandler)
+ attachHandler(http.MethodPut, BasePathWithID, m.ListUpdatePUTHandler)
+ attachHandler(http.MethodDelete, BasePathWithID, m.ListDELETEHandler)
+
+ // get / add / remove list accounts
+ attachHandler(http.MethodGet, AccountsPath, m.ListAccountsGETHandler)
+ attachHandler(http.MethodPost, AccountsPath, m.ListAccountsPOSTHandler)
+ attachHandler(http.MethodDelete, AccountsPath, m.ListAccountsDELETEHandler)
}
diff --git a/internal/api/client/lists/listaccounts.go b/internal/api/client/lists/listaccounts.go
new file mode 100644
index 000000000..3a24cab27
--- /dev/null
+++ b/internal/api/client/lists/listaccounts.go
@@ -0,0 +1,156 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListAccountsGETHandler swagger:operation GET /api/v1/list/{id}/accounts listAccounts
+//
+// Page through accounts in this list.
+//
+// The returned Link header can be used to generate the previous and next queries when scrolling up or down a timeline.
+//
+// Example:
+//
+// ```
+// <https://example.org/api/v1/list/01H0W619198FX7J54NF7EH1NG2/accounts?limit=20&max_id=01FC3GSQ8A3MMJ43BPZSGEG29M>; rel="next", <https://example.org/api/v1/list/01H0W619198FX7J54NF7EH1NG2/accounts?limit=20&min_id=01FC3KJW2GYXSDDRA6RWNDM46M>; rel="prev"
+// ````
+//
+// ---
+// tags:
+// - lists
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+// -
+// name: max_id
+// type: string
+// description: >-
+// Return only list entries *OLDER* than the given max ID.
+// The account from the list entry with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: since_id
+// type: string
+// description: >-
+// Return only list entries *NEWER* than the given since ID.
+// The account from the list entry with the specified ID will not be included in the response.
+// in: query
+// -
+// name: min_id
+// type: string
+// description: >-
+// Return only list entries *IMMEDIATELY NEWER* than the given min ID.
+// The account from the list entry with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: limit
+// type: integer
+// description: Number of accounts to return.
+// default: 20
+// in: query
+// required: false
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// headers:
+// Link:
+// type: string
+// description: Links to the next and previous queries.
+// name: accounts
+// description: Array of accounts.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/account"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListAccountsGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.List().GetListAccounts(
+ c.Request.Context(),
+ authed.Account,
+ targetListID,
+ c.Query(MaxIDKey),
+ c.Query(SinceIDKey),
+ c.Query(MinIDKey),
+ limit,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Items)
+}
diff --git a/internal/api/client/lists/listaccountsadd.go b/internal/api/client/lists/listaccountsadd.go
new file mode 100644
index 000000000..5cf907b06
--- /dev/null
+++ b/internal/api/client/lists/listaccountsadd.go
@@ -0,0 +1,120 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListAccountsPOSTHandler swagger:operation POST /api/v1/list/{id}/accounts addListAccounts
+//
+// Add one or more accounts to the given list.
+//
+// ---
+// tags:
+// - lists
+//
+// consumes:
+// - application/json
+// - application/xml
+// - application/x-www-form-urlencoded
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+// -
+// name: account_ids
+// type: array
+// items:
+// type: string
+// description: >-
+// Array of accountIDs to modify.
+// Each accountID must correspond to an account
+// that the requesting account follows.
+// in: formData
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// description: list accounts updated
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListAccountsPOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form := &apimodel.ListAccountsChangeRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if len(form.AccountIDs) == 0 {
+ err := errors.New("no account IDs given")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if errWithCode := m.processor.List().AddToList(c.Request.Context(), authed.Account, targetListID, form.AccountIDs); errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{})
+}
diff --git a/internal/api/client/lists/listaccountsremove.go b/internal/api/client/lists/listaccountsremove.go
new file mode 100644
index 000000000..6ce7e3cd3
--- /dev/null
+++ b/internal/api/client/lists/listaccountsremove.go
@@ -0,0 +1,120 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListAccountsDELETEHandler swagger:operation DELETE /api/v1/list/{id}/accounts removeListAccounts
+//
+// Remove one or more accounts from the given list.
+//
+// ---
+// tags:
+// - lists
+//
+// consumes:
+// - application/json
+// - application/xml
+// - application/x-www-form-urlencoded
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+// -
+// name: account_ids
+// type: array
+// items:
+// type: string
+// description: >-
+// Array of accountIDs to modify.
+// Each accountID must correspond to an account
+// that the requesting account follows.
+// in: formData
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// description: list accounts updated
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListAccountsDELETEHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form := &apimodel.ListAccountsChangeRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if len(form.AccountIDs) == 0 {
+ err := errors.New("no account IDs given")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if errWithCode := m.processor.List().RemoveFromList(c.Request.Context(), authed.Account, targetListID, form.AccountIDs); errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{})
+}
diff --git a/internal/api/client/lists/listcreate.go b/internal/api/client/lists/listcreate.go
new file mode 100644
index 000000000..09a654c74
--- /dev/null
+++ b/internal/api/client/lists/listcreate.go
@@ -0,0 +1,106 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/validate"
+)
+
+// ListCreatePOSTHandler swagger:operation POST /api/v1/list listCreate
+//
+// Create a new list.
+//
+// ---
+// tags:
+// - lists
+//
+// consumes:
+// - application/json
+// - application/xml
+// - application/x-www-form-urlencoded
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - write:lists
+//
+// responses:
+// '200':
+// description: "The newly created list."
+// schema:
+// "$ref": "#/definitions/list"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListCreatePOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form := &apimodel.ListCreateRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if err := validate.ListTitle(form.Title); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ repliesPolicy := gtsmodel.RepliesPolicy(strings.ToLower(form.RepliesPolicy))
+ if err := validate.ListRepliesPolicy(repliesPolicy); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ apiList, errWithCode := m.processor.List().Create(c.Request.Context(), authed.Account, form.Title, repliesPolicy)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, apiList)
+}
diff --git a/internal/api/client/lists/listdelete.go b/internal/api/client/lists/listdelete.go
new file mode 100644
index 000000000..394ddfb6b
--- /dev/null
+++ b/internal/api/client/lists/listdelete.go
@@ -0,0 +1,91 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListDELETEHandler swagger:operation DELETE /api/v1/list/{id} listDelete
+//
+// Delete a single list with the given ID.
+//
+// ---
+// tags:
+// - lists
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - write:lists
+//
+// responses:
+// '200':
+// description: list deleted
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListDELETEHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if errWithCode := m.processor.List().Delete(c.Request.Context(), authed.Account, targetListID); errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{})
+}
diff --git a/internal/api/client/lists/listget.go b/internal/api/client/lists/listget.go
new file mode 100644
index 000000000..3aed594d4
--- /dev/null
+++ b/internal/api/client/lists/listget.go
@@ -0,0 +1,95 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListGETHandler swagger:operation GET /api/v1/list/{id} list
+//
+// Get a single list with the given ID.
+//
+// ---
+// tags:
+// - lists
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// name: list
+// description: Requested list.
+// schema:
+// "$ref": "#/definitions/list"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.List().Get(c.Request.Context(), authed.Account, targetListID)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, resp)
+}
diff --git a/internal/api/client/lists/listsgets.go b/internal/api/client/lists/listsget.go
index 66b713611..f16152a9d 100644
--- a/internal/api/client/lists/listsgets.go
+++ b/internal/api/client/lists/listsget.go
@@ -26,9 +26,42 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-// ListsGETHandler returns a list of lists created by/for the authed account
+// ListsGETHandler swagger:operation GET /api/v1/lists lists
+//
+// Get all lists for owned by authorized user.
+//
+// ---
+// tags:
+// - lists
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// name: lists
+// description: Array of all lists owned by the requesting user.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/list"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) ListsGETHandler(c *gin.Context) {
- if _, err := oauth.Authed(c, true, true, true, true); err != nil {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
@@ -38,6 +71,11 @@ func (m *Module) ListsGETHandler(c *gin.Context) {
return
}
- // todo: implement this; currently it's a no-op
- c.JSON(http.StatusOK, []string{})
+ lists, errWithCode := m.processor.List().GetAll(c.Request.Context(), authed.Account)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, lists)
}
diff --git a/internal/api/client/lists/listupdate.go b/internal/api/client/lists/listupdate.go
new file mode 100644
index 000000000..80c5a8be3
--- /dev/null
+++ b/internal/api/client/lists/listupdate.go
@@ -0,0 +1,152 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package lists
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/validate"
+)
+
+// ListUpdatePUTHandler swagger:operation PUT /api/v1/list listUpdate
+//
+// Update an existing list.
+//
+// ---
+// tags:
+// - lists
+//
+// consumes:
+// - application/json
+// - application/xml
+// - application/x-www-form-urlencoded
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+// -
+// name: title
+// type: string
+// description: Title of this list.
+// in: formData
+// example: Cool People
+// -
+// name: replies_policy
+// type: string
+// description: |-
+// RepliesPolicy for this list.
+// followed = Show replies to any followed user
+// list = Show replies to members of the list
+// none = Show replies to no one
+// in: formData
+// example: list
+//
+// security:
+// - OAuth2 Bearer:
+// - write:lists
+//
+// responses:
+// '200':
+// description: "The newly updated list."
+// schema:
+// "$ref": "#/definitions/list"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) ListUpdatePUTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form := &apimodel.ListUpdateRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if form.Title != nil {
+ if err := validate.ListTitle(*form.Title); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+ }
+
+ var repliesPolicy *gtsmodel.RepliesPolicy
+ if form.RepliesPolicy != nil {
+ rp := gtsmodel.RepliesPolicy(strings.ToLower(*form.RepliesPolicy))
+
+ if err := validate.ListRepliesPolicy(rp); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ repliesPolicy = &rp
+ }
+
+ if form.Title == nil && repliesPolicy == nil {
+ err = errors.New("neither title nor replies_policy was set; nothing to update")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ apiList, errWithCode := m.processor.List().Update(c.Request.Context(), authed.Account, targetListID, form.Title, repliesPolicy)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, apiList)
+}
diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go
index d41222dd2..f67144ce1 100644
--- a/internal/api/client/media/mediacreate_test.go
+++ b/internal/api/client/media/mediacreate_test.go
@@ -44,6 +44,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -90,6 +91,13 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go
index cd0e65013..c436ee000 100644
--- a/internal/api/client/media/mediaupdate_test.go
+++ b/internal/api/client/media/mediaupdate_test.go
@@ -42,6 +42,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -87,6 +88,13 @@ func (suite *MediaUpdateTestSuite) SetupSuite() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
diff --git a/internal/api/client/notifications/notificationget.go b/internal/api/client/notifications/notificationget.go
index 3efdf171d..98e32498b 100644
--- a/internal/api/client/notifications/notificationget.go
+++ b/internal/api/client/notifications/notificationget.go
@@ -77,7 +77,7 @@ func (m *Module) NotificationGETHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.NotificationGet(c.Request.Context(), authed.Account, targetNotifID)
+ resp, errWithCode := m.processor.Timeline().NotificationGet(c.Request.Context(), authed.Account, targetNotifID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/notifications/notificationsclear.go b/internal/api/client/notifications/notificationsclear.go
index 17592f36d..cf3706a7c 100644
--- a/internal/api/client/notifications/notificationsclear.go
+++ b/internal/api/client/notifications/notificationsclear.go
@@ -69,7 +69,7 @@ func (m *Module) NotificationsClearPOSTHandler(c *gin.Context) {
return
}
- errWithCode := m.processor.NotificationsClear(c.Request.Context(), authed)
+ errWithCode := m.processor.Timeline().NotificationsClear(c.Request.Context(), authed)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go
index 6ce8adcab..fd175a115 100644
--- a/internal/api/client/notifications/notificationsget.go
+++ b/internal/api/client/notifications/notificationsget.go
@@ -138,7 +138,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
limit = int(i)
}
- resp, errWithCode := m.processor.NotificationsGet(
+ resp, errWithCode := m.processor.Timeline().NotificationsGet(
c.Request.Context(),
authed,
c.Query(MaxIDKey),
diff --git a/internal/api/client/reports/reports_test.go b/internal/api/client/reports/reports_test.go
index bf0514122..a28f8ffa3 100644
--- a/internal/api/client/reports/reports_test.go
+++ b/internal/api/client/reports/reports_test.go
@@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -77,6 +78,12 @@ func (suite *ReportsStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
@@ -85,8 +92,6 @@ func (suite *ReportsStandardTestSuite) SetupTest() {
suite.reportsModule = reports.New(suite.processor)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *ReportsStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/search/search_test.go b/internal/api/client/search/search_test.go
index 626a366f3..95507fcd9 100644
--- a/internal/api/client/search/search_test.go
+++ b/internal/api/client/search/search_test.go
@@ -35,6 +35,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -81,6 +82,12 @@ func (suite *SearchStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ testrig.NewTestTypeConverter(suite.db),
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
@@ -89,8 +96,6 @@ func (suite *SearchStandardTestSuite) SetupTest() {
suite.searchModule = search.New(suite.processor)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *SearchStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/statuses/status_test.go b/internal/api/client/statuses/status_test.go
index 0a006631c..84e71f9c1 100644
--- a/internal/api/client/statuses/status_test.go
+++ b/internal/api/client/statuses/status_test.go
@@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -83,6 +84,12 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.tc = testrig.NewTestTypeConverter(suite.db)
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
@@ -91,8 +98,6 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.statusModule = statuses.New(suite.processor)
-
- suite.NoError(suite.processor.Start())
}
func (suite *StatusStandardTestSuite) TearDownTest() {
diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go
index f41bc0ac2..88c682a75 100644
--- a/internal/api/client/streaming/stream.go
+++ b/internal/api/client/streaming/stream.go
@@ -82,6 +82,20 @@ import (
// `direct`: receive updates for direct messages.
// in: query
// required: true
+// -
+// name: list
+// type: string
+// description: |-
+// ID of the list to subscribe to.
+// Only used if stream type is 'list'.
+// in: query
+// -
+// name: tag
+// type: string
+// description: |-
+// Name of the tag to subscribe to.
+// Only used if stream type is 'hashtag' or 'hashtag:local'.
+// in: query
//
// security:
// - OAuth2 Bearer:
@@ -164,8 +178,16 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
}
// Get the initial stream type, if there is one.
- // streamType will be an empty string if one wasn't supplied. Open() will deal with this
+ // By appending other query params to the streamType,
+ // we can allow for streaming for specific list IDs
+ // or hashtags.
streamType := c.Query(StreamQueryKey)
+ if list := c.Query(StreamListKey); list != "" {
+ streamType += ":" + list
+ } else if tag := c.Query(StreamTagKey); tag != "" {
+ streamType += ":" + tag
+ }
+
stream, errWithCode := m.processor.Stream().Open(c.Request.Context(), account, streamType)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
@@ -240,28 +262,41 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
// If the message contains 'stream' and 'type' fields, we can
// update the set of timelines that are subscribed for events.
- // everything else is ignored.
- action := msg["type"]
- streamType := msg["stream"]
+ updateType, ok := msg["type"]
+ if !ok {
+ l.Warn("'type' field not provided")
+ continue
+ }
+
+ updateStream, ok := msg["stream"]
+ if !ok {
+ l.Warn("'stream' field not provided")
+ continue
+ }
- // Ignore if the streamType is unknown (or missing), so a bad
- // client can't cause extra memory allocations
- if !slices.Contains(streampkg.AllStatusTimelines, streamType) {
- l.Warnf("Unknown 'stream' field: %v", msg)
+ // Ignore if the updateStreamType is unknown (or missing),
+ // so a bad client can't cause extra memory allocations
+ if !slices.Contains(streampkg.AllStatusTimelines, updateStream) {
+ l.Warnf("unknown 'stream' field: %v", msg)
continue
}
- switch action {
+ updateList, ok := msg["list"]
+ if ok {
+ updateStream += ":" + updateList
+ }
+
+ switch updateType {
case "subscribe":
stream.Lock()
- stream.Timelines[streamType] = true
+ stream.StreamTypes[updateStream] = true
stream.Unlock()
case "unsubscribe":
stream.Lock()
- delete(stream.Timelines, streamType)
+ delete(stream.StreamTypes, updateStream)
stream.Unlock()
default:
- l.Warnf("Invalid 'type' field: %v", msg)
+ l.Warnf("invalid 'type' field: %v", msg)
}
}
}()
@@ -276,7 +311,7 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
case msg := <-stream.Messages:
l.Tracef("sending message to websocket: %+v", msg)
if err := wsConn.WriteJSON(msg); err != nil {
- l.Errorf("error writing json to websocket: %v", err)
+ l.Debugf("error writing json to websocket: %v", err)
return
}
@@ -290,7 +325,7 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
websocket.PingMessage,
[]byte{},
); err != nil {
- l.Errorf("error writing ping to websocket: %v", err)
+ l.Debugf("error writing ping to websocket: %v", err)
return
}
}
diff --git a/internal/api/client/streaming/streaming.go b/internal/api/client/streaming/streaming.go
index 71b325089..edddeab73 100644
--- a/internal/api/client/streaming/streaming.go
+++ b/internal/api/client/streaming/streaming.go
@@ -27,17 +27,12 @@ import (
)
const (
- // BasePath is the path for the streaming api, minus the 'api' prefix
- BasePath = "/v1/streaming"
-
- // StreamQueryKey is the query key for the type of stream being requested
- StreamQueryKey = "stream"
-
- // AccessTokenQueryKey is the query key for an oauth access token that should be passed in streaming requests.
- AccessTokenQueryKey = "access_token"
- // AccessTokenHeader is the header for an oauth access token that can be passed in streaming requests instead of AccessTokenQueryKey
- //nolint:gosec
- AccessTokenHeader = "Sec-Websocket-Protocol"
+ BasePath = "/v1/streaming" // path for the streaming api, minus the 'api' prefix
+ StreamQueryKey = "stream" // type of stream being requested
+ StreamListKey = "list" // id of list being requested
+ StreamTagKey = "tag" // name of tag being requested
+ AccessTokenQueryKey = "access_token" // oauth access token
+ AccessTokenHeader = "Sec-Websocket-Protocol" //nolint:gosec
)
type Module struct {
diff --git a/internal/api/client/streaming/streaming_test.go b/internal/api/client/streaming/streaming_test.go
index b429461c6..cece99bac 100644
--- a/internal/api/client/streaming/streaming_test.go
+++ b/internal/api/client/streaming/streaming_test.go
@@ -41,6 +41,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -94,6 +95,13 @@ func (suite *StreamingTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
@@ -102,7 +110,6 @@ func (suite *StreamingTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.streamingModule = streaming.New(suite.processor, 1, 4096)
- suite.NoError(suite.processor.Start())
}
func (suite *StreamingTestSuite) TearDownTest() {
diff --git a/internal/api/client/timelines/home.go b/internal/api/client/timelines/home.go
index f63d14fd3..f64d61287 100644
--- a/internal/api/client/timelines/home.go
+++ b/internal/api/client/timelines/home.go
@@ -18,9 +18,7 @@
package timelines
import (
- "fmt"
"net/http"
- "strconv"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
@@ -120,49 +118,27 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
return
}
- maxID := ""
- maxIDString := c.Query(MaxIDKey)
- if maxIDString != "" {
- maxID = maxIDString
- }
-
- sinceID := ""
- sinceIDString := c.Query(SinceIDKey)
- if sinceIDString != "" {
- sinceID = sinceIDString
- }
-
- minID := ""
- minIDString := c.Query(MinIDKey)
- if minIDString != "" {
- minID = minIDString
- }
-
- limit := 20
- limitString := c.Query(LimitKey)
- if limitString != "" {
- i, err := strconv.ParseInt(limitString, 10, 32)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- limit = int(i)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- local := false
- localString := c.Query(LocalKey)
- if localString != "" {
- i, err := strconv.ParseBool(localString)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- local = i
+ local, errWithCode := apiutil.ParseLocal(c.Query(apiutil.LocalKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
+ resp, errWithCode := m.processor.Timeline().HomeTimelineGet(
+ c.Request.Context(),
+ authed,
+ c.Query(MaxIDKey),
+ c.Query(SinceIDKey),
+ c.Query(MinIDKey),
+ limit,
+ local,
+ )
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/list.go b/internal/api/client/timelines/list.go
new file mode 100644
index 000000000..4f5232d8b
--- /dev/null
+++ b/internal/api/client/timelines/list.go
@@ -0,0 +1,152 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package timelines
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// ListTimelineGETHandler swagger:operation GET /api/v1/timelines/list/{id} listTimeline
+//
+// See statuses/posts from the given list timeline.
+//
+// The statuses will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
+//
+// The returned Link header can be used to generate the previous and next queries when scrolling up or down a timeline.
+//
+// Example:
+//
+// ```
+// <https://example.org/api/v1/timelines/list/01H0W619198FX7J54NF7EH1NG2?limit=20&max_id=01FC3GSQ8A3MMJ43BPZSGEG29M>; rel="next", <https://example.org/api/v1/timelines/list/01H0W619198FX7J54NF7EH1NG2?limit=20&min_id=01FC3KJW2GYXSDDRA6RWNDM46M>; rel="prev"
+// ````
+//
+// ---
+// tags:
+// - timelines
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the list
+// in: path
+// required: true
+// -
+// name: max_id
+// type: string
+// description: >-
+// 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
+// required: false
+// -
+// name: since_id
+// type: string
+// description: >-
+// Return only statuses *NEWER* than the given since 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 since status ID.
+// The status with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: limit
+// type: integer
+// description: Number of statuses to return.
+// default: 20
+// in: query
+// required: false
+//
+// security:
+// - OAuth2 Bearer:
+// - read:lists
+//
+// responses:
+// '200':
+// name: statuses
+// description: Array of statuses.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/status"
+// headers:
+// Link:
+// type: string
+// description: Links to the next and previous queries.
+// '401':
+// description: unauthorized
+// '400':
+// description: bad request
+func (m *Module) ListTimelineGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ targetListID := c.Param(IDKey)
+ if targetListID == "" {
+ err := errors.New("no list id specified")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.Timeline().ListTimelineGet(
+ c.Request.Context(),
+ authed,
+ targetListID,
+ c.Query(MaxIDKey),
+ c.Query(SinceIDKey),
+ c.Query(MinIDKey),
+ limit,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Items)
+}
diff --git a/internal/api/client/timelines/public.go b/internal/api/client/timelines/public.go
index a8a61c398..5be9fcaa8 100644
--- a/internal/api/client/timelines/public.go
+++ b/internal/api/client/timelines/public.go
@@ -18,9 +18,7 @@
package timelines
import (
- "fmt"
"net/http"
- "strconv"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
@@ -131,49 +129,27 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
return
}
- maxID := ""
- maxIDString := c.Query(MaxIDKey)
- if maxIDString != "" {
- maxID = maxIDString
- }
-
- sinceID := ""
- sinceIDString := c.Query(SinceIDKey)
- if sinceIDString != "" {
- sinceID = sinceIDString
- }
-
- minID := ""
- minIDString := c.Query(MinIDKey)
- if minIDString != "" {
- minID = minIDString
- }
-
- limit := 20
- limitString := c.Query(LimitKey)
- if limitString != "" {
- i, err := strconv.ParseInt(limitString, 10, 32)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- limit = int(i)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- local := false
- localString := c.Query(LocalKey)
- if localString != "" {
- i, err := strconv.ParseBool(localString)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- local = i
+ local, errWithCode := apiutil.ParseLocal(c.Query(apiutil.LocalKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
+ resp, errWithCode := m.processor.Timeline().PublicTimelineGet(
+ c.Request.Context(),
+ authed,
+ c.Query(MaxIDKey),
+ c.Query(SinceIDKey),
+ c.Query(MinIDKey),
+ limit,
+ local,
+ )
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/timeline.go b/internal/api/client/timelines/timeline.go
index bf8ef1e2e..2580333d9 100644
--- a/internal/api/client/timelines/timeline.go
+++ b/internal/api/client/timelines/timeline.go
@@ -27,10 +27,12 @@ import (
const (
// BasePath is the base URI path for serving timelines, minus the 'api' prefix.
BasePath = "/v1/timelines"
+ IDKey = "id"
// HomeTimeline is the path for the home timeline
HomeTimeline = BasePath + "/home"
// PublicTimeline is the path for the public (and public local) timeline
PublicTimeline = BasePath + "/public"
+ ListTimeline = BasePath + "/list/:" + IDKey
// MaxIDKey is the url query for setting a max status ID to return
MaxIDKey = "max_id"
// SinceIDKey is the url query for returning results newer than the given ID
@@ -56,4 +58,5 @@ func New(processor *processing.Processor) *Module {
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
attachHandler(http.MethodGet, HomeTimeline, m.HomeTimelineGETHandler)
attachHandler(http.MethodGet, PublicTimeline, m.PublicTimelineGETHandler)
+ attachHandler(http.MethodGet, ListTimeline, m.ListTimelineGETHandler)
}
diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go
index c26a04f31..06fc2c000 100644
--- a/internal/api/client/user/user_test.go
+++ b/internal/api/client/user/user_test.go
@@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -73,6 +74,13 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
suite.sentEmails = make(map[string]string)
@@ -81,8 +89,6 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.userModule = user.New(suite.processor)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *UserStandardTestSuite) TearDownTest() {
diff --git a/internal/api/fileserver/fileserver_test.go b/internal/api/fileserver/fileserver_test.go
index c2433d94a..70bd23c15 100644
--- a/internal/api/fileserver/fileserver_test.go
+++ b/internal/api/fileserver/fileserver_test.go
@@ -33,6 +33,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -81,6 +82,13 @@ func (suite *FileserverTestSuite) SetupSuite() {
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil)
diff --git a/internal/api/model/list.go b/internal/api/model/list.go
index d50c68f70..f897bcc88 100644
--- a/internal/api/model/list.go
+++ b/internal/api/model/list.go
@@ -17,14 +17,57 @@
package model
-// List represents a list of some users that the authenticated user follows.
+// List represents a user-created list of accounts that the user follows.
+//
+// swagger:model list
type List struct {
- // The internal database ID of the list.
+ // The ID of the list.
ID string `json:"id"`
// The user-defined title of the list.
Title string `json:"title"`
- // followed = Show replies to any followed user
+ // RepliesPolicy for this list.
+ // followed = Show replies to any followed user
// list = Show replies to members of the list
// none = Show replies to no one
RepliesPolicy string `json:"replies_policy"`
}
+
+// ListCreateRequest models list creation parameters.
+//
+// swagger:parameters listCreate
+type ListCreateRequest struct {
+ // Title of this list.
+ // example: Cool People
+ // in: formData
+ // required: true
+ Title string `form:"title" json:"title" xml:"title"`
+ // RepliesPolicy for this list.
+ // followed = Show replies to any followed user
+ // list = Show replies to members of the list
+ // none = Show replies to no one
+ // example: list
+ // default: list
+ // in: formData
+ RepliesPolicy string `form:"replies_policy" json:"replies_policy" xml:"replies_policy"`
+}
+
+// ListUpdateRequest models list update parameters.
+//
+// swagger:parameters listUpdate
+type ListUpdateRequest struct {
+ // Title of this list.
+ // example: Cool People
+ // in: formData
+ Title *string `form:"title" json:"title" xml:"title"`
+ // RepliesPolicy for this list.
+ // followed = Show replies to any followed user
+ // list = Show replies to members of the list
+ // none = Show replies to no one
+ // in: formData
+ RepliesPolicy *string `form:"replies_policy" json:"replies_policy" xml:"replies_policy"`
+}
+
+// swagger:ignore
+type ListAccountsChangeRequest struct {
+ AccountIDs []string `form:"account_ids[]" json:"account_ids" xml:"account_ids"`
+}
diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go
new file mode 100644
index 000000000..92578a739
--- /dev/null
+++ b/internal/api/util/parsequery.go
@@ -0,0 +1,58 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 <http://www.gnu.org/licenses/>.
+
+package util
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+const (
+ LimitKey = "limit"
+ LocalKey = "local"
+)
+
+func ParseLimit(limit string, defaultLimit int) (int, gtserror.WithCode) {
+ if limit == "" {
+ return defaultLimit, nil
+ }
+
+ i, err := strconv.Atoi(limit)
+ if err != nil {
+ err := fmt.Errorf("error parsing %s: %w", LimitKey, err)
+ return 0, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ return i, nil
+}
+
+func ParseLocal(local string, defaultLocal bool) (bool, gtserror.WithCode) {
+ if local == "" {
+ return defaultLocal, nil
+ }
+
+ i, err := strconv.ParseBool(local)
+ if err != nil {
+ err := fmt.Errorf("error parsing %s: %w", LocalKey, err)
+ return false, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ return i, nil
+}
diff --git a/internal/api/wellknown/webfinger/webfinger_test.go b/internal/api/wellknown/webfinger/webfinger_test.go
index 26143942c..df730e302 100644
--- a/internal/api/wellknown/webfinger/webfinger_test.go
+++ b/internal/api/wellknown/webfinger/webfinger_test.go
@@ -30,6 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -79,6 +80,13 @@ func (suite *WebfingerStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.tc = testrig.NewTestTypeConverter(suite.db)
+
+ testrig.StartTimelines(
+ &suite.state,
+ visibility.NewFilter(&suite.state),
+ suite.tc,
+ )
+
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
@@ -89,8 +97,6 @@ func (suite *WebfingerStandardTestSuite) SetupTest() {
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
-
- suite.NoError(suite.processor.Start())
}
func (suite *WebfingerStandardTestSuite) TearDownTest() {