diff options
Diffstat (limited to 'internal/api/client')
35 files changed, 1450 insertions, 133 deletions
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() { |