mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-16 11:37:28 -06:00
[feature] Add List functionality (#1802)
* start working on lists * further list work * test list db functions nicely * more work on lists * peepoopeepoo * poke * start list timeline func * we're getting there lads * couldn't be me working on stuff... could it? * hook up handlers * fiddling * weeee * woah * screaming, pissing * fix streaming being a whiny baby * lint, small test fix, swagger * tidying up, testing * fucked! by the linter * move timelines to state like a boss * add timeline start to tests using state * invalidate lists
This commit is contained in:
parent
282be6f26d
commit
f5c004d67d
123 changed files with 5654 additions and 970 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
97
internal/api/client/accounts/lists.go
Normal file
97
internal/api/client/accounts/lists.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
103
internal/api/client/accounts/lists_test.go
Normal file
103
internal/api/client/accounts/lists_test.go
Normal file
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
156
internal/api/client/lists/listaccounts.go
Normal file
156
internal/api/client/lists/listaccounts.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
120
internal/api/client/lists/listaccountsadd.go
Normal file
120
internal/api/client/lists/listaccountsadd.go
Normal file
|
|
@ -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{})
|
||||
}
|
||||
120
internal/api/client/lists/listaccountsremove.go
Normal file
120
internal/api/client/lists/listaccountsremove.go
Normal file
|
|
@ -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{})
|
||||
}
|
||||
106
internal/api/client/lists/listcreate.go
Normal file
106
internal/api/client/lists/listcreate.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
91
internal/api/client/lists/listdelete.go
Normal file
91
internal/api/client/lists/listdelete.go
Normal file
|
|
@ -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{})
|
||||
}
|
||||
95
internal/api/client/lists/listget.go
Normal file
95
internal/api/client/lists/listget.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
152
internal/api/client/lists/listupdate.go
Normal file
152
internal/api/client/lists/listupdate.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
// 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)
|
||||
updateType, ok := msg["type"]
|
||||
if !ok {
|
||||
l.Warn("'type' field not provided")
|
||||
continue
|
||||
}
|
||||
|
||||
switch action {
|
||||
updateStream, ok := msg["stream"]
|
||||
if !ok {
|
||||
l.Warn("'stream' field not provided")
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
sinceID := ""
|
||||
sinceIDString := c.Query(SinceIDKey)
|
||||
if sinceIDString != "" {
|
||||
sinceID = sinceIDString
|
||||
local, errWithCode := apiutil.ParseLocal(c.Query(apiutil.LocalKey), false)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
152
internal/api/client/timelines/list.go
Normal file
152
internal/api/client/timelines/list.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
sinceID := ""
|
||||
sinceIDString := c.Query(SinceIDKey)
|
||||
if sinceIDString != "" {
|
||||
sinceID = sinceIDString
|
||||
local, errWithCode := apiutil.ParseLocal(c.Query(apiutil.LocalKey), false)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue